
面向物联网的3D可视化应用开发平台
前言 随着社会的发展,城市中的汽车越来越多。车辆集中存放管理的场所被人类提出车辆进出的秩序、车辆存放的安全性、车辆存放管理的有偿性等要求。停车场系统应用现代机械电子及通讯科学技术,集控制硬件、软件于一体。随着科技的发展,停车场管理系统也日新月异,目前最为专业化的停车场系统为免取卡停车场。下面我们就用ThingJs平台来搭建一个3d可视化的停车场管理系统。 点击查看:DEMO 效果 停车场总览 车辆信息 车辆行动轨迹监控 车位信息展示 下面我们就用ThingJs平台来搭建一个3d可视化的停车场管理系统。 第一步 使用CampusBuilder来搭建一个模拟停车场。CampusBuider很好用在以往的文章中也多次提及过,丰富的模型库任你选择快速搭建3D场景。 第二步 初始化摄像机的位置并添加鼠标滑过,左键单击,右键单击,左键双击等事件。鼠标滑过,车勾边变红色,车位勾边边蓝色。左键单击,车或车位弹出信息牌。右键单击,关闭当前信息牌,镜头初始化。getCarData() 与 getParkData() 为模拟数据,没有几个售出的车位和车就用了switch。 app.on('load', function (evt) { //初始化摄像机 init_camera(); //滑过勾边 var campus = evt.campus; var objs = app.query('.Building').add(campus.things); objs.on('mouseon', function (ev) { if (ev.object.name.search("car") == 0) { this.style.outlineColor = '#ff0000'; } if (ev.object.name.search("park") == 0) { this.style.outlineColor = '#0000ff'; } }); objs.on('mouseoff', function () { this.style.outlineColor = null; }); //单击事件 app.on('click', function (ev) { if (ev.button == 2) { destroy_ui(); init_camera(); } if (ev.object.name.search("car") == 0) { destroy_ui(); getCarData(ev.object); create_ui_car(); } if (ev.object.name.search("park") == 0) { destroy_ui(); getParkData(ev.object); create_ui_park(); } }); //双击事件 app.on('dblclick', function (ev) { if (ev.object.name.search("car") == 0) { app.camera.flyTo({ 'time': 1500, 'object': ev.object, 'position': [0, 0, 0], 'complete': function () { } }); } if (ev.object.name.search("park") == 0) { app.camera.flyTo({ 'time': 1500, 'object': ev.object, 'position': [0, 5, 0], 'complete': function () { } }); } }); }); //初始化摄像机 function init_camera() { // 摄像机飞行到某位置 app.camera.flyTo({ 'position': [-67.95670997548082, 49.69517426520041, -42.88366089402964], 'target': [-7.188588318222256, 14.094194791658271, -12.724756207211417], 'time': 800, 'complete': function () { console.log("Camera ready"); } }); } //创建面板 var panel; var dataObj; var carInfo; var parkInfo; function create_ui_car() { panel = new THING.widget.Panel({ titleText: "车辆信息", closeIcon: true, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, titleImage: 'https://www.thingjs.com/static/images/example/icon.png' }); panel.position = [0, 326]; // 创建任意对象 dataObj = { name: carInfo[0], info: carInfo[1], park: carInfo[2], plateNum: carInfo[3], state: carInfo[4], contactNum: carInfo[5] }; // 动态绑定物体 var name = panel.addString(dataObj, 'name').caption('车主姓名'); var info = panel.addString(dataObj, 'info').caption('车主信息'); var park = panel.addString(dataObj, 'park').caption('车位编号'); var plateNum = panel.addString(dataObj, 'plateNum').caption('车牌号码'); var contactNum = panel.addString(dataObj, 'contactNum').caption('联系电话'); var state = panel.addString(dataObj, 'state').caption('车位状态'); } function create_ui_park() { panel = new THING.widget.Panel({ titleText: "车位信息", closeIcon: true, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, titleImage: 'https://www.thingjs.com/static/images/example/icon.png' }); panel.position = [0, 326]; dataObj = { park: parkInfo[0], name: parkInfo[1], state: parkInfo[2], date: parkInfo[3] }; var park = panel.addString(dataObj, 'park').caption('车位编号'); var name = panel.addString(dataObj, 'name').caption('车主姓名'); var state = panel.addString(dataObj, 'state').caption('车位状态'); var date = panel.addString(dataObj, 'date').caption('车位期限'); } function destroy_ui() { if (panel) { panel.destroy(); panel = null; } } function getCarData(obj) { switch (obj.name) { case "car_0": carInfo = ['张三', '28#1-302', 'A-06', '吉K49278', '未交费', '13159828222']; break; case "car_1": carInfo = ['李四', '18#2-1202', 'B-04', '吉A46154', '已交费', '13159828222']; break; case "car_2": carInfo = ['王五', '13#2-702', 'B-05', '吉D95868', '已交费', '13159828222']; break; case "car_3": carInfo = ['郭富贵', '3#3-802', 'B-09', '吉B46278', '已交费', '13159828222']; break; case "car_4": carInfo = ['薛展畅', '8#3-1302', 'C-03', '吉A44278', '未交费', '13159828222']; break; case "car_5": carInfo = ['李文忠', '6#2-302', 'C-05', '黑B77865', '已交费', '13159828222']; break; case "car_6": carInfo = ['李洪春', '8#2-402', 'D-08', '吉CJ87821', '未交费', '13159828222']; break; case "car_7": carInfo = ['孟旭浩', '9#2-801', 'D-16', '吉A4U278', '已交费', '13159828222']; break; case "car_8": carInfo = ['刘星辰', '4#2-502', 'D-20', '吉A98378', '已交费', '13159828222']; break; case "car_9": carInfo = ['张星辰', '4#1-302', 'E-04', '吉A98378', '已交费', '13159828222']; break; case "car_10": carInfo = ['张星辰', '8#2-302', 'D-01', '京A44378', '已交费', '13159228222']; break; } } function getParkData(obj) { switch (obj.name) { case "park_5": parkInfo = ['A-06', '张三', '已交', '2018.5.10-2020.5.11']; break; case "park_11": parkInfo = ['B-09', '郭富贵', '已交', '2018.5.10-2020.5.11']; break; case "park_16": parkInfo = ['B-05', '王五', '欠费', '2018.5.10-2020.5.11']; break; case "park_17": parkInfo = ['B-04', '李四', '已交', '2018.5.10-2020.5.11']; break; case "park_40": parkInfo = ['C-03', '薛展畅', '已交', '2018.5.10-2020.5.11']; break; case "park_44": parkInfo = ['C-05', '李文忠', '已交', '2018.5.10-2020.5.11']; break; case "park_68": parkInfo = ['D-08', '李洪春', '已交', '2018.5.10-2020.5.11']; break; case "park_78": parkInfo = ['E-04', '张星辰', '已交', '2018.5.10-2020.5.11']; break; case "park_59": parkInfo = ['D-16', '孟旭浩', '已交', '2018.5.10-2020.5.11']; break; case "park_67": parkInfo = ['D-20', '刘星辰', '已交', '2018.5.10-2020.5.11']; break; case "park_3": parkInfo = ['A-04', '刘地辰', '已交', '2018.5.10-2020.5.11']; break; case "park_54": parkInfo = ['D-1', '龙的辰', '未交', '2018.5.10-2020.5.11']; break; default: parkInfo = ['X-xx', 'XXX', '未售出', '2000.1.1-2020.1.1']; } } 第三步 创建主面板添加空间统计,闸门管理,播放动画,出入登记等功能按钮,同时创建闸门管理子面板。 //主面板 var toolbar = new THING.widget.Panel({ width: '163px' }); var mainDataObj = { spaceStatistics: false, gateManagement: false, video: false, registrationForm: false } //闸门管理面板 var gateToolbar = new THING.widget.Panel({ width: '163px' }); gateToolbar.position = [450, 0]; gateToolbar.visible = false; var gateDataObj = { entrance: false, exit: false, } //面板按钮组件及事件 Loader.sync(['lib/iconfont.js'], function () { //主面板 var button0 = toolbar.addImageBoolean(mainDataObj, 'spaceStatistics').caption('空间统计').url('#momoda_lc-icontubiao'); var button1 = toolbar.addImageBoolean(mainDataObj, 'gateManagement').caption('闸门管理').url('#momoda_lc-icontubiao21'); var button2 = toolbar.addImageBoolean(mainDataObj, 'video').caption('播放动画').url('#momoda_lc-icontubiao9'); var button3 = toolbar.addImageBoolean(mainDataObj, 'registrationForm').caption('出入登记').url('#momoda_lc-icontubiao10'); //闸门面板 var button4 = gateToolbar.addImageBoolean(gateDataObj, 'entrance').caption('入口管理').url('#momoda_lc-icontubiao21'); var button5 = gateToolbar.addImageBoolean(gateDataObj, 'exit').caption('出口管理').url('#momoda_lc-icontubiao21'); //第四步中的功能实现 }); 第四步 为上面创建的功能按钮实现功能。 //空间统计 var opacityFlag = true; button0.on('change', function () { if (opacityFlag) { opacityFlag = false; app.query(/park/).forEach( function (obj) { var str = obj.name; switch (str) { case "park_5": break; case "park_11": break; case "park_16": break; case "park_17": break; case "park_40": break; case "park_44": break; case "park_68": break; case "park_78": break; case "park_59": break; case "park_67": break; case "park_33": break; case "park_54": break; case "park_3": break; default: obj.style.opacity = 0.3; } } ); } else { opacityFlag = true; app.query(/park/).forEach( function (obj) { obj.style.opacity = 1; } ) } }); //闸门管理,入口管理,出口管理 var gateToolbarFlag = true; var entranceFlag = false; var exitFlag = false; button1.on('change', function () { if (gateToolbarFlag) { app.camera.flyTo({ 'position': [-69.15232764795844, 12.556743445078443, -4.722896106654333], 'target': [-6.75806618043438, 11.584727439263146, -5.077821719000649], 'time': 1000 }); gateToolbarFlag = false; gateToolbar.visible = true; } else { init_camera(); gateToolbarFlag = true; gateToolbar.visible = false; } }); button4.on('change', function () { var entry = app.query('入口')[0]; if (!entranceFlag) { entranceFlag = true; entry.rotateX(45.0); entry.moveY(2); entry.moveZ(-1); } else { entranceFlag = false; entry.rotateX(-45.0); entry.position = [0, 0, 0]; } }); button5.on('change', function () { var exit = app.query('出口')[0]; if (!exitFlag) { exitFlag = true; exit.rotateX(-45.0); exit.moveY(9.2); exit.moveZ(4.3); } else { exitFlag = false; exit.rotateX(-315.0); exit.position = [0, 0, 0]; } }); //播放动画 button2.on('change', function () { //飞向每一个摄像机的位置 console.log("监控设备!"); playCar(); }); //出入登记 registrationFlag = true; button3.on('change', function () { //显示两块信息板,镜头飞向门禁 // 摄像机飞行到某位置 if (registrationFlag) { app.camera.flyTo({ 'position': [-13.229586070519874, 13.062016938601909, -14.789241424512456], 'target': [-21.25078065116403, 11.949594230222267, -11.972835509196605], 'time': 1000, }); registrationFlag = false; create_ui_gate_exit(); create_ui_gate_entry(); } else { registrationFlag = true; entryUi.destroy(); entryUi = null; exitUi.destroy(); exitUi = null; } }); 播放动画 var car = app.create({ type: 'Thing', name: 'car_10', url: 'http://model.3dmomoda.cn/models/c6ed424627234a298c1921950eb8534c/0/gltf/', // 模型地址 position: [-45.89714816093272, 0.043936770289323, 0.312388718621647], // 位置 angle: 90, }); var points = []; points.push([-45.89714816093272, 0.043936770289323, 0.312388718621647]); points.push([-38.89714816093272, 0.043936770289323, 0.312388718621647]); var radius = 2 for (var degree = 0, y = 0; degree <= 90; degree += 20) { var x = Math.sin(degree * 2 * Math.PI / 360) * radius - 35.89714816093272; var z = -Math.cos(degree * 2 * Math.PI / 360) * radius + 2.312388718621647; points.push([x, y, z]); console.log([x, y, z]); } points.push([-33.927532654908305, 0, 4.9650923632877861]); points.push([-33.927532654908305, 0, 7.9650923632877861]); points.push([-33.927532654908305, 0, 10.9650923632877861]); points.push([-33.927532654908305, 0, 13.9650923632877861]); var line = app.create({ type: 'Line', color: 0xFFFF00, // 轨迹线颜色 dotSize: 2, // 轨迹点的大小 points: points, }); line.visible = false; function playCar() { var car = app.query('car_10')[0]; var entry = app.query('入口')[0]; entry.rotateX(45.0); entry.moveY(2); entry.moveZ(-1); car.movePath({ 'path': line.points, // 轨迹路线 'time': 5000, // 移动时间 'orientToPath': true, // 物体移动时沿向路径方向 }); setTimeout(function () { entry.rotateX(-45.0); entry.position = [0, 0, 0]; }, 2000) } 出入登记 //出入登记 function create_html_entry() { var sign1 = `<div class="sign1" id="board1" style="font-size: 12px;width: 230px;text-align: center;background-color: rgba(0, 0, 0, .6);border: 3px solid #eeeeee;border-radius: 8px;color: #eee;position: absolute;top: 0;left: 0;z-index: 10;display: none;"> <div class="s1" style="margin: 5px 0px 5px 0px;line-height: 32px;overflow: hidden;"> <span class="span-l icon" style="float: left;width: 30px;height: 30px;"></span> <span class="span-l font" style="float: left;margin: 0px 0px 0px 3px;">车辆进入</span> <span class="span-r point" style="float: right;width: 12px;height: 12px;background-color: #18EB20;border-radius: 50%;margin: 10px 5px 10px 0px;"></span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;width:70px;margin: 0px 10px 0px 10px;">进车时间</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">车牌号</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">9:15</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉K49278</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">10:15</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A46154</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">10:17</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉D95868</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">10:25</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉B46278</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">10:39</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">黑B77865</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">11:19</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉CJ87821</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">11:21</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A4U278</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">11:35</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A98378</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">12:50</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A98778</span> </div> <div class="point-top" style="position: absolute;top: -7px;right: -7px;background-color: #3F6781;width: 10px;height: 10px;border: 3px solid #eee;border-radius: 50%;"></div> </div>` $('#div3d').append($(sign1)); } function create_html_exit() { var sign2 = `<div class="sign2" id="board2" style="font-size: 12px;width: 230px;text-align: center;background-color: rgba(0, 0, 0, .6);border: 3px solid #eeeeee;border-radius: 8px;color: #eee;position: absolute;top: 0;left: 0;z-index: 10;display: none;"> <div class="s1" style="margin: 5px 0px 5px 0px;line-height: 32px;overflow: hidden;"> <span class="span-l icon" style="float: left;width: 30px;height: 30px;"></span> <span class="span-l font" style="float: left;margin: 0px 0px 0px 3px;">车辆进入</span> <span class="span-r point" style="float: right;width: 12px;height: 12px;background-color: #18EB20;border-radius: 50%;margin: 10px 5px 10px 0px;"></span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;width:70px;margin: 0px 10px 0px 10px;">出车时间</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">车牌号</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">7:15</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">黑B77865</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">8:45</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A4U278</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">8:57</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A98378</span> </div> <div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"> <span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">10:01</span> <span class="span-l font2" style="float: left;width: 140px;background-color: #2480E3;">吉A98778</span> </div> <div class="point-top" style="position: absolute;top: -7px;right: -7px;background-color: #3F6781;width: 10px;height: 10px;border: 3px solid #eee;border-radius: 50%;"></div> </div>` $('#div3d').append($(sign2)); } function create_element(str) { var srcElem = document.getElementById(str); var newElem = srcElem.cloneNode(true); newElem.style.display = "block"; app.domElement.insertBefore(newElem, srcElem); return newElem; } var entryUi = null; function create_ui_gate_entry() { create_html_entry(); entryUi = app.create({ type: 'UIAnchor', position: [-39.89714816093272, 3.043936770289323, 2.312388718621647], element: create_element("board1"), offset: [0, 2, 0], pivot: [0.5, 1] // 界面的重心 }); } var exitUi = null; function create_ui_gate_exit() { create_html_exit(); exitUi = app.create({ type: 'UIAnchor', position: [-34.89714816093272, 6.059100472147456, -14.950719696627075], element: create_element("board2"), offset: [0, 2, 0], pivot: [0.5, 1] // 界面的重心 }); } 演示地址
你能担任超过5个足球场大小和16层楼高的超级油轮的船长么? 除了导航之外,您还必须及时了解监测船上数千个爆炸、火灾、温湿度等物联网传感器数据。 面对大量物联网传感器带来的数据过载,你需要一个3D可视化的船长控制台。 物联网数据解决方案的基本组件 物联网系统产生的海量数据难以处理。监控多个实时数据提要增加了复杂性。人们如何面对如此海量的数据处理呢? 物联网与网络世界相同,都是有大量数据组成。 传统的管理控制台不太适合管理实时、复杂的系统。通过可视化,决策变得更加容易——尤其是在大型结构(如超级油轮、工业车间)中安装了数千个传感器。 解决方案需要三个组成部分: 3D仿真模型(本文中的超级油轮) 覆盖实时传感器数据 结合物联网和操作系统的交互式管理控制台 3D可视化 我们选择使用ThingJS平台及其组件来制作3D场景,通过ThingJS提供的搭建工具CampusBuilder,可以自由上传obj、max等格式模型,并且支持鼠标拖拽来制作3D场景,场景搭建好后,导出tjs格式文件,上传到ThingJS在线开发平台。 如何工作 ThingJS平台支持通过Ajax 和 Websoket等对接不同传感器数据,通过面板、顶牌、变色、闪烁灯形式将数据直接显示在3D模型上。 船长可以通过浏览器和移动设备随时查看船上的监测情况,对不同类型的警报和读数进行优先级排序并处理。当发现故障情况时,船长可以托转放大并获取故障详细信息,通过三维模型直观定位故障发生的位置。 当传感器收到特定数据信息时,三维可视化控制台中,对应位置的传感器模型显示不同颜色的告警信息,船长可以点击3D模型中的传感器,确定告警原因,传感器从数据库中检索细节数据并显示在一个单独的信息面板中。 5G时代万物互联,在物联网管理应用中,将传感器数据和状态直观展示在三维模型上,将极大地增强人们理解和处理物联网数据的能力。
前言 利用CampusBuilder来搭建自己的虚拟世界过程有这样一个问题:如何快速聚焦到虚拟场景的某一位置。当然我们可以创建几个按钮对应查找我们需要去的位置(参照物)并聚焦,但是按钮并不是很炫酷也不能很好的反馈给我们一些信息。接下来我们就用平面导航图来解决这一问题。 实现 第一步,使用CampusBuilder搭建模拟场景,CampusBuilder操作简单,分分钟就可以上手。这里为每一个房间都创建一个小球作为视点参照物体并勾选预览时隐藏,这样不会对我们的场景造成影响,也便于我们聚焦到指定房间。注意:要将我们每个房间中的设备框选之后组合在一起,为下一阶段的做准备。 第二步,把我们编辑好的场景加载到ThingJS中。 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/S2Vyd2lu/scene/Campus04", }); //场景相关 //************************************************************************************/ app.on('load', function () { app.camera.flyTo({ 'position': [36.357131498969785, 61.953024217074265, 69.12160670337104], 'target': [-1.3316924326803257, -4.9370371421622625, 33.619521849828544], 'time': 2000, }); }); 第三步,为平面图创建一块面板,并调整一下面板的位置以及大小。 图片下载地址: 链接:https://pan.baidu.com/s/1gmNjIj2ekbw1rO3MoujHqQ 提取码:i0c1 //面板相关 //************************************************************************************/ var panel = new THING.widget.Panel({ closeIcon: false, dragable: false, retractable: true, opacity: 0.9, hasTitle: true, }); panel.width = 600; panel.position = [0, 200]; var dataObj = { iframe: '' }; var iframe = panel.addIframe(dataObj, 'iframe').caption('').setHeight("290px"); 第四步,编写iframe页。写完记得将这个页面和图片上传到页面资源,资源 => 页面资源 => 按钮(上传) 。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .total_image { margin : 20px; } .total_image img{ cursor: pointer; transition: all 0.6s; width: 50px; } .total_image img:hover{ transform: scale(1.5); position:relative; z-index:100; } </style> </head> <body> <div class="total_image" style="width: 500px;height: 280px;background-size: 100% auto"> <img class="model_imag" src="发电室1.jpg" style="float: left;display: block;width: 85px;height: 84px" onclick="onClick('PowerGenerationGroup_01','viewPoint_1')" > <img class="model_imag" src="发电室2.jpg" style="float: left;display: block;width: 78px;height: 84px" onclick="onClick('PowerGenerationGroup_02','viewPoint_2')" > <img class="model_imag" src="发电室3.jpg" style="float: left;display: block;width:170px;height: 84px" onclick="onClick('PowerGenerationGroup_03','viewPoint_3')" > <img class="model_imag" src="发电室4.jpg" style="float: left;display: block;width:167px;height: 84px" onclick="onClick('PowerGenerationGroup_04','viewPoint_4')" > <div style="display: block;float: left;width: 100px;height: 145px;background-color:white"> <img class="model_imag" src="办公室1.jpg" style="float: left;display: block;width:100px;height: 60px" onclick="onClick('Office','viewPoint_5')" > <img class="model_imag" src="返回.png" style="float: left;display: block;width:100px;height: 80px" onclick="initViewPoint()"> </div> <img class="model_imag" src="发电室5.jpg" style="float: right;display: block;width:123px" onclick="onClick('PowerGenerationGroup_05','viewPoint_8')" > <img class="model_imag" src="会议室1.jpg" style="float: left;display: block;width: 138px;height: 145px" alt="" onclick="onClick('BoardRoom_01','viewPoint_6')"> <img class="model_imag" src="会议室2.jpg" style="float: left;display: block;width: 138px;height: 145px" alt="" onclick="onClick('BoardRoom_02','viewPoint_7')" > </div> <script> function onClick(viewPoint,target){ window.parent.onClick(viewPoint,target); } function initViewPoint(){ window.parent.initViewPoint(); } </script> </body> </html> 第五步,完成onClick()和initViewPoint()方法。 //事件相关 //************************************************************************************/ var currentModule = null; //点击事件 function onClick(targetObj, viewPoint) { currentModule = app.query(targetObj)[0]; currentModule.position = [0, 0, 0]; currentModule.style.opacity = 1; app.camera.flyTo({ 'object': app.query(viewPoint)[0], 'offset': [0, 13, 7], 'time': 1000, complete: function () { currentModule.brothers.style.opacity = 0.3; } }); } //返回事件 function initViewPoint() { currentModule.brothers.style.opacity = 1; currentModule = null; app.camera.flyTo({ 'position': [36.357131498969785, 61.953024217074265, 69.12160670337104], 'target': [-1.3316924326803257, -4.9370371421622625, 33.619521849828544], 'time': 1000, }); } 小结 第一部分我们主要完成了iframe与我们的3D场景的简单交互,这里也没有做什么特效只是做了一个点击事件。这里值得一提的是currentModule这个全局变量,开始我没有创建这个变量只是将我当前点击的物体obj.style.opacity = 1;obj.brothers.style.opacity = 0.3, 但是执行initViewPoint(){app.query(’.Thing’).style.opacity=1}无法将场景的opacity 属性还原(自己可以试一下,或者有解决方案留言)。第二部分我会给iframe页加上鼠标悬停事件让iframe页的img标签和我们场景中的obj一起动起来! 完整代码 可以粘到 ThingJS 网站在线开发环境运行http://www.thingjs.com/guide/?m=sample //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/S2Vyd2lu/scene/Campus04", }); //场景相关 //************************************************************************************/ app.on('load', function () { app.camera.flyTo({ 'position': [36.357131498969785, 61.953024217074265, 69.12160670337104], 'target': [-1.3316924326803257, -4.9370371421622625, 33.619521849828544], 'time': 2000, }); }); //面板相关 //************************************************************************************/ var panel = new THING.widget.Panel({ closeIcon: false, dragable: false, retractable: true, opacity: 0.9, hasTitle: true, }); panel.width = 600; panel.position = [0, 200]; var dataObj = { iframe: '/uploads/wechat/S2Vyd2lu/file/平面图导航/ifram.html' }; var iframe = panel.addIframe(dataObj, 'iframe').caption('').setHeight("290px"); //事件相关 //************************************************************************************/ var currentModule = null; //点击事件 function onClick(targetObj, viewPoint) { currentModule = app.query(targetObj)[0]; currentModule.position = [0, 0, 0]; currentModule.style.opacity = 1; app.camera.flyTo({ 'object': app.query(viewPoint)[0], 'offset': [0, 13, 7], 'time': 1000, complete: function () { currentModule.brothers.style.opacity = 0.3; } }); } //返回事件 function initViewPoint() { currentModule.brothers.style.opacity = 1; currentModule = null; app.camera.flyTo({ 'position': [36.357131498969785, 61.953024217074265, 69.12160670337104], 'target': [-1.3316924326803257, -4.9370371421622625, 33.619521849828544], 'time': 1000, }); }
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
地下的管线错综复杂如何,图纸并不能完全满足实际需求,我们就用ThingJS平台来模拟一个小区水,电,天然气管线演示。 查看DEMO 第一步,利用CampusBuilder搭建模拟场景。CampusBuilder的模型库有各种各样的模型,使我们搭建出的场景更逼真。使用CampusBuilde创建层级,之后再给层级加外立面就出现了当前的效果。详情移步:CampusBuilder3D场景制作工具 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/管线演示2", //背景设置 "skyBox": "BlueSky" }); 第二步, 初始化摄像机位置,添加四个按钮,并创建四个方法。 app.on('load', function () { //摄像机位置初始化 app.camera.position = [0.4374202706634094, 101.92917348318593, 97.06808820543526]; app.camera.target = [52.75056074670042, -18.885239034825123, -20.619558480451797]; new THING.widget.Button('水管演示', water); new THING.widget.Button('电线演示', electric); new THING.widget.Button('燃气演示', gas); }); function water() { } function electric() { } function gas() { } 第三步,创建管线,我们这里写水管线以后的电线,燃气管线同理。这里简单说一下PolygonLine,它继承THING.LineBase,同样有贴图属性,可以自己从本地上传图片至页面资源后使用。 var line = null; function buildLine(points, color) { line = app.create({ type: 'PolygonLine', points: points, style: { color: color, } }); line.scrollUV = true; } function water() { var waterUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([5, -0.8, 0])); waterUnderPoints.push(points[0]); for (var i = 3; i <= 24; i += 3) { points.push(parentObj.selfToWorld([5, i, 0])); points.push(parentObj.selfToWorld([5, i, 3])); points.push(parentObj.selfToWorld([-5, i, 3])); points.push(parentObj.selfToWorld([-5, i, -3])); points.push(parentObj.selfToWorld([5, i, -3])); points.push(parentObj.selfToWorld([5, i, 0])); } points.push(parentObj.selfToWorld([5, 24, 0])); buildLine(points, '#0000FF'); } ); waterUnderPoints.push([15.32711, -0.79, -55.655339999999999]); buildLine(waterUnderPoints, '#0000FF'); //renderOrder(); } function buildingOpacity(num) { app.query("*").forEach(function (obj) { obj.style.opacity = num; }); } function renderOrder(){ app.query('.PolygonLine').forEach( function (obj) { obj.renderOrder = -10000; } ); } 这里用到了两个方法: 1) buildingOpacity(num); 这个方法的主要作用就是为了将场景虚化,更好的展示我们的管线。 2)renderOrder(); 这个我在代码中先注释掉了我们先看一下现在的效果: 注意:如果没有设置renderOrder();属性的 ,管线的渲染层级没有building高 ,就会导致有被building遮盖的PolygonLine无法正常显示,设置renderOrder();属性后,渲染效果正常 最后一步,创建出电线以及燃气线 function electric() { var electricUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([3, -0.8, 0])); electricUnderPoints.push(points[0]); for (var i = 3; i <= 24; i += 2.5) { points.push(parentObj.selfToWorld([3, i, 0])); points.push(parentObj.selfToWorld([-3, i, 2])); } points.push(parentObj.selfToWorld([3, 24, 0])); buildLine(points, '#00FF00'); console.log(points); } ); electricUnderPoints.push([16.690666, -0.79, -55.115203999999999]); buildLine(electricUnderPoints, '#00FF00'); renderOrder(); } function gas() { var gasUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([-6.2, -0.3, 0])); gasUnderPoints.unshift(points[0]); for (var i = 3; i <= 24; i += 3) { points.push(parentObj.selfToWorld([-6.2, i, 0])); points.push(parentObj.selfToWorld([-6.2, i, 2])); points.push(parentObj.selfToWorld([6.2, i, 2])); points.push(parentObj.selfToWorld([6.2, i, -2])); points.push(parentObj.selfToWorld([-6.2, i, -2])); points.push(parentObj.selfToWorld([-6.2, i, 0])); } points.push(parentObj.selfToWorld([-6.2, 24, 0])); buildLine(points, '#FF0000'); console.log(points); } ); gasUnderPoints.unshift([22.963023600000003, -0.3, 57.8305784]); buildLine(gasUnderPoints, '#FF0000'); renderOrder(); } 附上完整代码,可以直接在ThingJS平台调试 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/管线演示2", //背景设置 "skyBox": "BlueSky" }); app.on('load', function () { //摄像机位置初始化 app.camera.position = [0.4374202706634094, 101.92917348318593, 97.06808820543526]; app.camera.target = [52.75056074670042, -18.885239034825123, -20.619558480451797]; new THING.widget.Button('水管演示', water); new THING.widget.Button('电线演示', electric); new THING.widget.Button('燃气演示', gas); }); function water() { var waterUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([5, -0.8, 0])); waterUnderPoints.push(points[0]); for (var i = 3; i <= 24; i += 3) { points.push(parentObj.selfToWorld([5, i, 0])); points.push(parentObj.selfToWorld([5, i, 3])); points.push(parentObj.selfToWorld([-5, i, 3])); points.push(parentObj.selfToWorld([-5, i, -3])); points.push(parentObj.selfToWorld([5, i, -3])); points.push(parentObj.selfToWorld([5, i, 0])); } points.push(parentObj.selfToWorld([5, 24, 0])); buildLine(points, '#0000FF'); } ); waterUnderPoints.push([15.32711, -0.79, -55.655339999999999]); buildLine(waterUnderPoints, '#0000FF'); renderOrder(); } function electric() { var electricUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([3, -0.8, 0])); electricUnderPoints.push(points[0]); for (var i = 3; i <= 24; i += 2.5) { points.push(parentObj.selfToWorld([3, i, 0])); points.push(parentObj.selfToWorld([-3, i, 2])); } points.push(parentObj.selfToWorld([3, 24, 0])); buildLine(points, '#00FF00'); console.log(points); } ); electricUnderPoints.push([16.690666, -0.79, -55.115203999999999]); buildLine(electricUnderPoints, '#00FF00'); renderOrder(); } function gas() { var gasUnderPoints = []; buildingOpacity(0.3); app.query(/building_0/).forEach( function (parentObj) { var points = []; points.push(parentObj.selfToWorld([-6.2, -0.3, 0])); gasUnderPoints.unshift(points[0]); for (var i = 3; i <= 24; i += 3) { points.push(parentObj.selfToWorld([-6.2, i, 0])); points.push(parentObj.selfToWorld([-6.2, i, 2])); points.push(parentObj.selfToWorld([6.2, i, 2])); points.push(parentObj.selfToWorld([6.2, i, -2])); points.push(parentObj.selfToWorld([-6.2, i, -2])); points.push(parentObj.selfToWorld([-6.2, i, 0])); } points.push(parentObj.selfToWorld([-6.2, 24, 0])); buildLine(points, '#FF0000'); console.log(points); } ); gasUnderPoints.unshift([22.963023600000003, -0.3, 57.8305784]); buildLine(gasUnderPoints, '#FF0000'); renderOrder(); } /************************************************************************ * common */ function buildingOpacity(num) { app.query("*").forEach(function (obj) { obj.style.opacity = num; }); } function renderOrder(){ app.query('.PolygonLine').forEach( function (obj) { obj.renderOrder = -10000; } ); } var line = null; function buildLine(points, color) { line = app.create({ type: 'PolygonLine', points: points, style: { color: color, } }); line.scrollUV = true; } 现实中社区管线远比DEMO中复杂,开发者可以根据自身业务实际,使用ThingJS开发出更多三维可视化应用。
ThingJS是优锘科技开发的一套面向物联网应用的在线3D可视化应用开发及运营PaaS平台,以“ ThingJS云视PaaS服务”形式面向广大物联网企业提供全生命周期在线3D可视化服务。 “人人都能用3D”是ThingJS的使命。ThingJS基于WebGL协议,使用JavaScript开发语言,前端工程师无需专门去学习three.js也可以轻松开发3D可视化应用,兼容各种浏览器及移动设备,一次开发即可实现多终端的快速访问。 ThingJS物联网三维仿真场景 在线开发界面 体验建筑可视化管理DEMO 建筑外景 楼层展开 单楼层浏览 支持对接各类传感器数据 三维全景图巡游 目前ThingJS支持使用Ajax和Websoket方式进行数据对接,同时在积极开发大数据对接和处理能力。 Ajax 数据对接 Ajax,即异步 JavaScript 和 XML ,是一种创建交互式网页应用的网页开发技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新,这意味着可以在不重新加载整个网页的情况下,对网页的部分进行加载更新。 Ajax 是优点在于它在浏览器与web服务器之间使用异步数据传输(HTTP请求),不阻塞用户,核心对象是 XMLHTTPRequest 。通过这个对象,js 可在不重新加载页面的情况下与 web 服务器交换数据。由于Ajax 已流行这么多年,浏览器对它的兼容非常完美,并且很多成熟的框架可以直接使用。 Websoket数据对接 WebSocket 是 HTML5 一种新的协议,实现了浏览器与服务器全双工通信。其本质是先通过 HTTP/HTTPS 协议进行握手后创建一个用于交换数据的 TCP 连接,服务端与客户端通过此 TCP 连接进行实时通信。 WebSocket API 最大的优点在于服务器和浏览器可以在给定的时间范围内的任意时刻,相互推送信息。在建立连接之后,服务器可以主动传送数据给浏览器。此外,服务器与浏览器之间交换的表头信息很小。WebSocket 并不限于以 Ajax (或 XHR )方式通信,因为 Ajax 技术需要客户端发起请求,而 WebSocket 服务器和客户端可以相互推送信息。 除了在线开发平台之外,ThingJS还提供了城市、园区、图表三中搭建工具,鼠标拖拽即可快速搭建三维仿真场景。 ThingJS开发平台和配套组件让3D开发变得简单高效,让3D可视化交互能力在多种物联网场景中得以应用。
国内高层建筑不断兴建,它的特点是高度高、层数多、体量大。面积可达几万平方米到几十万平方米。这些建筑都是一个个庞然大物,高高的耸立在地面上,这是它的外观,而随之带来的内部的建筑设备也是大量的。为了提高设备利用率,合理地使用能源,加强对建筑设备状态的监视等,自然地就提出了楼宇自动化控制系统。下面我们将用ThingJS平台来模拟一个设备管理系统。 第一步,利用CampusBuilder搭建模拟场景。CampusBuilder的模型库有各种各样的模型,使我们搭建出的场景更逼真。使用CampusBuilde创建层级,之后再给层级加外立面就出现了当前的效果。详情移步:CampusBuilder3D场景制作工具 先看结果:演示地址 第二步,创建Equipment类,这里创建。switchControl方法主要一个完成一个计时器的功能来模拟设备警报。 class Equipment extends THING.Thing { constructor(app, name, obj, url) { super(app); this.name = name; this.obj = obj; this.url = url; this.interval = null; this.localPosition = [Math.floor(Math.random() * 7), 2.9, Math.floor(Math.random() * 7)]; } createSelf() { app.create({ type: 'Equipment', name: this.name, url: this.url, parent: this.obj, localPosition: this.localPosition, angle: 0 }); } switchControl(ev) { var flag; var equipment = app.query(this.name)[0]; app.level.change(equipment); if (ev) { this.interval = setInterval(function () { if (flag) { equipment.style.color = '#FF0000'; flag = false; } else { flag = true; equipment.style.color = ''; } }, 500); console.log(this.interval + "查看是否创建了定时器"); } else { console.log(this.interval); clearInterval(this.interval); if (equipment.style.color == '#FF0000') equipment.style.color = ''; } } } THING.factory.registerClass('Equipment', Equipment); 第三步,创建摄像机面板,烟感报警面板以及控制设备的开关,这里简单调整一下面板位置之后会增加两个创建设备的按钮。 //创建主面板 var panel1 = new THING.widget.Panel({ titleText: '摄像机列表', closeIcon: false, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, }); panel1.position = [80, 0]; var panel2 = new THING.widget.Panel({ titleText: '烟感报警列表', closeIcon: false, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, }); panel2.position = [80, 320]; // 创建任意对象 var dataObj1 = { open1: false, open2: false, open3: false, open4: false, } var dataObj2 = { open5: false, open6: false, open7: false, open8: false, }; // 动态绑定物体 var open1 = panel1.addBoolean(dataObj1, 'open1').caption('设备01'); var open2 = panel1.addBoolean(dataObj1, 'open2').caption('设备02'); var open3 = panel1.addBoolean(dataObj1, 'open3').caption('设备03'); var open4 = panel1.addBoolean(dataObj1, 'open4').caption('设备04'); var open5 = panel2.addBoolean(dataObj2, 'open5').caption('设备01'); var open6 = panel2.addBoolean(dataObj2, 'open6').caption('设备02'); var open7 = panel2.addBoolean(dataObj2, 'open7').caption('设备03'); var open8 = panel2.addBoolean(dataObj2, 'open8').caption('设备04'); 第四步,开启场景层级切换,创建摄像机和烟感报警器各四个,创建一个数字标识index和保存equipment对象的数组equipmentGroup。 //创建equipment对象数组,以及数组标识 var equipmentGroup = []; var index = 0; app.on('load', function (ev) { app.level.change(ev.campus); for (var i = 0; i < 8; i++) { var type = null; if (i < 4) { type = 'http://model.3dmomoda.cn/models/62A8A75C75044E6AB3D8463FA0CB67AF/0/gltf/'; } else { type = 'http://model.3dmomoda.cn/models/641A9B800DE5431E8C84DC290F8EFDE6/0/gltf/'; } var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type); equipment.createSelf(); equipmentGroup.push(equipment); } }); 第五步,为每个设备对应的创建控制开关。 open1.on('change', function (ev) { equipmentGroup[0].switchControl(ev); }); open2.on('change', function (ev) { equipmentGroup[1].switchControl(ev); }); open3.on('change', function (ev) { equipmentGroup[2].switchControl(ev); }); open4.on('change', function (ev) { equipmentGroup[3].switchControl(ev); }); open5.on('change', function (ev) { equipmentGroup[4].switchControl(ev); }); open6.on('change', function (ev) { equipmentGroup[5].switchControl(ev); }); open7.on('change', function (ev) { equipmentGroup[6].switchControl(ev); }); open8.on('change', function (ev) { equipmentGroup[7].switchControl(ev); }); 最后一步,创建两个按钮来控制创建设备。 new THING.widget.Button('创建烟感报警', function () { var type = 'http://model.3dmomoda.cn/models/641A9B800DE5431E8C84DC290F8EFDE6/0/gltf/'; var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type,); equipment.createSelf(); equipmentGroup.push(equipment); app.level.change(app.query(equipment.name)[0]); }); new THING.widget.Button('创建摄像头', function () { var type = 'http://model.3dmomoda.cn/models/62A8A75C75044E6AB3D8463FA0CB67AF/0/gltf/'; var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type,); equipment.createSelf(); equipmentGroup.push(equipment); app.level.change(app.query(equipment.name)[0]); }); 在编写过程还是走了不少弯路的,最主要的就是计时器的卸载问题,最初的版本写来写去发现不能控制警报的关闭,后来才 发现计时器没有卸载,警报不但不会关闭而且闪动的频率越来越快。更改之后创建了Equipment这个类来控制所有设备,通过创建这个类的对象给他赋id,父物体,模型地址。这里控制器在开关被触发的时候创建一个新的计时器并赋给的这个对象,再次触发时清除这个计时器,警报的动画就关闭了。演示地址 最后附上完整代码: /** * 说明:创建App,url为场景地址(可选) */ var app = new THING.App({ url: "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/CampusBuilder20181126134710", // 场景地址 "skyBox": "BlueSky" }); //创建主面板 var panel1 = new THING.widget.Panel({ titleText: '设备列表', closeIcon: false, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, titleImage: 'https://www.thingjs.com/static/images/example/icon.png' }); panel1.position = [80, 0]; var panel2 = new THING.widget.Panel({ titleText: '设备列表', closeIcon: false, // 是否有关闭按钮 dragable: true, retractable: true, opacity: 0.9, hasTitle: true, titleImage: 'https://www.thingjs.com/static/images/example/icon.png' }); panel2.position = [80, 320]; // 创建任意对象 var dataObj1 = { open1: false, open2: false, open3: false, open4: false, } var dataObj2 = { open5: false, open6: false, open7: false, open8: false, }; // 动态绑定物体 var open1 = panel1.addBoolean(dataObj1, 'open1').caption('设备01'); var open2 = panel1.addBoolean(dataObj1, 'open2').caption('设备02'); var open3 = panel1.addBoolean(dataObj1, 'open3').caption('设备03'); var open4 = panel1.addBoolean(dataObj1, 'open4').caption('设备04'); var open5 = panel2.addBoolean(dataObj2, 'open5').caption('设备01'); var open6 = panel2.addBoolean(dataObj2, 'open6').caption('设备02'); var open7 = panel2.addBoolean(dataObj2, 'open7').caption('设备03'); var open8 = panel2.addBoolean(dataObj2, 'open8').caption('设备04'); new THING.widget.Button('创建烟感报警', function () { var type = 'http://model.3dmomoda.cn/models/641A9B800DE5431E8C84DC290F8EFDE6/0/gltf/'; var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type,); equipment.createSelf(); equipmentGroup.push(equipment); app.level.change(app.query(equipment.name)[0]); }); new THING.widget.Button('创建摄像头', function () { var type = 'http://model.3dmomoda.cn/models/62A8A75C75044E6AB3D8463FA0CB67AF/0/gltf/'; var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type,); equipment.createSelf(); equipmentGroup.push(equipment); app.level.change(app.query(equipment.name)[0]); }); //创建equipment对象数组,以及数组标识 var equipmentGroup = []; var index = 0; app.on('load', function (ev) { app.level.change(ev.campus); for (var i = 0; i < 8; i++) { var type = null; if (i < 4) { type = 'http://model.3dmomoda.cn/models/62A8A75C75044E6AB3D8463FA0CB67AF/0/gltf/'; } else { type = 'http://model.3dmomoda.cn/models/641A9B800DE5431E8C84DC290F8EFDE6/0/gltf/'; } var equipment = new Equipment(app, 'equipment' + index++, app.query('floor' + (Math.floor(Math.random() * 5) + 1))[0], type); equipment.createSelf(); equipmentGroup.push(equipment); } open1.on('change', function (ev) { equipmentGroup[0].switchControl(ev); }); open2.on('change', function (ev) { equipmentGroup[1].switchControl(ev); }); open3.on('change', function (ev) { equipmentGroup[2].switchControl(ev); }); open4.on('change', function (ev) { equipmentGroup[3].switchControl(ev); }); open5.on('change', function (ev) { equipmentGroup[4].switchControl(ev); }); open6.on('change', function (ev) { equipmentGroup[5].switchControl(ev); }); open7.on('change', function (ev) { equipmentGroup[6].switchControl(ev); }); open8.on('change', function (ev) { equipmentGroup[7].switchControl(ev); }); }); class Equipment extends THING.Thing { constructor(app, name, obj, url) { super(app); this.name = name; this.obj = obj; this.url = url; this.interval = null; } createSelf() { app.create({ type: 'Equipment', name: this.name, url: this.url, parent: this.obj, localPosition: [Math.floor(Math.random() * 7), 2.9, Math.floor(Math.random() * 7)], angle: 0 }); } switchControl(ev) { var flag; var equipment = app.query(this.name)[0]; app.level.change(equipment); if (ev) { this.interval = setInterval(function () { if (flag) { equipment.style.color = '#FF0000'; flag = false; } else { flag = true; equipment.style.color = ''; } }, 500); console.log(this.interval + "查看是否创建了定时器"); } else { console.log(this.interval); clearInterval(this.interval); if (equipment.style.color == '#FF0000') equipment.style.color = ''; } } } THING.factory.registerClass('Equipment', Equipment);
新风系统是根据在密闭的室内一侧用专用设备向室内送新风,再从另一侧由专用设备向室外排出,在室内会形成“新风流动场”,从而满足室内新风换气的需要。实施方案是:采用高风压、大流量风机、依靠机械强力由一侧向室内送风,由另一侧用专门设计的排风风机向室外排出的方式强迫在系统内形成新风流动场。在送风的同时对进入室内的空气进过滤、消毒、杀菌、增氧、预热(冬天)。 接下来就用ThingJs平台来搭建一个新风系统第一步,利用CampusBuilder搭建模拟场景。CampusBuilder的模型库有各种各样的模型,使我们搭建出的场景更逼真。 1 //加载场景代码 2 var app = new THING.App({ 3 // 场景地址 4 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/新风演示2", 5 6 }); 第二步,创建三个数组来保存每个风的模型。 1 var hotWindGroup = []; 2 var coolWindGroup = []; 3 var newWindGroup = []; 4 var wind = null; 第三步,构造一个创建风的函数,为了方便创建风及其位置,我们选取排风设备为风的父物体,将创建出来的风的visiable属性设置为false(这里这个坐标问题可以看一下官网的教程中控制物体中的坐标转换)。 1 function createWind(parent, x, y, angle, localPosition, color, group) { 2 rs = app.query(parent)[0]; 3 wind = app.create({ 4 type: 'Thing', 5 name: 'hotWind', 6 url: 'http://model.3dmomoda.cn/models/4da706d8a37047298c0318a5b9546abd/0/gltf/', 7 localPosition: localPosition, 8 scale: [1, 2, 1], 9 angle: angle, 10 parent: rs, 11 }); 12 wind.style.color = color; 13 wind.visible = false; 14 wind.rotateX(x); 15 wind.rotateY(y); 16 group.push(wind); 17 } 第四步,开始创建风模型,并调整一下摄像机的角度及位置。 1 app.on('load', function () { 2 //摄像机角度 3 app.camera.position = [-22.91452445633646, 30.46296743148116, -23.83548169673341]; 4 app.camera.target = [-13.532807014407252, 5.6565539015865856, -3.3431546399681276]; 5 //hotWind 6 createWind('空调1', 0, 0, 0, [0, -2, 0], '#FF0000', hotWindGroup); 7 createWind('空调1', 0, 0, 0, [0, -2, 0.5], '#FF0000', hotWindGroup); 8 createWind('空调1', 0, 0, 0, [0, -2, 1], '#FF0000', hotWindGroup); 9 createWind('空调2', 0, 0, 0, [0, -2, 0], '#FF0000', hotWindGroup); 10 createWind('空调2', 0, 0, 0, [0, -2, 0.5], '#FF0000', hotWindGroup); 11 createWind('空调2', 0, 0, 0, [0, -2, 1], '#FF0000', hotWindGroup); 12 //coolWind 13 createWind('空调1', 0, 0, 0, [0, -2, 0], '#0000FF', coolWindGroup); 14 createWind('空调1', 0, 0, 0, [0, -2, 0.5], '#0000FF', coolWindGroup); 15 createWind('空调1', 0, 0, 0, [0, -2, 1], '#0000FF', coolWindGroup); 16 createWind('空调2', 0, 0, 0, [0, -2, 0], '#0000FF', coolWindGroup); 17 createWind('空调2', 0, 0, 0, [0, -2, 0.5], '#0000FF', coolWindGroup); 18 createWind('空调2', 0, 0, 0, [0, -2, 1], '#0000FF', coolWindGroup); 19 //newWind 20 createWind('排风1', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); 21 createWind('排风1', -50, 0, 0, [0, -0.5, 4], '#00FF00', newWindGroup); 22 createWind('排风1', -50, 0, 0, [0, -0.5, 6], '#00FF00', newWindGroup); 23 createWind('排风1', -50, 50, 50, [2, -0.5, 7], '#00FF00', newWindGroup); 24 createWind('排风1', -50, 50, 50, [4, -0.5, 8], '#00FF00', newWindGroup); 25 createWind('排风1', -50, 50, 50, [6, -0.5, 9], '#00FF00', newWindGroup); 26 createWind('排风1', -50, 50, 50, [8, -0.5, 12], '#00FF00', newWindGroup); 27 createWind('排风1', -50, 50, 50, [10, -0.5, 15], '#00FF00', newWindGroup); 28 createWind('排风1', -50, 50, 50, [12, -0.5, 18], '#00FF00', newWindGroup); 29 createWind('排风1', -50, 50, 50, [10, -0.5, 9], '#00FF00', newWindGroup); 30 createWind('排风1', -50, 50, 50, [14, -0.5, 9], '#00FF00', newWindGroup); 31 createWind('排风1', -50, 50, 50, [18, -0.5, 9], '#00FF00', newWindGroup); 32 createWind('排风1', -50, 50, 50, [22, -0.5, 9], '#00FF00', newWindGroup); 33 createWind('排风1', -50, 50, 50, [26, -0.5, 9], '#00FF00', newWindGroup); 34 createWind('排风2', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); 35 createWind('排风3', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); 36 createWind('排风4', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); 37 createWind('排风4', -50, 0, 0, [0, -0.5, 4], '#00FF00', newWindGroup); 38 createWind('排风4', -50, 0, 0, [0, -0.5, 6], '#00FF00', newWindGroup); 39 createWind('排风4', -50, 0, 0, [0, -0.5, 8], '#00FF00', newWindGroup); 40 createWind('排风4', -50, 0, 0, [0, -0.5, 10], '#00FF00', newWindGroup); 41 createWind('排风4', -50, 0, 0, [0, -0.5, 12], '#00FF00', newWindGroup); 42 createWind('排风4', -90, 50, 50, [2, -0.6, 12], '#00FF00', newWindGroup); 43 createWind('排风4', -90, 50, 50, [4, -0.7, 12], '#00FF00', newWindGroup); 44 createWind('排风4', -90, 50, 50, [6, -0.8, 13], '#00FF00', newWindGroup); 45 createWind('排风4', -90, 50, 90, [8, -0.8, 11], '#00FF00', newWindGroup); 46 createWind('排风4', -90, 50, 90, [12, -0.8, 9], '#00FF00', newWindGroup); 47 createWind('排风4', -90, 50, 90, [16, -0.8, 7], '#00FF00', newWindGroup); 48 createWind('排风4', -90, 50, 90, [20, -0.8, 5], '#00FF00', newWindGroup); 49 createWind('排风4', -90, 50, 90, [24, -0.8, 3], '#00FF00', newWindGroup); 50 createWind('排风4', -90, 0, 90, [8, -0.8, 13], '#00FF00', newWindGroup); 51 createWind('排风4', -90, 0, 90, [12, -0.8, 13], '#00FF00', newWindGroup); 52 createWind('排风4', -90, 0, 90, [16, -0.7, 13], '#00FF00', newWindGroup); 53 createWind('排风4', -90, 0, 90, [20, -0.6, 13], '#00FF00', newWindGroup); 54 //createWind('排风4', -90, 0, 90, [24, -0.5, 13], '#00FF00', newWindGroup); 55 56 //热风演示 57 new THING.widget.Button('热风演示', function () { 58 for (let i of coolWindGroup) { 59 i.visible = false; 60 }; 61 for (let i of hotWindGroup) { 62 i.visible = true; 63 }; 64 65 }); 66 67 //冷风演示 68 new THING.widget.Button('冷风演示', function () { 69 for (let i of coolWindGroup) { 70 i.visible = true; 71 }; 72 for (let i of hotWindGroup) { 73 i.visible = false; 74 }; 75 }); 76 77 //新风演示 78 new THING.widget.Button('新风演示', function () { 79 playNewWind(); 80 }); 81 82 function playNewWind() { 83 for (var i = 0; i < newWindGroup.length; i++) { 84 if(i==newWindGroup.length-1) 85 return; 86 newWindGroup[i].visible = true; 87 newWindGroup[i].moveTo({ 88 "time": 4000, 89 "position": newWindGroup[i+1].position, 90 }); 91 } 92 }); 第五步,运行项目。演示地址 思考与总结:首先就是空间坐标系下转父物体坐标真是弄晕了,看了官网的教程多少理解了一点。刚开始的时候我一直认为我的子物体以父物体坐标下放置的时候,子物体坐标轴的问题指向的问题。看了教程发现自己的担心多余了,就好像是人戴鸭舌帽帽子不管戴在谁的头上都一样。其次就是让风动起来,最初是想让一个模型在空间中运动,发现效果不好用。最后就创建了多个模型他们对应着有各自的点,让每一个模型向他下一个模型的位置移动实现运动。 完整代码 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/新风演示2", }); var hotWindGroup = []; var coolWindGroup = []; var newWindGroup = []; var wind = null; function createWind(parent, x, y, angle, localPosition, color, group) { rs = app.query(parent)[0]; wind = app.create({ type: 'Thing', name: 'hotWind', url: 'http://model.3dmomoda.cn/models/4da706d8a37047298c0318a5b9546abd/0/gltf/', localPosition: localPosition, scale: [1, 2, 1], angle: angle, parent: rs, }); wind.style.color = color; wind.visible = false; wind.rotateX(x); wind.rotateY(y); group.push(wind); } app.on('load', function () { //摄像机角度 app.camera.position = [-22.91452445633646, 30.46296743148116, -23.83548169673341]; app.camera.target = [-13.532807014407252, 5.6565539015865856, -3.3431546399681276]; //hotWind createWind('空调1', 0, 0, 0, [0, -2, 0], '#FF0000', hotWindGroup); createWind('空调1', 0, 0, 0, [0, -2, 0.5], '#FF0000', hotWindGroup); createWind('空调1', 0, 0, 0, [0, -2, 1], '#FF0000', hotWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 0], '#FF0000', hotWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 0.5], '#FF0000', hotWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 1], '#FF0000', hotWindGroup); //coolWind createWind('空调1', 0, 0, 0, [0, -2, 0], '#0000FF', coolWindGroup); createWind('空调1', 0, 0, 0, [0, -2, 0.5], '#0000FF', coolWindGroup); createWind('空调1', 0, 0, 0, [0, -2, 1], '#0000FF', coolWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 0], '#0000FF', coolWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 0.5], '#0000FF', coolWindGroup); createWind('空调2', 0, 0, 0, [0, -2, 1], '#0000FF', coolWindGroup); //newWind createWind('排风1', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); createWind('排风1', -50, 0, 0, [0, -0.5, 4], '#00FF00', newWindGroup); createWind('排风1', -50, 0, 0, [0, -0.5, 6], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [2, -0.5, 7], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [4, -0.5, 8], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [6, -0.5, 9], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [8, -0.5, 12], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [10, -0.5, 15], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [12, -0.5, 18], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [10, -0.5, 9], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [14, -0.5, 9], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [18, -0.5, 9], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [22, -0.5, 9], '#00FF00', newWindGroup); createWind('排风1', -50, 50, 50, [26, -0.5, 9], '#00FF00', newWindGroup); createWind('排风2', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); createWind('排风3', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 2], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 4], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 6], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 8], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 10], '#00FF00', newWindGroup); createWind('排风4', -50, 0, 0, [0, -0.5, 12], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 50, [2, -0.6, 12], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 50, [4, -0.7, 12], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 50, [6, -0.8, 13], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 90, [8, -0.8, 11], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 90, [12, -0.8, 9], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 90, [16, -0.8, 7], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 90, [20, -0.8, 5], '#00FF00', newWindGroup); createWind('排风4', -90, 50, 90, [24, -0.8, 3], '#00FF00', newWindGroup); createWind('排风4', -90, 0, 90, [8, -0.8, 13], '#00FF00', newWindGroup); createWind('排风4', -90, 0, 90, [12, -0.8, 13], '#00FF00', newWindGroup); createWind('排风4', -90, 0, 90, [16, -0.7, 13], '#00FF00', newWindGroup); createWind('排风4', -90, 0, 90, [20, -0.6, 13], '#00FF00', newWindGroup); //createWind('排风4', -90, 0, 90, [24, -0.5, 13], '#00FF00', newWindGroup); //热风演示 new THING.widget.Button('热风演示', function () { for (let i of coolWindGroup) { i.visible = false; }; for (let i of hotWindGroup) { i.visible = true; //playWind(i,[0,-0.6499999999999999,0],[0,-0.7234152255572697,0.46352549156242107],[0,-1.2683221215612903,1.2135254915624212],[0,-2.15,1.5]) }; }); //冷风演示 new THING.widget.Button('冷风演示', function () { for (let i of coolWindGroup) { i.visible = true; }; for (let i of hotWindGroup) { i.visible = false; }; }); //新风演示 new THING.widget.Button('新风演示', function () { playNewWind(); }); function playNewWind() { for (var i = 0; i < newWindGroup.length; i++) { if(i==newWindGroup.length-1) return; newWindGroup[i].visible = true; newWindGroup[i].moveTo({ "time": 4000, "position": newWindGroup[i+1].position, }); } } });
基于ThingJS平台开发的智慧楼宇3D可视化系统,是以3D虚拟化技术为基础,以数字化、可视化、智能化理念为目标,构建园区、楼宇、室内、智能设备的逐级可视;基于三维场景及集成的智能楼宇管理系统,以直观、动态的形式展示楼宇内所有智能设备的空间分布及工况;以高亮、动画的形式展示楼宇内业务管线流向及工作原理;以悬浮信息牌的形式展示楼宇内智能实时监测、告警信息;以图表、数据对比形式展示楼宇内综合能耗统计;对需控制或应急处理的智能设备可进行远程开关操作。 视频地址:https://www.bilibili.com/video/av37685869/
2018年10月29日重庆新闻联播《我市举行山城·2018大学城地区高校反恐实战演练》片段画面中,ThingJS 为实战演练提供了反恐3D可视化预案系统。 新闻视频:http://news.cctv.com/2018/10/29/VIDEJXPaPzKXoPDo9WG1uTRv181029.shtml 新闻画面 多警力综合应急预案3D可视化系统是以3D虚拟化技术为基础,以数字化、可视化、智能化、网络化、集成化的理念为目标,用户可以使用3D搭建工具,构建目标区域及周边商圈的园区、建筑、道路及特定的安保设备模型并实现逐级可视。 构建多警种综合应急预案3D可视化管理系统的目的,是要实现在同一平台管理及掌握预案区域内场景的全面状况,形象地再现物理场景内的道路、建筑分布及重点设施设备类型及分布。从园区、建筑、室内到重点设施设备,均按照其在真实世界中的位置和关系在计算机屏幕上展现,形象化展示预案区域内及周边环境情况,从而降低理解门槛,达到多警种综合应急预案管理效率的目的。 所以,深入理解“可视化”的概念,以及“可视化”在安防及消防管理领域的最佳运用方式,是本项目的关键成功因素。 可视化管理是指利用信息系统,让相关管理人员清晰直观地掌握组织有效信息,实现透明化与可视化的管理,尤其适用于运营管理过程,实现人员可视化,管理对象可视化,评测指标可视化,流程可视化等效果。可视化管理能让组织流程更加直观,使组织内部的信息易于表达、理解和传播,从而消除运营过程中不同角色之间的认知偏差和监管盲区,实现管理的透明化,提升过程可控度。概括起来说,可视化管理就是“将需管理的对象用一目了然的方式体现” 优锘科技官网:www.uinnova.cn ThingJS三维可视化开发平台:www.thingjs.com
小车行走路线演示New VS Old 刚接触ThingJS的时候,写的一个小车开进小区的演示,今天又看了教程中有movePath这个方法就重新写了一遍,其中也遇到了一些问题,尤其突出的问题就是小车过弯的尴尬表现。 先给大家看看Old版本,Old版演示地址。 再看看New版本,AE86过弯不再笨拙顺畅无比,New版演示地址。 第二张效果图上可以看到由点连起来的路线,我的AE86也是严格按照路线行进的,相比于Old版本过弯的时候舒畅多了。Old版本是用setTimout(),rotateY(),moveTo()方法设置了几个点跑出来的。New版本放弃了这种办法,这种过弯实在让人看着不舒服,也不符合现实世界(排除醉酒驾驶司机)。下面就说一下New版本。 第一步,进入ThingJS在线开发,将场景地址替换成我们今天要用的,也可以自己去搭一个场景,campusBuilder下载地址。 /** * 说明:创建App,url为场景地址(可选) */ var app = new THING.App({ //场景地址 url: 'http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/2018-10-30-17-20-23' }); 第二步,在模型库里挑选一个你喜欢的车,种类丰富任你选择。 app.create({ type: 'Car', name: 'Car01', angle: 90, // 旋转 //这里我选了一台黑车,没找到AE86 url: 'http://model.3dmomoda.cn/models/c6ed424627234a298c1921950eb8534c/0/gltf/', //位置 position: [-32, 0, 26.2], complete: function () { console.log('Black Car !'); } }); 第三步,创建小车的行进路线和演示按钮。 app.on('load', function () { // 创建blackCar行进路线 var points = []; for (var x = -32, y = 0, z = 26.2 ; x <= 28; x += 4) { points.push([x, y, z]); } var radius = 1.75; for (var degree = 0, y = 0; degree <= 180; degree += 20) { var x = Math.sin(degree * 2 * Math.PI / 360) * radius + 28.5; var z = Math.cos(degree * 2 * Math.PI / 360) * radius + 24.45; points.push([x, y, z]); } for (var x = 27.5, y = 0, z = 22.7 ; x >= -6; x -= 4) { points.push([x, y, z]); } for (var degree = 0, y = 0; degree >= -90; degree -= 20) { var x = Math.sin(degree * 2 * Math.PI / 360) * radius -5.5; var z = Math.cos(degree * 2 * Math.PI / 360) * radius + 20.95; points.push([x, y, z]); } for (var x = -7.2, y = 0, z = 20.25 ; z >= 6; z -= 2) { points.push([x, y, z]); } //创建轨迹线 line = app.create({ type: 'Line', color: 0xFFFF00, // 轨迹线颜色 dotSize: 2, // 轨迹点的大小 points: points, }) THING.widget.Button('车辆路线', play); }); 最后一步,让小车动起来创建play()方法。 function play() { var car = app.query('Car01')[0]; car.movePath({ 'path': line.points, // 轨迹路线 'time': 10000, // 移动时间 'orientToPath': true, }); app.camera.lookAt(car); // app.camera.followObject({ // object:car, // }); } 做到这里小车已经可以动起来了,但是这个视角并不好,下面介绍一下摄像机中的followObject(params)。API地址 创建一个Car类继承THING.BaseObject,将物体类型转换成Car类型 //创建Car类 class Car extends THING.BaseObject{ constructor(app) { super(app); } } THING.Utils.addCastType('Car', /Car/); THING.factory.registerClass('Car', Car); //加到play() app.camera.followObject({ object:car, }); 下图是加了摄像机跟随物体的效果短短80行代码,其中我也遇到了不少问题划重点的就是app.camera.followObject()中object的类型问题,还有就是模型库里有一些车的模型比较奇怪,比如下图。以下是完整代码 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p05lsWJZUIxnIWsNXAzJ40X8/scene/2018-10-30-17-20-23", //背景设置 }); // 创建Car app.create({ type: 'Car', name: 'Car01', angle: 90, // 旋转 url: 'http://model.3dmomoda.cn/models/c6ed424627234a298c1921950eb8534c/0/gltf/', //位置 position: [-32, 0, 26.2], complete: function () { console.log('Black Car !'); } }); //创建Car类 class Car extends THING.BaseObject{ constructor(app) { super(app); } } THING.Utils.addCastType('Car', /Car/); THING.factory.registerClass('Car', Car); person.on('load',function()) app.on('load', function () { // 创建blackCar行进路线 var points = []; for (var x = -32, y = 0, z = 26.2 ; x <= 28; x += 4) { points.push([x, y, z]); } var radius = 1.75; for (var degree = 0, y = 0; degree <= 180; degree += 20) { var x = Math.sin(degree * 2 * Math.PI / 360) * radius + 28.5; var z = Math.cos(degree * 2 * Math.PI / 360) * radius + 24.45; points.push([x, y, z]); } for (var x = 27.5, y = 0, z = 22.7 ; x >= -6; x -= 4) { points.push([x, y, z]); } for (var degree = 0, y = 0; degree >= -90; degree -= 20) { var x = Math.sin(degree * 2 * Math.PI / 360) * radius -5.5; var z = Math.cos(degree * 2 * Math.PI / 360) * radius + 20.95; points.push([x, y, z]); } for (var x = -7.2, y = 0, z = 20.25 ; z >= 6; z -= 2) { points.push([x, y, z]); } //创建轨迹线 line = app.create({ type: 'Line', color: 0xFFFF00, // 轨迹线颜色 dotSize: 2, // 轨迹点的大小 points: points, }) THING.widget.Button('车辆路线', play); }); function play() { var car = app.query('Car01')[0]; car.movePath({ 'path': line.points, // 轨迹路线 'time': 10000, // 移动时间 'orientToPath': true, }); //app.camera.lookAt(car); app.camera.followObject({ object:car, }); } --------------------- 作者:extends Thread 来源:CSDN 原文:https://blog.csdn.net/nioooom/article/details/84069266 版权声明:本文为博主原创文章,转载请附上博文链接!
【教程】ThingJS 3D开发快速入门 第一讲 开发概述·优势·项目流程 优酷:http://v.youku.com/v_show/id_XMzkwOTQ1MDYwMA==.html 腾讯视频:https://v.qq.com/x/page/z0784xo63h2.html 爱奇艺:http://www.iqiyi.com/w_19s3fktt4d.html bilibili:https://www.bilibili.com/video/av35528748
动画的播放和暂停是3D场景中常用的功能,本文以机柜为例为您讲解在thingjs中使用playAnimation(params)接口控制机柜门开启和关闭,同理,房门开关、电器运行停止等3D模型的动画启停均可以使用此方法。 用到的Thingjs API: 播放动画 playAnimation(params) 参数 Name Type Description name String 动画名 frames? Array 帧播放比例范围 speed? Number 播放速度 loop? Boolean 是否循环 loopType? String 循环类型 reverse? Boolean 是否倒播 Example obj.playAnimation('open1'); obj.playAnimation({ name: 'open1', reverse: true, }); obj.playAnimation({ name: 'open1', loop: true }); obj.playAnimation({ name: ['open1', 'open2'], loop: true, loopType: 'pingpong', speed: 0.4 }); obj.playAnimation({ name: ['open1'], frames: [0.25, 0.5], // 动画序列从 25% 播放到 50% (时间) loop: true, loopType: 'pingpong', speed: 0.4 }); 机柜示例 //加载场景代码 var app = new THING.App({ // 场景地址 "url": "http://www.thingjs.com/./uploads/wechat/oLX7p04daC2OdoZCbP6VihD_0XCo/scene/men", //背景设置 "skyBox" :"BlueSky" }); var l = null; // 初始化完成后进行场景层次管理注册操作 app.on('load', function (ev) { app.level.change(app.buildings[2]); var cabinet = app.query(/001/) cabinet.on('click',function() { if (l) l.playAnimation('close1'); this.playAnimation('open1'); l = this; }); }); ThingJS ™(www.thingjs.com):优锘科技开发的面向物联网的 3D 可视化 PaaS 开发平台.基于 WebGL 兼容各种浏览器及移动设备.零门槛、高效率、低成本开发各类 3D 应用。
将人物的位置信息在3D 场景中展现,让我们能够观察人员的实时位置及历史轨迹,为传统的人员位置信息提供三维可视化的展现方式。 通过ThingJS可以将人员的定位信息在3D场景中对应起来,进行描点,画线,并创建相关人物的模型,根据实时数据驱动人物行走。并且支持楼层内外的相结合。使我们能更加直观的了解人物所处位置及行走路线。 人物户外行走 方案:根据最新请求的位置数据与上一次请求所得的位置数据,可计算两个位置之间的距离,再根据两次请求数据的时差,即可得到人物行走的速度,使用api(请参考物体下延路径移动demo,http://www.thingjs.com/guide/?m=sample)驱动人物行走。 2. 人物即将进入建筑和进入建筑内 方案:当最新的人员位置数据在建筑内部时,隐藏建筑外立面(building.showFacades),显示人物即将进入该建筑的楼层(building.showFloors,详见http://www.thingjs.com/guide/?m=api 下 Building),并使用api驱动人物行走。 3. 人物上楼的效果 方案:当最新请求的人员位置数据在不同楼层时(比如:从一层到二层),首先显示该楼层并将所有楼层透明(透明便于观察,方法:floor.style.opacity = 0.5 --- 详见http://www.thingjs.com/guide/?m=api)。并使用api驱动人物行走。
随着flash的没落,浏览器的原生能力的兴起。在3D方面WebGL不管从功能还是性能方面都在逐渐加强。2D应用变为3D应用的需求也越来越强烈。 win10的画图板支持3D图片,2d工具photoshop也开始逐步集成了3D工具。 下面就基于WebGL技术探讨一下现在的两款3D框架。Threejs(http://threejs.org/) 目前最流行的开源3D框架,2009年4月诞生,2005年adobe收购了macromedia的flash产品,2008,2009年正是flash如日中天之时,threejs也识时务的选择了flash的ActionScript平台,后来flash没落之后选择了WebGL。 ThingJS(http://thingjs.com/) 新兴的3D框架,2018年诞生,是针对物联网领域的JavaScript 3D Library。它是由在3D领域经营多年的优锘科技公司研发,旨在简化3D应用开发。 设计角度 WebGL可以处理3D图像,听起来是非常高兴的一件事,但是WebGL实在是太底层了,WebGl解决是如何再画布上画图的问题,怎么画点,线,面,怎么上色,怎么贴图,怎么处理光线,视角转动之后怎么换算绘制等等。这些对于一个做3D应用的开发者来说要学的东西太多了。 Threejs库的出现解决了底层的渲染细节和复杂的数据结构,终于将复杂的底层细节抽象出来,使得大家开发3d应用更容易了一些。和很多开发者交流threejs都是他们首次接触的WebGL 3D库,并能很容易的就能开始做一些实验。 但是使用Threejs开发应用还是门槛很高,但就一个加载模型,调光,选择模型弹框的功能,就能干出Threejs上百行代码。同时还有很多复杂的3D概念需要理解。 这时就需要ThingJS了。 ThingJS是更为上层的抽象,不用关心,渲染,mesh,光线等复杂概念。它抽象是一个个具体的模型,ThingJS封装了对模型交互事件的各种api,比如单击,左键,鼠标滑过等,ThingJS封装了对模型的操作,例如移动,放大缩小,上色,勾边,甚至开门,ThingJS还封装了模型的层次关系,例如物体是放在某个房间里的,房间又在某个楼层,楼层又是某个大楼的。大楼在园区里。 编码对比 这里仅仅从3D模型加载这个小点进行对比说明。更多内容大家可参考各自的网站www.three.org 和 www.thingjs.com进行详细对比。 three的模型加载 function load3DModel(){ / 1、collada是一种基于XML的3D模型交互方案,简单来说,就是一种3D模型可以通过collada转换成另一种3D模型, 从而,各种3D模型都可以通过collada转换成web支持的3D模型。 2、。dae是一个钟3D模型的格式 3、加载时注意浏览器同源策略的限制 / var loader = new THREE.ColladaLoader(); loader.load( "./model/avatar.dae", function ( collada ) { //找到模型中需要的对象。将相机看向这个对象是为了让这个对象显示在屏幕中心 collada.scene.traverse( function ( child ) { if ( child instanceof THREE.SkinnedMesh ) { modelObj = child; camera.lookAt( child.position ); } } ); //将模型的场景加入到整体的场景 modelObj.material.opacity = 0.8; scene.add( collada.scene ); //每个模型都要添加到场景 //显示出模型的骨骼的代码,不需要可删去 var helper = new THREE.SkeletonHelper( modelObj ); helper.material.linewidth = 3; scene.add( helper ); } ); } threejs 加载模型到场景每个细节都得自己处理。 我们再来看下ThingJS的模型加载。 var app = new THING.App({ container: 'div3d', url: 'https://speech.uinnova.com/static/models/building' }); 只关注场景再页面的div的id和场景存放的地址,所有的细节ThingJS都处理好了。 场景加载完之后便可从场景获得加载内容,并进行交互应用开发。 // 获取建筑对象 var building = app.buildings[0]; // 打印建筑中所有的楼层 building.floors.forEach(function(floor) { console.log('Floor: ' + floor.id); }); // 获取室外对象 var outdoors = app.outdoors; // 打印室外所有物体 outdoors.things.forEach(function(thing) { console.log('Thing: ' + thing.id); }); 多么完美的封装方式。更多细节可以到www.thingjs.com查看 总结 three.js(www.three.org)和ThingJS(www.thingjs.com)都是JavaScript 3D Library,都对webGL的3D处理能力进行了封装,但是three.js 更偏三维技术底层,适用于3D爱好者学习3D技术;ThingJS更偏物联网应用功能开发,重在开发效率,降低开发成本,适合于使用3D技术做项目的开发者。 全文地址请点击:https://blog.csdn.net/liuwei000000/article/details/80623532?utm_source=copy