基于 HTML5 WebGL 构建智能数字化城市 3D 全景
前言
自 2011 年我国城镇化率首次突破 50% 以来,《新型城镇化发展规划》将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市。截至现今,全国 95% 的副省级以上城市、76% 的地级以上城市,总计约 500 多个城市提出或在建智慧城市。
基于这样的背景,本系统采用 Hightopo 的 HT for Web 产品来构造轻量化的 智慧城市 3D 可视化场景,通过三个角度的转换,更清晰让我们感知到 5G 时代下数字化智能城市的魅力
预览地址:HT 智慧城市
整体预览图
第一个视角下,城市以市中心为圆心缓缓浮现,市中心就如同整座城的大脑
第二个视角下,在楼房间穿过,细致的感受这城市的面貌
第三个视角下,鸟瞰整座城,体会智慧城市带来的不可思议的欣喜
是不是觉得有些神奇,我们接下来就是对项目的具体分析,手把手教你如何搭建一个自己心中的梦想城市
场景搭建
该系统中的大部分模型都是通过 3dMax 建模生成的,该建模工具可以导出 obj 与 mtl 文件,在 HT 中可以通过解析 obj 与 mtl 文件来生成 3D 场景中的所有复杂模型,(当然如果是某些简单的模型可以直接使用 HT 来绘制,这样会比 obj 模型更轻量化,所以大部分简单的模型都是采用 HT for Web 产品轻量化 HTML5/WebGL 建模的方案)我们先看下项目结构,源码都在 src 文件夹中
storage 保存的便是 3D 场景文件。 index.js 是 src 下的入口文件,创建了一个 由 main.js 中导出的 Main 类,Main 类创建了一个 3D 画布,用于绘制我们的 3D 场景,如下
1 import event from '../util/NotifierManager';
2 import Index3d from './3d/Index3d';
3 import { INDEX, EVENT_SWITCH_VIEW } from '../util/constant';
4
5 export default class Main {
6 constructor() {
7 let g3d = this.g3d = new ht.graph.Graph3dView(),
8
9 //将3d图纸添加到dom对象中
10 g3d.addToDOM();
11
12 this.event = event;
13 //创建一个Index3d类,作为场景初始化
14 this.index3d = new Index3d(g3d);
15 //调用switch方法派发EVENT_SWITCH_VIEW事件,并传入事件类型 INDEX
16 this.switch(INDEX);
17 }
18 switch(key = INDEX) {
19 event.fire(EVENT_SWITCH_VIEW, key);
20 }
21 //
22 }
我们用 new ht.graph.Graph3dView() 的方式创建了一个 3D 画布,画布的顶层是 canvas 。并创建了一个 index3d 对象,看到后面我们就能知道其实这一步就如同我们把场景“画”上去。在 main 对象中我们还引用了 util 下的 NotifierManager 文件,这个文件中的 event 对象为穿插在整个项目中事件总线,使用了 HT 自带的事件派发器,可以很方便的手动的订阅事件和派发事件,感兴趣可以进一步了解 HT 入门手册 ,下面便是文件内容
1 class NotifierManager {
2 constructor() {
3 this._eventMap ={};
4 }
5
6 add(key, func, score, first = false) {
7 let notify = this._eventMap[key];
8 if (!notify) notify = this._eventMap[key] = new ht.Notifier();
9
10 notify.add(func, score, first);
11 }
12
13 remove(key, func, score) {
14 const notify = this._eventMap[key];
15 if (!notify) return;
16
17 notify.remove(func, score);
18 }
19
20 fire(key, e) {
21 const notify = this._eventMap[key];
22 if (!notify) return;
23
24 notify.fire(e);
25 }
26 }
27
28 const event = new NotifierManager();
29 export default event;
notify.fire() 和 notify.add() 分别是派发和订阅事件,类似于设计模式中的订阅者模式,我们很清楚的能看到,NotifierManager 类就是对 HT 原有的派发器做了一个简单地封装 ,并在创建 main 对象的时候,调用event.fire() 自动派发了 EVENT_SWITCH_VIEW 这一事件并且传入了事件类型 Index 。
画布我们有了,接下来我们就应在画布上“画”上我们的 3D 场景了。上面我们也说过了这一步由 new Index3d() 实现的, 那么它是如何实现 “画” 这一步骤的呢?
我们看看较为重要的两个文件 ui 文件夹下的 Index3d 文件和 View 文件,两个文件分别导出了 Index3d 和 View 两个类, Inde3d 类继承于 View 类,我们先来看一下 View 类的实现
1 import event from "../util/NotifierManager";
2 import util from '../util/util';
3 import { EVENT_SWITCH_VIEW } from "../util/constant";
4
5 export default class View {
6 constructor(view) {
7 this.url = '';
8 this.key = '';
9 this.active = false;
10 this.view = view;
11 this.dm = view.dm();
12
13 event.add(EVENT_SWITCH_VIEW, (key) => {
14 this.handleSwitch(key);
15 });
16 }
17 handleSwitch(key) {
18 if (key === this.key) {
19 if (!this.active) {
20 this.active = true;
21 this.onUp();
22 }
23 this.dm.clear();
24 util.deserialize(this.view, this.url, this.onPostDeserialize.bind(this));
25 }
26 // 目前是这个场景,执行 tearDown
27 else if (this.active) {
28 this.onDown();
29 this.active = false;
30 }
31 }
32 /**
33 * 加载这个场景前调用
34 */
35 onUp() {
36 }
37 /**
38 * 离开这个场景时会调用
39 */
40 onDown() {
41 }
42 /**
43 * 加载完场景处理
44 */
45 onPostDeserialize() {
46 console.log(this)
47 }
48
49 }
其它内容我们就不做过多阐述了,主要说一下我们加载场景使用的 deserialize 方法,我们打开 util 下的 util 文件找到这个方法
1 deserialize: (function() {
2 let cacheMap = {};
3 /**
4 * 加载 json 并反序列化
5 *
6 */
7 return function(view, url, cb, notUseCache) {
8 let json, cache = !notUseCache;
9 if (!notUseCache) {
10 json = cacheMap[url];
11 }
12 else {
13 cache = false;
14 }
15 // 不使用缓存,重新加载
16 view.deserialize(json || url, (json, dm, view, list) => {
17 cacheMap[url] = json;
18 cb && cb(json, dm, view, list, cache);19 }
20})()
其中的 view 就是传入的我们之前创建的 g3d 画布,它上面有个 deserialize 方法,用来反序列化我们的 json 格式的场景文件。可能这个时候大家会发问了,明明之前提到场景文件的是 obj 和 mtl 文件,怎么现在又成了 json 了。不要急,要明白这些我们得先了解一下 HT 的其它基础知识
大家肯定对一些其它框架的设计模式有所了解,像早期 JAVA/Spring 的 mvc ,vue 的 mvvm 等,而 HT 的整体框架类似于 mvp 或 mvvm 模式,采用了统一的 DataModel 数据模型和 SelectionModel 选择模型来驱动所有的 HT 视图组件。HT 官方更愿意把这个模式称之为 ovm 即 Object Vue Mapping。基于这样的设计,用户只需掌握统一的数据接口,就能熟练地使用 HT 了,并不会因为增加了视图组件带来额外的学习成本,这也是为什么 HT 容易上手的原因。
说完这个我们在来谈谈上面 3D 场景文件格式的问题,HT 给我们提供了 ht.JSONSerialize 对象让我们可以对 DataModel 进行 json 格式的序列化和反序列化,而上面的 3D 场景 json 文件就是对我们 3D 模型序列化之后的文件,调用 g3d.deserialize 方法将反序列化的对象加进 DataModel 中,那么我们的画布就会根据传入的 DataModel 绘制出我们的场景了。
那么接下来我们只要重写 Inded3d 类上的 onPostDeserialize 方法,即绘制完场景之后的回调。就能对我们主场景进行基本操作了。
视角转换动画
首先,我们先完成的是三个视角转换的动画
我们直接写在 util 文件当中 ,给它添加一个方法 moveEveAction。方法传入了三个参数,首先是我们的画布 g3d,第二个参数就是我们的视角对象,它记录了每一步转换的初始视角和结束视角。第三个参数是为了衔接每一步视角转换,让其有一个过渡的动画而传入的一个函数 cover
1 moveEyeAction: function(g3d,moveEyeConfig,cover){
2 if (!moveEyeConfig) return;
3 let moveEye = function(obj,time,eas = 'liner'){
4 return new Promise((res,rej) => {
5 g3d.setEye(obj.initEye);
6 g3d.setCenter(obj.initCenter);
7 g3d.moveCamera(obj.moveEye,obj.moveCenter, {
8 duration:time,
9 easing: function(t){
10 if(t < 0.5){
11 cover(t,'up');
12 }
13 if (eas === 'ease-in'){
14 return t * t;
15 }
16 else if (eas === 'liner'){
17 return t
18 }
19 else {
20 return t
21 }
22 },
23 finishFunc: ()=>{
24 cover(1,'down');
25 res(time);
26 }
27 });
28 })
29 }
30
31 moveEye(moveEyeConfig[0],moveEyeConfig[0].time,moveEyeConfig[0].eas)
32 .then((res)=>{
33 console.log(1)
34 return moveEye(moveEyeConfig[1],moveEyeConfig[1].time,moveEyeConfig[1].eas)
35 })
36 .then((res)=>{
37 moveEye(moveEyeConfig[2],moveEyeConfig[2].time,moveEyeConfig[2].eas)
38 )}
39})
我们在函数中创建了一个方法 moveEye,它创建并返回了一个 promise ,方便我们做回调,防止出现回调地狱的情况。然后我们只要提前先配置好每一步的视角,传入函数中,函数便会依次调用 g3d 上的 moveCamera 方法,在每一步动画结束的时候,调用 cover 函数作为过渡。
我们再来看一下 cover 函数的实现,在 3D 场景初始化时便会调用下方的 create2dCover 方法创建 cover,其实就是在最外层盖上了一层 div ,每一步动画结束的时候,根据传入的参数决定是否变暗完成过渡
1create2dCover(){
2 let div = document.createElement("div");
3 div.style.position = 'absolute';
4 div.style.background = 'black';
5 div.style.opacity = 0;
6 div.style.top = '0';
7 div.style.right = '0';
8 div.style.bottom = '0';
9 div.style.left = '0';
10 div.style.pointerEvents = 'none';
11 document.body.appendChild(div);
12 let dire = 'up';
13 let cover = function(t,direction,num){
14 if (direction === 'up' && dire === 'down'){
15 div.style.opacity = 1- t * 4;
16 if (t > 0.5) dire = 'up';
17 }
18 if (direction === 'down' && dire === 'up'){
19 if (t === 1) {
20 div.style.opacity = t;
21 dire = 'down';
22 }
23 }
24 }
25 return cover;
26}
我们再来看一下动画效果
第一个视角下的建筑浮现动画
我们先看下 Index3d 类的实现,再加载完场景的时候,我们便会调用上面我们说过的视角转换函数 moveEyeAction , 和我们接下来要讲的城市浮现函数 upCityDemo。
1 onPostDeserialize(json, dm, view) {
2 const g3d = this.view;
3 g3d.setFar(100000);
4 const nodeUpArr1 = [], nodeUpArr2 = [], nodeUpArr3 = [];
5 //视角配置参数
6 const moveEyeConfig = [{
7 initEye:[-700,390,-974],
8 initCenter:[-1596,25,-518],
9 moveEye:[-2572, 390, -974],
10 moveCenter:[-1596,25,-518],
11 time: 9000,
12 eas: 'ease-in'
13 },{
14 initEye:[1500,71,900],
15 initCenter:[-1823,25,-636],
16 moveCenter:[-1823,25,-636],
17 moveEye:[-1678, 18, -558],
18 time:8000
19 },{
20 initEye:[2491,600,-1026],
21 initCenter:[0,0,0],
22 moveEye:[-3105, 500, -1577],
23 moveCenter:[-1034, -12, -41],
24 time:8000
25 }]
26 //创建一个蒙板div并返回cover函数
27 let cover = this.create2dCover();
28 //浮现城市的属性初始化
29 dm.each(fnode => {
30 //第一批楼房-市中心
31 if (fnode.getDisplayName() === "up1"){
32 fnode.a('startE',fnode.getElevation());
33 fnode.setElevation(-200);
34 nodeUpArr1.push(fnode);
35 }
36 //第二批城市-市中心附近建筑
37 if (fnode.getDisplayName() === "up2"){
38 fnode.a('startE',fnode.getElevation())
39 fnode.setElevation(-100);
40 nodeUpArr2.push(fnode);
41 }
42 //第三批城市-外围建筑
43 if (fnode.getDisplayName() === "up3"){
44 fnode.a('startE',fnode.getElevation())
45 fnode.setElevation(-100);
46 nodeUpArr3.push(fnode);
47 }
48
49 if(fnode.getDisplayName() === '飞光组'){
50 fnode.eachChild(node => {
51 node.s('shape3d.opacity',0);
52 })
53 }
54})
55
56 //视角开始变换
57 util.moveEyeAction(g3d,moveEyeConfig,cover)
58 //城市浮现
59 let upCityDemo = function(nodeArr,time,T = 0.6){
60 return new Promise((res,rej)=>{
61 ht.Default.startAnim({
62 duration:time,
63 action: (v,t) => {
64 nodeArr.forEach((node)=>{
65 if(t > T) res('已完成');
66 let org = node.getElevation();
67 let tar = node.a('startE');
68 node.setElevation(org + (tar - org) * v)
69 })
70 }
71 })
72 })
73 }
74
75 upCityDemo(nodeUpArr1,11000,0.4).then((res)=>{
76 // console.log(res)
77 return upCityDemo(nodeUpArr2,2000,0.4)
78 }).then((res)=>{
79 return upCityDemo(nodeUpArr3,2000);
80 }).then((res)=>{
81 //城市出现,开始动画
82 //this.startAnimation(g3d,dm);
83 })
84}
首先我们将城市分别分为三批放入不同的数组中,然后类似的,创建了 upcityDemo 并返回了一个 promise,我们只需要调用并传入每批城市节点,它们便会依次执行建筑上升。还有一点要提的是这里动画用的是 HT 提供的动画函数 ht.Default.startAnim 。这里我们简单介绍一下,HT 提供了 Frame-Based 和 Time-Based 两种动画方式,根据是否设置了 frames 和 interval 属性来决定是哪种方式。 第一种方式用户通过指定 frames 动画帧数, 以及 interval 动画帧间隔参数控制动画效果。 第二种 Time-Based 用户只需要指定 duration 的动画周期的毫秒数即可,HT 将在指定的时间周期内完成动画, 值得一提的是不同于 Frame-Based 方式有明确固定的帧数即 action 函数被调用的次数,Time-Based 方式的帧数或 action 函数被调用次数取决于系统环境 (类似于 setinterval 和 requestAnimate 的区别)
我们先看下动画效果,第一步视角下的动画转换我们就算完成了
贯穿全部视角下的动画
我们所有的动画和上面一样通过 ht.Default.startAnim 函数实现,我们只需要将不同的动画函数放入 action 中,并通过控制它们不同的步数就能实现不一样的速度效果。
我们共有五个动画效果,旋转动画可以归为一类
· 建筑下的水波扩散动画
· 风车,建筑底下光圈旋转动画
· 道路偏移动画
· 市中心上方光线流动动画
· 建筑上面的数字飞光动画
1 ht.Default.startAnim({
2 frames: Infinity,
3 interval: 20,
4 action: () => {
5 //扩散水波动画
6 waveScale(scaleList,dltScale,maxScale,minScale);
7 //风车旋转,建筑底下光圈旋转
8 rotationAction(roationFC,dltRoattion);
9 rotationAction(roationD,dltRoattionD);
10 rotationAction(roationD2,-dltRoattionD2);
11 //道路偏移
12 uvFlow(roadSmall,dltRoadSmall);
13 uvFlow(roadMedium,dltRoadMedium);
14 uvFlow(roadBig,dltRoadBig);
15 //光亮建筑下的数字飞光
16 numberArr.forEach((node,index)=>{
17 blockFloat(node,numFloadDis);
18 })
19 //市中心上方亮线的流动
20 float.eachChild(node => {
21 let offset = node.s('shape3d.uv.offset') || [0, 0];
22 node.s('shape3d.uv.offset', [offset[0] + 0.05, offset[1]]);
23 })
24 }
25 });
我们先讲前面四种较为简单动画的实现,像市中心上方亮线的流动动画逻辑简单,我们就直接写在了 action 函数中,每一步控制 x 方向上的贴图偏移即可
其它动画我们都封装为了对应的函数,如下
1 //道路偏移动画
2 //定义三种道路的步进
3 const dltRoadSmall = 0.007, dltRoadMedium = 0.009, dltRoadBig = 0.01;
4 //获取三种道路节点
5 let roadSmall = dm.getDataByTag('roadSmall');
6 let roadMedium = dm.getDataByTag('roadMedium');
7 let roadBig = dm.getDataByTag('roadBig');
8 let float = dm.getDataByTag('float');
9 //定义偏移动画函数
10 let uvFlow = function(obj,dlt){
11 let offset = obj.s('all.uv.offset') || [0, 0];
12 obj.s('all.uv.offset', [offset[0] + dlt, offset[1]]);
13 }
14
15 //水波缩放动画
16 //定义扩大范围和每步扩大速度
17 const maxScale = 1.5, dltScale = 0.06;
18 //获取缩放节点
19 let scaleList = dm.getDataByTag('scale');
20 //定义缩放函数
21 let waveScale = function(obj, dlt, max, min){
22 obj.eachChild(node => {
23 // 扩散半径增加
24 if (!node.a('max')) node.a('max', node.getScaleX() + max);
25 if (!node.s('shape3d.opacity')) node.s('shape3d.opacity',1);
26 let s = node.getScaleX() + dlt;
27 let y = node.getScale3d()[1]
28 let opa = node.s('shape3d.opacity') - 0.02;
29 // 扩散半径大于最大值的时候,重置为最小值,透明度设为1
30 if (s >= node.a('max')){
31 opa = 1;
32 s = 0;
33 }
34 // 设置x,y,z方向的缩放值
35 node.s('shape3d.opacity',opa)
36 node.setScale3d(s, y, s);
37 });
38 }
39 //旋转图元
40 //定义三种不同旋转图元数组和旋转速度
41 const roationFC = [], roationD = [], roationD2 = [], dltRoattionD = Math.PI / 90, dltRoattionD2 = Math.PI / 60, dltRoattion = Math.PI / 30;
42 //获取所有旋转图元并分别放入数组中
43 let roationFCDatas = dm.getDataByTag('roationFC');
44 let roationdDatas = dm.getDataByTag('di');
45 roationFCDatas.eachChild(node =>{
46 node.eachChild(node => {
47 if (node.getDisplayName() === '风机叶片'){
48 roationFC.push(node);
49 }
50 })
51 });
52 roationdDatas.eachChild(node => {
53 if (node.getDisplayName() === '底'){
54 roationD.push(node)
55 }
56 if (node.getDisplayName() === '底2'){
57 roationD2.push(node)
58 }
59 });
60 //定义旋转函数
61 let rotationAction = function(obj,dlt){
62 obj.forEach(node => {
63 if (node.getDisplayName() === '风机叶片'){
64 //获得当前旋转角度
65 let rotationZ = node.getRotation3d()[2];
66 //每步增加dlt
67 node.setRotation3d([0,0,rotationZ + dlt]);
68 }
69 if (node.getDisplayName() === '底' || node.getDisplayName() === '底2'){
70 //获得当前旋转角度
71 let rotationY = node.getRotation3d()[1];
72 //每步增加dlt
73 node.setRotation3d([0,rotationY + dlt,0]);
74 }
75 })
76 }
写完之后我们再看一下动画效果
最后就是我们的稍微繁琐一点的数字飞光动画了。每座城市上方都有不同的六条飞光,我们需要每次都是随机出现两条,并且每条的速度都是不一样的。和之前的动画一样的,我们先获取所有的飞光节点并分类好,如下
1 //数字浮动
2 let numberArr, numFloadDis = 15, numFloatDlt = 0.07;
3 numberArr = new Array(28);
4 for (let i = 0;i < 28; i++){
5 numberArr[i] = new Array(6)
6 }
7 //产生两个随机数,并以数组形式返回
8 let randerdom2 = function(){
9 let num1 = Math.floor(Math.random() * 3);
10 let num2 = Math.floor((Math.random() * 3 + 3));
11 return [num1,num2];
12 }
13 //将所有的浮动数字按城市分组添加进数组
14 let i = 0,j=0;
15 dm.each(node => {
16 if (node.getDisplayName() === '飞光组'){
17 node.eachChild(node => {
18 node.s('shape3d.opacity',0);
19 node.setElevation(0);
20 numberArr[i][j++] = node;
21 })
22 j=0;
23 i++;
24 }
25 });
26 //属性初始化
27 let initArrAtr = function(){
28 for (let i = 0; i < numberArr.length; i++){
29 for (let j = 0; j < numberArr[i].length; j++){
30 //每条数字的随机数度
31 numberArr[i][j].a('randomSpeed', (numFloatDlt * 100 + Math.floor(Math.random() * 5))/100);
32 //控制每条数字是否停止上升
33 numberArr[i][j].a('stop',false);
34 //每栋楼上的已升起的飞光数量
35 numberArr[i].comNum = 0;
36 //每栋楼层当前的两条飞光
37 numberArr[i].one = randerdom2()[0];
38 numberArr[i].two = randerdom2()[1];
39 }
40 }
41 }
42 initArrAtr();
43 //重置单楼属性
44 let czArr = function(singleRoom){
45 //每栋楼上的已升起的数量
46 singleRoom.comNum = 0;
47 //重新随机设置每栋楼层出现的两条飞光
48 singleRoom.one = randerdom2()[0];
49 singleRoom.two = randerdom2()[1];
50 //设置飞光的随机速度
51 singleRoom.forEach((node, index)=>{
52 node.a('stop',false);
53 node.a('randomSpeed', (numFloatDlt * 100 + Math.floor(Math.random() * 5))/100);
54 })
55 }
当初始属性都设置完成后就该定义我们的动画函数了
1 let blockFloat = function(obj, dis){
2 //获取当前建筑
3 let allNumArr = obj;
4 //获取当前建筑出现的两条飞光
5 let floatArr = [allNumArr[allNumArr.one],allNumArr[allNumArr.two]];
6 let lth = floatArr.length;
7 //遍历并控制这两条飞光及动画
8 for (let j = 0; j < lth; j++){
9 let node = floatArr[j];
10 //如果当前飞光已停则停止此条飞光下一步动画
11 if (node.a('stop')) continue;
12 //获得当前飞光初始高度如果没有则手动设置当前为初始高度
13 let startE = node.a('startE');
14 if (startE == null) node.a('startE', startE = node.getElevation());
15 // 获得当前飞光速度和透明度值
16 let dlt = node.a('randomSpeed');
17 let float = node.a('float') || 0;
18 let opa = node.s('shape3d.opacity') || 0,
19 opaDlt = 0.01;
20
21 node.setElevation(startE + dis * float);
22 //上升的高度到达一定值设置透明度为1
23 if (float > 8){
24 node.s('shape3d.opacity',1)
25 opaDlt = -0.02
26 }
27 //上升的高度到达最高则让当前建筑飞光到达数量加一,并停止进一步上升
28 if (float > 12){
29 allNumArr.comNum ++;
30 node.a('stop',true);
31 node.a('float', 0);
32 node.setElevation(startE);
33 node.s('shape3d.opacity',0);
34 //当前建筑飞光到达数量到达两条,重置建筑上所有飞光属性
35 if (allNumArr.comNum === 2){
36 czArr(allNumArr);
37 }
38 continue;
39 }
40 float += dlt;
41 opa += opaDlt;
42 node.s('shape3d.opacity',opa)
43 node.a('float', float);
44 }
45 }
我们看下效果
到这,我们所有的动画就已经写完了。还等什么呢,一起来创建一个属于你自己心中理想的智能化城市吧
(ps: 不仅如此,HT官网中 还包含了数百个工业互联网 2D 3D 可视化应用案例,点击这里体验把玩:http://www.hightopo.com/demos/index.html)
基于 HTML5 WebGL 构建智能数字化城市 3D 全景的更多相关文章
- 基于 HTML5 WebGL 构建智能城市 3D 场景
前言 随着城市规模的扩大,传统的方式很难彻底地展示城市的全貌,但随着 3D 技术的应用,出现了 3D 城市群的方式以动态,交互式地把城市全貌呈现出来.配合智能城市系统,通过 Web 可视化的方式,使得 ...
- 基于 HTML5 WebGL 构建 3D 智能数字化城市全景
前言 自 2011 年我国城镇化率首次突破 50% 以来,<新型城镇化发展规划>将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市.截至现今,全国 ...
- 基于 HTML5 WebGL 和 VR 技术的 3D 机房数据中心可视化
前言 在 3D 机房数据中心可视化应用中,随着视频监控联网系统的不断普及和发展, 网络摄像机更多的应用于监控系统中,尤其是高清时代的来临,更加快了网络摄像机的发展和应用. 在监控摄像机数量的不断庞大的 ...
- 基于 HTML5 WebGL 的智慧城市(一)
前言 中共中央.国务院在今年12月印发了<长江三角洲区域一体化发展规划纲要>(下文简称<纲要>),并发出通知,要求各地区各部门结合实际认真贯彻落实. <纲要>强调, ...
- 基于 HTML5 WebGL 的加油站 3D 可视化监控
前言 随着数字化,工业互联网,物联网的发展,我国加油站正向有人值守,无人操作,远程控制的方向发展,传统的人工巡查方式逐渐转变为以自动化控制为主的在线监控方式,即采用数据采集与监控系统 SCADA.SC ...
- 基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用
基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用 前言 在目前大数据时代背景之下,数据可视化的需求也变得越来越庞大,在数据可视化的背景之下,通过智能机器间的链接并最终将人机链接 ...
- 基于 HTML5 WebGL 的挖掘机 3D 可视化应用
前言 在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求.如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因 ...
- 基于HTML5 WebGL的工业化3D电子围栏
前言 现代工业化的推进在极大加速现代化进程的同时也带来的相应的安全隐患,在传统的可视化监控领域,一般都是基于 Web SCADA 的前端技术来实现 2D 可视化监控,本系统采用 Hightopo 的 ...
- 基于 HTML5 + WebGL 的 3D 可视化挖掘机
前言 在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求.如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因 ...
随机推荐
- Pashmak and Graph(dp + 贪心)
题目链接:http://codeforces.com/contest/459/problem/E 题意:给一个带权有向图, 找出其中最长上升路的长度. 题解:先按权值对所有边排序, 然后依次 u -& ...
- 痞子衡嵌入式:飞思卡尔Kinetis系列MCU启动那些事(10)- KBOOT特性(可靠升级)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔Kinetis系列MCU的KBOOT之可靠升级(Reliable Update)特性. 所谓可靠升级机制,即在更新Applica ...
- API的描述语言--Swagger
Swagger是一种Rest API的表示方式. 有时也可以作为Rest API的交互式文档,描述形式化的接口描述,生成客户端和服务端的代码. 一,描述语言:Spec Swagger API Spec ...
- Mysql 存储过程声明及使用
存储过程(Stored Procedure):是一组用于完成特定数据库功能的sql语句集,该sql语句集经过编译后存储在数据库系统中,在使用的时候,用户通过调用指定已经定义好的存储过程并执行它,从而完 ...
- 树莓派4B安装netcore
准备材料 SDFormatter.exe ---格式化SD卡,空的SD就可以不用了 2019-09-26-raspbian-buster.img ---下载好树莓派系统镜像 win32diskimag ...
- 在UEFI+GPT下使用rEFind实现Win10 + Kali2.0 双引导
转载自:在UEFI+GPT下使用rEFind实现Win10 + Kali2.0 双引导 https://www.linuxidc.com/Linux/2016-07/133717.htm
- Spring与Shiro整合 登陆操作
Spring与Shiro整合 登陆操作 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 编写登陆Controller方法 讲解: 首先,如果你登陆失败的时候,它会把你的异常信息丢到 ...
- Ajax与Http协议
目录 Ajax与Http协议详解 Xhr对象 xhr对象发送请求整体感知 xhr对象的常用属性和方法 xhr对象发送post请求 xhr对象的兼容性问题 请求超时timeout与监听超时ontimeo ...
- ASP.NET Core 中的 ObjectPool 对象重用(一)
前言 对象池是一种设计模式,一个对象池包含一组已经初始化过且可以使用的对象,而可以在有需求时创建和销毁对象.池的对象可以从池中取得对象,对其进行操作处理,并在不需要时归还给池子而非直接销毁他,他是一种 ...
- Java核心技术第八章-泛型
摘要 本文根据<Java核心技术 卷一>一书的第八章总结而成,部分文章摘抄书内,作为个人笔记. 文章不会过于深入,望读者参考便好. 为什么要使用泛型程序设计 泛型程序设计(Generic ...