一、你遇到的“物体消失”,大概率是这俩“隐形规则”搞的鬼
做Three.js开发时,很多人都碰过这种怪事:明明场景里放了个立方体,代码没报错,控制台也没异常,可就是死活看不见;更离谱的是,转个视角、挪下相机,它又突然冒出来了。这时候别乱调材质、改位置,八成是你没摸透Three.js里两个藏得深的隐形规则——“视锥体剔除”和“深度缓冲冲突”,这俩规则就像游戏里的隐形关卡,没提前解锁就会踩坑。
先给大家说个真实踩坑场景:上个月帮同事排查问题,他做了个虚拟展厅,展厅中间放了个3米高的玻璃展台,展台正中间摆了个1米高的金属奖杯,结果奖杯怎么都看不见,只有把相机拉到展台外面、从斜上方往下拍时,奖杯才会出现。当时我们先排除了材质(奖杯用的是标准材质,没开透明也没调可见度)、位置(坐标没写错,就在展台正中间),最后才定位到是这俩规则的问题。
接下来就用大白话讲透这俩规则,再给完整的排查和解决步骤。
二、第一个隐形规则:视锥体剔除——只有“在相机视野里”的物体才会被画
先举个生活化的例子:你站在客厅里,眼睛能看到沙发、茶几、电视,但看不到卧室的床、厨房的锅,因为这些东西在你视野之外。Three.js里的相机就像你的眼睛,它有个“视野范围”,只有在这个范围内的物体,才会被Three.js“画”出来,这个范围就叫“视锥体”,而Three.js会自动把不在这个范围里的物体排除掉,这个过程就是“视锥体剔除”。
很多人踩坑,是因为没搞懂视锥体的边界到底怎么算,尤其是容易忽略“近裁剪面”和“远裁剪面”这两个关键参数。
2.1 视锥体的核心参数:近裁剪面和远裁剪面
相机的视野范围不是无限的,它有两个“截止线”:
- 近裁剪面:离相机太近的物体,会被切掉,比如你把手指贴到眼睛上,就会看不到手指的完整形状,甚至看不到;
- 远裁剪面:离相机太远的物体,也会被切掉,比如你看月亮,只能看到轮廓,看不到上面的陨石坑,太远的物体就会被“忽略”。
举个具体的错误代码示例,这个代码会导致物体消失: 技术栈:Three.js r150(这是目前用的比较多的稳定版本,所有示例都用这个版本)
// 1. 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 2. 初始化相机:透视相机(最常用的相机类型)
// 透视相机的参数:视野角度、宽高比、近裁剪面、远裁剪面
const camera = new THREE.PerspectiveCamera(
75, // 视野角度:数字越大,能看到的范围越宽
window.innerWidth / window.innerHeight, // 宽高比:和画布一致,防止变形
0.1, // 近裁剪面:离相机小于0.1单位的物体会被切掉
10 // 远裁剪面:离相机大于10单位的物体会被切掉
);
// 把相机放在(0, 0, 5)的位置,朝向场景中心(0, 0, 0)
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
// 3. 初始化渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 4. 添加物体:一个红色立方体,位置在(0, 0, 20)
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 0, 20); // 立方体离相机的距离是20单位
scene.add(cube);
// 5. 添加光源,不然标准材质的物体看不到颜色
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
// 6. 渲染循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
这段代码跑起来,你会看不到红色立方体,为什么?因为相机的远裁剪面设的是10,而立方体的位置在(0,0,20),离相机的距离是20,超过了远裁剪面的范围,所以被视锥体剔除了。
2.2 视锥体剔除的踩坑场景和解决办法
除了上面的“远裁剪面太小”,还有几个常见的踩坑场景:
- 场景缩放时没同步调整裁剪面:比如你做的是大型场景(比如城市、宇宙),物体的距离是几百、几千单位,这时候如果远裁剪面设成10,所有远处的物体都会消失;
- 近裁剪面太小:比如你做VR场景,相机离物体只有0.05单位,这时候近裁剪面设成0.1,物体会被切掉;
- 相机移动时的临时剔除:比如你用轨道控制器控制相机,当相机快速移动时,Three.js可能会误判物体不在视野里,临时剔除。
解决办法也很简单:
- 先根据场景的最大尺寸调整裁剪面:比如你的场景最大范围是1000单位,那远裁剪面就设成1000以上(比如1500),近裁剪面设成0.1(一般场景足够,VR场景设成0.01);
- 禁用不需要的物体的视锥体剔除:Three.js里的Mesh有个属性叫
frustumCulled,默认是true(开启剔除),如果某个物体不管在不在视野里都要画,就把它设成false; - 调整相机的视野角度:如果视野角度太小,也会导致物体不在视野里。
比如上面的错误代码,把相机的远裁剪面改成25,就能看到立方体了:
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
25 // 远裁剪面改成25,超过立方体的距离20
);
三、第二个隐形规则:深度缓冲冲突——两个物体“叠在一起”时,Three.js不知道该画哪个
这个规则的名字有点绕,其实就是“深度冲突”,举个生活化的例子:你拿两张A4纸,叠在一起放在桌子上,眼睛看过去,只能看到上面的那张纸,下面的纸被挡住了;但如果两张纸完全重合,没有前后距离,你就分不清哪张在上哪张在下,甚至可能看到两张纸的图案混在一起。
Three.js里的深度缓冲就是用来判断“哪个物体在前、哪个在后”的,它会给每个像素记录一个“深度值”(离相机的距离),画物体的时候,只有当前像素的深度值比之前的小(离相机更近),才会覆盖之前的像素。但如果两个物体的深度值完全一样,Three.js就会出现“判断混乱”,导致其中一个物体被“覆盖”,看起来就像消失了。
3.1 深度缓冲冲突的核心原因:两个物体的深度值完全相同
还是拿之前的展厅例子来说:展台是一个透明的立方体,奖杯是一个金属立方体,两者的位置都是(0,0,0),也就是说,它们的深度值完全一样,所以Three.js分不清哪个在前,就会出现“只有从特定角度才能看到奖杯”的情况。
再给个具体的错误代码示例:
// 1. 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 2. 初始化相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);
// 3. 初始化渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 4. 添加第一个物体:红色立方体(深度值1)
const geometry1 = new THREE.BoxGeometry(2, 2, 2);
const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const cube1 = new THREE.Mesh(geometry1, material1);
cube1.position.set(0, 0, 0); // 位置(0,0,0)
scene.add(cube1);
// 5. 添加第二个物体:蓝色立方体(深度值和红色立方体完全一样)
const geometry2 = new THREE.BoxGeometry(2, 2, 2);
const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const cube2 = new THREE.Mesh(geometry2, material2);
cube2.position.set(0, 0, 0); // 位置和红色立方体完全一样!
scene.add(cube2);
// 6. 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
// 7. 渲染循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
这段代码跑起来,你只会看到蓝色立方体,红色立方体消失了,因为两个物体的深度值完全一样,Three.js先画红色立方体,再画蓝色立方体,蓝色立方体的深度值和红色的一样,就把红色的覆盖了。
3.2 深度缓冲冲突的踩坑场景和解决办法
深度缓冲冲突最常见的场景就是“两个物体完全重合”,比如:
- 模型导入时,同一个位置有两个完全一样的模型;
- 透明物体和它的反射、折射物体重合;
- 粒子效果和背景物体重合。
解决办法有四个,根据场景选合适的:
- 错开物体的位置:这是最直接的办法,只要两个物体的深度值不一样,就不会冲突;
- 调整渲染顺序:Three.js里的Mesh有个属性叫
renderOrder,值越大,越晚画,晚画的物体会覆盖早画的物体; - 关闭深度测试:如果某个物体不管深度值如何,都要画在最前面,就把它的材质的
depthTest属性设为false; - 调整深度写入:如果某个物体不需要写入深度值(比如透明物体),就把它的材质的
depthWrite属性设为false。
比如上面的错误代码,把蓝色立方体的位置改成(0,0,0.1),错开深度值,就能看到两个立方体了:
cube2.position.set(0, 0, 0.1); // 位置错开0.1单位,深度值不同
如果不想错开位置,就调整渲染顺序,让红色立方体晚画,覆盖蓝色立方体:
cube1.renderOrder = 1; // 红色立方体的渲染顺序设为1,比蓝色的0大,晚画
四、排查“物体消失”的完整流程
现在把这两个规则结合起来,给大家一个完整的排查流程,不管遇到什么物体消失的问题,都能按这个流程找到原因:
- 检查物体的位置和裁剪面:先看物体的位置坐标,再看相机的近裁剪面和远裁剪面,确认物体在视锥体范围内;
- 检查物体的frustumCulled属性:如果物体不在视锥体范围内,看是不是把frustumCulled设成了true,导致被剔除;
- 检查有没有重合的物体:看场景里有没有和这个物体位置完全一样的物体,是不是深度缓冲冲突;
- 检查材质的深度属性:看材质的depthTest、depthWrite属性是不是设错了;
- 检查相机的移动状态:如果相机快速移动时物体消失,可能是视锥体剔除的临时误判。
五、总结和注意事项
最后给大家总结一下这两个规则的核心:
- 视锥体剔除:相机的视野范围是有限的,不在范围内的物体会被排除;
- 深度缓冲冲突:两个物体深度值一样时,Three.js会分不清前后,导致其中一个被覆盖。
注意事项:
- 场景越大,远裁剪面要设得越大,但也不能太大,不然会影响性能;
- 透明物体的深度属性要注意调整,不然会出现透明物体不透明的情况;
- 导入模型时,要检查有没有重合的模型,避免深度缓冲冲突;
- 调试时,可以把相机的近裁剪面设得很小,远裁剪面设得很大,先排除裁剪面的问题。
评论
围绕“Three.js场景中物体莫名消失不见?从视锥体剔除与深度缓冲冲突入手排查异常渲染根因并彻底解决”参与讨论