优锘科技&学堂在线 - 数字孪生可视化开发技术训练营学习笔记
1. 数字孪生的相关概念
ThingJS 与传统3D开发的区别
项目 | 传统3D开发 | ThingJS |
人员配备 | 需招募并长期保有专业3D开发团队,成本高,管理难度大 | 现有开发团队可立即上手开发3D可视化应用,无需组建新团队 |
开发效率 | 基于底层引擎开发,开发效率低,升级、维护难度大 | ThingJS比传统3D开发提升10倍以上开发效率,维护简单 |
3D场景制作 | 需招募或外包3D场景制作,成本高,交付时间长,不易修改 | 基于CampusBuilder 和 CityBuilder,非专业人员即可快速生成并修改3D场景 |
3D模型制作 | 需专业3D建模人员制作模型,增加成本和交付时间 | ThingDepot 为物联网管理场景提供包括上万种3D模型的模型库 |
系统部署 | 安装调试复杂,部署和维护的成本很高 | 提供共有云服务和私有云部署,开发完成后立即运行,降低成本,提升交付效率 |
2. ThingJS生态体系
工具 / 服务 | 描述 |
CampusBuilder |
3D 园区搭建工具:可快速搭建园区级别 3D 可视化场景。 |
CityBuilder |
3D 城市搭建工具:可快速搭建城市级别 3D 可视化场景。 |
ChartBuilder |
图表制作工具:用于项目数据可视化。 |
ThingJS 在线开发 | 了解 ThingJS 在线开发平台的界面和使用方法。 |
ThingJS 离线开发网络版 | 了解 ThingJS 离线开发网络版的使用方法。 |
ThingJS API | 强大的应用编程接口,轻松开发您的可视化项目应用。 |
3. 搭建园区
CampusBuilder(模模搭)是ThingJS体系内的3D园区场景搭建的客户端工具,如果你不想安装客户端,也可以在线使用 森园区(相当于 CampusBuilder 的网页版)搭建你的园区。在本文中,两种方式我们都会进行介绍。以下是我们本节实战小节搭建的效果:
3.1 CampusBuilder 客户端的安装和启动
登录完成后,你将看到如下界面:
3.2 CampusBuilder 的在线使用入口
你也可以不下载客户端而直接使用在线开发方式。在浏览器中进入 森园区 页面:https://studio.thingjs.com/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场景/拓扑拖入森大屏,实现图表等指标数据与三维场景/拓扑进行联动交互。
在 森工厂 中选择 大屏 或者直接在地址栏中输入地址 https://studio.thingjs.com/ui 可以进入森大屏主页:
点击 新建大屏 即可开启你的大屏构建:
5.2 拖拽现有大屏布局模板
进入 森大屏 后,在左侧面板 布局 下的 大屏模板 处可以看到有很多现成的大屏模板。我们可以选择其中的一个,使用鼠标左键点击并拖至右侧举行区域中松开,例如:
松开鼠标后,被托选的大屏将在这个举行区域中展示出来:
快捷键:
类型 | Windows用户 | Mac用户 |
保存 | Ctrl+S | ⌘S |
打开 | Ctrl+O | ⌘O |
重命名 | Ctrl+Shift+R | ⇧⌘R |
撤销 | Ctrl+Z | ⌘Z |
恢复 | Ctrl+Y | ⌘Y |
复制 | Ctrl+C | ⌘C |
粘贴 | Ctrl+V | ⌘V |
左侧面板 | Ctrl+1 | ⌘1 |
右侧面板 | Ctrl+2 | ⌘2 |
大纲 | Ctrl+3 | ⌘3 |
放大 | Ctrl+= | ⌘+ |
缩小 | Ctrl± | ⌘- |
放缩至100% | Ctrl+0 | ⌘0 |
置顶 | Ctrl+Shift+] | ⌥⌘] |
置底 | Ctrl+Shift+[ | ⌥⌘[ |
上移一层 | Ctrl+] | ⌘] |
下移一层 | Ctrl+[ | ⌘[ |
左对齐 | Alt+A | ⌥A |
右对齐 | Alt+D | ⌥D |
顶对齐 | Alt+W | ⌥W |
底对齐 | Alt+S | ⌥S |
水平对齐 | Alt+H | ⌥H |
垂直对齐 | Alt+V | ⌥V |
锁定 | Ctrl+Shift+L | ⇧⌘L |
打组 | Ctrl+G | ⌘G |
解除打组 | Ctrl+Shift+G | ⇧⌘G |
运行 | F5 | F5 |
5.3 自己搭建大屏布局
图表资源的布局方法包含资源移动、资源缩放、资源对齐、资源打组、取消打组、资源锁定、资源隐藏、资源复制、资源升级、资源图层位置移动、资源删除等,您可以通过这些方法快捷地进行图表资源布局。
5.4 画布中的图层
5.4.1 介绍
画布中新建 多个图层 来展示不同的业务场景,并将主场景设置为 常显,设置为常显后主场景在每个图层均显示,方便您根据主场景来搭建不同的业务场景。
如图所示:
- 【新建图层】在画布左下方单击“”图标新建图层,新建的图层展示在已有图层后面;
- 【常显图层】选择要设置为常显的图层,单击“”图标打开图层的菜单,选择常显选项,即可将当前图层设置为常显图层;
- 孪生资源拖入大屏后(大屏场景中仅支持拖入一个孪生资源),系统自动将展示资源的图层设置为3D图层,并将图层置为常显状态。
- 【图层顺序拖动】拖动图层名称可移动图层的前后位置;
- 【图层重命名】单击“”图标打开图层的菜单,选择 重命名 选项,可以对图层进行重命名;
- 【图层删除】单击图标“”打开图层的菜单,选择 删除 按钮即可删除图层;
- 【图层复制】针对需要复制的图层,单击图标“”打开图层的菜单,选择复制按钮对图层进行复制。单击菜单栏的编辑>粘贴或者使用快捷键Ctrl+V即可粘贴复制的图层。粘贴的图层展示在已有图层后面。
- 3D图层不支持复制操作
5.4.2 小案例
先拖入一个孪生体 3D 图层:
可以看到这个图层有一个 锁定的符号,即,表明它是 常显图层。我们希望这个图层显示在最下方,也就是最底层。在其上面显示各种数据面板。
于是我们从 布局 中的 大屏模板 中,选择一个模板,作为第二个图层,这个图层在孪生体图层的上面:
大屏模板 图层在上面,由于它有一张自带的背景图,这个图似乎是不透明的,将下面的图层给 遮挡 住了。因此我们需要将该大屏模板用到的 背景图删除或者设置为隐藏。
点击 大纲,打开该 大屏模板的资源目录:
将鼠标逐个移动到列表中的资源项上,右侧将显示对应资源的控制图标,点击 图中 圈出控制图标,可以让相应的资源进行隐藏/显示:
最终,我们隐藏了背景图,就是这样的效果了:
5.5 森大屏资源
5.5.1 资源类型
类型 | 描述 |
孪生 | 包含园区、城市、拓扑、低代码以及零代码孪生资源,我的页签展示您在森园区、森城市、森拓扑、低代码平台以及零代码平台搭建的孪生资源,官方页签展示官方提供的园区、城市、拓扑、低代码以及零代码孪生资源,默认展示您搭建的孪生资源。 |
布局 | 包含官方提供的多个大屏模板和布局模板。 |
图表 | 包含官方提供的柱状图、条形图、折线图、曲线图、面积图、饼环图、散点图、雷达图、关系图以及其他类型的图表资源;全部展示官方提供的全部图表资源。 |
文表 | 包含官方提供的通用标题、业务指标趋势、多行文本、状态卡片、时间器、跑马灯、倒计时、时间轴、进度条、键值表格、进度条表格、轮播列表柱状图以及轮播列表等文表资源。 |
控件 | 包含官方提供的iframe、轮播页面、图片、图标、形状、视频、音频、下拉框选择器、单选框、时间选择器、地理搜索框、Tab列表、输入框、全屏切换、开关以及按钮等控件资源。 |
素材 | 包含官方提供的形状、背景图以及背景框等素材资源。 |
其他 | 包含官方提供的接数组件资源。 |
主题 | 包含官方提供的主题图表资源以及第三方公司或个人提交的经过审核的主题图表资源,目前包含矩阵革命和极光主题。 |
我的 | 包含您定义开发并发版的图表、另存的图表模板以及导入的布局资源。详情请参见下方我的资源。 |
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 官网https://studio.thingjs.com/lowCode进入其 低代码 模块,点击 新建 ThingJS 项目
浏览器将在新的标签中打开在线开发工具:
可以看到,新打开的这个 ThingJS 已经由如下代码:
/** * 说明:创建App,url为园区地址(可选) * 使用App创建打开的三维空间我们称之为“场景”(scene)。场景包含地球、园区、模型等。 * 创建App时,传入的url就是园区的地址,不传url则创建一个空的场景。园区可在CampusBuilder * 中创建编辑,有两种方法可以将园区添加到线上资源面板,方法如下: * 1. 园区保存后,会自动同步到网页同一账号下 * 2. 园区保存后,导出tjs文件,在园区资源面板上传 * 上面两种方式生成的园区资源均可在资源面板中双击自动生成脚本 * 难度:★☆☆☆☆ */ // 加载场景代码 var app = new THING.App({ url: 'https://www.thingjs.com/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: 'https://www.thingjs.com/static/models/factory', // 场景地址 background: '#000000', env: 'Seaside', });
其中:
- url 指的是园区场景的一个地址,如这里为魔门提供的一个示例园区的地址:
https://www.thingjs.com/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: 'https://www.thingjs.com/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: 'https://www.thingjs.com/static/models/factory', complete(ev){ app.level.change(ev.object) } })
同样的方式,当我们再次调用 create()
方法时,就可以创建第二个、第三个园区。如果创建了多个场景,那可以通过一个按钮(面板),来切换当前看到的场景,例如这个是一个官方的案例:
/** * 说明:通过动态加载场景 动态加载建筑里的楼层 * 操作:双击建筑,动态加载场景 */ var dataObj = { progress: 0 }; // 场景加载进度条数据对象 var loadingPanel; // 进度条界面组件 var curCampus; // 配置相应建筑的园区场景url var campusUrl = [{ name: "园区A", url: "https://www.thingjs.com/./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: "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%A4%96" }]; var buildingConfig = { '商业A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AA%E6%A5%BC%E5%B1%82%E7%BA%A7', '商业B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AB%E6%A5%BC%E5%B1%82%E7%BA%A7', '商业C楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7', '商业D楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AD%E6%A5%BC%E5%B1%82%E7%BA%A7', '商业E楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AE%E6%A5%BC%E5%B1%82%E7%BA%A7', '住宅A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7', '住宅B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7', '图书馆': 'https://www.thingjs.com/./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": "https://www.thingjs.com/./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; // 当前园区url if (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', // 根据不同的建筑,传入园区相应的url url: 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('切换场景', ()=> { // 当前园区url var 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; // 将新园区赋给全局变量 curCampus curCampus.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: "https://www.thingjs.com/./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: "https://www.thingjs.com/./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: 'https://www.thingjs.com/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地址 // 也可直接使用能访问的网络图片url new THING.widget.Button('设置背景图片1', function () { app.background = 'https://www.thingjs.com/static/images/background_img_01.png'; }) new THING.widget.Button('设置背景图片2', function () { app.background = 'https://www.thingjs.com/static/images/background_img_02.png'; }) // 清除背景效果 new THING.widget.Button('清除背景', function () { app.skyBox = null; app.background = null; })
其效果如下:
(2)设置聚光灯
来看一个通过面板来设置聚光灯参数的官方示例:
// 加载场景代码 var app = new THING.App({ url: 'https://www.thingjs.com/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 中的层级关系可以用一个树状结构来表示,即所谓 层级关系树:
这里一般来说主要关注 园区、 建筑、 楼层、 房间之间的关系,图片描述比较形象,这里不做过多解释。
API | 类型 | 描述 |
app.level |
SceneLevel | 获取场景层次管理器 |
6.2.1 层级获取
API | 描述 |
app.level.current |
获取当前的层级对象 |
app.level.previos |
获取之前的层级对象 |
6.2.2 层级切换
API | 描述 |
app.level.change(object) |
将层级切换到指定的对象 |
app.level.back() |
返回当前层级的父物体层级 |
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: 'https://www.thingjs.com/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: 'https://www.thingjs.com/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": "https://www.thingjs.com/./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') { // 创建UIAnchor var 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: 'https://www.thingjs.com/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 创建对象的类型
类型 | 描述 |
Thing |
模型物体 |
Box 、Sphere 、Plane 、Cylinder 、Tetrahedron |
基本形体 |
Campus |
园区 |
UIAnchor 、Marker 、WebView |
界面 |
ParticleSystem |
粒子 |
Line 、RouteLine |
线 |
Heatmap |
热力图 |
拓展 ThingJS 类 | 自定义类 |
6.3.1.2 创建和删除对象的 API
API | 描述 |
app.create() |
创建对象 |
obj.destory() |
删除对象 |
obj.destoryAll() |
删除对象集合 |
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
API 用例 | 描述 | 说明 |
obj.style.color = 'red' |
设置颜色 | 值为颜色名字符串或颜色值 |
obj.style.color = null |
清除颜色 | |
obj.style.opacity = 0.5 |
设置透明度 | 透明度的值可以是 0~1 |
obj.style.outlineColor = 'green' |
设置勾边 | 值为颜色名字符串或颜色值 |
obj.style.outlineColor = null |
取消勾边 | |
obj.style.glow = true |
开启外发光 | |
obj.style.innerGlow = true |
开启内发光 | 可与 color 属性搭配使用 |
obj.style.image = '***.jpg' |
设置贴图 | |
obj.scale = [2, 2, 2] |
设置缩放值 | |
obj.size = [2, 2, 2] |
6.3.2.2 BaseStyle 类成员
BaseStyle 类是 ThingJS 提供的物体样式基类,物体对象中包含了style
属性是该类的实例,可以通过改变style
不同属性的属性值来实现对物体样式的控制。
(1)设置物体是否始终在最前端渲染显示
API | 类型 |
alwaysOnTop |
Boolean |
(2)显示/隐藏物体包围盒
API | 类型 |
boundingBox |
Boolean |
(3)设置包围盒颜色
API | 类型 |
boundingBoxColor |
Number | String |
(4)设置/获取物体颜色
API | 类型 |
color |
String|Number |
可填写 十六进制颜色值 或 rgb 字符串,取消颜色设置为 null
// 使用十六进制颜色 obj.style.color = '#ff0000'; // 使用 rgb 颜色 obj.style.color = 'rgb(255,0,0)'; // 取消颜色 obj.style.color = null;
(5)设置双面渲染
API | 类型 |
doubleSide |
Boolean |
(6)设置/获取材质自发光颜色
API | 类型 |
emissive |
String | Number |
obj.style.emissive = '#ffff00';
(7)设置/获取材质自发光滚动贴图
API | 类型 |
emissiveScrollImage |
String |
obj.style.emissiveScrollImage = 'https://www.thingjs.com/static/images/avatar.png';
(8)设置/获取反射贴图
API | 类型 |
environmentImage |
String|Array |
obj.style.environmentImage = 'BlueSky';
(9)设置/获取高亮颜色
默认值为 null。
API | 类型 |
highlight |
String|Number |
obj.style.highlight = '#ffff00';
(10)设置/获取高亮强度
默认为0.5。设置为null,则等效于恢复到0.5。 如果高亮颜色为null,则该属性没有实际效果。
API | 类型 |
highlightIntensity |
Number |
obj.style.highlightIntensity = 0.8;
(11)设置贴图 填写图片资源路径 或 image 对象
API | 类型 |
image |
String | Object |
// 使用图片路径 obj.style.image = 'https://www.thingjs.com/static/images/avatar.png';
(12)材质金属度系数
API | 类型 |
metalness |
Number |
(13)设置/获取物体不透明度
0 为全透明,1为不透明。
API | 类型 |
opacity |
Number |
obj.style.opacity = 0.8;
(14)设置/获取物体勾边颜色
API | 类型 |
outlineColor |
Number|String |
// 使用十六进制颜色 obj.style.outlineColor = '#ff0000'; // 使用 rgb 颜色 obj.style.outlineColor = 'rgb(255,0,0)'; // 取消勾边颜色 obj.style.outlineColor = null;
(15)设置/获取渲染排序值
数值越小越先渲染,默认值为 0
API | 类型 |
renderOrder |
Number |
(16)设置材质粗糙度系数
API | 类型 |
roughness |
Number |
(17)开启/禁用勾边
API | 类型 |
skipOutline |
Boolean |
(18)开启/关闭线框模式
API | 类型 |
wireframe | Boolean |
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 属性
名称 | 类型 | 默认值 | 描述 |
Complete | string | complete | 通知系统初始化完成 或 物体完成加载 |
Resize | string | resize | 通知窗口大小变化(width, height) |
Update | string | update | 通知每帧更新 |
Progress | string | progress | 通知场景资源加载进度 |
Load | string | load | 通知 App 初始化完成 或 场景、物体加载完成 |
Unload | string | unload | 通知物体卸载 |
Click | string | click | 通知鼠标点击,鼠标单击、双击均会触发 Click 事件(双击时候会触发两次) |
DBLClick | string | dblclick | 通知鼠标双击 |
SingleClick | string | singleclick | 通知鼠标单击(会有些许的延时,鼠标双击不会触发 SingleClick 单击事件) |
MouseUp | string | mouseup | 通知鼠标键抬起 |
MouseDown | string | mousedown | 通知鼠标键按下 |
MouseMove | string | mousemove | 通知鼠标移动 |
MouseWheel | string | mousewheel | 通知鼠标滚轮滚动 |
MouseEnter | string | mouseenter | 通知鼠标首次移入物体 |
MouseOver | string | mouseover | 通知鼠标首次移入物体, 会一直传递到父物体 |
MouseLeave | string | mouseleave | 通知鼠标首次移出物体 |
DragStart | string | dragstart | 通知物体拖拽开始 |
Drag | string | drag | 通知物体拖拽进行中 |
DragEnd | string | dragend | 通知物体拖拽结束 |
KeyDown | string | keydown | 通知键盘按键按下 |
KeyPress | string | keypress | 通知键盘按键一直被按下 |
KeyUp | string | keyup | 通知键盘按键抬起 |
CameraChangeStart | string | camerachangestart | 通知摄像机位置变动开始 |
CameraChangeEnd | string | camerachangeend | 通知摄像机位置变动结束 |
CameraChange | string | camerachange | 通知摄像机位置变动中 |
CameraZoom | string | camerazoom | 摄像机向前/后滚动 |
CameraViewChange | string | cameraviewchange | 通知摄像机观察模式改动 |
Create | string | create | 通知物体创建完成 |
Destroy | string | destroy | 通知物体删除完成 |
Expand | string | expand | 通知建筑楼层被展开 |
Unexpand | string | unexpand | 通知建筑楼层被合并 |
Select | string | select | 通知物体被选择 |
Deselect | string | deselect | 通知物体被取消选择 |
SelectionChange | string | selectionchange | 通知物体选择集合更新 |
LevelChange | string | levelchange | 通知场景层级发生改变 |
EnterLevel | string | enterLevel | 通知进入下一层级 |
LeaveLevel | string | leaveLevel | 通知退出当前层级 |
LevelFlyEnd | string | levelflyend | 通知摄像机飞入下一层级结束 |
【Tip】:
在低代码开发界面,你可以打开开发者工具,将 JavaScript 上下文选择为 https://www.thingjs.com ,然后通过控制台的交互式输入:
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'; }) // 触发自定义事件 trigger truck.trigger('customAlarm', {alarm: true})
6.4.7 实例
6.5 视角(摄影机)
6.5.1 摄像机的基本概念
在 3D 开发中,摄像机指的是 用来确定观察 3D 场景的视角,它包含两个重要的位置参数:
- 镜头位置(position)
- 目标点(target)
6.5.2 ThingJS 中常用的 摄像机 API
API | 描述 | 说明 |
app.camera.position |
镜头位置 | |
app.camera.target |
目标点 | 被拍摄物位置 |
app.camera.lookAt () |
“盯着”某一位置或物体看 | 可传参数:位置、对象、null |
app.camera.fit () |
设置最佳看点 | 注意:不能设置回调 |
app.camera.flyto () |
设置摄像机飞行 | |
app.camera.flying |
判断摄影机是否在飞行 | 返回布尔值 |
app.camera.stopFlying () |
停止摄影机飞行 | |
app.camera.ratateAround () |
让摄影机环绕某坐标点或某物体旋转飞行 | |
app.camera.floowObject (obj) |
摄影机跟随物体 | |
app.camera.stopFloowingObject () |
停止摄影机跟随物体 |
6.5.2.1 官方案例解析 - 飞行控制
这部分案例的效果如下:
代码如下:
// 加载场景代码 var app = new THING.App({ url: 'https://www.thingjs.com/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.45386120167032, 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: 'https://www.thingjs.com/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(['https://www.thingjs.com/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}.is.autonavi.com/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, // 飞行时间 ms lonlat: sceneLonlat, // 要飞到的目标点的经纬度 height: 200, // 摄像机离地高度 heading: 0, // 水平角(方位角) 单位度 pitch: 45, // 垂直角(俯仰角) 单位度 complete: function() { // 创建Campus var campus = app.create({ type: 'Campus', name: '建筑', url: 'https://www.thingjs.com/static/models/storehouse', // 园区地址 position: position, // 位置 angles: angles, // 旋转 complete: function() { // 创建成功以后执行函数 initThingJsTip("本例程展示了地图摄像机的交互控制,地图交互默认为左键移动,右键旋转。与园区中不同的是地图上使用经纬度控制相机位置,在使用时需要注意。点击按钮,查看效果"); // 加载场景后执行 app.level.change(campus); // 添加Button let 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]; // 设置摄像机俯仰角度范围[最小值, 最大值] }