three.js使用的人太少了,一个博文就几百个人看,之前发js基础哪怕是d3都会有几千的阅读量,看看以后考虑说一说d3了,哈哈。吐槽完毕回归正题。前几天郭先生看到网上有人开发了3D机房,正愁博客没什么写的,于是昨天熬夜也做了一个,今天就把大体的流程告诉萌新们,先说说主要功能模块。
- 墙体、地面、窗户以及门的实现(双击门禁门可开关)。
- 机柜实现(机柜门的开关、机箱的推拉以及开关推拉的条件)。
- 机箱存储占用比率(用颜色表示占用率,并附颜色谱图)。
- 监控摄像视角(包括监控摄像机的模型导入,和四视角监控)。
- 红外防控报警。
- 强弱电线的铺设。
- 以及风向。
下面对应这7个功能模块附图。
看图是不是感觉很好呢?不过细分下来每个点都是十分简单的。那么我们就按照模块分析一下。
1. 墙体、地面、窗户以及门的实现
这一块主要就是对于3d空间位置的理解,旋转的使用以及uv的使用。考虑到墙面和窗户代码的重复使用,这里封装一下。封装完以后,改变更加灵活方便,门的旋转参见之前发的博客模拟门转动。
1. 墙的实现
这里我们看下墙的数据,数组的每一项就是一面墙(这里我要求每一面墙最多只能有一个门位和窗户位,如果想两个窗户,那么就在原本的一面墙上设置两个数组),s表示墙的size,p表示墙的position(这里用不到选不考虑旋转),hasDoor表示有门,ds表示门的size,dp表示门的position,hasWindow表示是否有窗,ws表示窗的size,wp表示窗的position,是不是挺简单的(当然每个人设计的都不相同)。这样绘制出来的图,就如我上面发的图。下面上代码
var wallArr = [ {s: [1, 20, 61], p: [45, 10, 0], dir: 'z', hasDoor: true, ds: [1, 18, 18], dp: [45, 9, 15], hasWindow: true, ws: [1, 10, 24], wp: [45, 10, -13]}, {s: [1, 20, 61], p: [-45, 10, 0], dir: 'z', hasDoor: false, hasWindow: true, ws: [1.2, 10, 50], wp: [-45, 10, 0]}, // {s: [46, 20, 1], p: [22.5, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [22.5, 10, 30]}, // {s: [46, 20, 1], p: [-22.5, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [-22.5, 10, 30]}, // {s: [46, 20, 1], p: [22.5, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [22.5, 10, -30]}, // {s: [46, 20, 1], p: [-22.5, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [30, 10, 1], wp: [-22.5, 10, -30]}, {s: [91, 20, 1], p: [0, 10, 30], dir: 'x', hasDoor: false, hasWindow: true, ws: [80, 10, 1], wp: [0, 10, 30]}, {s: [91, 20, 1], p: [0, 10, -30], dir: 'x', hasDoor: false, hasWindow: true, ws: [80, 10, 1], wp: [0, 10, -30]} ];
就是一个这样的数组。我们将注释打开,并注释掉后两行,得到如图效果。
ok测试没有问题。
再说说墙的实现,这里使用了ThreeBSP,之前我也说过这个东西,它可以实现几何体的二元操作(A与B的和、A与B的差,A与B的交集)。这个东西我们用来在墙体中扣出窗户和门的位置。
2. 门的实现
接下来说一说门的纹理,ps一张门的图,记得将底图加上颜色和透明度,门把手不加透明,导出png,然后制作材质记得加上transparent。这里会有一个问题,那就是uv,因为boxGeometry各个面的uv都是[0,1],[1,1],[0,0],[1,0](如果没记错的话),多以这个门的反正面的把手肯定是不一样的方向,这样我们就改变一下uv
doorGeom1.faceVertexUvs[0][2] = [new THREE.Vector2(1,1), new THREE.Vector2(1,0), new THREE.Vector2(0,1)]; doorGeom1.faceVertexUvs[0][3] = [new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0,1)]; doorGeom2.faceVertexUvs[0][0] = [new THREE.Vector2(1,1), new THREE.Vector2(1,0), new THREE.Vector2(0,1)]; doorGeom2.faceVertexUvs[0][1] = [new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0,1)];
具体的旋转,之前博客有实现过。
3. 地面的实现
地面相信大家都会弄,主要是调整一个repeat和wrap,不多说。
floorT.wrapS = floorT.wrapT = THREE.RepeatWrapping;
floorT.repeat.x = floorT.repeat.y = 12;
2. 机柜的实现
这是里面对相对复杂的模块,不过里面还是用到了ThreeBSP,平移旋转、uv以及射线方面的知识,对于单个绘制来说,相信大家都能后信手拈来,不多对于大数量的机柜实现开关门、推拉服务器的点击操作逻辑(机柜关门是不允许推拉服务器操作,机柜中有服务器来出来是,不允许开关门操作),和机柜的隐藏显示。
1. 机柜架子的实现
机柜框架使用了ThreeBSP,将两个BoxGeometry相减既会出现一个没有门的框架,我们在加上门即可,门的旋转之前讲过了,
2. 服务器的实现
服务器的uv贴图只需要正面的即可,所以除了前两个三角形,其他的设置成这样就可以了。
for(let i=2; i<serGeom.faceVertexUvs[0].length; i++) { serGeom.faceVertexUvs[0][i] = [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]; }
3.服务器和柜门的点击事件
这里面我们考虑使用THREE.Raycaster类。
这是一个射线类,原理是鼠标在屏幕上点击的时候,得到二维坐标p(x, y),再加上深度坐标的范围(0, 1), 就可以形成两个三位坐标A(x1, y1, 0), B(x2, y, 1),这两个点的线穿过的Object3D由近及远返回一个数组,第一个便是我们点击到的对象。我们给之前的服务器机柜和服务器都加上名字方便我们知道点到的是哪一个。
let intersectFrameDoor = raycaster.intersectObjects(motorGroup.children, true); let tempArr = intersectFrameDoor[0].object.name.split('-');//得到[机柜门/服务器名字,机柜编号,服务器编号[ if(tempArr[0] == 'mdoor') { if(!this.motorServerFlag[tempArr[1]]) { if(this.motorDoorFlag[tempArr[1]]) { this.doAnimate(Math.PI * 3 / 5, 0, 500, intersectFrameDoor[0].object.parent, ['rotation', 'y']) this.motorDoorFlag[tempArr[1]] = false; } else { this.doAnimate(0, Math.PI * 3 / 5, 500, intersectFrameDoor[0].object.parent, ['rotation', 'y']) this.motorDoorFlag[tempArr[1]] = true; } } } else if(tempArr[0] == 'mserver') { if(this.motorDoorFlag[tempArr[1]]) { let posx = intersectFrameDoor[0].object.position.x; if(posx == 0) { this.doAnimate(0, 2, 500, intersectFrameDoor[0].object, ['position', 'x']); this.motorServerFlag[tempArr[1]] += 1; } else { this.doAnimate(2, 0, 500, intersectFrameDoor[0].object, ['position', 'x']); this.motorServerFlag[tempArr[1]] -= 1; } } }
4. 封装动画
这里面有很多动画,例如各种门的转动,服务器的平移,如果直接改变属性闲得很突兀,那么我们有几种选择,
- 关键帧动画
- Tween动画
- 自制动画
这里我们练习自己封装一个小动画,他虽然可能不够精确,但是十分实用。
doAnimate(s, e, t, o, a) { //开始的属性值、结束的属性值 时间 对象 属性 let temp = s; let step = t / 20; let stepLen = (e - s) / step; let animationObj = setInterval(() => { temp += stepLen; if(stepLen > 0 && temp >= e) { o[a[0]][a[1]] = e; clearInterval(animationObj); } else if(stepLen < 0 && temp <= e) { o[a[0]][a[1]] = e; clearInterval(animationObj); } else { o[a[0]][a[1]] = temp; } }, 20) }
在fpx大于50的情况下,基本准确。
今天就先讲这两个模块,下一篇继续,觉得可以的话,点个赞吧。
转载请注明地址:郭先生的博客