華文網

3D繪畫,圖形學理門指南

介紹

當我還小的時候,我曾以為電腦圖形學是最酷的玩意兒。但是隨即我認識到,學習圖形學——創建那些超級閃亮的電腦程式——比我想像的要難上許多。我四處出擊,閱讀OpenGL渲染管線詳解之類的文章,

流覽關於圖形工作原理的博客、網站等,對照著教程學習,試圖搞懂一切。結果呢,一無所獲。當我按照NeHe的教程設置好一切,卻因為錯誤的調用了某個glXXX()函數,導致各種錯誤。我不具備正確偵錯工具的基礎理論知識,所以我放棄了——就像我那個年紀的少年在遇到挫折時通常會做的那樣。

然而,在若干年之後,我有機會能夠在大學裡參加一些電腦圖形學的課程。

這次我終於知道它們是如何正確工作了。如果我早知道這些,我那時應該能獲得更多成功。所以,為了幫助和我有類似困境的人們,我打算分享下我學到的東西。

圖形學背後的理念

概覽

先想想真實世界的樣子。在3D真實世界裡,光線從許多個不同的光源發出,在多個物體間跳轉,然後一部分光子通過眼球刺激到你的視網膜。在真實的場景裡,3D的世界投影到2D的表面。雖然你的大腦從環境中獲取各種視覺元素然後組成一個立體的影像來反映整個3D空間,

但這些都源於2D資訊。當場景中的物體移動,或者你相對於你的場景發生移動,或光照改變時,視網膜上的2D圖像也立刻發生改變。我們的視覺系統快速處理圖像,然後大腦據此構造出3D模型。

如果我們能夠獲取一些圖片,然後以類似或更高的速率來交替顯示它們,就能生成一個看起來像真實空間的場景。電影大致基於同一原理工作。在電影裡,3D場景的圖片高速閃過,看起來就像連續的一樣。請參照上面馬的例子。如果我們能夠在電腦上持續的繪製一個運動的場景,那麼它看起來就像一個3D的世界一樣。圖形學就是這樣工作的:將虛擬的3D世界快速轉換成2D表現形式,

讓大腦感覺像是一個3D的場景一樣。

約束條件

人類視覺將一系列圖片看作連續的閾值大約時16Hz。對電腦來說,我們有最多62.5毫秒來完成下列事情:

判斷虛擬場景中眼睛看向哪裡。

計算場景在這個角度下如何呈現。

計算需要被繪製在螢幕上的圖元的顏色。

用這些顏色填充幀緩存。

將緩存發送至顯示裝置。

顯示圖片。

這是一個複雜的問題。時間上的限制意味著我們不能直接硬來——比如往3D場景裡扔一堆光子,

計算它們的軌跡和強度,算出哪些能夠照進眼睛並將之映射到2D的圖片上,最後繪製。(這並不完全正確,因為這有點像光線追蹤時做的事。但光線追蹤技術相當複雜,而且完全是另一回事,因此也可以這麼說。)幸運的是,我們可以利用一些很酷的技巧來大大縮減計算量。

基礎圖形理論

整個世界是一個舞臺

打個比方。假設你在一個山谷裡面,四面環山,河流前方是一片草地,類似上面這張Bob Ross的繪畫作品中的場景。你想用一張圖展現這個3D場景。該怎麼做呢?嗯,我們可以嘗試繪製一張包含所有景色的圖。那意味著我們必須得挑選一個觀察角度,只繪製能看到的部分,忽略其餘的。然後還得決定哪些部分在前,哪些部分在後。我們能看到草地,但它遮擋了部分河流。群山在遠處,但它們遮擋了後方的一切景色。由於場景的真實大小遠大於我們畫布的大小,我們還得計算如何縮放。然後我們繪製場景,將光照和陰影考慮進去,以及遠處山間的霧氣,等等。這和電腦中的圖形處理過程十分相像。主要步驟如下:

決定世界中的物體長什麼樣子。

決定物體在世界中的位置。

決定相機的位置和哪些場景需要被繪製。

決定物體相對於相機的位置。

繪製場景中的物體。

將場景縮放到視窗大小一致的圖像上。

