基于HTML5 WebGL 构建智能数字化城市 3D 全景 前言
年我国城镇化率首次突破 50% 以来,《新型城镇化发展规划》将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市。截至现今,全国 95% 的副省级以上城市、76% 的地级以上城市,总计约 500 多个城市提出或在建智慧城市。
第一个视角下,城市以市中心为圆心缓缓浮现,市中心就如同整座城的大脑
第二个视角下,在楼房间穿过,细致的感受这城市的面貌
第三个视角下,鸟瞰整座城,体会智慧城市带来的不可思议的欣喜
是不是觉得有些神奇,我们接下来就是对项目的具体分析,手把手教你如何搭建一个自己心中的梦想城市
场景搭建
该系统中的大部分模型都是通过 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事件,并传入事件类型 INDEX16 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 // 目前是这个场景,执行 tearDown27 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 t21 } 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