当前位置: 首页 > news >正文

tinyrenderer笔记(透视矫正)

  • tinyrenderer
  • 个人代码仓库:tinyrenderer个人练习代码

引言

还要从上一节知识说起,在上一节中我为了调试代码,换了一个很简单的正方形 obj 模型,配上纹理贴图与法线贴图进行渲染,得了下面的结果:

image.png|475

what?这是啥,为什么正方形中间纹理出现了扭曲,当我调大旋转的角度,这个扭曲越发明显:

image.png

很明显这是纹理坐标出了问题,这就引出了另外一个知识:透视矫正插值,也就是 tinyrenderer 的这篇文章:Technical difficulties: linear interpolation with perspective deformations

透视矫正插值

首先思考我们现在的插值是怎么做的?

image.png

如上图所示,假设我们渲染一个 △ A B C \bigtriangleup ABC ABC 内的 P P P 点,经透视投影后,被投影为 △ a b c \bigtriangleup abc abc 内的 p p p 点。我们是如何计算重心坐标的?

Vec3f bc_screen = glm::barycentric(viewport_coords[0], viewport_coords[1], viewport_coords[2], P);

上述代码中,我们拿的是屏幕空间的坐标去计算重心坐标。思考这样一个问题:假设观察空间下 △ A B C \bigtriangleup ABC ABC P P P 点的重心坐标为 ( α , β , γ ) (\alpha,\beta,\gamma) (α,β,γ) △ a b c \bigtriangleup abc abc p p p 点为 ( α ′ , β ′ , γ ′ ) (\alpha^\prime,\beta^\prime,\gamma^\prime) (α,β,γ)。你觉得 ( α , β , γ ) = ( α ′ , β ′ , γ ′ ) (\alpha,\beta,\gamma)=(\alpha^\prime,\beta^\prime,\gamma^\prime) (α,β,γ)=(α,β,γ) 吗?

答案为否,因为透视投影是一种非线性变换, △ A B C \bigtriangleup ABC ABC 经过透视投影之后会发生畸变,整体比例会发生变化。但我们却用 ( α ′ , β ′ , γ ′ ) (\alpha^\prime,\beta^\prime,\gamma^\prime) (α,β,γ) 去插值,希望得到 △ A B C \bigtriangleup ABC ABC 内点 P P P 的属性,这肯定会造成误差!

可能有同学很快会想出一种思路:既然计算 △ a b c \bigtriangleup abc abc p p p 点的重心坐标去插值不准确,那么我是否可以将屏幕空间下的 p p p 点经过逆变换得到 P P P 点,然后计算 P P P 点关于 △ A B C \bigtriangleup ABC ABC 的重心坐标呢?

理论上没有问题,但是思考一下我们是如何获得 p p p 点的 z z z (深度值)的,我们是通过插值获得的(这不套娃吗?),所以屏幕空间下 p p p 点的 z z z 坐标是无法准确得到的。同时,在经过透视投影后,我们进行了透视除法, w w w 分量也被丢弃了。所以,我们将 p p p 逆变换成 P P P 困难重重。

虽然 p p p 点的信息我们无法确定,但是三角形三个顶点的所有信息我们是能够知道的,那么我们是否能够通过已知变量来建立 ( α ′ , β ′ , γ ′ ) (\alpha^\prime,\beta^\prime,\gamma^\prime) (α,β,γ) ( α , β , γ ) (\alpha,\beta,\gamma) (α,β,γ) 的映射关系呢?前辈们已经为我们做好了这个工作,下面将开始推导。

推导

推导过程来自:perspective-correct-interpolation.dvi

下图展示了透视投影在视图空间形成的视锥体,相机位于原点:

image.png

设点 A , B , C , P A,B,C,P A,B,C,P 经过齐次矩阵 M M M 转化为了 A ′ , B ′ , C ′ , P ′ A^\prime,B^\prime,C^\prime,P^\prime A,B,C,P(经过透视投影与透视除法的坐标):