上述步驟大致是為了把3D世界中的某個點映射到螢幕上的2D圖像上。乍一看工作量很大,其實可以利用一些數學技巧來快速完成工作。還記得學代數的時候曾想,“學這個有什麼用?”其中一個用處就是圖形學!

我們可以用矩陣來將世界中的座標映射到圖像上。為什麼選擇矩陣?其中一個原因是很多操作都可以用矩陣來表示。最重要的是我們可以通過矩陣乘法將多個操作連接起來,最終只用一個矩陣來代表一系列操作!即使要做50個變換,只需將所有矩陣相乘就得到一個同時執行所有變換的矩陣。

我們可以定義一些矩陣來完成我們上述繪畫示例中的操作(定義場景,定義視野等)。這些矩陣能夠將場景從一個坐標系轉換到另一個坐標系。這些坐標系之間的轉換稱為變換。我們將要討論每一個坐標系以及不同坐標系之間的轉換過程。

模型坐標系——分解模型

如何在螢幕上快速繪製物體呢?電腦相當擅長連續快速完成大量相對簡單的指令。為了利用這一點,如果我們能夠用簡單的形狀來表示整個世界,就能夠通過快速處理許多簡單的形狀來優化圖形演算法。因此,電腦不需要知道一座山或一片草地到底是什麼就能夠繪製它們。

我們需要創建一些演算法把圖形分解成簡單的多邊形。這個過程稱作曲面細分(Tessellation)。雖然我們也可以用正方形,但三角形更好一些。使用三角形有很多優勢,例如所有的三角形的點都是共平面(coplanar)的,而且幾乎所有形狀都能近似分解成多個三角形。唯一的問題是圓形物體會看起來像是有棱角的。不過,要是三角面足夠小的話,比如1個圖元大小,我們就不會注意到了。關於如何曲面細分的“最佳方法”有很多,具體取決於你要分解的物體的形狀。

假設我們要分解一個球形。我們可以將球體的中心位置定為本地的原點。這樣我們就可以用一個公式來獲得球面上的一些點然後將這些點連接成多邊形以供繪製。一個常用的公式是S(u,v)=[r sin u cos v,r sin u sin v,r cos v],u和v的取值範圍分別是u∈[0,π],v∈[0,2π],r是球體的半徑。就像你在圖中看到的那樣,球體表面的點被繪製成矩形。我們能夠很方便的把它們連成三角形。

球面上的點位於所謂的模型坐標系。座標相對于本地的原點定義,比如示例中球體的中心位置。如果我們想要將物體放置於場景中,我們可以定義一個從場景原點到場景中球體的原點的向量,然後把這個向量和球面上每個點的座標相加。這樣我們就將模型放置到了世界坐標系中。

世界坐標系——將物體置於世界中

到這兒我們的圖形學之旅才真正開始。我們在某處定義一個原點,場景中的每個點都基於從原點到該點的一個向量來定義。雖然場景是3D的,我們還是得用一個4維的座標來定義每個點[x,y,z,w],代表該點的座標為[x/w,y/w,z/w]。這種映射稱為齊次座標。使用齊次座標有一些好處,但是這裡不做討論。只需知道我們使用齊次座標就夠了。

假設我們要在場景中移動,那麼問題來了。如果我們要移動視線,或者移動相機到另一個位置,或者讓整個世界圍著相機移動。在電腦的世界裡,移動整個世界更容易一點,所以我們就這樣做,讓相機固定不動。模型-視圖矩陣(modelview matrix)是一個4x4矩陣,可以用來移動世界中的每一個點,然後讓相機固定不動。這個矩陣基本上就是一系列旋轉、位移、縮放的集合。我們在世界坐標系中將點和模型-視圖矩陣相乘,這將使我們進入觀察坐標系(viewing coordinates)。

觀察坐標系—選擇能看到的

在我們旋轉、位移、縮放了世界之後,我們可以只選擇世界中的一部分加以呈現。我們通過定義一個視錐體來實現。視錐由觀察坐標系中的六個裁剪面組成。在最終繪製階段,所有位於視錐範圍以外的物體都將被裁掉,或丟棄掉。視錐通過一個4x4矩陣來定義。OpenGL的glFrustrum()方法是這樣定義這個矩陣的:

