优锘科技&学堂在线 - 数字孪生可视化开发技术训练营学习笔记
【文章简介】:笔者参加了一个由学堂在线和优锘科技(以下简称该公司)举办的数字孪生可视化开发技术训练营培训,该公司研发了低代码 3D可视化开发平台 ThingJS 相关生态链,主要用于以一种低代码简单的方式构建城市、园区等生产生活应用场景下数据展示。这些相关培训的内容也是本文的记录点。
注:本文仍在编辑,由于包含大量动态图,文章加载很慢,如无显示可以刷新试试 (编辑时间:08-09)
jcLee95 的个人博客
邮箱 :291148484@
CSDN 主页:/qq_28550263?spm=1001.2101.3001.5343
本文地址:/qq_28550263/article/details/126156985
目 录1. 数字孪生的相关概念
2. ThingJS生态体系
3. 搭建园区
3.1 CampusBuilder 客户端的安装和启动3.2 CampusBuilder 的在线使用3.3 实战环节 3.3.1 室内场景搭建实例3.3.2 室外与室内
4. CityBuilder 搭建城市
4.1 登录系统4.2 添加图层
5. ChartBuilder 构建大屏
5.1 概述5.2 拖拽现有大屏布局模板5.3 自己搭建大屏布局5.4 画布中的图层 5.4.1 介绍5.4.2 小案例 5.5 森大屏资源 5.5.1 资源类型5.5.2 资源操作5.5.2.1 搜索资源5.5.2.2 添加资源5.5.2.3 删除资源5.5.2.4 重命名资源5.5.2.5 移动资源所在图层5.5.2.6 对齐资源5.5.2.7 打组资源5.5.2.8 锁定资源5.5.2.9 隐藏资源5.5.2.10 复制资源5.5.2.11 设置资源属性5.5.2.12 管理资源样式5.5.2.13 管理颜色方案
6. ThingJS API 使用
6.1 场景与园区
6.1.1 场景与园区的概念6.1.2 如何创建场景6.1.3 如何让导出的模型可拾取6.1.4 如何加载多个园区6.1.5 场景效果配置 (1)设置背景(2)设置聚光灯
6.2 层级
6.2.1 层级获取6.2.2 层级切换6.2.3 层级事件 (1)进入层级事件(2)层级改变事件(3)离开层级事件(4)层级飞行结束事件(5)进入层级场景响应事件(6)进入层级飞行响应事件(7)进入层级背景设置事件(8)默认层级拾取结果(9)退出层级场景响应事件(10)修改默认的拾取物体操作(11)修改进入层级操作(12)修改退出层级操作 6.2.4 展示场景层级示例6.2.5 展示建筑外部结构示例6.2.6 展示建筑内部结构示例6.2.7 场景层级控制示例
6.3 对象控制
6.3.1 对象的增删查 6.3.1.1 创建对象的类型6.3.1.2 创建和删除对象的 API6.3.1.3 示例 6.3.2 对象效果设置 6.3.2.1 基础效果常用的 API6.3.2.2 BaseStyle 类成员 (1)设置物体是否始终在最前端渲染显示(2)显示/隐藏物体包围盒(3)设置包围盒颜色(4)设置/获取物体颜色(5)设置双面渲染(6)设置/获取材质自发光颜色(7)设置/获取材质自发光滚动贴图(8)设置/获取反射贴图(9)设置/获取高亮颜色(10)设置/获取高亮强度(11)设置贴图 填写图片资源路径 或 image 对象(12)材质金属度系数(13)设置/获取物体不透明度(14)设置/获取物体勾边颜色(15)设置/获取渲染排序值(16)设置材质粗糙度系数(17)开启/禁用勾边(18)开启/关闭线框模式 6.3.2.3 实例
6.4 事件绑定
6.4.1 事件的全局绑定6.4.2 事件的局部绑定6.4.3 内核事件EventType 属性6.4.4 事件的暂停和恢复6.4.5 事件的卸载6.4.6 自定义事件6.4.7 实例
6.5 视角(摄影机)
6.5.1 摄像机的基本概念6.5.2 ThingJS 中常用的 摄像机 API 6.5.2.1 官方案例解析 - 飞行控制6.5.2.2 官方案例解析 - 控制交互6.5.2.3 官方案例解析 - 控制地球相机
1. 数字孪生的相关概念
ThingJS 与传统3D开发的区别
2. ThingJS生态体系
3. 搭建园区
CampusBuilder(模模搭)是ThingJS体系内的3D园区场景搭建的客户端工具,如果你不想安装客户端,也可以在线使用森园区(相当于 CampusBuilder 的网页版)搭建你的园区。在本文中,两种方式我们都会进行介绍。以下是我们本节实战小节搭建的效果:
3.1 CampusBuilder 客户端的安装和启动
登录完成后,你将看到如下界面:
3.2 CampusBuilder 的在线使用入口
你也可以不下载客户端而直接使用在线开发方式。在浏览器中进入森园区页面:/campus,点击新建园区,如图所示:
页面将打开一个网页版的园区构建工具:
3.3 实战环节
这里我们随便在某度搜索一张室内设计为例,当然你也可以搜索一张产业园区的规划图纸,或者办公室的室内布局图纸:
以下是我选择的图片:
这章图片上有清晰的尺度标识,对于我们后续设定比例尺的工作是有帮助的。
接着我们以在线编辑为例进行讲解。
我们先新建园区:
3.3.1 室内场景搭建实例
点击“参考图”,导入我们的底图:
新建园区后,我们在森园区中,首先我们需要加载该图片,作为后续放置物件的蓝图:
图片导入后,调整比例尺的两端,到已知实际长度的位置,并在中间的实际大小中输入长度值,构建绘图的真实比例尺。
最后点击“完成”,完成后布局图片将默认水平铺在地面上:
这时你可以从左侧“模型库”中选择相应的模型。比如选择 **“室内”**中的“墙”:
你可可以删掉默认的小人,在**“生物”->“人”**中选择人物,放置在某个位置:
对于放置好的强,是可以拖动位置,以及拖动长度的:
在 公共库的“室内”->“家具”,找到椅子,放置在图纸上,通过拖到和旋转的方式调整位置、角度、大小:
以上都是直接放置在地面的,吊灯、电视都是相对于悬挂在空中的。这个需要调整 竖直方向上的高度。先简单放置一个吊灯:
使用鼠标拖动**“上下位移”**按钮,向上拖动:
直到达到你需要的高度:
你可以在右上角的切换按钮切换3D和2D视图:
可想而知,如果你的3D园区完成了,比平面布局更美观的俯视3D场景的2D图也就有了。
接着,完成门窗、电器,以及其它的内容,直到完成。最后就是本节开头时我们展现的图片:
3.3.2 室外与室内
到此为止,我们还只是介绍了园区的搭建,这仅仅是3D场景一部分,还不算数字孪生。
4. CityBuilder 搭建城市
森城市CityBuilder面向城市复杂场景的可视化需求,内置了全国范围内110多个城市的标准3D场景和酷炫的效果模板,使您分钟级构建心仪的3D城市。同时,森城市提供多种城市数据的插入和编辑能力,轻松让您的城市数据3D起来,实现整个城市的数字化及可视化。
4.1 登录系统
1.登录ThingStudio 森工厂。
2.在ThingStudio页面上方选择城市。
3.在城市页面单击新建城市,单击后跳转到CityBuilder主界面,如下图。
4.2 添加图层
CityBuilder提供了标准的城市三维场景资源——“森城市”,方便用户快速创建城市三维场景;同时您也可以插入自己的城市场景数据,满足用户个性化的场景需求。
初次进入到CityBuilder页面,系统会提醒您立马插入个人数据或选择需要添加的森城市资源,此时添加图层数据后,一个城市三维场景也随即创建成功。CityBuilder为方便用户快速获取城市三维场景,提供一套覆盖全国的标准城市三维场景资源,您可以在系统中森城市资源里直接选择区域添加至我的图层里;
CityBuilder还支持添加用户本地或已上传的用户资源(矢量数据),目前我们可以且仅可以使用本地矢量数据添加图层。
CityBuilder还支持在城市场景里加载森园区中搭建的园区,并提供了对园区位置和属性的编辑功能;
CityBuilder为方便用户自由的进行矢量数据的编辑,支持矢量数据图层的新增,并提供相应“矢量图层”对象的新增、删除和属性编辑功能。
一键添加
你可以在“森城市”资源信息面板里直接选择区域添加至我的图层里,区域范围支持以行政区划、自定义范围-多边形、自定义范围-矩形和自定义-圆形四种方式来进行选择。在搜索框,搜索,并选择你的城市,如郴州市:
点击**“添加至我的图层”**,可以将当前选择范围的城市加载到 **“我的图层”**中:
5. ChartBuilder 构建大屏
5.1 概述
森大屏是一个拖拽组装数字孪生可视化大屏的软件工具,提供丰富模板库,让可视化大屏无需从零开始搭建;提供数据接入和处理功能,实时展现图表数据;同时可以将3D场景/拓扑拖入森大屏,实现图表等指标数据与三维场景/拓扑进行联动交互。
在森工厂中选择大屏或者直接在地址栏中输入地址/ui可以进入森大屏主页:
点击新建大屏即可开启你的大屏构建:
5.2 拖拽现有大屏布局模板
进入森大屏后,在左侧面板布局下的大屏模板处可以看到有很多现成的大屏模板。我们可以选择其中的一个,使用鼠标左键点击并拖至右侧举行区域中松开,例如:
松开鼠标后,被托选的大屏将在这个举行区域中展示出来:
快捷键:
5.3 自己搭建大屏布局
图表资源的布局方法包含资源移动、资源缩放、资源对齐、资源打组、取消打组、资源锁定、资源隐藏、资源复制、资源升级、资源图层位置移动、资源删除等,您可以通过这些方法快捷地进行图表资源布局。
5.4 画布中的图层
5.4.1 介绍
画布中新建 多个图层 来展示不同的业务场景,并将主场景设置为常显,设置为常显后主场景在每个图层均显示,方便您根据主场景来搭建不同的业务场景。
如图所示:
【新建图层】在画布左下方单击“”图标新建图层,新建的图层展示在已有图层后面;【常显图层】选择要设置为常显的图层,单击“”图标打开图层的菜单,选择常显选项,即可将当前图层设置为常显图层; 孪生资源拖入大屏后(大屏场景中仅支持拖入一个孪生资源),系统自动将展示资源的图层设置为3D图层,并将图层置为常显状态。 【图层顺序拖动】拖动图层名称可移动图层的前后位置;【图层重命名】单击“”图标打开图层的菜单,选择重命名选项,可以对图层进行重命名;【图层删除】单击图标“”打开图层的菜单,选择删除按钮即可删除图层;【图层复制】针对需要复制的图层,单击图标“”打开图层的菜单,选择复制按钮对图层进行复制。单击菜单栏的编辑>粘贴或者使用快捷键Ctrl+V即可粘贴复制的图层。粘贴的图层展示在已有图层后面。 3D图层不支持复制操作
5.4.2 小案例
先拖入一个孪生体 3D 图层:
可以看到这个图层有一个 锁定的符号,即,表明它是常显图层。我们希望这个图层显示在最下方,也就是最底层。在其上面显示各种数据面板。
于是我们从布局中的大屏模板中,选择一个模板,作为第二个图层,这个图层在孪生体图层的上面:
大屏模板图层在上面,由于它有一张自带的背景图,这个图似乎是不透明的,将下面的图层给遮挡住了。因此我们需要将该大屏模板用到的 背景图删除或者设置为隐藏。
点击大纲,打开该 大屏模板的资源目录:
将鼠标逐个移动到列表中的资源项上,右侧将显示对应资源的控制图标,点击 图中 圈出控制图标,可以让相应的资源进行隐藏/显示:
最终,我们隐藏了背景图,就是这样的效果了:
5.5 森大屏资源
5.5.1 资源类型
5.5.2 资源操作
5.5.2.1 搜索资源
5.5.2.2 添加资源
5.5.2.3 删除资源
5.5.2.4 重命名资源
5.5.2.5 移动资源所在图层
5.5.2.6 对齐资源
5.5.2.7 打组资源
5.5.2.8 锁定资源
5.5.2.9 隐藏资源
5.5.2.10 复制资源
5.5.2.11 设置资源属性
5.5.2.12 管理资源样式
5.5.2.13 管理颜色方案
颜色方案由色卡、背景色/图、文本、辅助色、网格线色等要素组成:
色卡:一个颜色方案可以定义多张色卡,系统内置了12套色卡,每张色卡由纯色、渐变色、图片组成,您可以选择系统内置的色卡使用,也可以自定义新建色卡。背景色/图:图表的背景色或者背景图,开启后系统会将您选择的颜色或图片渲染到图表的背景框中,作为图表的背景色或者背景图。文本:图表主体的文本颜色,默认为白色。辅助色:图表主体的辅助色,辅助色在森大屏中用作保留色,主要用于不太起眼的点缀,烘托、支持和融合主色调,用于衬托图表主体的饱满性。网格线色:图表主体的网格线色(例如列表中的表格边框色),图表主体中如果有线条作为重要组成部分,建议线条颜色的取色从颜色方案中获取,以便于整屏换色时更美观。
单击菜单栏的视图>颜色方案管理,进入颜色方案管理页面:
在颜色方案管理页面单击新建颜色方案,弹出颜色方案设置弹框:
设置颜色方案色卡,色卡由纯色、渐变色和图片组成:
设置纯色:单击弹出颜色选择框,选择颜色后,单击颜色方案设置弹框中除颜色选择框外的其他位置可为色卡设置纯色。
设置渐变色:单击弹出颜色选择框,选择渐变色后,单击颜色方案设置弹框中除颜色选择框外的其他位置可为色卡设置渐变色,渐变色支持设置线性渐变和径向渐变,设置线型渐变时可设置线性渐变的角度。颜色选择框会自动记录您最近使用的16个颜色,当您需要使用同样的颜色时,可单击该颜色色块,将其应用到色卡上。
设置图片:单击弹出上传图片弹框,在弹框中单击上传/选择图片进入资源管理器页面,选择官方素材或者我的素材中的图片后,单击确认即可为色卡设置图片,设置图片时可在上传图片弹框右下角单击图标,设置图片的展示效果,支持选择拉伸、自适应和实际大小。
新增色卡:鼠标悬浮于数量后的色卡数字,单击图标可新增色卡。
删除色卡:单击色卡后的“
”图标可删除当前色卡,鼠标悬浮于数量后的色卡数字,单击图标可删除色卡列表中最下方的色卡。
排序色卡:单击色卡前的“
”图标拖动可调整当前色卡的顺序,单击色卡下方的“
”图标可将当前颜色方案中的色卡按相反顺序排列。
重命名色卡:单击页面上方的色卡名称,名称进入编辑模式,输入新的名称即可重命名色卡。
设置背景色/图:单击背景色/图前的图标开启设置功能:
6. ThingJS API 使用
本节的目标是熟练并掌握ThingJS一些常用的API,通过一些案例demo的穿插,加深大家对于ThingJS,开发在线项目或功能的能力。
6.1 场景与园区
6.1.1 场景与园区的概念
【场景】:当我们使用 App 启动了 ThingJS,ThingJS 就会创建一个三维空间,整个三维空间我们称之为“场景”(scene),在场景内我们可以创建对象,比如园区,建筑,等等。
在ThingJS中主要包括两类场景,一个是园区场景,另外一个是地球场景。
【园区】(campus):是一个对象。
6.1.2 如何创建场景
打开 ThingJS studio 官网/lowCode进入其低代码模块,点击新建 ThingJS 项目
浏览器将在新的标签中打开在线开发工具:
可以看到,新打开的这个ThingJS已经由如下代码:
/*** 说明:创建App,url为园区地址(可选)*使用App创建打开的三维空间我们称之为“场景”(scene)。场景包含地球、园区、模型等。*创建App时,传入的url就是园区的地址,不传url则创建一个空的场景。园区可在CampusBuilder*中创建编辑,有两种方法可以将园区添加到线上资源面板,方法如下:*1. 园区保存后,会自动同步到网页同一账号下*2. 园区保存后,导出tjs文件,在园区资源面板上传*上面两种方式生成的园区资源均可在资源面板中双击自动生成脚本* 难度:★☆☆☆☆*/// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});// 创建提示initThingJsTip(`使用 App 创建的三维空间称之为“场景”。有两种方法可以将客户端保存的园区添加到园区资源面板:<br>1. 园区保存后,会自动同步到网页同一账号下;<br>2. 园区保存后导出tjs文件,在园区资源面板上传。<br>`);
前面说过,当我们使用 App 启动了 ThingJS,ThingJS 就会创建一个三维空间,启动的代码就是new THING.App
这部分:
var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});
其中:
url指的是园区场景的一个地址,如这里为魔门提供的一个示例园区的地址:/static/models/factory
。background是场景的背景色。env是场景所处的一个虚拟环境,指定一个 env 在场景中存在如 镜子之类的,可以看到镜子中反射的周围环境的效果,如图 镜子反射了Seaside
:
除了这几个属性外,还有其它的属性,详细可以参考其 API文档。
你可以使用自己的园区,需要点击这个按钮进行选取:
可以看到,我这里没有显示任何园区可以拾取。因此需要使用一个办法,让我们在森园区中搭建好的园区能够显示在这里的拾取区中。
6.1.3 如何让导出的模型可拾取
上面一节,我们知道,园区搭建好之后需要在 ThingJS 项目中拾取以使用它。那么如何才能让我们搭建好的园区可以拾取呢——只有在编辑了UserID、Name或者自定义属性后,导入到ThingJS中才能成为独立的管理对象,被程序读取或修改。
比如绘制的一个门,我们可以使用鼠标选中它,在右侧的编辑面板中给定它属性值,要让它可拾取,则指定其孪生体ID,这里我们指定其 孪生体ID 和 名称 分别为 door_01 以及 door:
打开与效果预览,可以看到将鼠标移动到这个门上时能够显示黄色的线框,这就表明这扇门已经可以被拾取了。
在开发中只有一个对象可以被拾取,才表明它具有独立的身份,之所以叫数字孪生提是因为往往它对应着显示中的某个我们关注其某些具体参数的东西,是现实世界中的物体在数字世界的表示。如果不可拾取,则 ThingJS 会出于性能的考虑,将其认为是与环境融为一体。
6.1.4 如何加载多个园区
在之前的案例中,我们用过new THING.App({...})
指定园区的 url 来创建园区:
var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});
实际上,这个例子中,我们可以先不指定园区的url
,在之后使用app.create({})
在其中给定创建类型为Campus
(园区),并给出园区的url
,即:
var app = new THING.App({background: '#000000',env: 'Seaside',});app.create({type:'Campus',url: '/static/models/factory',complete(ev){app.level.change(ev.object)}})
同样的方式,当我们再次调用create()
方法时,就可以创建第二个、第三个园区。如果创建了多个场景,那可以通过一个按钮(面板),来切换当前看到的场景,例如这个是一个官方的案例:
/*** 说明:通过动态加载场景 动态加载建筑里的楼层* 操作:双击建筑,动态加载场景*/var dataObj = {progress: 0 }; // 场景加载进度条数据对象var loadingPanel; // 进度条界面组件var curCampus;// 配置相应建筑的园区场景urlvar campusUrl = [{name: "园区A",url: "/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2"}, {name: "园区B",url: "/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%A4%96"}];var buildingConfig = {'商业A楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AA%E6%A5%BC%E5%B1%82%E7%BA%A7','商业B楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AB%E6%A5%BC%E5%B1%82%E7%BA%A7','商业C楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7','商业D楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AD%E6%A5%BC%E5%B1%82%E7%BA%A7','商业E楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AE%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅A楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅B楼': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7','图书馆': '/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7',};var app = new THING.App({"url": "/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2","skyBox": "Universal",});// 主场景加载完后 删掉楼层app.on('load', function (ev) {curCampus = ev.campus;// 进入层级切换app.level.change(ev.campus);initThingJsTip("本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:园区A");// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < ev.buildings.length; i++) {ev.buildings[i].floors.destroy();}new THING.widget.Button('切换场景', changeScene); // 切换场景createWidgets();});/*** 切换场景*/function changeScene() {var url = curCampus.url; //当前园区url// 动态创建园区if (url === campusUrl[0].url) {createCampus(campusUrl[1]);} else {createCampus(campusUrl[0]);}}/*** 创建园区*/function createCampus(obj) {app.create({type: "Campus",url: obj.url,position: [0, 0, 0],visible: false, // 创建园区过程中隐藏园区complete: function (ev) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + obj.name);curCampus.destroy(); // 新园区创建完成后删除之前的curCampus = ev.object; // 将新园区赋给全局变量curCampus.fadeIn(); // 创建完成后显示(渐现)app.level.change(curCampus); // 开启层级切换var building = app.query(".Building"); // 获取园区中的建筑// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < building.length; i++) {building[i].floors.destroy();}}});}/*** 卸载动态创建的园区*/app.on(THING.EventType.LeaveLevel, '.Building', function (ev) {var current = ev.current;if (current.type == "Campus") {var building = ev.previous; // 获取之前的层级if (!building) return;building._isAlreadyBuildedFloors = false;if (building.floors) building.floors.destroy();var url = curCampus.url; //当前园区urlif (url === campusUrl[0].url) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + campusUrl[0].name);} else {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + campusUrl[1].name);}}}, '退出建筑时卸载建筑下的楼层');// 进入建筑时 动态加载园区app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var buildingMain = ev.object; // 获取当前建筑对象var buildingName = buildingMain.name; // 获取当前建筑名称var preObject = ev.previous; // 上一层级的物体// 如果是从楼层退出 进入Building的 则不做操作if (preObject instanceof THING.Floor) return;initThingJsTip(buildingName + '正在加载!');loadingPanel.visible = true;// 暂停进入建筑时的默认飞行操作,等待楼层创建完成app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 暂停单击右键返回上一层级功能app.pauseEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 动态创建园区var campusTmp = app.create({type: 'Campus',// 根据不同的建筑,传入园区相应的urlurl: buildingConfig[buildingName],// 在回调中,将动态创建的园区和园区下的建筑删除 只保留楼层 并添加到相应的建筑中complete: function () {var buildingTmp = campusTmp.buildings[0];buildingTmp.floors.forEach(function (floor) {buildingMain.add({object: floor,// 设置相对坐标,楼层相对于建筑的位置保持一致localPosition: floor.localPosition});})// 楼层添加后,删除园区以及内部的园区建筑buildingTmp.destroy();campusTmp.destroy();loadingPanel.visible = false;// 恢复默认的进入建筑飞行操作app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 恢复单击右键返回上一层级功能app.resumeEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 这一帧内 暂停自定义的 “进入建筑创建楼层” 响应app.pauseEventInFrame(THING.EventType.EnterLevel, '.Building', '进入建筑创建楼层');// 触发进入建筑的层级切换事件 从而触发内置响应buildingMain.trigger(THING.EventType.EnterLevel, ev);initThingJsTip(buildingName + '加载完成!');}});}, '进入建筑创建楼层', 51);app.on(THING.EventType.LoadCampusProgress, function (ev) {var value = ev.progress;dataObj.progress = value;}, '加载场景进度');/*** 创建进度条组件*/function createWidgets() {// 进度条界面组件loadingPanel = new THING.widget.Panel({titleText: '场景加载进度',opacity: 0.9, // 透明度hasTitle: true});// 设置进度条界面位置loadingPanel.positionOrigin = 'TR'// 基于界面右上角定位loadingPanel.position = ['100%', 0];loadingPanel.visible = false;loadingPanel.addNumberSlider(dataObj, 'progress').step(0.01).min(0).max(1).isPercentage(true);}
其效果如下:
说明:
在这个案例中,主场景加载完后(使用app.on('load', (ev)=>{})
)使用new THING.widget.Button
,来创建了一个回调函数能够控制场景切换的按钮:
app.on('load', (ev)=> {curCampus = ev.campus;// 进入层级切换app.level.change(ev.campus);initThingJsTip("本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:园区A");// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < ev.buildings.length; i++) {ev.buildings[i].floors.destroy();}new THING.widget.Button('切换场景', ()=>{//当前园区urlvar url = curCampus.url; // 动态创建园区if (url === campusUrl[0].url) {createCampus(campusUrl[1]);} else {createCampus(campusUrl[0]);}}); // 切换场景createWidgets();});
而这里,又用到了一个createCampus
函数,这个函数是用来创建园区的:
function createCampus(obj) {app.create({type: "Campus",url: obj.url,position: [0, 0, 0],visible: false, // 创建园区过程中隐藏园区complete: function (ev) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + obj.name);// curCampus 是一个该函数外面的全局变量,一开始时是用于容纳所有的园区的信息[{url:'xxx', name:'xxx'},...]// 主场景加载完后, curCampus 被赋值为 ev.campus;curCampus.destroy();// 新园区创建完成后删除之前的园区curCampus = ev.object; // 将新园区赋给全局变量 curCampuscurCampus.fadeIn(); // 创建完成后显示(渐现)app.level.change(curCampus); // 开启层级切换// 获取园区中的建筑var building = app.query(".Building"); // 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < building.length; i++) {building[i].floors.destroy();}}});}
该函数的唯一参数obj
实际上就是用于指定园区的信息,包括url
(园区地址)和name
(用于展示的园区名称)两个属性,比如:
{name: "园区A",url: "/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2"}
又比如:
{name: "园区B",url: "/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%A4%96"}
6.1.5 场景效果配置
场景效果配置,顾名思义,就是通过配置相关的参数,让场景看上去更符合我们的心意。
(1)设置背景
来看一个空过按钮控制背景的官方示例:
// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址skyBox: 'Night',env: 'Seaside',});app.on('load', function () {initThingJsTip("天空盒是一个包裹整个场景的立方体,可以很好地渲染并展示整个场景环境。</br>点击左侧按钮,设置天空盒效果、背景色以及背景图片。");// 摄像机飞行到某位置app.camera.flyTo({'position': [14.929613003036518, 26.939904587373245, 67.14964454354718],'target': [2.1474740033704594, 17.384929223259824, 10.177959375514941],'time': 2000});})// 设置天空盒(目前仅能使用系统内置天空盒效果)new THING.widget.Button('蓝天', function () {app.skyBox = 'BlueSky';});new THING.widget.Button('银河', function () {app.skyBox = 'MilkyWay';});new THING.widget.Button('黑夜', function () {app.skyBox = 'Night';});new THING.widget.Button('多云', function () {app.skyBox = 'CloudySky';});new THING.widget.Button('灰白', function () {app.skyBox = 'White';});new THING.widget.Button('暗黑', function () {app.skyBox = 'Dark';});// 背景色颜色可使用十六进制颜色或rgb字符串new THING.widget.Button('设置背景色1', function () {app.background = '#0a3d62';})new THING.widget.Button('设置背景色2', function () {app.background = 'rgb(68,114,196)';})// 图片可在资源、页面资源上传// 上传完成后,点击需要使用的图片,即可在代码编辑器中出现图片url地址// 也可直接使用能访问的网络图片urlnew THING.widget.Button('设置背景图片1', function () {app.background = '/static/images/background_img_01.png';})new THING.widget.Button('设置背景图片2', function () {app.background = '/static/images/background_img_02.png';})// 清除背景效果new THING.widget.Button('清除背景', function () {app.skyBox = null;app.background = null;})
其效果如下:
(2)设置聚光灯
来看一个通过面板来设置聚光灯参数的官方示例:
// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址skyBox: 'Night',env: 'Seaside',});// 参数var dataObj = {'type': 'SpotLight','lightAngle': 30,'intensity': 1,'penumbra': 0.5,'castShadow': false,'position': null,'height': 0,'color': 0xFFFFFF,'distance': null,'target': null,'helper': true,'follow': true,};// 叉车let car1;let car2;// 当前灯光let curLight;let curLightPosition;// 创建聚光灯方法function createSpotLight(position, target) {dataObj['lightAngle'] = 30;dataObj['intensity'] = 0.5;dataObj['penumbra'] = 0.5;dataObj['castShadow'] = false;dataObj['position'] = position;dataObj['distance'] = 25;dataObj['color'] = 0xFFFFFF;dataObj['helper'] = true;dataObj['follow'] = true;//创建聚光灯var spotLight = app.create(dataObj);curLight = spotLight;curLightPosition = spotLight.position;createSpotLightControlPanel(spotLight);curLight.lookAt(car1);}/*** 灯光控制面板*/function createSpotLightControlPanel() {var panel = new THING.widget.Panel({isDrag: true,titleText: "灯光参数调整",width: '260px',hasTitle: true});// 设置 panel 位置 panel.position = [10, 35];panel.addNumberSlider(dataObj, 'lightAngle').caption('灯光角度').step(1).min(0).max(180).isChangeValue(true).on('change', function(value) {curLight.lightAngle = value;});panel.addNumberSlider(dataObj, 'intensity').caption('亮度').step(0.01).min(0).max(1).isChangeValue(true).on('change', function(value) {curLight.intensity = value;});panel.addNumberSlider(dataObj, 'penumbra').caption('半影').step(0.01).min(0).max(1).isChangeValue(true).on('change', function(value) {curLight.penumbra = value;});panel.addNumberSlider(dataObj, 'distance').caption('距离').step(0.1).min(0).max(200).isChangeValue(true).on('change', function(value) {curLight.distance = value;});panel.addNumberSlider(dataObj, 'height').caption('高度').step(0.1).min(0).max(200).isChangeValue(true).on('change', function(value) {curLight.position = [curLightPosition[0], curLightPosition[1] + value, curLightPosition[2]];});panel.addBoolean(dataObj, 'castShadow').caption('影子').on('change', function(value) {curLight.castShadow = value;});panel.addBoolean(dataObj, 'helper').caption('辅助线').on('change', function(value) {curLight.helper = value;});panel.addBoolean(dataObj, 'follow').caption('跟随物体').on('change', function(value) {if (value) {curLight.lookAt(car1);} else {curLight.lookAt(null);}});panel.addColor(dataObj, 'color').caption('颜色').on('change', function(value) {curLight.lightColor = value;});}/*** 注册鼠标移动事件,检查是否按下'shift'键, 按下设置聚光灯跟随鼠标位置*/app.on('mousemove', function(ev) {if (!curLight) {return;}if (!ev.shiftKey) {return;}var pickedPosition = ev.pickedPosition;if (pickedPosition) {curLight.lookAt(pickedPosition);}})/*** 注册场景load事件*/app.on('load', function(ev) {// createTip();// 主灯强度设置为0,突出聚光灯效果app.lighting = {mainLight: {intensity: 0}};// 获取场景内id为'car01' 和 'car02' 的叉车car1 = app.query('car01')[0];car2 = app.query('car02')[0];// 参数1: 在car2上方5米创建一个聚光灯// 参数2: 初始target设置为car1的位置createSpotLight(THING.Math.addVector(car2.position, [0, 5, 0]), car1.position);// 创建一个圆形路径var path = [];var radius = 6;for (var degree = 0; degree <= 360; degree += 10) {var x = Math.cos(degree * 2 * Math.PI / 360) * radius;var z = Math.sin(degree * 2 * Math.PI / 360) * radius;path.push(THING.Math.addVector(car1.position, [x, 0, z]));}// 让 car1 沿圆形路径运动car1.movePath({orientToPath: true, // 物体移动时沿向路径方向path: path,time: 10 * 1000,loopType: THING.LoopType.Repeat // 循环类型});initThingJsTip("左侧面板可对灯光参数进行调整。按住 shift 键,聚光灯可追踪鼠标位置");$(".warninfo3").css("left", "55%");})
其效果如下:
6.2 层级
在 ThingJS 中的层级关系可以用一个树状结构来表示,即所谓层级关系树:
这里一般来说主要关注园区、建筑、楼层、房间之间的关系,图片描述比较形象,这里不做过多解释。
6.2.1 层级获取
6.2.2 层级切换
6.2.3 层级事件
(1)进入层级事件
// 进入层级// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;});
(2)层级改变事件
// 层级变化// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.LevelChange, function (ev) {var object = ev.current;if (object instanceof THING.Campus) {console.log('Campus: ' + object);}else if (object instanceof THING.Building) {console.log('Building: ' + object);}else if (object instanceof THING.Floor) {console.log('Floor: ' + object);}else if (object instanceof THING.Thing) {console.log('Thing: ' + object);}});
(3)离开层级事件
// 离开层级// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;});
(4)层级飞行结束事件
// 层级切换飞行结束// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.LevelFlyEnd, '.Thing', function (ev) {console.log(ev.object.id);});
(5)进入层级场景响应事件
// 修改进入层级场景响应// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体半透明var things = object.brothers.query('.Thing');things.style.opacity = 0.25;}, 'customEnterLevel');// 停止进入物体层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);
(6)进入层级飞行响应事件
// 修改进入层级飞行响应// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;app.camera.flyTo({object: object,xAngle: 45, //物体坐标系下沿x轴旋转角度yAngle: -45, //物体坐标系下沿y轴旋转角度radiusFactor: 2, //物体包围盒半径的倍数time: 3000,lerpType: THING.LerpType.Quartic.In,complete: function() {console.log("飞行结束");}});}, 'customLevelFly');// 停止进入物体层级的默认飞行行为app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelFly);
(7)进入层级背景设置事件
// 修改进入层级背景设置// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {app.skyBox = null;app.background = 0xffffff;}, 'customLevelSetBackground');// 停止进入物体层级的默认背景设置app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSetBackground);
(8)默认层级拾取结果
// 修改进入层级选择设置// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.EnterLevel, '.Building', function (ev) {app.picker.pickedResultFunc = function (obj) {return obj;}}, 'customLevelPickedResultFunc');// 暂停建筑层级的默认选择行为app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelPickedResultFunc);
(9)退出层级场景响应事件
// 修改退出层级场景响应// {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building// {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象)// {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象)app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;// 取消其他物体半透明var things = object.brothers.query('.Thing');things.style.opacity = null;}, 'customLevelSceneOperations');// 暂停默认退出行为app.pauseEvent(THING.EventType.LeaveLevel, '.Thing', THING.EventTag.LevelSceneOperations);
(10)修改默认的拾取物体操作
// 修改默认的拾取物体操作// 鼠标拾取到物体变红app.on(THING.EventType.MouseEnter, '.Thing', function(ev) {ev.object.style.color = '#FF0000';});// 鼠标离开物体取消变红app.on(THING.EventType.MouseLeave, '.Thing', function(ev) {ev.object.style.color = null;});// 暂停默认的拾取物体操作app.pauseEvent(THING.EventType.Pick, '*', THING.EventTag.LevelPickOperation);
(11)修改进入层级操作
// 修改进入层级操作// 单击进入app.on(THING.EventType.SingleClick, function (ev) {var object = ev.object;if (object) {object.app.level.change(object);}}, 'customLevelEnterMethod');// 暂停双击进入app.pauseEvent(THING.EventType.DBLClick, '*', THING.EventTag.LevelEnterOperation);
(12)修改退出层级操作
app.on('load', function (ev) {// 场景加载完成后 进入园区层级 开启默认的层级控制app.level.change(ev.campus);});// 修改退出层级操作// 双击右键回到上一层级app.on(THING.EventType.DBLClick, function (ev) {if (ev.button != 2) {return;}app.level.back();}, 'customLevelBackMethod');// 暂停单击返回上一层级功能app.pauseEvent(THING.EventType.Click, null, THING.EventTag.LevelBackMethod)
6.2.4 展示场景层级示例
下面来分析一个官方给出的展示场景层级示例:
// 引入jquery.easyui插件THING.Utils.dynamicLoad(['/guide/lib/jquery.easyui.min.js', '/guide/lib/default/easyui.css'], function () {var panel =`<div class="easyui-panel" style="display:none;padding:5px; width: 220px;height: auto;margin-top: 10px;margin-left: 10px; position: absolute; top: 0px; right: 0; z-index: 1;background-color: white"><ul id="objectTree" class="easyui-tree"></ul></div>`$('#div2d').append($(panel));})// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});// 定义父子树的显示状态var objectTreeState = true;// 这里使用了jquery.easyui的tree插件app.on('load', function (ev) {// 创建提示initThingJsTip("父子树:在 ThingJS 加载园区后,自动创建了由 campus,building,floor,room 和一些在模模搭中添加的Thing类物体。这些物体不是独立散落在场景中的,他们会相互关联,形成一棵树的结构。</br>点击左侧按钮,创建父子树,展示场景层级");app.camera.position = [45.620884740847416, 39.1713011011022, 57.12763372644285];app.camera.target = [1.7703319346792363, 4.877514886137977, -2.025030535593601];var buildings = app.query('.Building');// 创建父子树new THING.widget.Button('创建父子树', function () {// 提示内容修改initThingJsTip("点击右侧界面选择框,控制对应内容显隐");// 父子树界面创建以及控制$('#objectTree').parent().show();$('#objectTree').tree({data: getRootData(app.root),checkbox: true,cascadeCheck: false,onCheck: function (node, checked) {if (app.query('#' + node.id)[0]) {app.query('#' + node.id).visible = checked;if ((app.query('#' + node.id)[0].type) == "Campus") {changeBuilding(app.query('#' + node.id)[0].buildings);}if ((app.query('#' + node.id)[0].type) == "Building") {if (app.query('#' + node.id)[0].facades[0]) {app.query('#' + node.id)[0].floors.visible = false;}}} else {app.root.visible = checked;}},onClick: function (node, checked) {var id = node.id;var obj = app.query('#' + id)[0];if (obj) {app.level.change(obj);}}})});new THING.widget.Button('重置', function () {app.query("*").visible = true;app.query("*").style.opacity = 1;app.level.change(ev.campus);app.camera.position = [45.620884740847416, 39.1713011011022, 57.12763372644285];app.camera.target = [1.7703319346792363, 4.877514886137977, -2.025030535593601];buildings.forEach(function (item) {if (item.facades[0]) {item.floors.visible = false;}})$("#objectTree").html('');$(".easyui-panel").hide();initThingJsTip("父子树:在 ThingJS 加载园区后,自动创建了由 campus,building,floor,room 和一些在模模搭中添加的Thing类物体。这些物体不是独立散落在场景中的,他们会相互关联,形成一棵树的结构。</br>点击左侧按钮,创建父子树,展示场景层级");})});/*** 根节点信息* @param {Object} root - root类*/function getRootData(root) {var data = [];data.push(getSceneRoot(root));return data;}/*** 根节点信息* @param {Object} root - root类*/function getSceneRoot(root) {var data = {id: root.id,checked: true,state: 'open',text: 'root',};data["children"] = [];root.campuses.forEach(function (campus) {data["children"].push(getCampusData(campus));});return data;}/*** 根节点信息由建筑和室外物体组成* @param {Object} campus - 园区类*/function getCampusData(campus) {var data = {id: campus.id,checked: true,state: 'open',text: campus.type + ' (' + campus.id + ')'};data["children"] = [];campus.buildings.forEach(function (building) {data["children"].push(getBuildingData(building));});campus.things.forEach(function (thing) {data["children"].push(getThingData(thing));});return data;}/*** 收集建筑信息* @param {Object} building - 建筑对象*/function getBuildingData(building) {var data = {id: building.id,checked: true,state: 'open',text: building.type + ' (' + building.id + ')'};data["children"] = [];building.floors.forEach(function (floor) {data["children"].push(getFloorData(floor));});return data;}/*** 收集楼层信息* @param {Object} floor - 楼层对象*/function getFloorData(floor) {var data = {id: floor.id,checked: true,state: 'open',text: floor.type + ' (level:' + floor.levelNumber + ')'};data["children"] = [];floor.things.forEach(function (thing) {data["children"].push(getThingData(thing));});return data;}/*** 建筑对象* @param {Object} thing - 物对象*/function getThingData(thing) {return {id: thing.id,checked: true,text: thing.type + ' (' + thing.name + ')'};}/*** Building内部建筑隐藏(无外立面不隐藏内部建筑)* @param {Object} building - 建筑对象集合*/function changeBuilding(building) {for (let i = 0; i < building.length; i++) {if (building[i].facades[0]) {building[i].floors.visible = false;}}}
其效果如下:
6.2.5 展示建筑外部结构示例
下面来分析一个官方给出的展示建筑外部结构示例:
var campus;// 园区对象// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});app.on('load', function (ev) {// 创建提示initThingJsTip("点击按钮,可获取园区中的建筑(buildings)、物体(things)、地面(ground),设置建筑外立面显示隐藏");createHtml();campus = app.query(".Campus")[0]; // 获取园区对象new THING.widget.Button("获取buildings", function () {// 初始化设置reset();var buildings = campus.buildings; // 获取园区下的所有建筑,返回为 Selector 结构buildings.forEach(function (item) {// 创建标注var ui = app.create({type: 'UIAnchor',parent: item,element: createElement(item.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + item.id + ' .text').text(item.name);})})new THING.widget.Button("获取things", function () {// 初始化设置reset();// 获取园区下的所有 Thing 类物体,返回为 Selector 结构var things = campus.things;things.forEach(function (item) {// 创建标注var ui = app.create({type: 'UIAnchor',parent: item,element: createElement(item.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + item.id + ' .text').text(item.name);})})new THING.widget.Button("获取ground", function () {// 初始化设置reset()var ground = campus.ground; // 获取园区下的 ground// 创建标注var ui = app.create({type: 'UIAnchor',element: createElement(ground.id), // 此参数填写要添加的Dom元素position: [1.725, 0.02, 5.151],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + ground.id + ' .text').text('ground');})new THING.widget.Button("隐藏外立面", function () {// 初始化设置reset(true);var build = app.query('107')[0]; // 获取园区中的建筑if ($("input[value='隐藏外立面']").length) {$("input[value='隐藏外立面']").val('显示外立面');build.facade.visible = false; // 隐藏外立面build.floors.visible = true; // 显示楼层} else {$("input[value='显示外立面']").val('隐藏外立面');build.facade.visible = true; // 显示外立面build.floors.visible = false; // 隐藏楼层}})new THING.widget.Button("重置", function () {// 初始化设置reset();})/*** 恢复初始化*/function reset(flag) {$(".marker").remove(); // 移除标注if (flag) return;$("input[value='显示外立面']").val('隐藏外立面');var build = app.query('107')[0]; // 获取园区中的建筑build.facade.visible = true; // 显示外立面build.floors.visible = false; // 隐藏楼层createHtml();// 创建提示initThingJsTip("点击按钮,可获取园区中的建筑(buildings)、物体(things)、地面(ground),设置建筑外立面显示隐藏");}})/*** 创建html*/function createHtml() {var html =`<div id="board" class="marker" style="position: absolute;"><div class="text" style="color: #FF0000;font-size: 12px;text-shadow: white 0px 2px, white 2px 0px, white -2px 0px, white 0px -2px, white -1.4px -1.4px, white 1.4px 1.4px, white 1.4px -1.4px, white -1.4px 1.4px;margin-bottom: 5px;"></div><div class="picture" style="height: 30px;width: 30px;margin: auto;"><img src="/guide/examples/images/navigation/pointer.png" style="height: 100%;width: 100%;"></div></div>`;$('#div3d').append($(html));}/*** 创建元素*/function createElement(id) {var srcElem = document.getElementById('board');var newElem = srcElem.cloneNode(true);newElem.style.display = "block";newElem.setAttribute("id", id);app.domElement.insertBefore(newElem, srcElem);return newElem;}
其效果如下:
6.2.6 展示建筑内部结构示例
下面来分析一个官方给出的展示建筑内部结构示例:
// 加载场景代码var app = new THING.App({// 场景地址"url": "/./uploads/wechat/emhhbmd4aWFuZw==/scene/建筑测试03"});// 加载场景app.on('load', function (ev) {var campus = ev.campus; // 获取园区对象var floor = app.query('.Floor')[0]; // 获取楼层对象app.level.change(floor); // 开启层级切换new THING.widget.Button('获取墙', getWall); // 获取楼层中的墙new THING.widget.Button('获取门', getDoor); // 获取楼层中的门new THING.widget.Button('获取 Thing', getThing); // 获取Thing类物体,包含门new THING.widget.Button('获取 Misc', getMisc); // 获取楼层中的misc类物体new THING.widget.Button('获取楼层地板', getFloor); // 获取楼层地板new THING.widget.Button('获取楼层屋顶', getFloorRoof); // 获取楼层屋顶 new THING.widget.Button('获取房间面积', getRoomArea); // 获取房间面积new THING.widget.Button('重置', init); // 恢复初始化设置$("input[type='button']").hide(); // 隐藏按钮})/*** 获取当前楼层的Thing类物体*/function getThing() {// 初始化设置init();initThingJsTip("搭建园区时,设置了 ID、name、自定义属性的模型,在 ThingJS 中均为 Thing 类物体");var floor = app.level.current; // 当前楼层var things = floor.things; // 楼层内Thing类物体things.forEach(function (item) {// 创建标注createUIAnchor('ui', item);})}/*** 获取当前楼层中的门*/function getDoor() {// 初始化设置init();initThingJsTip("获取楼层中的门。设置了 ID、name、自定义属性的门模型,才可以被获取");var floor = app.level.current; // 当前楼层var doors = floor.doors; // 楼层中的门doors.forEach(function (item) {// 创建标注createUIAnchor('ui', item);})}/*** 获取当前楼层中的墙*/function getWall() {// 初始化设置init();initThingJsTip("设置墙的颜色为黄色");var floor = app.level.current; // 当前楼层var wall = floor.wall; // 楼层中的墙wall.style.color = '#ffff00'; // 设置墙的颜色}/*** 获取当前楼层misc类物体* 楼层下,只有在CampusBuilder中编辑了UserID、Name或自定义属性的物体(摆放的模型),* 才能在 ThingJS 中创建为 Thing 对象,否则将合并到楼层的 misc 中,无法单独进行管理。*/function getMisc() {// 初始化设置init();initThingJsTip("园区搭建时,没有设置 ID、name、自定义属性的模型,都将合并到楼层的 Misc 中,无法单独进行管理");var floor = app.level.current; // 当前楼层var misc = floor.misc; // 楼层内misc类物体misc.style.outlineColor = '#0000ff'; // 设置misc类物体的颜色}/*** 获取当前楼层的地板*/function getFloor() {// 初始化设置init();initThingJsTip("楼层地板不包含本楼层下独立管理的房间地板");var floor = app.level.current; // 当前楼层var plan = floor.plan; // 楼层地板plan.style.color = '#ffff00'; // 设置地板颜色//添加标注createUIAnchor('text', plan, '楼层地板');}/*** 获取当前楼层的屋顶*/function getFloorRoof() {// 初始化设置init();initThingJsTip("楼层屋顶不包含本楼层下独立管理的房间屋顶");var floor = app.level.current; // 当前楼层var roof = floor.roof; // 楼层屋顶roof.style.opacity = 0.8; // 设置屋顶透明度roof.style.color = '#0000ff'; // 设置屋顶颜色roof.visible = true;//添加标注createUIAnchor('text', roof, '楼层屋顶');}/*** 获取楼层内房间面积*/function getRoomArea() {// 初始化设置init();var floor = app.level.current; // 当前楼层var textRegions = app.query('.TextRegion'); // 获取TextRegion类var rooms = floor.rooms; // 楼层的房间rooms.forEach(function (room) {room.roof.visible = true; // 显示房间屋顶room.roof.style.opacity = 0.8; // 设置透明度room.roof.style.color = '#0000ff'; // 设置颜色var area = room.area.toFixed(2); // 获取房间面积 保留小数点后两位//添加标注createUIAnchor('text', room, area + '平方米');initThingJsTip("展示房间面积");})}/*** 初始化设置*/function init() {var floor = app.level.current; // 当前楼层floor.wall.style.color = null; // 设置墙体颜色floor.misc.style.outlineColor = null; // 设置misc类物体颜色floor.plan.style.color = null; // 设置楼层地板颜色floor.roof.style.color = null; // 设置楼层屋顶颜色floor.roof.visible = false; // 设置楼层屋顶隐藏floor.rooms.forEach(function (room) {room.roof.visible = false; // 设置楼层房间隐藏room.roof.style.color = null; // 设置楼层房间屋顶颜色room.plan.style.color = null; // 设置楼层房间地板颜色})app.query('.TextRegion').destroyAll(); // 获取TextRegion类$(".marker").remove(); // 移除标注// 创建元素createHtml();initThingJsTip("点击左侧按钮,查看具体效果");}/*** 创建html*/function createHtml() {var html =`<div id="board" class="marker" style="position: absolute;"><div class="text" style="color: #FF0000;font-size: 12px;text-shadow: white 0px 2px, white 2px 0px, white -2px 0px, white 0px -2px, white -1.4px -1.4px, white 1.4px 1.4px, white 1.4px -1.4px, white -1.4px 1.4px;margin-bottom: 5px;"></div><div class="picture" style="height: 30px;width: 30px;margin: auto;"><img src="/guide/examples/images/navigation/pointer.png" style="height: 100%;width: 100%;"></div></div>`;$('#div3d').append($(html));}/*** 创建元素*/function createElement(id) {var srcElem = document.getElementById('board');var newElem = srcElem.cloneNode(true);newElem.style.display = "block";newElem.setAttribute("id", id);app.domElement.insertBefore(newElem, srcElem);return newElem;}/*** 生成一个新面板*/function createUIAnchor(type, obj, value) {if (type == 'ui') {// 创建UIAnchorvar ui = app.create({type: 'UIAnchor',parent: obj,element: createElement(obj.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});if (!value) value = obj.name;$('#' + obj.id + ' .text').text(value);} else if (type == 'text') {// 创建文本var areaTxt = app.create({type: 'TextRegion',id: 'areaTxt_' + obj.id,parent: obj,localPosition: [0, 3.8, 0],text: value,inheritStyle: false,style: {fontColor: '#ff0000',fontSize: 20, // 文本字号大小}});areaTxt.rotateX(-90); // 旋转文本}}// 监听进入楼层事件app.on(THING.EventType.EnterLevel, '.Floor', function (ev) {init();if (ev.current.name == '办公楼一层') {$("input[type='button']").show();} else {$("input[type='button']").hide();}}, '进入楼层显示面板')// 监听退出楼层事件app.on(THING.EventType.LeaveLevel, '.Floor', function (ev) {init();$("input[type='button']").hide();}, '退出楼层隐藏面板')
其效果如下:
6.2.7 场景层级控制示例
下面来分析一个官方给出的场景层级控制示例:
/*** 说明:自定义层级切换效果* 功能:*1.进入建筑层级摊开楼层*2.进入楼层层级更换背景图*3.双击物体,播放模型动画* 操作:点击按钮*/// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址background: '#000000',skyBox: 'Night',env: 'Seaside',});// 初始化完成后开启场景层级var campus;app.on('load', function (ev) {campus = ev.campus;// 将层级切换到园区 开启场景层级app.level.change(ev.campus);initThingJsTip("本例程修改了原有进入层级的默认响应,自定义了新的层级响应。点击按钮,查看效果");new THING.widget.Button('修改层级飞行响应', setEnterFly);new THING.widget.Button('修改层级场景响应', setEnterLevel);new THING.widget.Button('修改层级背景', setEnterBack);new THING.widget.Button('重置', reset);});/*** 修改默认的层级飞行响应* 双击进入建筑层级,展开楼层* 退出建筑关闭摊开的楼层*/function setEnterFly() {// 重置reset();initThingJsTip("修改默认进入层级飞行响应,双击进入建筑层级,展开楼层");// 暂停默认退出园区行为app.pauseEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);// 进入建筑摊开楼层app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var previous = ev.previous; // 上一层级ev.current.expandFloors({'time': 1000,'complete': function () {console.log('ExpandFloor complete ');}});}, 'customEnterBuildingOperations');// 进入建筑保留天空盒app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);// 退出建筑关闭摊开的楼层app.on(THING.EventType.LeaveLevel, '.Building', function (ev) {var current = ev.current; // 当前层级ev.object.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});}, 'customLeaveBuildingOperations');}/*** 修改进入层级场景响应* @property {Object} ev 进入物体层级的辅助数据* @property {THING.BaseObject} ev.object 当前层级* @property {THING.BaseObject} ev.current 当前层级* @property {THING.BaseObject} ev.previous 上一层级*/function setEnterLevel() {// 重置reset();initThingJsTip("修改默认进入层级场景响应,双击飞到物体,其他物体渐隐(若物体存在动画,则播放动画)");// 修改进入层级场景响应app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐隐var things = object.brothers.query('.Thing');things.fadeOut();// 尝试播放动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],});}}, 'customEnterThingOperations');// 停止进入物体层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);// 修改退出层级场景响应app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐现var things = object.brothers.query('.Thing');things.fadeIn();// 反播动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],reverse: true});}}, 'customLeaveThingOperations');}/*** 进入楼层设置背景* 进入楼层层级,修改背景*/function setEnterBack() {// 重置reset();initThingJsTip("修改默认进入层级背景,进入楼层层级,修改背景");// 进入楼层设置背景app.on(THING.EventType.EnterLevel, '.Floor', function (ev) {var previous = ev.previous; // 上一层级// 从建筑进入楼层时if (previous instanceof THING.Building) {app.background = '/uploads/wechat/emhhbmd4aWFuZw==/file/img/bg_grid.png';}}, 'setFloorBackground');// 停止进入楼层层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);// 退出楼层设置背景app.on(THING.EventType.LeaveLevel, '.Floor', function (ev) {var current = ev.current; // 当前层级// 从楼层退出到建筑时if (current instanceof THING.Building) {app.background = null;app.skyBox = "Night";}}, 'customLeaveFloorOperations');}/*** 重置* app.resumeEvent 暂停事件* app.off 卸载事件*/function reset() {// 创建提示initThingJsTip('本例程修改了原有进入层级的默认响应,自定义了新的层级响应。点击按钮,查看效果');app.resumeEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);app.off(THING.EventType.EnterLevel, '.Building', 'customEnterBuildingOperations');app.off(THING.EventType.LeaveLevel, '.Building', 'customLeaveBuildingOperations');app.off(THING.EventType.EnterLevel, '.Floor', 'setFloorBackground');app.off(THING.EventType.LeaveLevel, '.Floor', 'customLeaveFloorOperations');app.off(THING.EventType.EnterLevel, '.Thing', 'customEnterThingOperations');app.off(THING.EventType.LeaveLevel, '.Thing', 'customLeaveThingOperations');var curLevel = app.level.current; // 当前层级app.skyBox = 'Night'; // 设置天空盒if (curLevel instanceof THING.Building) {curLevel.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});}app.level.change(campus);}
其效果如下:
6.3 对象控制
本小节说的 对象 是指 场景 中的 某个物体。
6.3.1 对象的增删查
6.3.1.1 创建对象的类型
6.3.1.2 创建和删除对象的 API
6.3.1.3 示例
【例子】使用 create() 方法 创建一个 类型为 Box 的对象
// 加载场景代码 var app = new THING.App({url: '/api/scene/a5bb1ed8259a2b023ae317d3'});// 默认创建一个长宽高为 1m ,轴心点在中心的正方体var box= app.create({type:'Box',position: [0, 0, 0] //世界坐标系下的位置});// 创建正方体参数设置var box = app.create({type: 'Box',width: 10,// 宽度height: 10,// 高度depth: 10,// 深度center: 'Bottom',// 轴心//widthSegments: 1,// 宽度上的节数//heightSegments: 1,// 高度上的节数//depthSegments: 1,// 深度上的节数position:[0,0,0]// 世界坐标系下的位置});// 创建提示initThingJsTip(`这是一个 Box 的示例`);
6.3.2 对象效果设置
6.3.2.1 基础效果常用的 API
6.3.2.2 BaseStyle 类成员
BaseStyle 类是 ThingJS 提供的物体样式基类,物体对象中包含了style
属性是该类的实例,可以通过改变style
不同属性的属性值来实现对物体样式的控制。
(1)设置物体是否始终在最前端渲染显示
(2)显示/隐藏物体包围盒
(3)设置包围盒颜色
(4)设置/获取物体颜色
可填写 十六进制颜色值 或 rgb 字符串,取消颜色设置为 null
// 使用十六进制颜色obj.style.color = '#ff0000';// 使用 rgb 颜色obj.style.color = 'rgb(255,0,0)';// 取消颜色obj.style.color = null;
(5)设置双面渲染
(6)设置/获取材质自发光颜色
obj.style.emissive = '#ffff00';
(7)设置/获取材质自发光滚动贴图
obj.style.emissiveScrollImage = '/static/images/avatar.png';
(8)设置/获取反射贴图
obj.style.environmentImage = 'BlueSky';
(9)设置/获取高亮颜色
默认值为 null。
obj.style.highlight = '#ffff00';
(10)设置/获取高亮强度
默认为0.5。设置为null,则等效于恢复到0.5。 如果高亮颜色为null,则该属性没有实际效果。
obj.style.highlightIntensity = 0.8;
(11)设置贴图 填写图片资源路径 或 image 对象
// 使用图片路径obj.style.image = '/static/images/avatar.png';
(12)材质金属度系数
(13)设置/获取物体不透明度
0 为全透明,1为不透明。
obj.style.opacity = 0.8;
(14)设置/获取物体勾边颜色
// 使用十六进制颜色obj.style.outlineColor = '#ff0000';// 使用 rgb 颜色obj.style.outlineColor = 'rgb(255,0,0)';// 取消勾边颜色obj.style.outlineColor = null;
(15)设置/获取渲染排序值
数值越小越先渲染,默认值为 0
(16)设置材质粗糙度系数
(17)开启/禁用勾边
(18)开启/关闭线框模式
6.3.2.3 实例
// 加载场景代码 var app = new THING.App({url: '/api/scene/a5bb1ed8259a2b023ae317d3'});// 默认创建一个长宽高为 1m ,轴心点在中心的正方体var box= app.create({type:'Box',position: [0, 0, 0] //世界坐标系下的位置});// 创建正方体参数设置var box = app.create({type: 'Box',width: 10,// 宽度height: 10,// 高度depth: 10,// 深度center: 'Bottom',// 轴心position:[0,0,0]// 世界坐标系下的位置});// 先获取上面的盒子对象var o = app.query(".Box");// 指定其颜色o.style.color = "#F400EE";// 指定其透明度o.style.opacity = 0.6;// 开启线框模式o.style.wireframe = true;
其效果如下:
6.4 事件绑定
ThingJS 内置事件包含:鼠标点击、键盘输入、层级变化。其中层级变化相关的事件已经在 6.2.3 层级事件 小节中介绍过了。
6.4.1 事件的全局绑定
通过app.on
可以绑定全局事件,例如:
// 不添加任何条件,鼠标点击即可触发app.on("click", function(ev){console.log("clicked!");})// 添加指定条件,这里表示只有 Thing 类型物体才会触发app.on("click", ".Thing", function(ev){console.log(ev.object.id+" clicked!");})// 添加多重条件,对建筑、楼层触发点击事件app.on("click", ".Building || .Floor",function(ev){console.log("You clicked "+ev.object.id);})
6.4.2 事件的局部绑定
事件的局部绑定针对一个 对象 或 Selector 集合,通过接口绑定:
// 当这个对象被点击,即可触发obj.on("click", function(ev){console.log(ev.object.name);});// 添加特定条件,当这个对象为 marker 类型,或带有 marker 类型的子孙,即可触发obj.on("click", ".Marker", function(ev){console.log(ev.object.name);});// 查询到 obj 下的所有 marker 物体(集合),注册 click 事件obj.query(".Marker").on("click", function(ev){console.log(ev.object.name);})
6.4.3 内核事件EventType 属性
【Tip】:
在低代码开发界面,你可以打开开发者工具,将 JavaScript 上下文选择为 ,然后通过控制台的交互式输入:
THING.EventType
如图:
可以看到能够查看到当前 EventType 的所有属性值:
{"Complete": "complete","Resize": "resize","Update": "update","Progress": "progress","Load": "load","Unload": "unload","Click": "click","DBLClick": "dblclick","SingleClick": "singleclick","MouseUp": "mouseup","MouseDown": "mousedown","MouseMove": "mousemove","MouseWheel": "mousewheel","MouseEnter": "mouseenter","MouseOver": "mouseover","MouseLeave": "mouseleave","DragStart": "dragstart","Drag": "drag","DragEnd": "dragend","KeyDown": "keydown","KeyPress": "keypress","KeyUp": "keyup","CameraChangeStart": "camerachangestart","CameraChangeEnd": "camerachangeend","CameraChange": "camerachange","CameraZoom": "camerazoom","CameraViewChange": "cameraviewchange","Create": "create","Destroy": "destroy","Expand": "expand","Unexpand": "unexpand","Select": "select","Deselect": "deselect","SelectionChange": "selectionchange","LevelChange": "levelchange","EnterLevel": "enterLevel","LeaveLevel": "leaveLevel","LevelFlyEnd": "levelflyend","AppComplete": "complete","LoadCampusProgress": "progress","LoadCampus": "load","UnloadCampus": "unload","PickObject": "pick","Dragging": "drag","CreateObject": "create","DestroyObject": "destroy","ExpandBuilding": "expand","UnexpandBuilding": "unexpand","SelectObject": "select","DeselectObject": "deselect","ObjectSelectionChanged": "selectionchange","PickedObjectChanged": "pickchange","BeforeLevelChange": "beforelevelchange","Pick": "pick","Unpick": "unpick","PickChange": "pickchange","AreaPickStart": "areapickstart","AreaPicking": "areapicking","AreaPickEnd": "areapickend","BeforeLoad": "beforeload"}
6.4.4 事件的暂停和恢复
// 注册事件app.on("click", ".Building", function(event){console.log("clicked!")},"tag1");// 暂停事件app.pauseEvent("click", ".Building", "tag1");// 恢复事件app.resumeEvent("click", ".Building", "tag1");
卸载/暂停/恢复 事件时第二个参数必须传条件,如果没有条件,又需要传 tag,将条件传 null 。
6.4.5 事件的卸载
// 注册事件app.on("click", function(event){console.log("clicked!")});// 卸载注册的事件app.off("click");// 带 tag 的事件app.on("click", ".Building", function(event){console.log("clicked!")});// 卸载所有 Building 的点击事件app.off("click", ".Building");// 只卸载带有该 tag 名的 Building 的点击事件app.off("click", ".Building", "tagName");
6.4.6 自定义事件
const truck = app.query('thing01')[0];// 监听自定义事件truck.on('customAlarm', (ev)=>{truck.style.color = 'red';})// 触发自定义事件 triggertruck.trigger('customAlarm', {alarm: true})
6.4.7 实例
6.5 视角(摄影机)
6.5.1 摄像机的基本概念
在 3D 开发中,摄像机指的是用来确定观察 3D 场景的视角,它包含两个重要的位置参数:
镜头位置(position)目标点(target)
6.5.2 ThingJS 中常用的 摄像机 API
6.5.2.1 官方案例解析 - 飞行控制
这部分案例的效果如下:
代码如下:
// 加载场景代码 var app = new THING.App({url: '/static/models/factory', // 场景地址skyBox: 'Night',env: 'Seaside',});// 定义全局变量var car;var car02// 加载场景后执行app.on('load', function () {// 通过 name 查询到场景中的车car = app.query('car01')[0];car02 = app.query('car02')[0];initThingJsTip("摄像机,如同大家拍照时使用的相机,用来确定观察 3D 场景的视角。</br>点击左侧按钮,体验设置场景视角,控制视角飞行效果");new THING.widget.Button('直接设置', set_camera);new THING.widget.Button('聚焦物体', fit_camera);new THING.widget.Button('飞到位置', flytoPos);new THING.widget.Button('环绕物体', rotate_around_obj);new THING.widget.Button('飞到物体左侧', flytoLeft);new THING.widget.Button('摄像机跟随物体', follow);new THING.widget.Button('跟随停止', followStop);new THING.widget.Button('重置', resetFly);})/*** 直接设置*/function set_camera() {initThingJsTip('直接设置摄像机的 position 和 target 属性控制相机位置');// 设置摄像机位置和目标点// 可利用 代码块——>摄像机——>设置位置快捷设置视角,也可以通过 app.camera.log() 获取app.camera.position = [-35.22051687793129, 53.18080934656332, 45.681456895731266];app.camera.target = [-2.945566289024588, 5.527822798932595, -11.021841570308316];}/*** 聚焦物体*/function fit_camera() {initThingJsTip('摄像机镜头“聚焦”到叉车,此时 ThingJS 会计算出该对象的“最佳看点”,从而“自适应”该对象来设置摄像机位置');app.camera.fit(car02);}/*** 飞到位置*/function flytoPos() {initThingJsTip('设置摄像机从当前位置,飞行到将要设置的位置');// 可直接利用 代码块——>摄像机——>飞到位置app.camera.flyTo({'position': [-9.31507492453225, 38.4538617032, 49.00948473033884],'target': [3.2145825289759062, 5.6950465199837375, -17.48975213256405],'time': 1000,'complete': function () {}});}/*** 环绕物体,围绕car在5秒内旋转360度*/function rotate_around_obj() {reset();initThingJsTip('设置摄像机绕车辆旋转360度');// 设置摄像机位置和目标点app.camera.position = [27.896481963404188, 10.436433735762211, 15.260481901440052];app.camera.target = [21.352, 1.1811385844099112, 8.715999938035866];app.camera.rotateAround({object: car,yRotateAngle: 360,time: 5000,});}/*** 飞到物体左侧* 可调节 xAngle、yAngle 设置相对飞行目标的摄像机位置* 可根据 radiusFactor 设置相对飞行目标的距离(物体包围盒半径倍数)*/function flytoLeft() {reset();initThingJsTip('设置摄像机飞到物体左侧');app.camera.flyTo({object: car02,xAngle: 0, // 绕物体自身X轴旋转角度yAngle: 90, // 绕物体自身Y轴旋转角度radiusFactor: 2, // 物体包围盒半径的倍数time: 1 * 1000,complete: function () {}});}/*** 摄像机跟随*/function follow() {initThingJsTip('设置摄像机跟随小车')// 世界坐标系下坐标点构成的数组 关于坐标的获取 可利用「工具」——>「拾取场景坐标」// 拐角处多取一个点,用于转向插值计算时更平滑var path = [[0, 0, 0], [2, 0, 0], [20, 0, 0], [20, 0, 2], [20, 0, 10], [18, 0, 10], [0, 0, 10], [0, 0, 8], [0, 0, 0]];car.position = path[0];car.movePath({path: path,orientToPath: true,loopType: THING.LoopType.Repeat,time: 10 * 1000})// 每一帧设置摄像机位置和目标点car.on('update', function () {// 摄像机位置为移动小车后上方// 为了便于计算,这里用了坐标转换,将相对于小车的位置转换为世界坐标app.camera.position = car.selfToWorld([0, 5, -10]);// 摄像机目标点为移动小车的坐标app.camera.target = car.position;}, '自定义摄像机跟随');}/*** 摄像机跟随停止*/function followStop() {initThingJsTip('设置摄像机停止跟随小车')car.off('update', null, '自定义摄像机跟随');}/*** 重置*/function reset() {app.camera.stopFlying();app.camera.stopRotateAround();car.stopMoving();car.position = [18.9440002, 0.009999999999999787, 6.7690000999999995];car.angles = [0, 0, 0];followStop();initThingJsTip('摄像机,如同大家拍照时使用的相机,用来确定观察 3D 场景的视角。</br>点击左侧按钮,体验设置场景视角,控制视角飞行效果')}/*** 初始摄像机视角*/function resetFly() {// 摄像机飞行到某位置app.camera.flyTo({'position': [36.013, 42.67799999999998, 61.72399999999999],'target': [1.646, 7.891, 4.445],'time': 1000,'complete': function () {reset();}});}
6.5.2.2 官方案例解析 - 控制交互
这部分案例的效果如下:
代码如下:
/*** 说明:摄像机操作控制* 功能:*1.2D/3D 切换*2.摄像机水平、垂直移动*3.摄像机前后推进*4.摄像机旋转*5.鼠标控制摄像机旋转、平移、缩放*6.限制摄像机俯仰、水平范围* 难度:★★☆☆☆*/var app = new THING.App({url: '/static/models/factory',skyBox: 'Night',env: 'Seaside',});// 加载完成事件 app.on('load', function (ev) {initThingJsTip("本例程展示了摄像机交互控制,点击按钮,查看效果");// 2D/3D 切换new THING.widget.Button('2D/3D 切换', function () {var viewMode = app.camera.viewMode;if (viewMode == "normal") {initThingJsTip("已切换至2D视图");app.camera.viewMode = THING.CameraView.TopView; // 切换为2D视图} else {initThingJsTip("已切换至3D视图");app.camera.viewMode = THING.CameraView.Normal; // 默认为3D视图}});// 摄像机移动new THING.widget.Button('摄像机移动', function () {initThingJsTip("摄像机移动水平移动5m");app.camera.move(5, 0); // 设置移动距离(水平移动, 垂直移动),正负代表方向});// 摄像机推进new THING.widget.Button('摄像机推进', function () {initThingJsTip("摄像机向前推进10m");app.camera.zoom(10); // 设置推进距离,正负代表方向});// 摄像机旋转new THING.widget.Button('摄像机旋转', function () {initThingJsTip("摄像机同时环绕 Y 轴、X 轴旋转10度");app.camera.rotateAround({target: app.camera.target,yRotateAngle: 10, // 环绕Y轴旋转角度(俯仰面(竖直面)内的角度)xRotateAngle: 10, // 环绕X轴旋转角度(方位面(水平面)内的角度)time: 1000 // 环绕飞行的时间});});// 禁用/启用 左键旋转new THING.widget.Button('禁用旋转', function () {initThingJsTip("禁用鼠标左键旋转");app.camera.enableRotate = false; // 禁用旋转});// 禁用/启用 右键平移new THING.widget.Button('禁用平移', function () {initThingJsTip("禁用鼠标右键平移");app.camera.enablePan = false; // 禁用平移});// 禁用/启用 滚轮缩放new THING.widget.Button('禁用缩放', function () {initThingJsTip("禁用鼠标滚轮缩放");app.camera.enableZoom = false; // 禁用缩放});// 限制摄像机俯仰范围new THING.widget.Button('限制俯仰范围', function () {reset();initThingJsTip("设置摄像机俯仰角度范围[0, 90],上下移动鼠标查看效果");app.camera.xAngleLimitRange = [0, 90]; // 设置摄像机俯仰角度范围[最小值, 最大值]});// 限制摄像机水平范围new THING.widget.Button('限制水平范围', function () {reset();initThingJsTip("设置摄像机水平角度范围[30, 60],左右移动鼠标查看效果");app.camera.yAngleLimitRange = [30, 60]; // 设置摄像机水平角度范围[最小值, 最大值]});// 重置new THING.widget.Button('重置', function () {resetFly();});});/*** 重置*/function reset() {initThingJsTip("本例程展示了摄像机交互控制,点击按钮,查看效果");app.camera.viewMode = THING.CameraView.Normal; // 默认为3D视图app.camera.enableRotate = true; // 启用旋转app.camera.enablePan = true; // 启用平移app.camera.enableZoom = true; // 启用缩放app.camera.xAngleLimitRange = [-90, 90]; // 设置摄像机俯仰角度范围[最小值, 最大值]app.camera.yAngleLimitRange = [-360, 360]; // 设置摄像机水平角度范围[最小值, 最大值]}/*** 重置摄像机视角*/function resetFly() {// 摄像机飞行到某位置app.camera.flyTo({'position': [36.013, 42.67799999999998, 61.72399999999999],'target': [1.646, 7.891, 4.445],'time': 1000,'complete': function () {reset()}});}
6.5.2.3 官方案例解析 - 控制地球相机
这部分案例的效果如下:
代码如下:
/*** 说明:地球上摄像机常用方法* 功能:*1.飞到物体*2.摄像机水平旋转、停止*3.限制俯仰范围*4.限制摄像机距离*5.取消摄像机限制* 难度:★★☆☆☆*/var app = new THING.App();app.background = [0, 0, 0];// 引用地图组件脚本THING.Utils.dynamicLoad(['/uearth/uearth.min.js'], function() {// 新建一个地图var map = app.create({type: 'Map',style: {night: false},attribution: '高德'});// 新建一个瓦片图层var tileLayer = app.create({type: 'TileLayer',name: 'tileLayer1',maximumLevel: 18,url: 'https://webst0{1,2,3,4}./appmaptile?style=6&x={x}&y={y}&z={z}',});// 将瓦片图层添加到map中map.addLayer(tileLayer);// 园区的经纬度坐标(GCJ_02坐标系)var sceneLonlat = [116.4641, 39.98606];// 将园区的经纬度坐标转为三维坐标,第二个参数代表离地高度var position = CMAP.Util.convertLonlatToWorld(sceneLonlat, 0.5);// 计算园区在地球上的旋转角度,第二个参数可以调整,对园区在地球表面进行旋转var angles = CMAP.Util.getAnglesFromLonlat(sceneLonlat, 220);// 摄像机飞到指定的地理位置和指定高度app.camera.earthFlyTo({time: 3000, // 飞行时间 mslonlat: sceneLonlat, // 要飞到的目标点的经纬度height: 200, // 摄像机离地高度heading: 0, // 水平角(方位角) 单位度pitch: 45, // 垂直角(俯仰角) 单位度complete: function() {// 创建Campusvar campus = app.create({type: 'Campus',name: '建筑',url: '/static/models/storehouse', // 园区地址position: position, // 位置angles: angles, // 旋转complete: function() {// 创建成功以后执行函数initThingJsTip("本例程展示了地图摄像机的交互控制,地图交互默认为左键移动,右键旋转。与园区中不同的是地图上使用经纬度控制相机位置,在使用时需要注意。点击按钮,查看效果");// 加载场景后执行app.level.change(campus);// 添加Buttonlet car = app.query('car01')[0];// 旋转new THING.widget.Button('水平旋转', function() {initThingJsTip("设置摄像机水平旋转");reset();//地球上使用rotateAround需要加isEarth参数app.camera.rotateAround({target: app.camera.target,isEarth: true,yRotateAngle: 360,time: 5000});});new THING.widget.Button('飞到物体', function() {initThingJsTip("设置摄像机飞到物体");reset();app.camera.flyTo({object: car, // 飞行到的对象time: 3000, //飞行时间isEarth: true //地球上使用flyTo需要加isEarth参数});});new THING.widget.Button('限制俯仰范围', function() {initThingJsTip("设置摄像机俯仰角度范围[10, 40],按住鼠标右键上下移动查看效果");reset();app.camera.xAngleLimitRange = [10, 40]; // 设置摄像机俯仰角度范围[最小值, 最大值]});new THING.widget.Button('设置摄像机距离', function() {initThingJsTip("设置摄像机距离范围,可以通过鼠标滚轮滚动查看效果");reset();app.camera.distanceLimited = [30, 200]; // 设置摄像机距离范围});new THING.widget.Button('重置', function() {reset()initThingJsTip("本例程展示了地图摄像机的交互控制,地图交互默认为左键移动,右键旋转。与园区中不同的是地图上使用经纬度控制相机位置,在使用时需要注意。点击按钮,查看效果");});}});}});});/*** 重置*/function reset() {// 设置摄像机位置和目标点app.camera.position = [2177786.3907650434, 4098473.8561936556, 4374825.365330011];app.camera.target = [2177757.9857225236, 4098500.159908491, 4374763.90281313];app.camera.stopRotateAround({isEarth: true }); //地球上使用stopRotateAround需要加isEarth参数app.camera.stopFlying();app.camera.distanceLimited = [0, 1e10]; // 设置摄像机水平角度范围[最小值, 最大值] app.camera.xAngleLimitRange = [0, 90]; // 设置摄像机俯仰角度范围[最小值, 最大值]}