three.js+WebGL踩坑经验合集(10.1):镜像问题又一坑——THREE.InstancedMesh的正反面显示问题
本打算写9.3,拿个案例说明polygonOffsetUnits和polygonOffsetFactor这两个参数的计算方法的,但发现其枯燥程度远超作者想象,加上最近项目里又来镜像的bug了,并且这次还跟之前的对象类型不一样。这次是THREE.InstancedMesh。那今天就先让这个事情插个队。
正式开始前,先简单介绍下这种类型的对象。它用于把geometry和material完全一致,仅在位置和颜色有区分的很多个物体合并到同一个批次进行渲染,从而减少drawcall提升性能。webgl的核心api为drawElementsInstanced。我们的项目也用到了,同时也踩到了坑,所以就借着博客给大家分享出来,力求让后人少走弯路。
我们先用最普通的方法弄6个正方形的面片,只是把变换信息用矩阵代替设置position, rotation和scale,便于稍后切换成InstancedMesh。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>测试InstancedMesh的镜像问题</title><style>body {margin: 0;overflow: hidden;}</style><!--引入three.js三维引擎--><script src="three.js-master/build/three.js"></script><script src="three.js-master/examples/js/controls/OrbitControls.js"></script>
</head><body><script>/*** 创建场景对象Scene*/var scene = new THREE.Scene();var ambient = new THREE.AmbientLight(0x999999);scene.add(ambient);/*** 创建网格模型*/var geometry = new THREE.PlaneGeometry(100, 100, 1);var material = new THREE.MeshLambertMaterial({color: 0xFF3300, side: THREE.FrontSide});var matrixes = [];for(var j = 0; j < 2; j ++){for(var i = 0; i < 3; i ++){var posX = (i - 1) * 150;var posY = (j - 0.5) * 150;var rotationX = (i - 1) * Math.PI / 5;var rotationY = (j - 0.5) * Math.PI / 5;var rotationXMatrix = new THREE.Matrix4();rotationXMatrix.makeRotationX(rotationX);var rotationYMatrix = new THREE.Matrix4();rotationYMatrix.makeRotationY(rotationY);var matrix = new THREE.Matrix4();matrix.premultiply(rotationXMatrix);matrix.premultiply(rotationYMatrix);matrix.setPosition(posX, posY, 0);matrixes.push(matrix);}}for(let matrix of matrixes){var mesh = new THREE.Mesh(geometry, material);mesh.matrix.copy(matrix);mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale)scene.add(mesh);}/*** 光源设置*///点光源var light = new THREE.DirectionalLight(0xffffff);light.position.set(0, 0, 300); //点光源位置scene.add(light); //点光源添加到场景中/*** 相机设置*/var width = window.innerWidth; //窗口宽度var height = window.innerHeight; //窗口高度var k = width / height; //窗口宽高比//创建相机对象var camera = new THREE.PerspectiveCamera(60, k, 0.2, 1000);camera.position.set(0, 0, 600);/*** 创建渲染器对象*/var renderer = new THREE.WebGLRenderer({antialias: true});renderer.setSize(width, height);//设置渲染区域尺寸renderer.setClearColor(0x000000, 1); //设置背景颜色document.body.appendChild(renderer.domElement); //body元素中插入canvas对象function render() {renderer.render(scene, camera);}render();setInterval("render()",20);var controls = new THREE.OrbitControls(camera,renderer.domElement);//创建控件对象controls.addEventListener('change', render);//监听鼠标、键盘事件// 辅助坐标系 参数250表示坐标系大小,可以根据场景大小去设置var axisHelper = new THREE.AxesHelper(250);scene.add(axisHelper);</script>
</body>
</html>
运行效果如下图所示,面片只在正面显示,跑到后面就看不见了。
然后我们改用InstancedMesh,并且加个布尔变量作为开关。
var useInstancing = true;if(useInstancing)
{var instanceCount = matrixes.length;var instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount);for(var i = 0; i < instanceCount; i ++){instancedMesh.setMatrixAt(i, matrixes[i]);}scene.add(instancedMesh);
}else
{for(let matrix of matrixes){var mesh = new THREE.Mesh(geometry, material);mesh.matrix.copy(matrix);mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale)scene.add(mesh);}
}
这个时候,我们会发现效果没有任何变化,但是加到场景中的mesh数量从6个变成1个了,并且mesh的material也没有因此变成数组,所以drawCall降了,性能好了。这当中的重点api是setMatrixAt,也是InstancedMesh类设置单个实例旋转缩放平移等变换的唯一入口,所以前面没有再用以前常用的rotation,position等属性。
然而这个优化在我们项目里面出bug了,因为有的模型需要镜像,比如要把scale.x设置为-1。在这个案例里面,我们给上面3个面片的矩阵加一个x方向为-1的缩放。
for(var j = 0; j < 2; j ++)
{for(var i = 0; i < 3; i ++){var posX = (i - 1) * 150;var posY = (j - 0.5) * 150;var rotationX = (i - 1) * Math.PI / 5;var rotationY = (j - 0.5) * Math.PI / 5;var rotationXMatrix = new THREE.Matrix4();rotationXMatrix.makeRotationX(rotationX);var rotationYMatrix = new THREE.Matrix4();rotationYMatrix.makeRotationY(rotationY);var matrix = new THREE.Matrix4();if(j == 1){matrix.makeScale(-1, 1, 1);}matrix.premultiply(rotationXMatrix);matrix.premultiply(rotationYMatrix);matrix.setPosition(posX, posY, 0);matrixes.push(matrix);}
}
再次运行,上面那一排面片看不见了,要把相机旋转到背面才可见。
但是如果我们把useInstancing设置为false,用回不合批的做法是没这个问题的。
普通渲染方法之所以还可见,原因在笔者早期的一篇博文里面已有讲述。
three.js+WebGL踩坑经验合集(5.2):THREE.Mesh和THREE.Line2在镜像处理上的区别_three line2-CSDN博客
笔者把这篇博文里面提及的几处代码都搬过来。
改变面的正反显示,用的是webgl层上的gl.frontFace方法。如果object.matrixWorld为负定矩阵(参考three.js+WebGL踩坑经验合集(6.1):负缩放,负定矩阵和行列式的关系(2D版本)-CSDN博客)
则正反显示会发生变更。
但是这个地方并没有针对InstancedMesh中的matrixAt进行调整,而且做这种调整也是不合理的,因为如果一个InstancedMesh里面既有正定矩阵也有负定矩阵,那么不管怎么改都会显示不全。而gl.frontFace的设置是以Mesh为单位(多材质的则以材质),不能再往单个实例拆分(拆了就相当于没合批)
那这个问题是不是改成双面就能好呢?我们看一下效果。
显示是显示了,但是颜色变暗,旋转到背面会发现,镜像后,面片的向光面和背光面反了。
且不说双面的性能问题,效果都是错误的,怎么弄都没意义。
我们不妨再试试把material的side改成BackSide看看。
这下好了,全显示背光面,如果我们把useInstancing再次关掉用回普通渲染又会是啥样的呢?
嗯,确实,这个时候就应该全显示背光面,但是背光面都应该在相机旋转到后面的时候才可见,而不是一半一半地显示着。
不管从原理还是现象来看,我们都不应该在把正定矩阵和负定矩阵都往同一个THREE.InstancedMesh上去放,哪怕使用了双面。
所以我们的项目对同一个geometry和material的物体都弄了两个THREE.InstancedMesh,按照矩阵的正负分开存放。
var instanceCount = matrixes.length;
material.side = THREE.FrontSide;
var instancedMesh = new THREE.InstancedMesh(geometry, material, instanceCount >> 1);
for(var i = 0; i < instanceCount >> 1; i ++)
{instancedMesh.setMatrixAt(i, matrixes[i]);
}
scene.add(instancedMesh);
var material_mirror = material.clone();
material_mirror.side = THREE.BackSide;
var instancedMesh_mirror = new THREE.InstancedMesh(geometry, material_mirror, instanceCount >> 1);
for(var i = instanceCount >> 1; i < instanceCount; i ++)
{instancedMesh_mirror.setMatrixAt(i - (instanceCount >> 1), matrixes[i]);
}
scene.add(instancedMesh_mirror);
}else
{
for(let matrix of matrixes)
{var mesh = new THREE.Mesh(geometry, material);mesh.matrix.copy(matrix);mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale)scene.add(mesh);
}
运行效果如下,最起码面的正反显示对了,但是向光和背光依然是错的,只不过这个时候我们处理起来就可以方便很多,不用顾此失彼。
本来想在这一篇把向光和背光的问题都写上,无奈光是个可见的问题就占了这么多篇幅,那么我们先来小结一下,向光背光的问题就留到下一篇。
1 普通的Mesh在使用MeshLambert等跟光照有关的材质时,three.js通过全局矩阵的佚(正负性)把显隐和向光背光性都处理得相当不错。
2 THREE.InstancedMesh可以把多个不同矩阵,但是geometry和material相同的物体合到一个批次进行渲染来提高性能,但是遇到负定矩阵的时候,显示和向光背光性都会有bug,并且无法用很简单的方法来规避,哪怕是开启双面。
3 使用进行性能优化的时候,如果同时包含正定矩阵和负定矩阵的物体,则应该创建两个THREE.InstancedMesh,把正负性相同的物体放到一起。
分开存放以后,我们再到下一篇研究光照相关的问题,大家先慢慢消化一下。光照那里涉及的问题比显隐可要复杂不少的呢。