( A ′ w a w a ) = M ( A 1 ) ( B ′ w b w b ) = M ( B 1 ) ( C ′ w c w c ) = M ( C 1 ) ( P ′ w p w p ) = M ( P 1 ) \begin{align*} \begin{pmatrix} A'w_a \\ w_a \end{pmatrix} &= \mathbf{M} \begin{pmatrix} A \\ 1 \end{pmatrix}\\ \begin{pmatrix} B'w_b \\ w_b \end{pmatrix} &= \mathbf{M} \begin{pmatrix} B \\ 1 \end{pmatrix}\\ \begin{pmatrix} C'w_c \\ w_c \end{pmatrix} &= \mathbf{M} \begin{pmatrix} C \\ 1 \end{pmatrix}\\ \begin{pmatrix} P'w_p \\ w_p \end{pmatrix} &= \mathbf{M} \begin{pmatrix} P \\ 1 \end{pmatrix} \end{align*} (Awawa)(Bwbwb)(Cwcwc)(Pwpwp)=M(A1)=M(B1)=M(C1)=M(P1)

真正的重心坐标权重为 α , β , γ \alpha,\beta,\gamma α,β,γ,经过透视投影变为了 α ′ , β ′ , γ ′ \alpha^\prime,\beta^\prime,\gamma^\prime α,β,γ

P = α A + β B + γ C P ′ = α ′ A ′ + β ′ B ′ + γ ′ C ′ \begin{align*} P &= \alpha A + \beta B + \gamma C\\ P' &= \alpha' A' + \beta' B' + \gamma' C' \end{align*} PP=αA+βB+γC=αA+βB+γC

在光栅化阶段我们可以直接计算 α ′ , β ′ , γ ′ \alpha^\prime,\beta^\prime,\gamma^\prime α,β,γ,但我们需要 α , β , γ \alpha,\beta,\gamma α,β,γ 来正确的插值顶点的属性。

( P 1 ) = α ( A 1 ) + β ( B 1 ) + γ ( C 1 ) M ( P 1 ) = α M ( A 1 ) + β M ( B 1 ) + γ M ( C 1 ) ( P ′ w p w p ) = α ( A ′ w a w a ) + β ( B ′ w b w b ) + γ ( C ′ w c w c ) \begin{align*} \begin{pmatrix} P \\ 1 \end{pmatrix} &= \alpha \begin{pmatrix} A \\ 1 \end{pmatrix} + \beta \begin{pmatrix} B \\ 1 \end{pmatrix} + \gamma \begin{pmatrix} C \\ 1 \end{pmatrix}\\ \mathbf{M} \begin{pmatrix} P \\ 1 \end{pmatrix} &= \alpha \mathbf{M} \begin{pmatrix} A \\ 1 \end{pmatrix} + \beta \mathbf{M} \begin{pmatrix} B \\ 1 \end{pmatrix} + \gamma \mathbf{M} \begin{pmatrix} C \\ 1 \end{pmatrix}\\ \begin{pmatrix} P'w_p \\ w_p \end{pmatrix} &= \alpha \begin{pmatrix} A'w_a \\ w_a \end{pmatrix} + \beta \begin{pmatrix} B'w_b \\ w_b \end{pmatrix} + \gamma \begin{pmatrix} C'w_c \\ w_c \end{pmatrix} \tag{1} \end{align*} (P1)M(P1)(Pwpwp)=α(A1)+β(B1)+γ(C1)=αM(A1)+βM(B1)+γM(C1)=α(Awawa)+β(Bwbwb)+γ(Cwcwc)(1)

将 1 式分解则有:

P ′ w p = α A ′ w a + β B ′ w b + γ C ′ w c w p = α w a + β w b + γ w c \begin{align*} P' w_p &= \alpha A' w_a + \beta B' w_b + \gamma C' w_c \tag{2}\\ w_p &= \alpha w_a + \beta w_b + \gamma w_c \tag{3}\\ \end{align*} Pwpwp=αAwa+βBwb+γCwc=αwa+βwb+γwc(2)(3)

将 3 式带入 2 式可得:

