Solve abnormal situations such as ghosting or flickering caused by Z conflict when osg draws a scene

Table of Contents

1. Raising the question

2. Introduction to Z-fighting

2.1. Causes of Z-fighting

2.2. How to eliminate Z-conflict (z-fighting)

3. Code implementation


1. Question raising

Today I drew a checkerboard. I clicked the mouse on the checkerboard and drew a red circle at the click point. However, the circle was always abnormal. The color of the circle was present in some places and not in others, as follows:

Under normal circumstances, it should look like the following:

This problem is caused by Z-conflict (z-fighting) caused by OpenGL depth testing. The initial code for drawing a circle is as follows:

// Draw dots. Represented by a small circle, where the parameter pt represents the world coordinate when the mouse is clicked.
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);

    ...... // Other codes are omitted
}

2. Introduction to Z-conflict (z-fighting)

2.1. Causes of Z-conflict (z-fighting)

Why does the z-fighting phenomenon occur?

The first reason:

When rendering multiple three-dimensional objects in the scene, when the multiple three-dimensional objects are placed very close to each other, accuracy errors will occur during the depth buffer test, which will cause the fragment values between several objects to be in the same range. When passing the depth test, sometimes object A passes, and sometimes object B passes, causing the color values of these objects to be displayed alternately, and then a flickering phenomenon will occur. This flickering phenomenon is especially obvious when the scene is rotated.

Second reason:

For a scene rendered using a perspective projection matrix, the depth buffer stores the depth value, and the depth value is also stored in the ndc space. The depth value of the ndc space is converted through the perspective space. The conversion of the depth value of the ndc space and the depth value of the perspective space is not linear, but nonlinear. As we all know, there is a one-step perspective division when converting perspective space to ndc space, which is dividing by the z value. This will lead to the more accurate the depth value of the fragment of the object that is closer to the viewpoint, and the less accurate the depth value of the fragment of the object that is farther from the viewpoint. This will cause z-fighting problems.

When using an orthogonal perspective matrix to render a scene, its transformation is linear. Why? Because the perspective division used when converting the perspective space to ndc space is divided by 1,so the depth value of the fragment from the viewpoint is linear, unless you set the two objects very close to each other. , otherwise the phenomenon of z-fighting will not occur.

2.2. How to eliminate Z conflict (z-fighting)

1. The first method

The first and most important tip is to never place multiple objects so close together that some of their triangles overlap. You can completely avoid depth conflicts between two objects by setting an offset between them that the user cannot notice. In the box and floor example, we can move the box a little bit in the upward direction along the floor. This small change in the position of the box will be unlikely to be noticed, but it can completely reduce the occurrence of depth conflicts. However, this requires manual adjustments for each object and thorough testing to ensure that no objects in the scene create depth conflicts.

2. The second method

The second tip is to set the near plane as far away as possible. Earlier we mentioned that the accuracy is very high close to the near plane, so if we move the near plane away from the observer, we will have greater accuracy for the entire frustum. However, setting the near plane too far away will cause nearby objects to be clipped, so it usually requires experimentation and fine-tuning to determine the near plane distance that works best for your scene.

3. The third method

Another good tip is to sacrifice some performance and use a higher precision depth buffer. Most depth buffers have a 24-bit accuracy, but most graphics cards now support 32-bit depth buffers, which will greatly improve accuracy. So, sacrificing some performance, you can get more accurate depth testing and reduce depth conflicts.

The three techniques we discussed above are the most common and easily implemented anti-deep conflict techniques. There are some more sophisticated techniques, but they still don’t completely eliminate deep conflicts. Depth conflicts are a common problem, but if you use a combination of the techniques listed above, you may never have to deal with them again.

3. Code implementation

In the code in section 1, add the code to eliminate Z conflicts as follows:

// Draw dots. Represented by a small circle, where the parameter pt represents the world coordinate when the mouse is clicked.
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);

    ...... // Other codes are omitted
}

The above code solves the Z conflict problem by constructing an osg::PolygonOffset object and adding polygon drift. The function of the osg::PolygonOffse class encapsulates the glPolygonOffset function in OPenGL. For the specific usage of this function, see the following link:

  • glPolygonOffset usage.
  • Problems caused by OpenGL depth testing—-Z conflict.

You can use the osg::PolygonOffset class and uncomment the 16 lines of code, that is, add a small value to the z value. This value can be fine-tuned until the human eye cannot detect the separation of the circle and the checkerboard and eliminate the problem mentioned in Section 1. to the phenomenon.