- tinyrenderer
- 個人代碼倉庫:tinyrenderer個人練習代碼
引言
還要從上一節知識說起,在上一節中我為了調試代碼,換了一個很簡單的正方形 obj 模型,配上紋理貼圖與法線貼圖進行渲染,得了下面的結果:
what?這是啥,為什么正方形中間紋理出現了扭曲,當我調大旋轉的角度,這個扭曲越發明顯:
很明顯這是紋理坐標出了問題,這就引出了另外一個知識:透視矯正插值,也就是 tinyrenderer 的這篇文章:Technical difficulties: linear interpolation with perspective deformations
透視矯正插值
首先思考我們現在的插值是怎么做的?
如上圖所示,假設我們渲染一個 △ 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
下圖展示了透視投影在視圖空間形成的視錐體,相機位于原點:
設點 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*} (A′wa?wa??)(B′wb?wb??)(C′wc?wc??)(P′wp?wp??)?=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?)(P′wp?wp??)?=α(A1?)+β(B1?)+γ(C1?)=αM(A1?)+βM(B1?)+γM(C1?)=α(A′wa?wa??)+β(B′wb?wb??)+γ(C′wc?wc??)?(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*} P′wp?wp??=αA′wa?+βB′wb?+γC′wc?=α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*} P′P′?=αwa?+βwb?+γwc?αA′wa?+βB′wb?+γC′wc??=αwa?+βwb?+γwc?αwa??A′+αwa?+βwb?+γwc?βwb??B′+αwa?+βwb?+γwc?γwc??C′?
所以有:
{ α ′ = α 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?+γwc?1?
則有:
{ α ′ = α 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} ? ? ??α′β′γ′?=αwa?k=βwb?k=γwc?k??
{ α = α ′ 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} ? ? ??αβγ?=wa?kα′?=wb?kβ′?=wc?kγ′??(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?=α+β+γ=wa?kα′?+wb?kβ′?+wc?kγ′?=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);
結果:
最后還需要提一點,本文提到的透視矯正只針對透視投影來說,正交投影不存在這個問題。
本次代碼提交記錄:
這個版本的
LookAt
函數存在錯誤!2025-4-29 16.23 提交修復
參考
- Technical difficulties: linear interpolation with perspective deformations
- perspective-correct-interpolation.dvi