目录
1. 问题的提出
2. Z冲突(z-fighting)简介
2.1. Z冲突(z-fighting)产生的原因
2.2. 如何消除Z冲突(z-fighting)
3. 代码实现
1. 问题的提出
今天绘制了一个棋盘格,鼠标在棋盘格上单击,在单击点绘制一个红色的圆,但圆形始终不正常,圆的颜色有的地方有,有的地方没有,如下:
正常的情况下,应该向下面那样:
这个问题是由于OpenGL深度测试带来Z冲突(z-fighting)引起的。起始的绘制圆的代码如下:
// 画点。以小圆表示,其中参数pt表示鼠标单击时的世界坐标
void osgCardinal::drawEllipse(const osg::Vec3d& pt)
{
auto pGeometry = new osg::Geometry;
auto pVertArray = new osg::Vec3Array;
_radius = 0.2;
auto twoPi = 2 * 3.1415926;
for (auto iAngle = 0.0; iAngle < twoPi; iAngle += 0.001)
{
auto x = pt.x() + _radius * std::cosf(iAngle);
auto y = pt.y() + _radius * std::sinf(iAngle);
auto z = pt.z();
pVertArray->push_back(osg::Vec3d(x, y, z));
}
pGeometry->setVertexArray(pVertArray);
auto pColorArray = new osg::Vec4Array;
pColorArray->push_back(osg::Vec4d(1.0, 0.0, 0.0, 1.0));
pGeometry->setColorArray(pColorArray/*, osg::Array::BIND_OVERALL*/);
pGeometry->setColorBinding(osg::Geometry::BIND_OVERALL);
...... // 其它代码略
}
2. Z冲突(z-fighting)简介
2.1. Z冲突(z-fighting)产生的原因
为什么会产生z-fighting现象?
第一点原因:
场景中渲染多个三维物体的时候,当这多个三维物体摆放的位置很接近时,导致在深度缓冲测试的时候,会产生精度的误差,然后会导致几个物体之间的片段值在通过深度测试时,有时A物体通过,有时B物体通过,导致交替显示这几个物体的颜色值,然后那就会产生闪烁的现象,这种闪烁现象在场景旋转时,尤其明显。
第二点原因:
采用透视投影矩阵渲染的场景,其深度缓冲区存储深度值,ndc空间中也存储了深度值。而ndc空间的深度值是经由透视空间转换过来的,ndc空间的深度值与透视空间的深度值转换并非是线性的,而是非线性。大家都知到,透视空间转换到ndc空间会有一步透视除法,是除以z值。这样就会导致,离视点越近的物体的片段深度值是越精确的,离视点距离越远的物体的片段的深度值是越不精确的。这样就会导致z-fighting问题。
而采用正交透视矩阵渲染场景,其变换是线性的,为什么?因为其透视空间转换为ndc空间的时候采用的透视除法是除以1,所以其片段距离视点的深度值是线性的,这样除非你把两个物体设置的位置非常接近,否则是产生不了z-fighting这种现象的。
2.2. 如何消除Z冲突(z-fighting)
1.第一种方法
第一个也是最重要的技巧是永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。通过在两个物体之间设置一个用户无法注意到的偏移值,你可以完全避免这两个物体之间的深度冲突。在箱子和地板的例子中,我们可以将箱子沿着地板向上方向稍微移动一点。箱子位置的这点微小改变将不太可能被注意到,但它能够完全减少深度冲突的发生。然而,这需要对每个物体都手动调整,并且需要进行彻底的测试来保证场景中没有物体会产生深度冲突。
2.第二种方法
第二个技巧是尽可能将近平面设置远一些。在前面我们提到了精度在靠近近平面时是非常高的,所以如果我们将近平面远离观察者,我们将会对整个平截头体有着更大的精度。然而,将近平面设置太远将会导致近处的物体被裁剪掉,所以这通常需要实验和微调来决定最适合你的场景的近平面距离。
3.第三种方法
另外一个很好的技巧是牺牲一些性能,使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。所以,牺牲掉一些性能,你就能获得更高精度的深度测试,减少深度冲突。
我们上面讨论的三个技术是最普遍也是很容易实现的抗深度冲突技术了。还有一些更复杂的技术,但它们依然不能完全消除深度冲突。深度冲突是一个常见的问题,但如果你组合使用了上面列举出来的技术,你可能不会再需要处理深度冲突了。
3. 代码实现
在1节代码中,加入消除Z冲突的代码如下:
// 画点。以小圆表示,其中参数pt表示鼠标单击时的世界坐标
void osgCardinal::drawEllipse(const osg::Vec3d& pt)
{
auto pGeometry = new osg::Geometry;
auto pVertArray = new osg::Vec3Array;
auto pPgo = new osg::PolygonOffset();
pPgo->setFactor(-1.0);
pPgo->setUnits(-1.0);
pGeometry->getOrCreateStateSet()->setAttributeAndModes(pPgo);
_radius = 0.2;
auto twoPi = 2 * 3.1415926;
for (auto iAngle = 0.0; iAngle < twoPi; iAngle += 0.001)
{
auto x = pt.x() + _radius * std::cosf(iAngle);
auto y = pt.y() + _radius * std::sinf(iAngle);
auto z = pt.z()/* + 0.001*/;
pVertArray->push_back(osg::Vec3d(x, y, z));
}
pGeometry->setVertexArray(pVertArray);
auto pColorArray = new osg::Vec4Array;
pColorArray->push_back(osg::Vec4d(1.0, 0.0, 0.0, 1.0));
pGeometry->setColorArray(pColorArray/*, osg::Array::BIND_OVERALL*/);
pGeometry->setColorBinding(osg::Geometry::BIND_OVERALL);
...... // 其它代码略
}
上述代码通过构造osg::PolygonOffset对象,加入了多边形漂移,从而解决了Z冲突问题。osg::PolygonOffse类的功能封装了OPenGL中的glPolygonOffset函数,关于该函数的具体用法,参见如下链接:
- glPolygonOffset用法。
- OpenGL深度测试带来的问题----Z冲突 。
可以不用osg::PolygonOffset类,将16行代码的注释取消,即将z值加个微小的值,这个值自己可以进行微调,直到人眼觉察不到圆和棋盘格脱离且消除了1节中提到的现象为止。