Threejs开发3D地图实践总结
前段时间连续上了⼀个⽉班,加班加点完成了⼀个3D攻坚项⽬。也算是由传统web转型到webgl图形学开发中,坑不少,做了⼀下总结分享。
1、法向量问题
法线是垂直于我们想要照亮的物体表⾯的向量。法线代表表⾯的⽅向因此他们为光源和物体的交互建模中具有决定性作⽤。每⼀个顶点都有⼀个关联的法向量。
如果⼀个顶点被多个三⾓形共享,共享顶点的法向量等于共享顶点在不同的三⾓形中的法向量的和。N=N1+N2;
所以如果不做任何处理,直接将3维物体的点传递给BufferGeometry,那么由于法向量被合成,经过⽚元着⾊器插值后,就会得到这个⿊不溜秋的效果
我的处理⽅式使顶点的法向量保持唯⼀,那么就需要在共享顶点处,拷贝⼀份顶点,并重新计算索引,是的每个被多个⾯共享的顶点都有多份,每⼀份有⼀个单独的法向量,这样就可以使得每个⾯都有⼀个相同的颜⾊
2、光源与⾯块颜⾊
开发过程中设计给了⼀套配⾊,然⽽⼀旦有光源,⾯块的最终颜⾊就会与光源混合,颜⾊⾃然与最终设计的颜⾊⼤相径庭。下⾯是Lambert光照模型的混合算法。
⽽且产品的要求是顶⾯保持设计的颜⾊,侧⾯需要加⼊光源变化效果,当对地图做操作时,侧⾯颜⾊需要根据视⾓发⽣变化。那么我的处理⽅式是将顶⾯与侧⾯分别绘制(创建两个Mesh),顶⾯使⽤MeshLambertMaterial的emssive属性设置⾃发光颜⾊与设计颜⾊保持⼀致,也就不会有光照效果,侧⾯综合使⽤Emssive与color来应⽤光源效果。
var material1 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({
emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0], style.fillStyle[1], style.fillStyle[2]),
side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"],
shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"],
vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"]
});
var material2 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({
color: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.1, style.fillStyle[1] * 0.1, style.fillStyle[2] * 0.1),
emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.9, style.fillStyle[1] * 0.9, style.fillStyle[2] * 0.9),
side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"],
shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"],
vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"]
});
View Code
3、POI标注
Three中创建始终朝向相机的POI可以使⽤Sprite类,同时可以将⽂字和图⽚绘制在canvas上,将canvas作为纹理贴图放到Sprite上。但这⾥的⼀个问题是canvas图像将会失真,原因是没有合理的设置sprite的scale,导致图⽚被拉伸或缩放失真。
问题的解决思路是要保证在3d世界中的缩放尺⼨,经过⼀系列变换投影到相机屏幕后仍然与canvas在
屏幕上的⼤⼩保持⼀致。这需要我们计算出屏幕像素与3d世界中的长度单位的⽐值,然后将sprite缩放到合适的3d长度。
4、点击拾取问题
webgl中3D物体绘制到屏幕将经过以下⼏个阶段
所以要在3D应⽤做点击拾取,⾸先要将屏幕坐标系转化成ndc坐标系,这时候得到ndc的xy坐标,由于2d屏幕并没有z值所以,屏幕点转化成3d坐标的z可以随意取值,⼀般取0.5(z在-1到1之间)。
function fromSreenToNdc(x, y, container) {
return {
x: x / container.offsetWidth * 2 - 1,
y: -y / container.offsetHeight * 2 + 1,
z: 1
};
}
function fromNdcToScreen(x, y, container) {
return {
x: (x + 1) / 2 * container.offsetWidth,
y: (1 - y) / 2 * container.offsetHeight
};
}
然后将ndc坐标转化成3D坐标:
ndc = P * MV * Vec4
Vec4 = MV-1 * P -1 * ndc
这个过程在Three中的Vector3类中已经有实现:
unproject: function () {
var matrix = new Matrix4();
return function unproject( camera ) {
matrix.multiplyMatrices( camera.matrixWorld, Inverse( camera.projectionMatrix ) );
return this.applyMatrix4( matrix );
};
}(),
将得到的3d点与相机位置结合起来做⼀条射线,分别与场景中的物体进⾏碰撞检测。⾸先与物体的外包球进⾏相交性检测,与球不相交的排除,与球相交的保存进⼊下⼀步处理。将所有外包球与射线相交的物体按照距离相机远近进⾏排序,然后将射线与组成物体的三⾓形做相交性检测。求出相交物体。当然这个过程也由Three中的RayCaster做了封装,使⽤起来很简单:
mouse.x = ndcPos.x;
mouse.y = ndcPos.y;
this.raycaster.setFromCamera(mouse, camera);
var intersects = this.raycaster.intersectObjects(this._getIntersectMeshes(floor, zoom), true);
5、性能优化
随着场景中的物体越来越多,绘制过程越来越耗时,导致⼿机端⼏乎⽆法使⽤。
在图形学⾥⾯有个很重要的概念叫“one draw all”⼀次绘制,也就是说调⽤绘图api的次数越少,性能越⾼。⽐如canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;所以这⾥的解决⽅案是对相同样式的物体,把它们的侧⾯和顶⾯统⼀放到⼀个BufferGeometry中。这样可以⼤⼤降低绘图api的调⽤次数,极⼤的提升渲染性能。
这样解决了渲染性能问题,然⽽带来了另⼀个问题,现在是吧所有样式相同的⾯放在⼀个BufferGeometry中(我们称为样式图形),那么在⾯点击时候就⽆法单独判断出到底是哪个物体(我们称为物体图形)被选中,也就⽆法对这个物体进⾏⾼亮缩放处理。我的处理⽅式是,把所有的物体单独⽣成物体图形保存在内存中,做⾯点击的时候⽤这部分数据来做相交性检测。对于选中物体后的⾼亮缩放处理,⾸先把样式⾯中相应部分裁减掉,然后把选中的物体图形加⼊到场景中,对它进⾏缩放⾼亮处理。裁剪⽅法是,记录每个物体在样式图形中的其实索引位置,在需要裁切时候将这部分索引制零。在需要恢复的地⽅在把这部分索引恢复成原状。
6、⾯点击移动到屏幕中央
这部分也是遇到了不少坑,⾸先的想法是:
⾯中⼼点⽬前是在世界坐标系内的坐标,先⽤center.project(camera)得到归⼀化设备坐标,在根据ndc得到屏幕坐标,⽽后根据⾯中⼼点屏幕坐标与屏幕中⼼点坐标做插值,得到偏移量,在根据OribitControls中的pan⽅法来更新相机位置。这种⽅式最终以失败告终,因为相机可能做各种变换,所以屏幕坐标的偏移与3d世界坐标系中的位置关系并不是线性对应的。
最终的想法是:
我们现在想将点击⾯的中⼼点移到屏幕中⼼,屏幕中⼼的ndc坐标永远都是(0,0)我们的观察视线与近景⾯的焦点的ndc坐标也是0,0;也就是说我们要将⾯中⼼点作为我们的观察点(屏幕的中⼼永远都是相机的观察视线),这⾥我们可以直接将⾯中⼼所谓视线的观察点,利⽤lookAt⽅法求取相机矩阵,但如果这样简单处理后的效果就会给⼈感觉相机的姿态变化了,也就是会感觉并不是平移过去的,所以我们要做的是保持相机当前姿态将⾯中⼼作为相机观察点。
回想平移时我们将屏幕移动转化为相机变化的过程是知道屏幕偏移求target,这⾥我们要做的就是知道target反推屏幕偏移的过程。⾸先根据当前target与⾯中⼼求出相机的偏移向量,根据相机偏移向量求出在相机x轴和up轴的投影长度,根据投影长度就能返推出应该在屏幕上的平移量。
this.unprojectPan = function(deltaVector, moveDown) {
// var getProjectLength()
var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
var cxv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 0);// 相机x轴
var cyv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 1);// 相机y轴
// 相机轴都是单位向量
var pxl = deltaVector.dot(cxv)/* / cxv.length()*/; // 向量在相机x轴的投影
var pyl = deltaVector.dot(cyv)/* / cyv.length()*/; // 向量在相机y轴的投影
// offset=dx * vector(cx) + dy * vector(cy.project(xoz).normalize)
// offset由相机x轴⽅向向量+相机y轴向量在xoz平⾯的投影组成
var dv = deltaVector.clone();
dv.sub(cxv.multiplyScalar(pxl));
pyl = dv.length();3d地图实景地图
if ( scope.object instanceof PerspectiveCamera ) {
// perspective
var position = scope.object.position;
var offset = new Vector3(0, 0, 0);
var distance = offset.length();
distance *= Math.tan(scope.object.fov / 2 * Math.PI / 180);
// var xd = 2 * distance * deltaX / element.clientHeight;
// var yd = 2 * distance * deltaY / element.clientHeight;
// panLeft( xd, scope.object.matrix );
// panUp( yd, scope.object.matrix );
var deltaX = pxl * element.clientHeight / (2 * distance);
var deltaY = pyl * element.clientHeight / (2 * distance) * (moveDown ? -1 : 1);
return [deltaX, deltaY];
} else if ( scope.object instanceof OrthographicCamera ) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论