深度测试

深度测试是用来解决三维渲染中物体遮挡的显示问题。一般来说,近的物体会遮挡住远的物体。而默认情况下,后绘制的像素又会把之前绘制的像素覆盖掉。当然,我们可以自己处理好物体的绘制顺序,但是深度测试是一种更加简洁的处理手段。

深度测试的实现也不复杂。我们使用一个额外的缓冲区,一般称作深度缓冲区或 z 缓冲区,来存储屏幕上每个像素的深度值。当新的像素需要绘制的时候,我们会比较这个像素的深度值与缓冲区中记录的之前的深度值。以前面挡住后面为例,如果新像素的深度值小于缓冲区里的值,新像素就会绘制;反之,这个新像素就不会被绘制。

1. OpenGL 接口

我们以 OpenGL 的接口作为参照,来了解深度测试功能。

OpenGL 中使用 glEnable 传递 GL_DEPTH_TEST 开启深度测试。

glDepthFunc 用于指定深度比较的条件。比如 GL_LESS 表示新的深度值小于存储的深度值才通过测试;GL_GREATER 表示新的深度值大于存储的深度值才通过测试。

  • void glDepthFunc(GLenum func);

2. 代码实现

我们用代码实现,对深度测试的细节进行具体说明。

如代码清单 1 所示,我们先仿照 OpenGL,实现自己的 glEnable 和 glDepthFunc 接口。

代码清单 1 接口
  1. void TSoftRenderer::Enable(TEnableCap cap)
  2. {
  3.     if (cap == TEnableCap::CullFace)
  4.         m_state.SetCulling(true);
  5.     else if (cap == TEnableCap::DepthTest)
  6.         m_state.SetDepthTest(true);
  7. }
  1. enum class TDepthFunc
  2. {
  3.     Less,
  4.     LessEqual,
  5.     Greater,
  6.     GreaterEqual,
  7. };
  8.  
  9. void TSoftRenderer::DepthFunc(TDepthFunc func)
  10. {
  11.     m_state.SetDepthFunc(func);
  12. }

如代码清单 2 所示,我们新增一个数组,用于充当深度缓冲区。并将缓冲区的大小设置成绘制屏幕的大小。

代码清单 2 深度缓冲区
  1. class TRasterizer
  2. {
  3. private:
  4.     uint32_t* m_pBits;  // raw pixel data
  5.     std::vector<float> m_depthBuffer;  // depth buffer
  6. };
  1. TRasterizer::TRasterizer(uint32_t* pBits, int width, int height, TRenderState* m_state)
  2.     : m_pBits(pBits),
  3.       m_width(width),
  4.       m_height(height),
  5.       m_state(m_state),
  6.       m_depthBuffer(width * height)
  7. {
  8. }

代码清单 3 是深度测试的具体实现。首先我们判断是否开启了深度测试,没开启的话,意味深度测试都会通过。接着我们定义各个深度比较枚举量实际对应的比较函数。然后我们用比较函数进行测试,如果通过就更新深度缓冲区中的值,并返回通过;否则就返回不通过。

代码清单 3 深度测试
  1. bool TRasterizer::DepthTest(int x, int y, float depth)
  2. {
  3.     if (m_state->IsDepthTestEnabled() == false)
  4.         return true;
  5.  
  6.     static const std::unordered_map<TDepthFunc, std::function<bool(float, float)>> depthFuncMap =
  7.     {
  8.         { TDepthFunc::Less,         [](float depth, float storedDepth) { return depth < storedDepth; } },
  9.         { TDepthFunc::LessEqual,    [](float depth, float storedDepth) { return depth <= storedDepth; } },
  10.         { TDepthFunc::Greater,      [](float depth, float storedDepth) { return depth > storedDepth; } },
  11.         { TDepthFunc::GreaterEqual, [](float depth, float storedDepth) { return depth >= storedDepth; } },
  12.     };
  13.  
  14.     int index = y * m_width + x;
  15.  
  16.     auto it = depthFuncMap.find(m_state->GetDepthFunc());
  17.     assert(it != depthFuncMap.end());
  18.  
  19.     if (it->second(depth, m_depthBuffer[index]))
  20.     {
  21.         m_depthBuffer[index] = depth;
  22.         return true;
  23.     }
  24.  
  25.     return false;
  26. }