我們可以改變這個矩陣以應對不同情況如正交或透視。透視圖裡有一個消失的點,正交視圖沒有。通常在繪畫裡見到的是透視圖,正交視圖在技術圖中中較為多見。因為這個矩陣決定了物體是如何投影到螢幕上的,所以也叫做投影矩陣。t,b,l,r,n,f代表頂部、底部、左側、右側、近處、原處的裁剪面的座標。乘以投影矩陣將使點從觀察坐標系前往所謂的裁剪坐標系(clip coordinates)。

裁剪坐標系——只繪製能看到的

這個坐標系有點不同,因為它是左手坐標系(在此之前我們一直使用的右手坐標系),而且是從我們之前定義的視錐體映射到一個x,y,z範圍都在(-1,1)之間的正方體。

到現在為止,我們一直追蹤場景中的所有點。然而,一旦進入裁剪空間,我們就可以開始裁掉一部分了。還記得座標從4D到3D的轉換嗎?我們曾說過,[x,y,z,w]4D=[x/w,y/w,z/w]3D。因為我們只需要位於視錐範圍以內的點,我們接下來只需處理符合−1≤x/w≤1或−w≤x≤w的點即可。y和z座標也一樣。這是一個簡單的辦法分辨一個點是否位於我們視野之內。

如果某些點位於視錐體內,我們對它們執行透視出發(perspective divide),對每個座標除以w來將其從4D座標轉換成3D座標。這些點還是位於左手裁剪坐標系中,但是到了這個階段,我們稱其為規格化設備座標(normalized device coordinates)。

規格化設備坐標系——計算遮擋關係

你可以把這個想成映射到圖像的中間步驟。想像一下所有可能的圖像大小,我們並不想渲染成一張圖片然後進行各種縮放或拉伸或當圖像大小發生改變時重新渲染。規格化設備座標(NDC)很有用,因為無論圖片最終大小是多少,你可以在NDC裡面針對性的進行合適的縮放。在NDC裡你將看到圖片如何被構造。被渲染的圖像是視錐體裡的物體在近裁剪面上的投影。因此,一個點在Z軸上的值越小,這個點就越近。

這個階段,通常我們不再進行矩陣計算,而是應用一個視窗變換。這通常只是拉伸座標來適應視窗,或最終圖像大小。最後一步是通過轉換座標到視窗座標來繪製圖像。

視窗坐標系——將物體縮放到畫布

視窗是圖像最終被繪製的地方。在這裡,我們的3D世界呈現為近裁剪面上的一張2D圖像。我們可以使用一系列的線條和多邊形演算法來繪製最終圖像。此時,一些2D效果,如抗鋸齒和多邊形裁剪,在圖片被被繪製之前執行。

然而,窗口可能有不同的坐標系統。比如,有時圖片基於向右為X+,向下為Y+繪製。為了正確繪製圖片,有時候可能需要做一些轉換。

又回來了——圖形渲染管線

上述步驟你不用都親歷親為。某種程度上,你會使用圖形渲染庫來定義諸如模型視圖矩陣、投影矩陣以及世界坐標系中的多邊形之類的東西,渲染庫會搞定一切。如果你在設計一個遊戲,你不需要在意多邊形是如何被繪製的,只需確保它們執行的正確又快速,對嗎?

OpenGL和DirectX之類的庫效率很高,而且能夠有效利用精密的圖形硬體來簡單又快速的執行這些計算。它們廣泛使用,所以最好適應它們。它們還給你留下很大空間來自訂一些事情,你會為你能做到的某些事感到驚訝的!

結論

這是一個關於圖形學理論的簡單概覽。渲染過程中後續還有很多步驟發生,但是這應該能給你一個大致的方向,讓你在閱讀其它文章或論壇裡的相關技術時能夠理解的更好。謝謝!

打個比方。假設你在一個山谷裡面,四面環山,河流前方是一片草地,類似上面這張Bob Ross的繪畫作品中的場景。你想用一張圖展現這個3D場景。該怎麼做呢?嗯,我們可以嘗試繪製一張包含所有景色的圖。那意味著我們必須得挑選一個觀察角度,只繪製能看到的部分,忽略其餘的。然後還得決定哪些部分在前,哪些部分在後。我們能看到草地,但它遮擋了部分河流。群山在遠處,但它們遮擋了後方的一切景色。由於場景的真實大小遠大於我們畫布的大小,我們還得計算如何縮放。然後我們繪製場景,將光照和陰影考慮進去,以及遠處山間的霧氣,等等。這和電腦中的圖形處理過程十分相像。主要步驟如下:

決定世界中的物體長什麼樣子。

決定物體在世界中的位置。

決定相機的位置和哪些場景需要被繪製。

決定物體相對於相機的位置。

繪製場景中的物體。

將場景縮放到視窗大小一致的圖像上。

上述步驟大致是為了把3D世界中的某個點映射到螢幕上的2D圖像上。乍一看工作量很大,其實可以利用一些數學技巧來快速完成工作。還記得學代數的時候曾想,“學這個有什麼用?”其中一個用處就是圖形學!

我們可以用矩陣來將世界中的座標映射到圖像上。為什麼選擇矩陣?其中一個原因是很多操作都可以用矩陣來表示。最重要的是我們可以通過矩陣乘法將多個操作連接起來,最終只用一個矩陣來代表一系列操作!即使要做50個變換,只需將所有矩陣相乘就得到一個同時執行所有變換的矩陣。

我們可以定義一些矩陣來完成我們上述繪畫示例中的操作(定義場景,定義視野等)。這些矩陣能夠將場景從一個坐標系轉換到另一個坐標系。這些坐標系之間的轉換稱為變換。我們將要討論每一個坐標系以及不同坐標系之間的轉換過程。

模型坐標系——分解模型

如何在螢幕上快速繪製物體呢?電腦相當擅長連續快速完成大量相對簡單的指令。為了利用這一點,如果我們能夠用簡單的形狀來表示整個世界,就能夠通過快速處理許多簡單的形狀來優化圖形演算法。因此,電腦不需要知道一座山或一片草地到底是什麼就能夠繪製它們。

我們需要創建一些演算法把圖形分解成簡單的多邊形。這個過程稱作曲面細分(Tessellation)。雖然我們也可以用正方形,但三角形更好一些。使用三角形有很多優勢,例如所有的三角形的點都是共平面(coplanar)的,而且幾乎所有形狀都能近似分解成多個三角形。唯一的問題是圓形物體會看起來像是有棱角的。不過,要是三角面足夠小的話,比如1個圖元大小,我們就不會注意到了。關於如何曲面細分的“最佳方法”有很多,具體取決於你要分解的物體的形狀。

假設我們要分解一個球形。我們可以將球體的中心位置定為本地的原點。這樣我們就可以用一個公式來獲得球面上的一些點然後將這些點連接成多邊形以供繪製。一個常用的公式是S(u,v)=[r sin u cos v,r sin u sin v,r cos v],u和v的取值範圍分別是u∈[0,π],v∈[0,2π],r是球體的半徑。就像你在圖中看到的那樣,球體表面的點被繪製成矩形。我們能夠很方便的把它們連成三角形。

球面上的點位於所謂的模型坐標系。座標相對于本地的原點定義,比如示例中球體的中心位置。如果我們想要將物體放置於場景中,我們可以定義一個從場景原點到場景中球體的原點的向量,然後把這個向量和球面上每個點的座標相加。這樣我們就將模型放置到了世界坐標系中。

世界坐標系——將物體置於世界中

到這兒我們的圖形學之旅才真正開始。我們在某處定義一個原點,場景中的每個點都基於從原點到該點的一個向量來定義。雖然場景是3D的,我們還是得用一個4維的座標來定義每個點[x,y,z,w],代表該點的座標為[x/w,y/w,z/w]。這種映射稱為齊次座標。使用齊次座標有一些好處,但是這裡不做討論。只需知道我們使用齊次座標就夠了。

假設我們要在場景中移動,那麼問題來了。如果我們要移動視線,或者移動相機到另一個位置,或者讓整個世界圍著相機移動。在電腦的世界裡,移動整個世界更容易一點,所以我們就這樣做,讓相機固定不動。模型-視圖矩陣(modelview matrix)是一個4x4矩陣,可以用來移動世界中的每一個點,然後讓相機固定不動。這個矩陣基本上就是一系列旋轉、位移、縮放的集合。我們在世界坐標系中將點和模型-視圖矩陣相乘,這將使我們進入觀察坐標系(viewing coordinates)。