P ′ = α A ′ w a + β B ′ w b + γ C ′ w c α w a + β w b + γ w c P ′ = α w a α w a + β w b + γ w c A ′ + β w b α w a + β w b + γ w c B ′ + γ w c α w a + β w b + γ w c C ′ \begin{align*} P' &= \frac{\alpha A' w_a + \beta B' w_b + \gamma C' w_c}{\alpha w_a + \beta w_b + \gamma w_c}\\ P' &= \frac{\alpha w_a}{\alpha w_a + \beta w_b + \gamma w_c} A' + \frac{\beta w_b}{\alpha w_a + \beta w_b + \gamma w_c} B' + \frac{\gamma w_c}{\alpha w_a + \beta w_b + \gamma w_c} C' \end{align*} PP=αwa+βwb+γwcαAwa+βBwb+γCwc=αwa+βwb+γwcαwaA+αwa+βwb+γwcβwbB+αwa+βwb+γwcγwcC

所以有:

{ α ′ = α w a α w a + β w b + γ w c β ′ = β w b α w a + β w b + γ w c γ ′ = γ w c α w a + β w b + γ w c \begin{cases} \begin{align*} \alpha' &= \frac{\alpha w_a}{\alpha w_a + \beta w_b + \gamma w_c}\\ \beta' &= \frac{\beta w_b}{\alpha w_a + \beta w_b + \gamma w_c}\\ \gamma' &= \frac{\gamma w_c}{\alpha w_a + \beta w_b + \gamma w_c} \end{align*} \end{cases} αβγ=αwa+βwb+γwcαwa=αwa+βwb+γwcβwb=αwa+βwb+γwcγwc

但我们希望用 α ′ , β ′ , γ ′ \alpha',\beta',\gamma' α,β,γ 来表示 α , β , γ \alpha,\beta,\gamma α,β,γ,设分母为 k k k

k = 1 α w a + β w b + γ w c k=\frac{1}{\alpha w_a + \beta w_b + \gamma w_c} k=αwa+βwb+γwc1

则有:

{ α ′ = α w a k β ′ = β w b k γ ′ = γ w c k \begin{cases} \begin{align*} \alpha' &= \alpha w_ak\\ \beta' &= \beta w_bk\\ \gamma' &= \gamma w_ck \end{align*} \end{cases} αβγ=αwak=βwbk=γwck

{ α = α ′ w a k β = β ′ w b k γ = γ ′ w c k \begin{cases} \begin{align*} \alpha &= \frac{\alpha'}{w_ak} \tag{4}\\ \beta &= \frac{\beta'}{w_bk}\tag{5}\\ \gamma &= \frac{\gamma'}{w_ck}\tag{6} \end{align*} \end{cases} αβγ=wakα=wbkβ=wckγ(4)(5)(6)

又因为 α + β + γ = 1 \alpha+\beta+\gamma=1 α+β+γ=1

1 = α + β + γ = α ′ w a k + β ′ w b k + γ ′ w c k k = α ′ w a + β ′ w b + γ ′ w c \begin{align*} 1&=\alpha+\beta+\gamma=\frac{\alpha'}{w_ak}+\frac{\beta'}{w_bk}+\frac{\gamma'}{w_ck}\\ k&=\frac{\alpha'}{w_a}+\frac{\beta'}{w_b}+\frac{\gamma'}{w_c} \tag{7} \end{align*} 1k=α+β+γ=wakα+wbkβ+wckγ=waα+wbβ+wcγ(7)

将 7 式带入 4、5、6 式即可得到:

{ α = α ′ w a α ′ w a + β ′ w b + γ ′ w c β = β ′ w b α ′ w a + β ′ w b + γ ′ w c γ = γ ′ w c α ′ w a + β ′ w b + γ ′ w c \begin{cases} \begin{align*} \alpha &= \frac{\frac{\alpha'}{w_a}}{\frac{\alpha'}{w_a}+\frac{\beta'}{w_b}+\frac{\gamma'}{w_c}}\\ \beta &= \frac{\frac{\beta'}{w_b}}{\frac{\alpha'}{w_a}+\frac{\beta'}{w_b}+\frac{\gamma'}{w_c}}\\ \gamma &= \frac{\frac{\gamma'}{w_c}}{\frac{\alpha'}{w_a}+\frac{\beta'}{w_b}+\frac{\gamma'}{w_c}} \end{align*} \end{cases} αβγ=waα+wbβ+wcγwaα=waα+wbβ+wcγwbβ=waα+wbβ+wcγwcγ

现在,我们就可以使用 α , β , γ \alpha,\beta,\gamma α,β,γ 来正确插值顶点的属性了。

代码

来完善这个函数,barycentricCorrect,它会返回经过透视矫正后的重心坐标。这是在二位平面上的插值,A、B、C 的 z 分量会存储它们经过透视投影后的 w 分量。

Vec3f glm::barycentricCorrect(const Vec3f& A, const Vec3f& B, const Vec3f& C, const Vec3f& P)
{if (IsNearlyZero(A.z) && IsNearlyZero(B.z) && IsNearlyZero(C.z)){std::cout << "glm::barycentricCorrect A, B, C w is zero!" << std::endl;return barycentric(A, B, C, P);}Vec3f bc = barycentric(A, B, C, P);float det = bc.x/A.z + bc.y/B.z + bc.z/C.z;if (IsNearlyZero(det)){std::cout << "glm::barycentricCorrect det is zero" << std::endl;return bc;}bc.x = bc.x / A.z / det;bc.y = bc.y / B.z / det;bc.z = bc.z / C.z / det;return bc;
}

剩下要做的就只有在 glProgram::Draw 函数内,把对 barycentric 改为 barycentricCorrect

Vec3f bc_screen = glm::barycentricCorrect(Vec3f(viewport_coords[0].x, viewport_coords[0].y, vertexs_w[0]),Vec3f(viewport_coords[1].x, viewport_coords[1].y, vertexs_w[1]),Vec3f(viewport_coords[2].x, viewport_coords[2].y, vertexs_w[2]), P);

结果:

image.png

最后还需要提一点,本文提到的透视矫正只针对透视投影来说,正交投影不存在这个问题。

本次代码提交记录:

image.png

这个版本的 LookAt 函数存在错误!2025-4-29 16.23 提交修复

参考

  • Technical difficulties: linear interpolation with perspective deformations
  • perspective-correct-interpolation.dvi

相关文章:

  • 如何搭建spark yarn模式集群的集群
  • 树上背包学习笔记
  • 【mysql】常用命令
  • vue源代码采用的设计模式分解
  • accept() reject() hide()
  • Select Rows组件研究
  • 使用Java和LangChain4j实现人工智能:从分类到生成式AI
  • stm32之输出比较OC和输入捕获IC
  • SQLite数据类型
  • Class AB OPA corner 仿真,有些corenr相位从0开始
  • 使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第十一讲)
  • 人工智能100问☞第15问:人工智能的常见分类方式有哪些?
  • 2025年软件工程与数据挖掘国际会议(SEDM 2025)
  • Three.js和WebGL区别、应用建议
  • 大模型在宫颈癌诊疗全流程预测与应用研究报告
  • 【免费试用】LattePanda Mu x86 计算模块套件,专为嵌入式开发、边缘计算与 AI 模型部署设计
  • [论文阅读]MCP Guardian: A Security-First Layer for Safeguarding MCP-Based AI System
  • VMware搭建ubuntu保姆级教程
  • NGINX `ngx_http_browser_module` 深度解析与实战
  • 数据中台架构设计
  • 罗马尼亚临时总统博洛让任命普雷多尤为看守政府总理
  • 柳向春:关于美国国会图书馆所藏《全芳备祖》的一些故事
  • 五一上海楼市热闹开局:售楼处全员到岗,热门楼盘连续触发积分
  • 新华社:让历史照鉴未来
  • 莫斯科一机场实施临时限制措施
  • 全国铁路迎来返程客流高峰,预计今日发送2040万人次