如代码清单 4 所示,我们在光栅化阶段添加深度测试功能,如果没通过深度测试,就不绘制这个像素。注意,因为需要用到深度值,所以我们这边新增插值点的 z 坐标插值。

代码清单 4 光栅化
  1. void TRasterizer::RasterizeTriangle(
  2.     const TVertexShaderOutputPrivate& v1,
  3.     const TVertexShaderOutputPrivate& v2,
  4.     const TVertexShaderOutputPrivate& v3,
  5.     FragmentShaderFunction fragShader)
  6. {
  7.     const tmath::Vec2i p1 = { (int)v1.position.x(), (int)v1.position.y() };
  8.     const tmath::Vec2i p2 = { (int)v2.position.x(), (int)v2.position.y() };
  9.     const tmath::Vec2i p3 = { (int)v3.position.x(), (int)v3.position.y() };
  10.  
  11.     int minX = std::min(p1.x(), std::min(p2.x(), p3.x()));
  12.     int maxX = std::max(p1.x(), std::max(p2.x(), p3.x()));
  13.     int minY = std::min(p1.y(), std::min(p2.y(), p3.y()));
  14.     int maxY = std::max(p1.y(), std::max(p2.y(), p3.y()));
  15.  
  16.     tmath::Vec2i p, pp1, pp2, pp3;
  17.     int c1, c2, c3;
  18.     float alpha, beta, gamma;
  19.     float interpInvW;
  20.     float area = (float)std::abs(tmath::cross(p2 - p1, p3 - p1));
  21.  
  22.     TFragmentShaderOutput fragOutput;
  23.     TVertexShaderOutput interpolatedInput;
  24.  
  25.     for (int i = minX; i <= maxX; i++)
  26.     {
  27.         p.x() = i;
  28.         for (int j = minY; j <= maxY; j++)
  29.         {
  30.             p.y() = j;
  31.  
  32.             pp1.x() = p1.x() - p.x(); pp1.y() = p1.y() - p.y(); // pp1 = p1 - p;
  33.             pp2.x() = p2.x() - p.x(); pp2.y() = p2.y() - p.y(); // pp2 = p2 - p;
  34.             pp3.x() = p3.x() - p.x(); pp3.y() = p3.y() - p.y(); // pp3 = p3 - p;
  35.  
  36.             c1 = tmath::cross(pp1, pp2);
  37.             c2 = tmath::cross(pp2, pp3);
  38.             c3 = tmath::cross(pp3, pp1);
  39.  
  40.             if ((c1 >= 0 && c2 >= 0 && c3 >= 0) ||
  41.                 (c1 <= 0 && c2 <= 0 && c3 <= 0))
  42.             {
  43.                 alpha = std::abs(c2) / area;
  44.                 beta = std::abs(c3) / area;
  45.                 gamma = std::abs(c1) / area;
  46.  
  47.                 interpInvW = v1.invW * alpha + v2.invW * beta + v3.invW * gamma;
  48.                 interpolatedInput.position.z() = (
  49.                     v1.position.z() * alpha +
  50.                     v2.position.z() * beta +
  51.                     v3.position.z() * gamma
  52.                     );
  53.  
  54.                 /**
  55.                  * 深度测试
  56.                  */
  57.                 if (DepthTest(i, j, interpolatedInput.position.z()) == false)
  58.                     continue;
  59.  
  60.                 if (v1.useColor)
  61.                 {
  62.                     interpolatedInput.color = tmath::interpolate(
  63.                         v1.color, alpha,
  64.                         v2.color, beta,
  65.                         v3.color, gamma
  66.                     ) / interpInvW;
  67.  
  68.                     fragShader(interpolatedInput, fragOutput);
  69.  
  70.                     SetPixel(i, j, TRGBA::FromVec4f(fragOutput.color));
  71.                 }
  72.                 else
  73.                 {
  74.                     interpolatedInput.uv = tmath::interpolate(
  75.                         v1.uv, alpha,
  76.                         v2.uv, beta,
  77.                         v3.uv, gamma
  78.                     ) / interpInvW;
  79.  
  80.                     switch (m_state->GetSampleMode())
  81.                     {
  82.                     case TSampleMode::Bilinear:
  83.                         SetPixel(i, j, SampleTextureBilinear(interpolatedInput.uv));
  84.                         break;
  85.  
  86.                     case TSampleMode::Nearest:
  87.                     default:
  88.                         SetPixel(i, j, SampleTextureNearest(interpolatedInput.uv));
  89.                         break;
  90.                     }
  91.                 }
  92.             }
  93.         }
  94.     }
  95. }

