最近要作一个基站站点的可视化呈现项目。 咱们首先尝试的是三维的可视化技术来程序,可是客户反馈的状况是他们的客户端电脑比较差,性能效率都会很差,甚至有的仍是云主机。 所以咱们先作了一个性能比较极致的3Ddemo,以下图所示:
前端
为了可以尽量的性能最优,因此想了各类性能优化手段。固然效果上也会有折扣,这个demo与咱们自己的一些产品好比3D机房等相比较,效果上面确定有了很大的差距。不过性能方面仍是很不错的。
然而,很不幸,客户在拿到demo测试以后,不满意...。性能还算凑合,但他们还以为效果不够酷。node
配置很低,又要性能高、又要效果炫。这只能化为一句话:
程序员
彷佛陷入了绝境...
然而 绝处每每逢生,绝处每每有新的但愿、新的机会。算法
忽然想到的是2.5D,这是一种伪3D效果,可是只能体现一个镜头角度的显示效果,不能实现镜头的旋转效果。性能优化
其实在很早的时候,咱们就有一些2.5D的雏形的东西,好比分层拓扑图和2.5D节点。分层拓扑图甚至能够追溯到Java时代。以下图所示:架构
把以前的2.5D源代码拿过来读一遍。读了以后,总的思路:主要经过拼凑三个平行四边形来模拟这种3D的效果,技术没有体系。布局
这种思路对于对象的位置定位和对齐会比较难,开发难度自己也比较大,另外要实现一些好的效果,难度也比较大,要知道客户对于效果的要求并不低。性能
所以须要想出新的技术思路,最好是有成体系的思路,要摆脱以前的技术思路。固然并不容易,当时我并无什么好的思路,有不少疑惑,有不少迷茫。以后的不少天里面,都是这种状态。测试
事情的起色在一次出差。优化
在拜访一个大客户回酒店的路上,我走在马路上,个人脑中忽然蹦出一个想法,为何不借助3D的思路和部分算法呢,2.5D要呈现的不就是3D的效果吗?所谓2.5D,顾名思义,就是取几勺2D技术,再取几勺3D的技术,一块儿放到锅里炒一炒,为啥要局限在2D的技术。
我自己研究3D技术不少年,对于3D的相关技术也算是很熟练,忽然,彷佛全部的事情的想通了,一套成体系的2.5D技术开始在心中生根,发芽,生长。
个人心里很欣喜。(可是表面很平静)
这个事情告诉咱们一个道理,弄不懂的问题,不要死抠,多出去走走,说不定就想通了。😄
接下来,我自信满满的和客户沟通,开始着手写相关的技术验证demo,其中涉及到一些技术会在后面说明。demo最终获得了客户的承认,最终咱们也拿下了这个项目。
而于我,本身创造了一套2.5D相关的技术体系,也算是一个小小的成就吧。
这是一次创做,而创做是让人愉悦的事情。
所谓的2.5D,就是经过2D绘制技术,实现3D的渲染效果。而这其中,势必须要用到一部分3D的技术:
为了可以实现2.5D的效果,咱们须要把原来的平面二维空间延伸到三维立体空间。三维立体空间中存在着X、Y、Z三个坐标轴,比原来的二维空间多出了一个Z坐标轴。
固然,三维空间定义是为了模型定义、模型位置定位和后续的投影算法。最终的绘制仍是会回到二维空间进行。
在真正的三维中,须要经过obj等模型文件来定义模型。 在2.5D中,只须要定义一个立方体的模型便可。 前面说过,2.5D只是呈现了三维对象的某个角度的一个面,所以其模型只须要这个面的一张图片便可,图片就是模型。
之因此要定义一个立方体的模型,是为了图片可以摆在合适的位置,以及约束合适的大小和长宽比。 这对于模型的摆放和对齐有很重要的意义。立方体在这里就相似真实模型的包围体。
经过指定宽、高、深等属性,即可以定义一个立方体。代码以下所示:
setSize3: function(w, h, d) { var oldValue = { w: this._width3, h: this._height3, d: this._depth3, }; this._width3 = w; this._height3 = h; this._depth3 = d; this.firePropertyChange('size3', oldValue, { w: w, h: h, d: d }); },
同时能够指定立方体的三维坐标位置,代码以下:
setPosition: function(x, y, z) { var oldValue = this.getPosition(); this._position = { x: x, y: y, z: z, }; this.firePropertyChange('position', oldValue, this._position); },
投影算法是三维图形学中很重要的一环。 投影算法主要有透视投影算法和平行投影算法。 2.5D中须要使用的是平行投影(也只能使用平行投影算法)
投影算法算是比较关键的一步。
要定义投影算法,咱们首先要模拟一个平行镜头,经过平行镜头定义镜头的位置,角度等,并由这些参数定义出一个投影的矩阵:
/** * 计算变换矩阵,变换矩阵由镜头参数决定 */ calMVMatrix: function() { var angle = this.getAngle3(), vAngle = this.getVAngle3(), radius = this.getRadius3(), viewMatrix = mat4.create(), projectMatrix = mat4.create(), mvMatrix = mat4.create(), winWidth = 1, winHeight = 1; mat4.lookAt( viewMatrix, [ radius * Math.cos(vAngle) * Math.sin(angle), -radius * Math.sin(vAngle), radius * Math.cos(vAngle) * Math.cos(angle), ], [0, 0, 0], [0, 1, 0] ); mat4.ortho( projectMatrix, -winWidth / 2, winWidth / 2, -winHeight / 2, winHeight / 2, 0.1, 1000 ); mat4.multiply(mvMatrix, projectMatrix, viewMatrix); this.mvMatrix = mvMatrix; },
上述代码中,定义投影矩阵使用了gl-matrix.js这个包。
在定义了投影矩阵以后,即可以经过投影算法计算出立方体上面每一个顶点在平面坐标上的位置:
/** * 布局,前面四个点 p1 - p4, 后面 四个点p 5 - p8 * * p8 p7 * * p5 p6 * * p4 p3 * * p1 p2 * */ var points1 = [ { x: -w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p1 { x: w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p2 { x: w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p3 { x: -w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: d3 / 2 + pos.z, }, // p4 { x: -w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p5 { x: w3 / 2 + pos.x, y: -h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p6 { x: w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p7 { x: -w3 / 2 + pos.x, y: h3 / 2 + pos.y, z: -d3 / 2 + pos.z, }, // p8 ]; var points = (this._points = []); points1.forEach(function(point) { var newPoint = self.getPositionByRotate( point, pos, rotationX, rotationY, rotationZ ); points.push({ x: newPoint[0], y: newPoint[1], z: newPoint[2], }); }); var ps = (this._projectPoints = points.map(function(point) { return self.getProjectionPoint(point); }));
有了8个顶点的投影点以后,能够绘制边框效果、能够绘制颜色填充效果,也能够绘制图片填充的效果。
把几个面的点按照顺序组织起来,便可以绘制边框的效果。 以下代码所示:
drawPoints: function (ctx, points, close, dash, fill, borderColor, image) {
if (!points || points.length == 0) {
return;
}
ctx.beginPath();
ctx.strokeStyle = "black";
if (borderColor) {
ctx.strokeStyle = borderColor;
}
ctx.lineWidth = 1;
ctx.fillStyle = 'rgb(102,204,255)';
if (dash) {
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
} else {
ctx.setLineDash([1, 0]);
}
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i++) { var p = points[i]; ctx.lineTo(p.x, p.y); } if (close) { ctx.lineTo(points[0].x, points[0].y); } ctx.closePath(); ctx.stroke();
}
最终的绘制效果以下图所示:
要绘制填充颜色的立方体,只须要在上边的绘制中加上这行代码便可:
if (fill) { ctx.fill(); // drawImageInPoints(ctx, image, points); }
最终的绘制效果以下:
绘制图片的时候,并不须要每一个面都去绘制图片,只须要把图片绘制到立方体投影的8个顶点所占据的区域里面,须要作到的是,其8个顶点的位置正好和图片的顶点重合,好比下图:
首先计算出投影顶点所占据的二维区域大小:
/** * 根据points中的8个点,找出包裹8个点的最小rect * * @param {Array} points - 8个点的2d坐标 * @returns {Object} - rect */ getRect: function(points) { var minX, minY, maxX, maxY; points.forEach(function(point) { if (minX == null) { minX = maxX = point.x; minY = maxY = point.y; } else { minX = Math.min(minX, point.x); maxX = Math.max(maxX, point.x); minY = Math.min(minY, point.y); maxY = Math.max(maxY, point.y); } }); return { x: minX, y: minY, width: maxX - minX, height: this.getElement().getClient('reflect') ? (maxY - minY) * 2 : maxY - minY, }; },
而后在该区域直接绘制图片:
ctx.drawImage( image, 0, image.height - 20, image.width, 20, rect.x, rect.y + rect.height - 20, rect.width, 20 );
最终绘制效果以下图:
有了立方体模型以后,即可以搭建地面 墙面场景效果,因为地面、墙面均可以使用立方体来组成。 所以能够很方便的搭建出来,只须要把相关的立方体模型设置好尺寸,添加到场景中便可:
var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', centerLocation: { x: 300, y: 200 }, width: 800 / 1, height: 360 /.775, }); node1.setImage(null); node1.setPosition(00,0,100); node1.setWidth3(1000); node1.setHeight3(10); node1.setDepth3(1200); // node1.setStyle('top.image','image0'); // ToDo 定义样式规则 // node1.setStyle('top.image.rule','pattern'); // node1.setClient('receiveShadow',true); box.add(node1); var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', }); node1.setImage(null); node1.setPosition(-250,155,-500); node1.setWidth3(500); node1.setHeight3(300); node1.setDepth3(1); box.add(node1); var node1 = new twaver.Node2_5({ styles: { 'body.type': 'vector', }, name: 'TWaver', }); node1.setImage(null); node1.setPosition(250,105,-500); node1.setWidth3(500); node1.setHeight3(200); node1.setDepth3(1); node1.setStyle('front.image','weilan'); // ToDo 定义样式规则 box.add(node1);
最终的显示效果以下:
对于地面的贴图和墙面的光照效果,会在后续讲解。
第一弹讲述到这里,先上一张总体的效果瞅瞅:
欢迎关注公众号“ITman彪叔”。彪叔,拥有10多年开发经验,现任公司系统架构师、技术总监、技术培训师、职业规划师。在计算机图形学、WebGL、前端可视化方面有深刻研究。对程序员思惟能力训练和培训、程序员职业规划有浓厚兴趣。