動手實現一個C++軟渲染器(二)給點顏色瞧瞧
在上一篇文章裡面,我實現了繪製點和線。那麼,這次我就來實現面的繪製,準確說是繪製Primitive(這個中文翻譯很不統一,有叫圖元的,有叫片元的,但是Fragment又有叫片元的)
首先,我們明確這個面的概念是在二維空間中的三角形畫素集。當然Primitive可不止這一種,之前的線段也屬於這個,還有三角形線框等。可能有些人會奇怪,我為什麼要說的這麼繁瑣。直接畫個三角形不就完了嗎?因為寫軟渲染器的目的是透過模擬渲染管線來理解渲染原理。這裡聯絡到我們經常說的光柵化(Rasterization),啥叫光柵化啊?根據RTR4書上描述,光柵化包含了三角形設定和三角形遍歷的過程。
光柵化這個名詞定義並不絕對,比如在RTR3中,光柵化又包含了後面兩個階段。
而這篇文章將要實現的DrawPrimitive(),就是光柵化的軟體實現。所以並不能說就簡單地去實現一個三角形繪製演算法,那樣和剛開始學程式設計的時候繪製多邊形有啥區別。這裡講這麼多廢話就是為了明確出這個函式在渲染管線中所處的位置。
三角形光柵化的演算法我採用的是重心座標(BarycentricCoordinate)插值演算法,這是現代顯示卡採用的演算法。相比於過去的掃描線(Scanline)演算法,更方便於顏色和紋理插值。這裡有個概念叫做重心座標。如圖,這是二維空間中的一個三角形。
T1,T2,T3為三角形的頂點,P為二維空間中任意一點。
先說結論:
對於任意P點座標都有:
,其中:
。
當
且
且
時, P點在三角形內(包含邊界)。至於證明過程wiki上面有,不過不太容懂。這裡貼出一個很好的證明過程:
有了這個公式,我們就可以給定三個頂點求出任意點P是否在三角形中,這個判斷過程就是三角形遍歷(Triangle Traversal)。那現在的問題就是如何求出u,v,w?
還是直接說結論:
u,v,w分別是P點與T1,T2,T3的對邊構成的三角形面積和T1,T2,T3構成的三角形面積的比值。這裡也貼出我認為比較好的證明過程:
也就是說u,v,w是P點分割出來的三個小三角形對總面積的佔比。而由於小三角形都對大三角形的一條邊共底,面積之比就是高之比。因此,u,v,w即就是P點到T1,T2,T3的對邊距離與T1,T2,T3到對邊的距離之比。那我們馬上拿出高中就學過的點到直線距離公式:
由於求的是兩點對同一條線的距離比值,因此可以將分母約去。而且,分子的正負性與點在直線兩側的位置有關,因此需要去掉絕對值符號(u,v,w的正負性代表P點與T1,T2,T3是否在其對邊的同側)。
終於獻出程式碼了,前面說了一大堆數學原理,其實程式碼就幾行:
Vector3
Canvas
::
GetBarycentricCoord
(
const
Vector2
&
P1
,
const
Vector2
&
P2
,
const
Vector2
&
P3
,
const
Vector2
&
P
)
{
float
u
=
((
P2
。
y
-
P3
。
y
)
*
P
。
x
+
(
P3
。
x
-
P2
。
x
)
*
P
。
y
+
(
P2
。
x
*
P3
。
y
-
P3
。
x
*
P2
。
y
))
/
((
P2
。
y
-
P3
。
y
)
*
P1
。
x
+
(
P3
。
x
-
P2
。
x
)
*
P1
。
y
+
(
P2
。
x
*
P3
。
y
-
P3
。
x
*
P2
。
y
));
float
v
=
((
P1
。
y
-
P3
。
y
)
*
P
。
x
+
(
P3
。
x
-
P1
。
x
)
*
P
。
y
+
(
P1
。
x
*
P3
。
y
-
P3
。
x
*
P1
。
y
))
/
((
P1
。
y
-
P3
。
y
)
*
P2
。
x
+
(
P3
。
x
-
P1
。
x
)
*
P2
。
y
+
(
P1
。
x
*
P3
。
y
-
P3
。
x
*
P1
。
y
));
//float w = ((P1。y - P2。y) * P。x + (P2。x - P1。x) * P。y + (P1。x * P2。y - P2。x * P1。y)) / ((P1。y - P2。y) * P3。x + (P2。x - P1。x) * P3。y + (P1。x * P2。y - P2。x * P1。y));
float
w
=
1
-
u
-
v
;
return
Vector3
(
u
,
v
,
w
);
}
注意這裡我傳入的三個頂點以及一個任意點P都是二維向量,而返回的卻是一個三維向量。還是拿出RTR4的管線圖:
其中Geometry Processing中已經將原始資料中的三維頂點對映到螢幕座標上了:
因此我們傳入的引數是對映後的螢幕座標,而返回值是u,v,w組成的重心座標,並不代表的是三維座標。
這個時候,其實已經可以繪製三角形了,但是重心座標的優勢——插值,還沒利用上。這裡我只先加一個顏色插值。前面提到了,在三角形內部的重心座標的三個分量是面積的比值,且範圍在
。因此,用來對映色彩空間再好不過了,而且顏色過度也是根據到頂點的距離變化。
void
Canvas
::
DrawPrimitive
(
const
vertex
&
V1
,
const
vertex
&
V2
,
const
vertex
&
V3
)
{
for
(
int
i
=
0
;
i
<
height
;
i
++
)
{
for
(
int
j
=
0
;
j
<
width
;
j
++
)
{
Vector3
BaryCoord
=
GetBarycentricCoord
(
V1
。
p
,
V2
。
p
,
V3
。
p
,
Vector2
(
j
,
i
));
if
(
BaryCoord
。
x
>=
0
&&
BaryCoord
。
x
<=
1
&&
BaryCoord
。
y
>=
0
&&
BaryCoord
。
y
<=
1
&&
BaryCoord
。
z
>=
0
&&
BaryCoord
。
z
<=
1
)
{
DrawPixel
(
V1
。
c
*
BaryCoord
。
x
+
V2
。
c
*
BaryCoord
。
y
+
V3
。
c
*
BaryCoord
。
z
,
j
,
i
);
}
}
}
}
顏色分別乘上u,v,w後相加,就得到畫素的顏色。
最後我們繪製一個RGB三角形:
好了,這篇文章就到此結束了,其實內容很少,都是些數學原理,寫完才發現廢話很多。之後我會把前面的管線補充完整。這部分內容涉及透視、矩陣變換、投影等更多的數學知識。所以我應該改變一下風格,數學原理部分我直接貼出更好的證明。更多的筆墨應該是著重於管線流程上面。