z 坐标插值不进行透视校正。一个原因是,不校正也“够用”了,不影响判断结果,减少计算量。

另一个原因是,OpenGL 中的深度值范围是 0 到 1。我们可以在 NDC 转屏幕坐标的时候进行转换。如果再校正,范围就不对了。

我们之前的屏幕变换矩阵,没有改变 z 值。所以,如代码清单 5 所示,我们把 z 值从 [-1,1] 映射到 [0,1]。

代码清单 5 屏幕变换矩阵
  1. template<typename T>
  2. Matrix<T, 4, 4> ScreenMatrix(int width, int height)
  3. {
  4.     T halfWidth = static_cast<T>(width) / 2;
  5.     T halfHeight = static_cast<T>(height) / 2;
  6.     T halfDepth = static_cast<T>(1) / 2;
  7.  
  8.     return Matrix<T, 4, 4>({
  9.         halfWidth, 0, 0, halfWidth,
  10.         0, -halfHeight, 0, halfHeight,
  11.         0, 0, halfDepth, halfDepth,
  12.         0, 0, 0, 1
  13.         });
  14. }

3. 测试

最后我们编写测试用例。如代码清单 6 所示,我们指定两个三角形,一个是渐变色的,在前面;另一个是纯色的,在后面。同时我们开启了深度测试,并指定 TDepthFunc::Less,即深度值小的在前面,会遮挡住后面深度值大的。

代码清单 6 数据定义
  1. TDepthTestRenderTask::TDepthTestRenderTask(TBasicWindow& win)
  2. {
  3.     float vertices[] = {
  4.         // 第一个三角形
  5.         -0.5f, 0.0f, 0.0f,
  6.          0.5f, 0.0f, 0.0f,
  7.         0.25f, 0.5f, 0.0f,
  8.  
  9.         // 第二个三角形
  10.         0.3f, 0.0f, 0.3f,
  11.         0.8f, 0.0f, 0.3f,
  12.         0.45f, 0.5f, 0.3f,
  13.     };
  14.  
  15.     float colors[] = {
  16.         // 第一个三角形
  17.         1.0f, 0.0f, 0.0f, 1.0f,
  18.         0.0f, 1.0f, 0.0f, 1.0f,
  19.         0.0f, 0.0f, 1.0f, 1.0f,
  20.  
  21.         // 第二个三角形
  22.         1.0f, 1.0f, 0.0f, 1.0f,
  23.         1.0f, 1.0f, 0.0f, 1.0f,
  24.         1.0f, 1.0f, 0.0f, 1.0f,
  25.     };
  26.  
  27.     uint32_t indices[] = {
  28.         // 第一个三角形
  29.         0, 1, 2,
  30.  
  31.         // 第二个三角形
  32.         3, 4, 5,
  33. };
  34.  
  35.     ////
  36.     sr.Enable(TEnableCap::DepthTest);
  37.     sr.DepthFunc(TDepthFunc::Less);
  38. }

为了说明问题,如代码清单 7 所示,特意调用两次绘制函数。先画前面的,后画后面的。

代码清单 7 绘制
  1. void TDepthTestRenderTask::Render(TSoftRenderer& sr)
  2. {
  3.     sr.ClearColor({ 0,0,0 });
  4.     sr.ClearDepth(1.0f);
  5.  
  6.     sr.DrawElements(TDrawMode::Triangles, 3, 0);
  7.     sr.DrawElements(TDrawMode::Triangles, 3, 3 * sizeof(uint32_t));
  8. }

图 1 是测试结果,可以看到虽然后面的纯色三角形是后画的,但是开启了深度测试,还是能正确绘制遮挡关系。

本节完整代码见 tag/depth_test

图1 结果