透视校正

现在我们的管线流程还存在一些问题。之前的三角形看着不直观,我们看到视频 1 中的棋盘格显示,可以发现效果是错误的。

视频 1 未透视校正

错误的原因如图 1 所示 [1],比如我们在透视投影之后得到的插值比例是 0.5,但是透视投影之前对应的插值点,比例不是 0.5。透视投影之后得到的插值是错误的,我们希望把它修正成透视投影之前正确的插值。

图1 错误原因

1. 透视校正插值

插值错误的本质原因还是透视投影不是线性变换导致的。主要表现在透视除法这步操作,所以我们这边针对透视除法前后进行推导。

直观可以感受到,只要是线性变化,线性重心插值的比例是不会变的。这边不进行证明。

我们设透视除法之前的点,有

\(A(x_1,y_1,z_1,w_1),B(x_2,y_2,z_2,w_2),C(x_3,y_3,z_3,w_3)\)

透视除法之后的点,为

\(A'(\frac{x_1}{w_1},\frac{y_1}{w_1},\frac{z_1}{w_1},1),B'(\frac{x_2}{w_2},\frac{y_2}{w_2},\frac{z_2}{w_2},1),C'(\frac{x_3}{w_3},\frac{y_3}{w_3},\frac{z_3}{w_3},1)\)

目前的流程,我们是根据 \(A',B',C'\) 得到重心插值坐标的,我们有插值点

\(O'=\alpha'A'+\beta'B'+\gamma'C'\)

这边的 \(O'\) 肯定也有对应的透视除法之前的原始点,我们假设它为

\(O=(x_o,y_o,z_o,w_o)=\alpha A+\beta B+\gamma C\)

这样 \(O'\) 就有另一种表达方式

\(O'=\frac{O}{w_o}\)

这种假设是符合直觉的:透视除法前的插值点,对应透视除法之后的插值点。

就算没有实际联系,我们也可以把它看成是人为的映射关系。因为我们现在讨论的问题就是,希望把 \(\alpha'\) 修正成透视投影之前空间里的 \(\alpha\),至于具体修正成哪个点,我们这边进行人为规定。

我是这么说服自己的。

\(O'\) 的两种表达方式,要表达同一个点,变量的系数要一样。我们可以得到关系

\(\frac{\alpha'}{w_1}=\frac{\alpha}{w_o},\frac{\beta'}{w_2}=\frac{\beta}{w_o},\frac{\gamma'}{w_3}=\frac{\gamma}{w_o}\)

现在 \(\alpha'\) 和 \(w_1\) 这些都可以看成常数,要求 \(\alpha\) 这个“变量”。现在遇到的问题就是, \(w_o\) 怎么表示?

根据插值我们有 \(w_o=\alpha w_1+\beta w_2 + \gamma w_3\),但是这条等式里都是代求“变量”,无法利用。

根据 \(\alpha'+\beta'+\gamma'=1\),代入 \(\alpha'=\frac{\alpha w_1}{w_o},\beta'=\frac{\beta w_2}{w_o},\gamma'=\frac{\gamma w_3}{w_o}\)

也能正确得到 \(w_o=\alpha w_1+\beta w_2 + \gamma w_3\)。可以侧面看到假设是合理的。

根据 \(\alpha+\beta+\gamma=1\),代入 \(\alpha=\frac{\alpha'}{w_1}w_o,\beta=\frac{\beta'}{w_2}w_o,\gamma=\frac{\gamma'}{w_3}w_o\)

可以得到 \(\frac{1}{w_o}=\frac{\alpha'}{w_1}+\frac{\beta'}{w_2}+\frac{\gamma'}{w_3}\)

这样,我们的问题就解决了。可以看到形式也很漂亮:透视除法之前 \(w\) 分量的倒数,是满足透视除法之后的插值比例的。

以上推导过程中,“不严谨”的地方有点多(可以看到自己的“旁白注释”非常多)。可以参考文章 [1],里面是纯几何推导,更直观。

看到文章 [1] 里最后的结论公式,和我们推导出的结论是一样的。

\(Z_t=\frac{1}{\frac{1}{Z_1}+s(\frac{1}{Z_2}-\frac{1}{Z_2})}\)

2. 代码实现

校正公式比较难,但代码实现并不复杂。因为光栅化阶段已经进行了透视除法,所以如代码清单 1 所示,我们保存 w 分量。因为计算用的都是 1/w,所以直接保存 w 的倒数。

代码清单 1 记录 w
  1. struct TVertexShaderOutput
  2. {
  3.     tmath::Vec4f position;
  4.  
  5.     bool useColor;
  6.     tmath::Vec4f color;
  7.  
  8.     bool useUV;
  9.     tmath::Vec2f uv;
  10.  
  11.     TVertexShaderOutput();
  12.     virtual ~TVertexShaderOutput();
  13. };
  14.  
  15. struct TVertexShaderOutputPrivate : public TVertexShaderOutput
  16. {
  17.     float invW;
  18.  
  19.     TVertexShaderOutputPrivate();
  20.     TVertexShaderOutputPrivate Lerp(const TVertexShaderOutputPrivate& other, float t) const;
  21. };

如代码清单 2 的第 41 至 54 行所示,我们对 w 分量进行记录。同时,我们对颜色和 uv 坐标也先进行除以 w 处理,“减轻”光栅化插值阶段的计算。

代码清单 2 记录
  1. void TSoftRenderer::DrawElements(
  2.     TDrawMode mode,
  3.     uint32_t size,
  4. #if 0
  5.     TIndexDataType type,
  6. #endif
  7.     uint32_t offset)
  8. {
  9.     uint32_t* indexData = (uint32_t*)(m_currentElementBuffer->GetBufferData() + offset);
  10.  
  11.     FragmentShaderFunction fragFunc = std::bind(&TShader::FragmentShader, m_currentShader, std::placeholders::_1, std::placeholders::_2);
  12.     TShaderContext context(m_currentVertexArray);
  13.     TVertexShaderOutputPrivate vertexOutputs[3];
  14.     std::vector<TVertexShaderOutputPrivate> clippedVertices(10);
  15.     int primitive = GetPrimitiveCount(mode);
  16.  
  17.     for (uint32_t i = 0; i < size; i += primitive)
  18.     {
  19.         for (uint32_t j = 0; j < primitive; j++)
  20.         {
  21.             context.SetVertexIndex(indexData[i + j]);
  22.  
  23.             // VertexShader
  24.             m_currentShader->VertexShader(context, vertexOutputs[j]);
  25.         }
  26.  
  27.         clippedVertices.clear();
  28.         switch (mode)
  29.         {
  30.         case TDrawMode::Triangles:
  31.             SutherlandHodgmanClipTriangle(vertexOutputs, clippedVertices);
  32.             break;
  33.         case TDrawMode::Lines:
  34.         default:
  35.             assert(0);
  36.             break;
  37.         }
  38.         if (clippedVertices.empty())
  39.             continue;
  40.  
  41.         for (TVertexShaderOutputPrivate& vertex : clippedVertices)
  42.         {
  43.             // 透视除法
  44.             vertex.invW = 1 / vertex.position.w();
  45.  
  46.             vertex.position *= vertex.invW;
  47.             if (vertex.useColor)
  48.                 vertex.color *= vertex.invW;
  49.             else
  50.                 vertex.uv *= vertex.invW;
  51.  
  52.             // NDC - 屏幕
  53.             vertex.position = m_screenMatrix * vertex.position;
  54.         }
  55.  
  56.         switch (mode)
  57.         {
  58.         case TDrawMode::Triangles:
  59.             for (uint32_t k = 1; k + 1 < clippedVertices.size(); k++)
  60.                 m_rz.RasterizeTriangle(clippedVertices[0], clippedVertices[k], clippedVertices[k + 1], fragFunc);
  61.             break;
  62.         case TDrawMode::Lines:
  63.         default:
  64.             assert(0);
  65.             break;
  66.         }
  67.     }
  68. }

光栅化阶段,如代码清单 3 的第 47 行所示,我们先求得 \(\frac{1}{w_o}\)。

因为之前颜色和 uv 坐标已经除以过 w 分量了,所以如第 55 行和第 67 行所示,我们再乘上 \(w_o\),就能得到最终校正后的值。

代码清单 3 透视校正
  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.  
  49.                 if (v1.useColor)
  50.                 {
  51.                     interpolatedInput.color = tmath::interpolate(
  52.                         v1.color, alpha,
  53.                         v2.color, beta,
  54.                         v3.color, gamma
  55.                     ) / interpInvW;
  56.  
  57.                     fragShader(interpolatedInput, fragOutput);
  58.  
  59.                     SetPixel(i, j, TRGBA::FromVec4f(fragOutput.color));
  60.                 }
  61.                 else
  62.                 {
  63.                     interpolatedInput.uv = tmath::interpolate(
  64.                         v1.uv, alpha,
  65.                         v2.uv, beta,
  66.                         v3.uv, gamma
  67.                     ) / interpInvW;
  68.  
  69.                     switch (m_state->GetSampleMode())
  70.                     {
  71.                     case TSampleMode::Bilinear:
  72.                         SetPixel(i, j, SampleTextureBilinear(interpolatedInput.uv));
  73.                         break;
  74.  
  75.                     case TSampleMode::Nearest:
  76.                     default:
  77.                         SetPixel(i, j, SampleTextureNearest(interpolatedInput.uv));
  78.                         break;
  79.                     }
  80.                 }
  81.             }
  82.         }
  83.     }
  84. }

测试

如代码清单 4 所示,我们写一个棋盘格渲染样例,更容易看清透视校正的结果。我们用两个三角形拼成一个矩形。黑白棋盘纹理是直接用代码生成的。

代码清单 4 棋盘格测试
  1. #include "TChessboardRenderTask.h"
  2.  
  3. TChessboardRenderTask::TChessboardRenderTask(TBasicWindow& win)
  4.     : m_angle(0)
  5. {
  6.     float vertices[] = {
  7.         -1.0f, -1.0f, 0.0f,
  8.         1.0f, -1.0f, 0.0f,
  9.         1.0f, 1.0f, 0.0f,
  10.         -1.0f, 1.0f, 0.0f
  11.     };
  12.  
  13.     float colors[] = {
  14.         1.0f, 0.0f, 0.0f, 1.0f,
  15.         0.0f, 1.0f, 0.0f, 1.0f,
  16.         0.0f, 0.0f, 1.0f, 1.0f,
  17.         0.0f, 0.0f, 0.0f, 1.0f,
  18.     };
  19.  
  20.     float uvs[] = {
  21.         0.0f, 0.0f,
  22.         1.0f, 0.0f,
  23.         1.0f, 1.0f,
  24.         0.0f, 1.0f
  25.     };
  26.  
  27.     uint32_t indices[] = {
  28.         0, 1, 2,
  29.         2, 3, 0,
  30.     };
  31.  
  32.     TSoftRenderer& sr = win.GetRenderer();
  33.  
  34.     uint32_t vao, vboPosition, vboColor, vboUv, ebo;
  35.     sr.GenVertexArrays(1, &vao);
  36.     sr.BindVertexArray(vao);
  37.  
  38.     sr.GenBuffers(1, &vboPosition);
  39.     sr.BindBuffer(TBufferType::ArrayBuffer, vboPosition);
  40.     sr.BufferData(TBufferType::ArrayBuffer, sizeof(vertices), vertices);
  41.     sr.VertexAttribPointer(0, 3, 3 * sizeof(float), 0);
  42.  
  43.     sr.GenBuffers(1, &vboColor);
  44.     sr.BindBuffer(TBufferType::ArrayBuffer, vboColor);
  45.     sr.BufferData(TBufferType::ArrayBuffer, sizeof(colors), colors);
  46.     sr.VertexAttribPointer(1, 4, 4 * sizeof(float), 0);
  47.  
  48.     sr.GenBuffers(1, &vboUv);
  49.     sr.BindBuffer(TBufferType::ArrayBuffer, vboUv);
  50.     sr.BufferData(TBufferType::ArrayBuffer, sizeof(uvs), uvs);
  51.     sr.VertexAttribPointer(2, 2, 2 * sizeof(float), 0);
  52.  
  53.     sr.GenBuffers(1, &ebo);
  54.     sr.BindBuffer(TBufferType::ElementArrayBuffer, ebo);
  55.     sr.BufferData(TBufferType::ElementArrayBuffer, sizeof(indices), indices);
  56.  
  57.     sr.PrintVAO(vao);
  58.  
  59.     ////
  60.     int width = win.GetWindowWidth();
  61.     int height = win.GetWindowHeight();
  62.  
  63.     float aspect = (float)width / height;
  64.  
  65.     m_shader.projectionMatrix = tmath::PerspectiveMatrix(tmath::degToRad(60.0f), aspect, 0.1f, 100.0f);
  66.     m_shader.viewMatrix = tmath::TranslationMatrix(0.0f, 0.0f, 3.0f);
  67.  
  68.     sr.UseProgram(&m_shader);
  69.  
  70.     const int textureSize = 8;  // 8x8
  71.     std::vector<uint32_t> chessboardTexture(textureSize * textureSize);
  72.  
  73.     // 生成棋盘格纹理
  74.     for (int y = 0; y < textureSize; ++y)
  75.     {
  76.         for (int x = 0; x < textureSize; ++x)
  77.         {
  78.             bool isBlack = (x + y) % 2 == 0;
  79.             chessboardTexture[y * textureSize + x] = isBlack ? 0xFF000000 : 0xFFFFFFFF; // 黑色或白色
  80.         }
  81.     }
  82.  
  83.     m_texture = TImage((const unsigned char*)chessboardTexture.data(), 8, 8);
  84.     sr.SetTexture(&m_texture);
  85. }
  86.  
  87. void TChessboardRenderTask::Render(TSoftRenderer& sr)
  88. {
  89.     Transform();
  90.  
  91.     sr.Clear({ 0,0,0 });
  92.  
  93.     sr.DrawElements(TDrawMode::Triangles, 6, 0);
  94. }
  95.  
  96. void TChessboardRenderTask::Transform()
  97. {
  98.     m_angle -= 0.01f;
  99.     m_shader.modelMatrix = tmath::RotationMatrix(tmath::Vec3f(0.0f, 1.0f, 0.0f), m_angle);
  100. }

本篇文章的完整代码见 tag/perspective_correction

最终的运行结果如视频 2 所示,对比文章开头的视频 1 内容,可以看到经过透视校正后,效果显示正常。

视频 2 未透视校正

References

[1] Low K L. Perspective-correct interpolation[J]. Technical writing, Department of Computer Science, University of North Carolina at Chapel Hill, 2002.