觀察坐標系—選擇能看到的

在我們旋轉、位移、縮放了世界之後,我們可以只選擇世界中的一部分加以呈現。我們通過定義一個視錐體來實現。視錐由觀察坐標系中的六個裁剪面組成。在最終繪製階段,所有位於視錐範圍以外的物體都將被裁掉,或丟棄掉。視錐通過一個4x4矩陣來定義。OpenGL的glFrustrum()方法是這樣定義這個矩陣的:

我們可以改變這個矩陣以應對不同情況如正交或透視。透視圖裡有一個消失的點,正交視圖沒有。通常在繪畫裡見到的是透視圖,正交視圖在技術圖中中較為多見。因為這個矩陣決定了物體是如何投影到螢幕上的,所以也叫做投影矩陣。t,b,l,r,n,f代表頂部、底部、左側、右側、近處、原處的裁剪面的座標。乘以投影矩陣將使點從觀察坐標系前往所謂的裁剪坐標系(clip coordinates)。

裁剪坐標系——只繪製能看到的

這個坐標系有點不同,因為它是左手坐標系(在此之前我們一直使用的右手坐標系),而且是從我們之前定義的視錐體映射到一個x,y,z範圍都在(-1,1)之間的正方體。

到現在為止,我們一直追蹤場景中的所有點。然而,一旦進入裁剪空間,我們就可以開始裁掉一部分了。還記得座標從4D到3D的轉換嗎?我們曾說過,[x,y,z,w]4D=[x/w,y/w,z/w]3D。因為我們只需要位於視錐範圍以內的點,我們接下來只需處理符合−1≤x/w≤1或−w≤x≤w的點即可。y和z座標也一樣。這是一個簡單的辦法分辨一個點是否位於我們視野之內。

如果某些點位於視錐體內,我們對它們執行透視出發(perspective divide),對每個座標除以w來將其從4D座標轉換成3D座標。這些點還是位於左手裁剪坐標系中,但是到了這個階段,我們稱其為規格化設備座標(normalized device coordinates)。

規格化設備坐標系——計算遮擋關係

你可以把這個想成映射到圖像的中間步驟。想像一下所有可能的圖像大小,我們並不想渲染成一張圖片然後進行各種縮放或拉伸或當圖像大小發生改變時重新渲染。規格化設備座標(NDC)很有用,因為無論圖片最終大小是多少,你可以在NDC裡面針對性的進行合適的縮放。在NDC裡你將看到圖片如何被構造。被渲染的圖像是視錐體裡的物體在近裁剪面上的投影。因此,一個點在Z軸上的值越小,這個點就越近。

這個階段,通常我們不再進行矩陣計算,而是應用一個視窗變換。這通常只是拉伸座標來適應視窗,或最終圖像大小。最後一步是通過轉換座標到視窗座標來繪製圖像。

視窗坐標系——將物體縮放到畫布

視窗是圖像最終被繪製的地方。在這裡,我們的3D世界呈現為近裁剪面上的一張2D圖像。我們可以使用一系列的線條和多邊形演算法來繪製最終圖像。此時,一些2D效果,如抗鋸齒和多邊形裁剪,在圖片被被繪製之前執行。

然而,窗口可能有不同的坐標系統。比如,有時圖片基於向右為X+,向下為Y+繪製。為了正確繪製圖片,有時候可能需要做一些轉換。

又回來了——圖形渲染管線

上述步驟你不用都親歷親為。某種程度上,你會使用圖形渲染庫來定義諸如模型視圖矩陣、投影矩陣以及世界坐標系中的多邊形之類的東西,渲染庫會搞定一切。如果你在設計一個遊戲,你不需要在意多邊形是如何被繪製的,只需確保它們執行的正確又快速,對嗎?

OpenGL和DirectX之類的庫效率很高,而且能夠有效利用精密的圖形硬體來簡單又快速的執行這些計算。它們廣泛使用,所以最好適應它們。它們還給你留下很大空間來自訂一些事情,你會為你能做到的某些事感到驚訝的!

結論

這是一個關於圖形學理論的簡單概覽。渲染過程中後續還有很多步驟發生,但是這應該能給你一個大致的方向,讓你在閱讀其它文章或論壇裡的相關技術時能夠理解的更好。謝謝!