Better

Ethan的博客,欢迎访问交流

Z-Fighting

编写 three 应用时,经常会碰到场景闪烁的问题,采取的解决办法是:手动设置一定的偏移,别让模型靠的那么近,但这对精确度要求高的数据分析场景而言就不行了,这里详细了解一下。

现象及原因

Z-Fighting 问题:当场景中的两个模型在同一个像素生成的渲染结果对应到一个相同的深度值时,渲染器就不知道该使用哪个模型的渲染结果了,或者说,不知道哪个面在前,哪个面在后,于是便开始“胡作非为”,这次让这个面在前面,下次让那个面在前面,于是模型的重叠部位便不停的闪烁起来。

我们先了解下 three 的渲染步骤

  1. 清空当前帧缓冲区,更新 MVP 矩阵;
  2. 将物体分为透明和不透明两类,按照离摄像机从近到远排序(也可在 Object3D 单独设置 renderOrder);
  3. 根据灯光信息,阴影计算,如果有开启平面裁剪就对进行剪裁;
  4. 开始逐个渲染物体,按以下顺序,背景、不透明物体、透明物体;
  5. 渲染前后还有两个类似于生命周期的回调函数,onBeforeRender 和 onAfterRender;
  6. 最后将深度、模版测试、多边形偏移恢复默认。

在 three.js 中,使用深度缓冲(Z-Buffer)来完成场景可见性计算,即确定场景哪部分可见,哪部分不可见。深度缓冲(Z-Buffer)是一个二维数组,其中的每一个元素对应屏幕上的一个像素,如果场景中的两个模型在同一个像素生成渲染结果,那么图形处理卡就会比较二者的深度,并且保留距离观察者较近的物体在该像素点的渲染结果,这样就形成了近的模型遮挡远的模型的结果。

深度缓冲(Z-Buffer)是一个二维数组,但是数组的元素类型却可以不同,不同的元素类型代表着不同的精度。这和颜色的精度很像,比如 GIF 图像最多用 8bit 保存一个颜色,也即 GIF 最多支持 256 种色彩。以此类推,如果深度缓冲的也用 8bit 来保存一个像素的深度,那就是说该深度缓存只有 256 个深度级别。在 three.js 中只实现了一种深度缓冲,但是在例子中,又实现了一个精度更高的深度缓冲(logarithmicdepthbuffer),可以看示例 webgl_camera_logarithmicdepthbuffer

可能的解决方案

解决 Z-Fighting 有两个思路

  • 让各模型渲染结果不要在同一个像素出现相同深度值
  • 人为设置渲染顺序,这样即使出现相同深度值,也能正确渲染

大致有如下解决办法

  • 手动设置一定的偏移,别让模型靠的那么近,但这对精确度要求高的数据分析场景而言就不行了
  • 设置合适的 near 和 far 值,在创建相机的时候,会有 near 和 far 两个参数,用来设置相机的近平面和远平面。这个两个参数其实和深度缓冲(Z-Buffer)也密切相关,深度缓冲其实是非线性的,靠近相机的地方精度更高
  • 设置多边形偏移,在 material 定义了三个多边形偏移相关的属性
    • polygonOffset 是否开启多边形偏移
    • polygonOffsetFactor 多边形偏移因子
    • polygonOffsetUnits 多边形偏移单位
    • 当发生两个面深度值相同时,设置了 polygonOffset 的面便会向前或向后偏移一小段距离,这样就能区分谁前谁后了
    • 当 polygonOffsetFactor 和 polygonOffsetUnits 的都是正值时,向远离相机的方向偏移,当两者都是负值时,向靠近相机的地方偏移
    • 细节解释参考:Z fighting & polygon offset
  • 设置 renderOrder
    • Object3D 对象定义一个 renderOrder 属性,指定对象的渲染顺序,小的先渲染,大的后渲染
    • 设置完 renderOrder 之后,就算两个面有同样的深度,但是因为有渲染顺序,后渲染的面会覆盖掉先渲染的面。也因为这样,设置正确的渲染顺序很重要。
  • 使用 logarithmicDepthBuffer 缓冲(自己项目中开启后,还是会存在问题)

经测试:设置 near 和 far 值、设置 renderOrder、使用 logarithmicDepthBuffer 缓冲,并没有起到明显的效果,需要进一步把玩

renderOrder

关于 renderOrder 无效的问题,网上有很多中说法。

在 three.js 中设置 renderOrder 不会导致可渲染对象处于“顶部”。它只是控制呈现顺序。如果某些对象是透明的,它可能是一个有用的工具。如果场景中的所有对象都是不透明的,那么改变渲染顺序将不会影响渲染输出。

其实上面的话,还是没明白 renderOrder 的作用。难道和 sortObjects 属性相关有关

  • 定义渲染器是否应对对象进行排序。默认是 true
  • sortObjects 设置 false 物体的渲染顺序将会由他们添加到场景中的顺序所决定,适合大部分场景
  • 给特定的物体设置 object.renderOrder 指定它的渲染顺序,默认 renderOrder 为 0

不好意思,我设置后还是没啥用,这里有个例子transparent-objects-in-threejs,有空再理解一下

其他尝试

还有地方提到了,material.depthWrite 属性???我试用好像没啥效果呀

  • 渲染此材质是否对深度缓冲区有任何影响,默认为 true
  • 在绘制 2D 叠加时,将多个事物分层在一起而不创建 z-index 时,禁用深度写入会很有用

解决方式:通过 clearDepth 清除深度缓存(这种方式会有问题,导致整体渲染异常)

object3D.onBeforeRender = (renderer) => {
  renderer.clearDepth();
};


留言