【threejs】从零到一的web3d时代
“未来早已到来,只是尚未流行。”——K.K
最近,由于业务的需求,笔者的团队终于迈进了3d时代。
其实,早在2017年,笔者便开始尝试前端的3d探索,因为当时主要的业务场景是运营活动,求新、创新便是活动的特性。不过,当时由于种种原因,最后未能落地,但未曾想到,会在3年后有了落地的时刻:
动态效果请查看这个demo: gis3dmodel
这也是笔者第一次在正式场景中做这种尝试,而且由于时效性特别强,中间过程伴随着各种各样的问题,尽管最终效果也没有尽善尽美,但也算是里程碑式的第一步,心想着记录下这次探索的积累,于是才有了此文。
其实,与当时相比,现在的业内的web3d环境已经有了很大的改观:前有U3D大量的商业案例成熟的开发模式,后有微软为babylon.js站台力挺,远不像当年只有3d领域three.js一家独奏的时代,不过比较下来,由于three.js海量的资料与文献,最终成为了笔者选择它作为团队撬开3d时代大门的钥匙(笔者这里直接略过了webgl,而直接选择了封装好一定功能上层库):
相比2d时代,除了原本的舞台(scene)、渲染器(renderer)之外,新加入了光源(light)、摄像头(camera),以用来在绘图区域,描述一个虚拟的3d场景。而原本在场景中的元素,也变得复杂了起来:有geometry来描述它们的几何形状,有material来描述他们的材质,然后共同作用与object3d及其子类上,最终成为虚拟3d空间中的2d或3d角色。
不过,要真正把three.js介绍完按笔者目前的熟悉程度,还是远远不够的,所以就以上做一个简单的介绍吧,然后回到笔者的实践中来。
在笔者的业务场景中,主要述求是需要在一个3d场景中基于GIS信息绘制2d地图对象,然后给它添加一些光柱、辉光等效果存在着一个和上图类似的基于GIS信息的2d地图对象,需要解决经纬度到虚拟三维空间坐标转换的问题,还有诸如渐变线条、镜头转动效果、辉光效果等等或模型上或交互上诸多问题的确认和解决,但整体来讲,其中最具有挑战性的却是一种新的工作流的建立:需要统一PM、UE/I、FE的想法,因为设计师提供的是一张静态的设计稿,而FE需要实现的则是包含比2d交互更多的3d交互效果(还有说镜头效果等)。
不过工作流的问题比较虚,还是简单整理点实际坑:
1.经纬度转换
其实拿到经纬度的时候,聪明的你一定会先有一个疑问,经纬度类似于球面坐标,你要在平面绘制,难道不需要先做墨卡托投影吗?笔者刚开始也有这个疑问,不过经过对数据源的了解,其实我们拿到的gis数据就是已经做过墨卡托投影的了,所以就不必画蛇添足呢。
另外,笔者需要将一连串经纬度数据,绘制到虚拟三维空间中去,而这些经纬度信息,由于本身的精度问题,直接绘制会导致整个地图在视觉上特别的小,那么如何解决呢?当然是做缩放咯。另外,笔者原以为需要对经纬度做球面坐标到平面坐标的转变,但是查阅资料后发现,并没有这个必要,于是就只需要进行缩放了,而缩放的逻辑也比较简单:
// 经纬度的最小值和最大值需在外层完成取值 function zoomXY(coords) { let per = (maxX - minX) / (maxY - minY); return coords.map(({ x, y }) => ({ x: (x - minX) / (maxX - minX) * 100, y: (y - minY) / (maxY - minY) * 100 * per })) }
笔者先找到这些经纬度的最大值与最小值,在把他们按照原本的xy比例映射到x总长为100的区间上去,对原本都在31.XXX的维度进行放大,让他们能够比较清晰的呈现在画布上。
2.渐变线绘制
这个就更简单了,因为笔者使用了业界一个泛用性比较多的库——threejs,在它的官方demo中便提供了一种线性材质LineMaterial,更够让使用者自己定义Line的颜色:
for (let i = 0, len = coords.length; i < len; i++) { const geometry = new LineGeometry(); const color = new THREE.Color(); const positions = []; const colors = []; for (let j = 0, _len = coords[i].length; j < _len; j++) { if (j < _len / 2) { color.setHSL(.56 + .05 * j / (coords[i].length / 2), 1, .49 + .01 * j / (coords[i].length / 2)); } else { color.setHSL(.61 - .05 * j / coords[i].length, 1, .5 - .01 * j / coords[i].length); } colors.push(color.r, color.g, color.b); positions.push(coords[i][j].x, coords[i][j].y, 0); } geometry.setPositions(positions); geometry.setColors(colors); const matLine = new LineMaterial({ color: 0xffffff, linewidth: LINEWIDTH, vertexColors: true, dashed: false, }); const line = new Line2(geometry, matLine);
值得注意的是,因为笔者的场景需要保证颜色的平滑过渡,所以笔者在前半段进行了颜色HSL值的递增,后半段进行递减,最终实现首尾闭合。同时,这个材质还有个特殊的地方是需要在RaF中进行逐帧更新:
matLine.resolution.set(this._dom.clientWidth, this._dom.clientHeight)
3.光柱光晕辉光等效果
这个就是比较基础的贴材质的功夫了,边缘的辉光笔者使用了Tween函数和scale去动态改变sprite的大小,做到让两个辉光能够跟着不规则路径进行“匀速运动”。另一方面,对于光晕和光柱则更简单了:
function getShiningCylinder(imgReousrce) { const texture = new THREE.TextureLoader().load(imgReousrce); texture.wrapS = THREE.RepeatWrapping; texture.repeat.set(10, 1); const geometry = new THREE.CylinderGeometry(radius, radius, height, 32, 1, true); const material = new THREE.MeshBasicMaterial({ color: 0xffff00, map: texture, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, transparent: true, opacity: 1, depthWrite: false, }) // 因为时间仓猝,并未进行类的抽象,而是直接以闭包实现的丑陋代码还请见谅 const cylinder = new THREE.Mesh(geometry, material); cylinder.rotation.x = Math.PI / 2; cylinder.position.set(0, 0, height / 2); cylinder.update = () => { cylinder._counter += .5; let angle = cylinder._counter * Math.PI / 180; cylinder._texture.offset.x = angle; if (cylinder.scale.y > 0) { cylinder.scale.x += 0.06; cylinder.scale.z += 0.06; cylinder.scale.y -= 0.005; cylinder.position.set(cylinder.position.x, cylinder.position.y, height / 2 * cylinder.scale.y); } else { cylinder.scale.set(1, 1, 1); cylinder.position.set(cylinder.position.x, cylinder.position.y, cylinder._orginZ); } } return cylinder; }
只要给一个开口的圆柱体贴上材质,然后逐帧改变它的位置信息,就能够实现。
不过,在整个过程中,笔者也发现threejs的文档体系整体并不是很完善,对于很多属性和方法的描述也不是很清楚,都需要开发者基于个人经验去做出决策,算是个不大不小的缺点。同时,由于threejs api的过于原子性特点,也让笔者产生了希望基于threejs打造一个小而美的3d引擎的想法。
与此同时,笔者在完成了团队3d可视化能力从0到1的突破后,也需要将这些经验的方法输送给团队的其他同学,而想到此处,笔者在激动之余,更深深的觉得:
“未来早已到来,只是尚未流行。”
写在最后
其实,本次的gis模型只是小试牛刀而已,紧接着就要去呈现数字城市了,其中笔者感觉又将扔掉多年的C捡了起来,开始拾掇GLSL,简单的来说,three.js提供的普通材质还不足以覆盖笔者面临的业务场景
图片来源于网络
更细致的纹理效果,很难依靠操作材质来实施,所以需要对纹理进行“定制”,而在web3d时代,webgl提供的定制材质的方式之一便是GLSL(OpenGL Shading Language),简单来说主要是使用着色器(shader),下面则是openGL中的两种着色器的工作流,主要就是从顶点着色器进行图元装配(primitive assembly),然后对它进行光栅化(Rasterization),之后再藉由片元着色器进行计算、混合、防抖,并进行一些必要裁剪(主要是超出显示区域的部分)。
最后,将数据推入帧缓冲区(frame buffer),再将它最终绘制到屏幕上。而GLSL正是能够通过C语言操作去顶点着色器和片元着色器,影响最终输出结果的语言,有了它才能够做出更精致的效果。
再写就扯远了,笔者团队对于web3d时代的探索才刚刚起步,希望有朝一日,也能够孵化出比肩业内优秀方案的web3d解决方案。