在项目开发过程中,笔者两次遇到同事的一个提问,我场景中的Line在相机旋转到某些角度或者移动到某些位置的时候会无故消失。由于业务场景复杂,所以这两位同事都是先花费了大量时间排查业务问题,然后才找我求助。这个问题抽象出来的一个测试单元代码如下所示:
javascript
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>threeLine</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="three/build/three.js"></script>
<script src="three/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<script>
var scene = new THREE.Scene();
var geometry = new THREE.BufferGeometry();
var material = new THREE.LineBasicMaterial({color: 0x000000, linewidth: 1});
var line = new THREE.Line(geometry, material);
scene.add(line);
var width = window.innerWidth;
var height = window.innerHeight;
var k = width / height;
var s = 200;
var camera = new THREE.PerspectiveCamera(60, 1, 0.1, 20000);
camera.position.set(100, 600, 300);
camera.lookAt(0, 500, 400);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
renderer.setClearColor(0xffffff, 1);
document.body.appendChild(renderer.domElement);
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
var controls = new THREE.OrbitControls(camera,renderer.domElement);
controls.addEventListener('change', render);
var axisHelper = new THREE.AxesHelper(250);
scene.add(axisHelper);
function onClick(){
var points = [new THREE.Vector3(0, -100, -100), new THREE.Vector3(0, -100, 100), new THREE.Vector3(0, 100, 100), new THREE.Vector3(0, 0, -100), new THREE.Vector3(0, -100, -100)];
geometry.setFromPoints(points);
}
window.addEventListener("click", onClick);
</script>
</body>
</html>
这是个绘图功能,通过在场景上点击鼠标在对应的位置上描点连线来显示绘制的轮廓。这里简化了一下,就在场景中心绘制一个固定尺寸的矩形。
然后我们试着通过滚轮把相机拉近拉远,奇怪的事情发生了,拉到比较近的时候线条突然无故消失,重新拉远又能看见了。
笔者本身就算不上精通three.js和WebGL,第一次遇到这个求助的时候也是花了不少时间在引擎源码上断点找问题。发现隐藏了它的代码在这里:
发现这根线就是被视锥剔除过滤掉了,three.js为了优化性能,会让不在画布可视范围(严格来说是个3D的视锥体)内的物体全部不参与渲染,这个线就是这样被过滤走的。
但是当时笔者没有细看,误以为只是视锥剔除算法为了性能而检测得比较粗糙,只检测了物体中心点,所以当时我给同事的解决方案是关闭线条的视锥剔除,代码如下:
javascript
line.frustumCulled = false;
设置以后,视锥剔除就会因为这个判断而不执行了:
有了经验之后,再有别的同事问我,我就三秒钟给他解决掉类似的问题。
视锥剔除(frustumCulled)默认值为true,这对于大多数物体来说都适用,引擎内唯一默认关闭的是InstancedMesh,多个相同几何体和材质的物体,可以通过这个对象进行合批渲染(减drawCall)来提升性能。开启这种物体的视锥剔除,很可能会因为合批的物体数量太大而增加cpu的开销,性能方面得不偿失。
言归正传,对于Line来说,关闭视锥剔除的代价是性能的下降。但是为了结果没办法。
然而这次写博客笔者才发现自己的这个做法是错误的,事实上three.js的视锥剔除并不是只检查中心点,而是检查了包围球,检测依据是球心和半径而并非只看中心点。
所以更地道的解决方案是:修改了线的顶点后,重新手动计算一次包围球,代码如下:
javascript
function onClick(){
var points = [new THREE.Vector3(0, -100, -100), new THREE.Vector3(0, -100, 100), new THREE.Vector3(0, 100, 100), new THREE.Vector3(0, 0, -100), new THREE.Vector3(0, -100, -100)];
geometry.setFromPoints(points);
//更新包围球,确保视锥剔除的结果准确无误
geometry.computeBoundingSphere();
}
简单小结一下:
解决线条乃至其他物体在某些角度下消失的方法有两种:
1 把物体的视锥剔除关闭
2 物体的包围球发生变更时重新手动计算一次
物体的几何信息变更频繁的话适合方案1,否则选择方案2。
可以看到,three.js出于性能考虑,没有把修改顶点和更新包围球两个操作合并到一个方法中,这会让萌新们觉得three.js学习门槛高,易用性差,或者bug很多。
而事实上,three.js是在易用性和性能之间寻求一个平衡点。而且这些东西只要踩过一次坑,后面再遇到类似问题的时候就会更加得心应手。
这也是笔者创作这一专栏的初衷,如果你们有幸在首次遇到这方面问题的时候能够通过搜索引擎找到笔者的文章,那也许第一次踩坑就能快速解决问题了。