基于 HTML5 WebGL 构建智能数字化城市 3D 全景
<p><strong>前言</strong></p><p>自 2011 年我国城镇化率首次突破 50% 以来,《新型城镇化发展规划》将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市。截至现今,全国 95% 的副省级以上城市、76% 的地级以上城市,总计约 500 多个城市提出或在建智慧城市。</p>
<p>基于这样的背景,本系统采用 Hightopo 的 HT for Web 产品来构造轻量化的 智慧城市 3D 可视化场景,通过三个角度的转换,更清晰让我们感知到 5G 时代下数字化智能城市的魅力</p>
<p>预览地址:HT 智慧城市</p>
<p>整体预览图</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191211173814774-1822546470.gif"></p>
<p>第一个视角下,城市以市中心为圆心缓缓浮现,市中心就如同整座城的大脑</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191211174403865-1236649754.gif"></p>
<p>第二个视角下,在楼房间穿过,细致的感受这城市的面貌</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191211174907278-317375886.gif"></p>
<p> 第三个视角下,鸟瞰整座城,体会智慧城市带来的不可思议的欣喜</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191211175438311-1003262590.gif"></p>
<p> 是不是觉得有些神奇,我们接下来就是对项目的具体分析,手把手教你如何搭建一个自己心中的梦想城市</p>
<p><strong>场景搭建</strong></p>
<p>该系统中的大部分模型都是通过 3dMax 建模生成的,该建模工具可以导出 obj 与 mtl 文件,在 HT 中可以通过解析 obj 与 mtl 文件来生成 3D 场景中的所有复杂模型,(当然如果是某些简单的模型可以直接使用 HT 来绘制,这样会比 obj 模型更轻量化,所以大部分简单的模型都是采用 HT for Web 产品轻量化 HTML5/WebGL 建模的方案)我们先看下项目结构,源码都在 src 文件夹中</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191212092014116-699629866.png"></p>
<p> storage 保存的便是 3D 场景文件。 index.js 是 src 下的入口文件,创建了一个 由 main.js 中导出的 Main 类,Main 类创建了一个 3D 画布,用于绘制我们的 3D 场景,如下</p>
<div class="cnblogs_code">
<pre> 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 }</pre>
</div>
<p>我们用 new ht.graph.Graph3dView() 的方式创建了一个 3D 画布,画布的顶层是 canvas 。并创建了一个 index3d 对象,看到后面我们就能知道其实这一步就如同我们把场景“画”上去。在 main 对象中我们还引用了 util 下的 NotifierManager 文件,这个文件中的 event 对象为穿插在整个项目中事件总线,使用了 HT 自带的事件派发器,可以很方便的手动的订阅事件和派发事件,感兴趣可以进一步了解 HT 入门手册 ,下面便是文件内容</p>
<div class="cnblogs_code">
<pre> 1 class NotifierManager {
2 constructor() {
3 this._eventMap ={};
4 }
5
6 add(key, func, score, first = false) {
7 let notify = this._eventMap;
8 if (!notify) notify = this._eventMap = new ht.Notifier();
9
10 notify.add(func, score, first);
11 }
12
13 remove(key, func, score) {
14 const notify = this._eventMap;
15 if (!notify) return;
16
17 notify.remove(func, score);
18 }
19
20 fire(key, e) {
21 const notify = this._eventMap;
22 if (!notify) return;
23
24 notify.fire(e);
25 }
26 }
27
28 const event = new NotifierManager();
29 export default event;</pre>
</div>
<p>notify.fire() 和 notify.add() 分别是派发和订阅事件,类似于设计模式中的订阅者模式,我们很清楚的能看到,NotifierManager 类就是对 HT 原有的派发器做了一个简单地封装 ,并在创建 main 对象的时候,调用event.fire() 自动派发了 EVENT_SWITCH_VIEW 这一事件并且传入了事件类型 Index 。</p>
<p>画布我们有了,接下来我们就应在画布上“画”上我们的 3D 场景了。上面我们也说过了这一步由 new Index3d() 实现的, 那么它是如何实现 “画” 这一步骤的呢?</p>
<p>我们看看较为重要的两个文件 ui 文件夹下的 Index3d 文件和 View 文件,两个文件分别导出了 Index3d 和 View 两个类, Inde3d 类继承于 View 类,我们先来看一下 View 类的实现 </p>
<div class="cnblogs_code">
<pre> 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 }</pre>
</div>
<p>其它内容我们就不做过多阐述了,主要说一下我们加载场景使用的 deserialize 方法,我们打开 util 下的 util 文件找到这个方法</p>
<div class="cnblogs_code">
<pre> 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;
11 }
12 else {
13 cache = false;
14 }
15 // 不使用缓存,重新加载
16 view.deserialize(json || url, (json, dm, view, list) => {
17 cacheMap = json;
18 cb && cb(json, dm, view, list, cache);19 }
20})()</pre>
</div>
<p>其中的 view 就是传入的我们之前创建的 g3d 画布,它上面有个 deserialize 方法,用来反序列化我们的 json 格式的场景文件。可能这个时候大家会发问了,明明之前提到场景文件的是 obj 和 mtl 文件,怎么现在又成了 json 了。不要急,要明白这些我们得先了解一下 HT 的其它基础知识</p>
<p>大家肯定对一些其它框架的设计模式有所了解,像早期 JAVA/Spring 的 mvc ,vue 的 mvvm 等,而 HT 的整体框架类似于 mvp 或 mvvm 模式,采用了统一的 DataModel 数据模型和 SelectionModel 选择模型来驱动所有的 HT 视图组件。HT 官方更愿意把这个模式称之为 ovm 即 Object Vue Mapping。基于这样的设计,用户只需掌握统一的数据接口,就能熟练地使用 HT 了,并不会因为增加了视图组件带来额外的学习成本,这也是为什么 HT 容易上手的原因。</p>
<p>说完这个我们在来谈谈上面 3D 场景文件格式的问题,HT 给我们提供了 ht.JSONSerialize 对象让我们可以对 DataModel 进行 json 格式的序列化和反序列化,而上面的 3D 场景 json 文件就是对我们 3D 模型序列化之后的文件,调用 g3d.deserialize 方法将反序列化的对象加进 DataModel 中,那么我们的画布就会根据传入的 DataModel 绘制出我们的场景了。</p>
<p>那么接下来我们只要重写 Inded3d 类上的 onPostDeserialize 方法,即绘制完场景之后的回调。就能对我们主场景进行基本操作了。</p>
<p><strong>视角转换动画</strong></p>
<p>首先,我们先完成的是三个视角转换的动画</p>
<p>我们直接写在 util 文件当中 ,给它添加一个方法 moveEveAction。方法传入了三个参数,首先是我们的画布 g3d,第二个参数就是我们的视角对象,它记录了每一步转换的初始视角和结束视角。第三个参数是为了衔接每一步视角转换,让其有一个过渡的动画而传入的一个函数 cover</p>
<div class="cnblogs_code">
<pre> 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,moveEyeConfig.time,moveEyeConfig.eas)
32 .then((res)=>{
33 console.log(1)
34 return moveEye(moveEyeConfig,moveEyeConfig.time,moveEyeConfig.eas)
35 })
36 .then((res)=>{
37 moveEye(moveEyeConfig,moveEyeConfig.time,moveEyeConfig.eas)
38 )}<br>39})</pre>
</div>
<p>我们在函数中创建了一个方法 moveEye,它创建并返回了一个 promise ,方便我们做回调,防止出现回调地狱的情况。然后我们只要提前先配置好每一步的视角,传入函数中,函数便会依次调用 g3d 上的 moveCamera 方法,在每一步动画结束的时候,调用 cover 函数作为过渡。</p>
<p>我们再来看一下 cover 函数的实现,在 3D 场景初始化时便会调用下方的 create2dCover 方法创建 cover,其实就是在最外层盖上了一层 div ,每一步动画结束的时候,根据传入的参数决定是否变暗完成过渡 </p>
<div class="cnblogs_code">
<pre> 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}</pre>
</div>
<p>我们再来看一下动画效果</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216115949310-1054262521.gif"> <img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216115915631-161253642.gif"> <img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216115921321-487879091.gif"></p>
<p><strong>第一个视角下的建筑浮现动画</strong></p>
<p>我们先看下 Index3d 类的实现,再加载完场景的时候,我们便会调用上面我们说过的视角转换函数 moveEyeAction , 和我们接下来要讲的城市浮现函数 upCityDemo。</p>
<div class="cnblogs_code">
<pre> 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:,
15 initCenter:[-1823,25,-636],
16 moveCenter:[-1823,25,-636],
17 moveEye:[-1678, 18, -558],
18 time:8000
19 },{
20 initEye:,
21 initCenter:,
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}</pre>
</div>
<p>首先我们将城市分别分为三批放入不同的数组中,然后类似的,创建了 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 的区别)</p>
<p>我们先看下动画效果,第一步视角下的动画转换我们就算完成了</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216120251644-1410164506.gif"></p>
<p><strong>贯穿全部视角下的动画</strong></p>
<p>我们所有的动画和上面一样通过 ht.Default.startAnim 函数实现,我们只需要将不同的动画函数放入 action 中,并通过控制它们不同的步数就能实现不一样的速度效果。</p>
<p>我们共有五个动画效果,旋转动画可以归为一类</p>
<p><strong>· </strong>建筑下的水波扩散动画</p>
<p><strong>· </strong>风车,建筑底下光圈旋转动画</p>
<p><strong>· </strong>道路偏移动画</p>
<p><strong>· </strong>市中心上方光线流动动画</p>
<p><strong>· </strong>建筑上面的数字飞光动画</p>
<div class="cnblogs_code">
<pre> 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') || ;
22 node.s('shape3d.uv.offset', + 0.05, offset]);
23 })
24 }
25 });</pre>
</div>
<p>我们先讲前面四种较为简单动画的实现,像市中心上方亮线的流动动画逻辑简单,我们就直接写在了 action 函数中,每一步控制 x 方向上的贴图偏移即可</p>
<p>其它动画我们都封装为了对应的函数,如下</p>
<div class="cnblogs_code">
<pre> 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') || ;
12 obj.s('all.uv.offset', + dlt, offset]);
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()
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();
66 //每步增加dlt
67 node.setRotation3d();
68 }
69 if (node.getDisplayName() === '底' || node.getDisplayName() === '底2'){
70 //获得当前旋转角度
71 let rotationY = node.getRotation3d();
72 //每步增加dlt
73 node.setRotation3d();
74 }
75 })
76 }</pre>
</div>
<p>写完之后我们再看一下动画效果</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216141021745-1901159598.gif"></p>
<p>最后就是我们的稍微繁琐一点的数字飞光动画了。每座城市上方都有不同的六条飞光,我们需要每次都是随机出现两条,并且每条的速度都是不一样的。和之前的动画一样的,我们先获取所有的飞光节点并分类好,如下</p>
<div class="cnblogs_code">
<pre> 1 //数字浮动
2 let numberArr, numFloadDis = 15, numFloatDlt = 0.07;
3 numberArr = new Array(28);
4 for (let i = 0;i < 28; i++){
5 numberArr = 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 ;
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 = 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.length; j++){
30 //每条数字的随机数度
31 numberArr.a('randomSpeed', (numFloatDlt * 100 + Math.floor(Math.random() * 5))/100);
32 //控制每条数字是否停止上升
33 numberArr.a('stop',false);
34 //每栋楼上的已升起的飞光数量
35 numberArr.comNum = 0;
36 //每栋楼层当前的两条飞光
37 numberArr.one = randerdom2();
38 numberArr.two = randerdom2();
39 }
40 }
41 }
42 initArrAtr();
43 //重置单楼属性
44 let czArr = function(singleRoom){
45 //每栋楼上的已升起的数量
46 singleRoom.comNum = 0;
47 //重新随机设置每栋楼层出现的两条飞光
48 singleRoom.one = randerdom2();
49 singleRoom.two = randerdom2();
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 }</pre>
</div>
<p>当初始属性都设置完成后就该定义我们的动画函数了</p>
<div class="cnblogs_code">
<pre> 1 let blockFloat = function(obj, dis){
2 //获取当前建筑
3 let allNumArr = obj;
4 //获取当前建筑出现的两条飞光
5 let floatArr = ,allNumArr];
6 let lth = floatArr.length;
7 //遍历并控制这两条飞光及动画
8 for (let j = 0; j < lth; j++){
9 let node = floatArr;
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 }</pre>
</div>
<p>我们看下效果</p>
<p><img src="https://img2018.cnblogs.com/i-beta/1496396/201912/1496396-20191216141309789-655369347.gif"></p>
<p>到这,我们所有的动画就已经写完了。还等什么呢,一起来创建一个属于你自己心中理想的智能化城市吧</p>
<p>(ps: 不仅如此,HT官网中 还包含了数百个工业互联网 2D 3D 可视化应用案例,点击这里体验把玩:http://www.hightopo.com/demos/index.html)</p><br><br>
来源:https://www.cnblogs.com/xhload3d/p/12068027.html
頁:
[1]