会员: 密码:  免费注册 | 忘记密码 | 会员登录 网页功能: 加入收藏 设为首页 网站搜索  
游戏开发 > 程序设计 > 3D图形
Per Pixel Lighting
发表日期:2007-01-25 17:49:43作者: 出处:  

Per Pixel Lighting

文档内容:
[Part 1]

由於 DirectX 8 的推出,許多和 3D 相關的討論,也就轉移到 DirectX 8 的 pixel shader 上面。DirectX 8 的 pixel shader 功能相當強大,不過,到目前為止(12/2000),市面上還沒有任何顯示晶片是支援 DirectX 8 pixel shader 的。但是,這並不表示現在的顯示晶片就沒辦法支援 per pixel lighting,只不過,現在的顯示晶片所能表現的效果有限而已。

倒底什麼是 Per Pixel Lighting?簡單的說,就是指 lighting (即打光)的運算是以 pixel(即像點)為單位來做的,而不是以 vertex(即頂點)為單位。如果還不清楚的話,請記得 pixel 是指螢幕上形成畫面的最小單位的小點;而 vertex 則是指多邊形的頂點。舉個例子來說,一個三角形有三個頂點,但是畫在螢幕上的時候,它可能會包含很多個像點。所以,很明顯的,以像點為單位來計算打光的動作,比用頂點為單位,要來得精確得多;但是,它的運算量當然也大得多。

目前絕大部分的 3D 遊戲,在 dynamic lighting(動態打光,指光源可以任意移動的情形)的時候,都還是使用 per vertex lighting,也就是以頂點為單位的打光方式。如果是 static lighting,即光源不能移動的情形,則可以使用 light map 的方式來達到 per pixel lighting 的目的。Per vertex lighting 的方式,是在三角面的頂點上計算出打光的結果,而在繪製三角面的內部時,則是利用內插的方式來產生結果。如果三角面都不太大,這樣做的效果還算好。不過,如果三角面很大的話,那就不太好了。而且,這種內插的方式,會產生一種稱為 Mach band effect 的現象。下面是一些例子:

Vertex Lighting with no tessellation Per vertex lighting - 使用 6 個三角面
Vertex Lighting with 3 tessellation Per vertex lighting - 使用 54 個三角面
Vertex Lighting with 10 tessellation Per vertex lighting - 使用 600 個三角面

上面的三張圖,是由一個點光源來打光,打在三個互相垂直的平面上,且都是使用 per vertex lighting。在最上面的圖中,每個平面是由兩個三角面組成的。所以,每個平面只有四個頂點(兩個三角面共用兩個頂點)。這樣打光的效果很差,根本看不出有什麼效果。這是因為頂點的數目太少了。

在中間的圖,情形完全一樣,但是每個平面切成 9 個小正方形,共 18 個三角面。所以,每個平面有 16 個頂點。可以看得出來,打光的效果比上面的圖要好一些。但是三角面相接的地方卻變得很明顯。這就是 Mach band effect,是相當討厭的問題。會產生這個問題,主要是因為三角面內部是用內插的方式繪製的,所以三角面內部的亮度變化,就會比三角面相接處的亮度變化還小。而人的眼睛對亮度變化較大的地方,比變化小的地方更為敏感,所以才會使三角面相接的地方變得這麼明顯。

如果再增加頂點的數目,就會得到最下面的圖。最下面的圖是把每個平面切成 100 個小正方形,共 200 個三角面。所以每個平面共有 121 個頂點。因為三角面的數目非常多,所以每個三角面都很小,在這張圖中可能只包含不到 10 個像點。所以,它的效果會非常接近 per pixel lighting 的結果。和上面的圖相比,可以發現 Mach band effect 幾乎已經完全消失了,不過,若仔細看應該還是可以看到一些。

也許有人會認為,如果把頂點的數目增多,就可以達到接近 per pixel lighting 的效果,那何必還要 per pixel lighting 呢?就某些情形來說,這是對的。但是,對一般的情形來說,就沒有這麼順利了。因為觀察者的位置和光源的位置都很接近,所以當光源和觀察者都很接近某個平面時,它就需要更多的頂點。所以,這就需要動態計算需要的頂點個數。再加上,頂點的數目增加,就會讓 CPU 的運算量,也增加 bus 的負擔。

如果顯示晶片本身就有進行 per pixel lighting 的能力的話,根本就不需要增加三角面的個數,就可以達到很好的打光效果。不過,要達到完整的 per pixel lighting,需要支援 DirectX 8 pixel shader 的顯示晶片才行。但是,以目前一般的顯示晶片,只要支援 DOT_PRODUCT3 這個功能,還是做到某個程度的 per pixel lighting 的。

後面將會提到利用 DOT_PRODUCT3 來產生 per pixel lighting 的方法。


[Part 2]

要怎麼做到 per pixel lighting 的效果呢?基本上,這需要兩樣工具:第一樣是 DOT_PRODUCT3,另一樣是 cubic environment map。如果你不知道這些是什麼東西,也沒關係,因為後面會解釋到。

要做 per pixel lighting,首先複習一下 local lighting 的做法好了……基本上不管是用什麼形式的 local lighting,它都會有這樣的形式:

亮度 = ambient + diffuse + specular + emission

Ambient 是指環境光。因為在 local lighting 的模型中,我們只考慮到光源對物體的直接影響。所以,有很多東西是沒辦法處理到的。最常見的情形,就是物體背對光源的部分,在實際世界中,並不是全黑的。這是因為,光源對其它物體的散射光,也會照亮這些部分,只不過強度很弱而已。為了模擬這種情形,可以做一個很簡單的假設:散射光是由四面八方均勻射入。因此,它照射在物體上,其中一部分被物體吸收後,未被吸收的部分,會均勻向四面八方反射出去。因此,對觀察者來說,不論物體的位置在什麼地方,所得到的反射光強度都是一樣的。所以,結果就是:

ambient = [物體的 ambient color] × [光源的 ambient 強度]

Diffuse 是指物體在光源下的實際表現。除了極少數的物體,像是磨得很亮的金屬、玻璃等會完全反射入射光的物體外,大部分的物體,在大部分的情形下會散射入射光,所以叫 diffuse。為什麼物體會散射入射光呢?這是因為物體的表面往往是粗糙的,而粗糙的表面就會散射入射光。由於光是散射的,所以無論觀察者在哪裡,看到的散射光強度都一樣。不過,散射光的強度又是如何呢?在這裡,有一個很常用的模型稱為 Lambertian model。這個 model 認為物體散射光的強度,和入射光的關係是:

散射光強度 = [物體表面的法向量] ‧ [入射光的向量]

也就是兩個向量的內積。有一點很重要,就是這兩個向量都是 normalized,即正規化,所以長度都是 1。另外,如果物體的法向量是背對光源,那散射光的強度當然會是 0。所以,比較正確的寫法是:

散射光強度 = max(0, [物體表面的法向量] ‧ [入射光的向量])

再加上物體本身所吸收的一些入射光,就變成:

diffuse = [物體的 diffuse color] × [入射光的 diffuse 強度] × max(0, [物體表面的法向量] ‧ [入射光的向量])

再來是 specular。Specular 是當入射光對物體產生全反射的情形。如果觀察者的角度十分剛好,就會產生這種現象。隨便看周圍的一個物體,只要它不是非常粗糙,就可以看到它的某些部分是直接反射光源的顏色,而和它本身的顏色無關。這就是 specular 的部分。要怎麼做出 specular 的效果呢?基本上有很多種不同的模型,分別適用在不同的情形。不過,最常見的有兩種:Phong modelBlinn model。不過,因為它們有點複雜,所以在這裡就不多作說明了。基本上,只要記住一個重要的性質就可以:「specular 的顏色和物體的顏色無關」,和 「specular 的亮度和光源、物體、及觀察者的位置都有關係」。第一點很容易,因為它是全反射,當然就不會被物體吸收,所以不會被物體的顏色所影響。不過,為了處理某些特別的情形,通常我們還是會給物體一個 specular color,不過通常是白色。第二點則比較不是這麼明顯,不過,是否能看到全反射,會和入射光的角度及觀察者的角度有關,應該也不會太難理解。

最後是 emission。這是指會發光的物體,自己所產生的光。這個項目很無趣,因為它和入射光完全沒關係,就是一個「物體所產生的光」。所以,一般情形下是:

emission = [物體的 emission color]

比 ambient 更無趣,對吧! :)

綜觀上面的四個大項目,可以看出最複雜的部分就是 specular。Emission 通常很少用,因為一般很少會需要自體發光的物體,而且它也很簡單。Ambient 和 diffuse 則十分重要,但也不至於太複雜。之前我們所提到的 per vertex lighting,就是在三角面的各個頂點進行上面的計算。而我們要做的 per pixel lighting,當然就是要在每個像點上都進行這樣的計算了。

不過,這些光源的計算,其計算量是很大的。首先,需要計算出物體表面在該像點上的法向量。一般來說,都是由三角面的三個頂點的法向量,內插而得。不過,內插得到的法向量,還需要做 normalize 的動作才行。得到法向量後,還需要計算入射光的向量。對於 directional light(即只具有方向性的光源,例如非常遠的光源,像是太陽),這倒是不難,因為對任何地方,入射光的向量都一樣,而且光的強度也一樣。但是,對於 point light點光源)就不同了,要計算入射光的向量,就得把光源位置減去物體表面的位置(即該像點在空間中的位置),最後還需要 normalize。另外,point light 有時還會有 attenuation(即衰減),這也需要計算,才能得到入射光的強度。最後才是到實際計算反射光強度的部分,也就是上面提到的公式。其中 ambient 和 diffuse 還算好解決,但是 specular 就麻煩了。像是 Phong model 和 Blinn model 都包含相當複雜的運算。所以,這麼多運算,要對每個像點去做,似乎是不太可能的事。甚至,就算是對每個頂點去做,也都還嫌太多了。

不過,如果這是不可能做到的,就不會有這篇文章了。當然,要做到完整且精確的打光計算,確實是不太可能的。以 specular 來說,就很難做到精確,因為它的計算量實在是太大了。不過,如果我們可以接受一些誤差,讓一些影響不是這麼大的動作,只在頂點進行,有些地方使用內插或是查表的方式,這樣還是可以得到相當不錯的結果的。下面的圖就是把前一頁中的 per vertex lighting 使用較為不精確的 per pixel lighting 打光所得到的結果:

Per pixel lighting with no tessellation

這張圖是使用 6 個三角面。和前一頁的 vertex lighting 相比,同樣是使用 6 個三角面,但是即使是粗糙的 per pixel lighting 還是比 vertex lighting 更加精確得多。不過,因為上圖沒有加入 attenuation 的因子,所以它比 vertex lighting 的結果要亮一些。另外,應該也可以看出這個 per pixel lighting 並不是非常精確,因為它的結果並不如使用 600 個三角面的 vertex lighting(已經非常接近真正 per pixel lighting)的結果。

上面的例子中,是使用點光源。在後面我們會先用較為簡單的 directional light 開始,介紹如何利用 Direct3D 8 配合一般的 3D 加速硬體,來達到 per pixel lighting 的效果。


[Part 3]

在上一頁的討論中,我們知道在 local lighting 的 model 中,打光的效果基本上是由觀察者的位置物體相對於光源的位置、和物體的表面法向量這三者所決定的。基本上這有點複雜。為了簡化問題,我們可以先只考慮 ambient 和 diffuse。因為在一般的光源下,大部分物體的顏色有很大部分是由這兩者決定的。而 ambient 和 diffuse 又和觀察者的位置無關,所以處理起來就容易得多。

再來,我們還要把問題更加簡化:使用 directional light(即平行光源)。什麼是 directional light?基本上,directional light 的一個重要特性就是,無論物體的位置在哪裡,光源的方向都是一致的,也就是平行的,所以才會稱為平行光源。當然,真實世界中並沒有所謂的平行光源,所有的光源都應該是來自某個點,或是某個區域。不過,如果光源的位置非常的遠,它的表現就會很類似平行光。最簡單的例子就是太陽光。所以,使用平行光的模式來處理太陽光,是很適合的。

現在來看看在 directional light 下,ambient 和 diffuse 會有什麼性質。首先,ambient 和光源的位置是無關的,所以在 directional light 下也是一樣。而 diffuse 就比較有趣了。由於 diffuse 是由「光源方向」和「法向量」的內積求得,而在 directional light 下,光源方向又是常數(對所有的 pixel 來說),所以 diffuse 就等於是物體表面的法向量的函數,姑且就稱之為 "diffuse 函數" 吧!

現在要做的是 per pixel lighting。所以,簡單的說,就是要對每個 pixel,以它的法向量求出 "diffuse 函數" 的值。在 3D 繪圖中,函數很容易解決:用貼圖就可以了。因為,貼圖等於是一個「內插+查表」的動作,所以可以想成:在頂點指定貼圖座標為法向量,則三角形內部的顏色就是三個頂點的法向量的內插,再查表的結果。不過,現在大部分的顯示晶片都只支援平面的貼圖,所以只是二維向量的函數。可是,法向量卻是三維向量,那要怎麼辦呢?難道一定要有三維貼圖嗎?答案當然是否定的。因為在 diffuse 的公式中,有一點非常重要:法向量需是正規化的。所以,這表示法向量的長度並不會影響 "diffuse 函數" 的值,也就是說,原來有三個自由度的法向量,就變成只有兩個自由度了。

眼尖的讀者可能已經看出來,這個問題就和地圖的投影是同樣的。因為單位長度的法向量會形成一個球,所以 "diffuse" 函數的空間是在一個球上。若想要用平面的貼圖來表示這個函數,就需要一個投影的方法,把球面投影到平面上。不幸的是,不管是什麼投影方法,都會引進某種形式的奇異點singularity),而且解析度也會在不同的地方,產生不同的扭曲現象。

在 3D 繪圖中,有一種專門的方法,用來處理這種和法向量有關的貼圖,就是 environment mapping環境貼圖)。環境貼圖常常用來表現出物體反射周圍環境的現象。在這裡,我們遇到是同樣的問題,所以可以直接應用 environment mapping。不過,傳統上經常使用的 spherical environment mapping球形環境貼圖),就是試圖把一個球面投影到平面上,因此會產生 singularity。在一般情形下,這個 singularity 並不是個問題,因為它總是背對者觀察者。但是,如果觀察者的位置改變,那這個 singularity 就會跑出來了。另外,spherical environment mapping 也和前面所提到的一樣,會有解析度扭曲的問題。

所以,基本上,spherical environment mapping 是不能用的。當然,如果有興趣的話,也可以試試使用其它的投影方式,例如 Gall's stereographic cylindrical projection。不過,這些投影方法通常都會有些問題,像是:觀察者不能動、有 singularity、嚴重的解析度扭曲情形、或是計算過於複雜。

另一個方法,是不要把它投影到一個平面,而是把它投影到六個平面。也就是說,用一個立方體包住球體。這樣一來,球體的每個面都能兼顧,不會有 singularity 的問題,解析度扭曲的情形也不會太嚴重。更重要的是,它可以轉,所以觀察者的位置可以改變。這樣的做法,就稱為 cubic environment mapping。不幸的是,目前有支援這個功能的顯示晶片並不多。

使用 cubic environment mapping 就可以做到 "diffuse 函數" 的功能。不過,現在還有一個問題:當物體移動位置時,光源相對物體的方向就會改變,"diffuse 函數" 當然也就改變了。可是總不能在光源或物體改變方向或位置時,就重新產生一個貼圖吧!這樣的工作量會非常大,而失去意義。不過,注意 diffuse 的計算方式:

diffuse 函數 = max(N.L, 0)

若光源相對物體改變了方向,例如 L 變成 RLR 是一個 3x3 矩陣),那就會變成:

diffuse 函數 = max(N.RL, 0) = max(NTRL, 0) = max(L.RTN, 0)

也就是說,這樣就可以固定住光源,只要轉動法向量就可以了。所以,只要選定一個光源的方向,算出相對的 cubic environment map,如果光源的方向改變,就不需要重新計算這個 cubic environment map,只需要改變物體的法向量就可以了。

說了這麼多,理論上的東西已經差不多了。接下來就以一個簡單的例子,來說明這個做法的細節。


[Part 4]

上一頁中,已經把理論的部分說明得差不多了。這裡則會開始討論實作的細節部分。

首先,先來看看我們的例子:光源是 directional light,物體則是一個「膠囊」,並有貼圖。如下圖所示:

A capsule on vertex lighting

這個物體的三角面數目並不多。基本上,它的橫切面是八邊形,而兩端的球形部分則各切成四段。所以,整個算起來有 128 個三角面。這個圖並不大,但是已經可以看到討厭的 Mach band effect。另外,還有一些 color bleeding 的現象,也就是在應該是背光的地方,卻因為內插的關係,而顯得過亮了。相反的,也有一些地方應該是面對著光源,卻顯得過暗。

現在把前一頁所討論的方法,用在同樣的東西上面。得到的結果變成:

A capsule on pixel lighting

看起來好多了。不過,雖然在討論理論的部分,看起來並不會太複雜,但是實際上的 3D 硬體是有限制的,所以並沒有這麼容易。

無論如何,要做到這樣的 per pixel lighting,最重要的是產生 "diffuse 函數",也就是用來查出 diffuse 亮度的 cubic environment map。為了簡單起見,可以把光源的方向定為 <0, 0, 1>,在 Direct3D 的座標系中,是朝向螢幕的方向的光源。

再來是產生 cubic environment map。產生的方法很簡單:對 cubic environment map 的六張貼圖,對其每個 texel 算出相對應的法向量後,把這個法向量 normalize,再把它和 <0, 0, 1> 求內積(就是取其 Z 的部分)。若結果小於 0,則設為 0。最後,把該 texel 的 RGB 三個 components 都設成計算的結果就可以了。

不過,實際上是會有很多麻煩的。首先,是貼圖的格式。各種顯示晶片支援的貼圖格式可能都會不太一樣。以 DirectX 8 所支援的格式來看,就有 32 bits、24 bits、16 bits、8 bits 等等許多不同的格式,而且像是 32 bits 就有兩種不同的格式,而 16 bits 有更多不同的組合方式。所以要處理這些東西是很麻煩的。為了避免這種問題,一個比較簡單的方法,是把 cubic environment map 分成六個不同的 BMP 檔案,再用 DirectX 8 SDK 的 Texture Tool 把它們組合成一個單一的貼圖檔(DDS 檔)。這樣一來,就可以在程式中用 D3DXCreateCubeTextureFromFile 這個函式來讀取這個貼圖檔。這個函式會自動處理各種不同的貼圖格式,所以相當方便。

下圖是一個產生好的 "diffuse 函數" 的例子:

  Positive Y    
Negative X Positive Z Positive X Negative Z
  Negative Y    

如果不想自己寫程式來產生這個 cubic environment map 的話,也可以從這裡下載。這是光源方向為 <0, 0, 1> 大小為 128x128 的 cubic environment map。

把 "diffuse 函數" 弄好之後,再來就是計算打光效果的部分了。不過,物體的 3D 模型還需要一些準備。基本上,因為 Direct3D 的自動產生貼圖座標的功能中,並沒有「複製法向量」的功能,所以只好自己指定一組三維的貼圖座標,並把它們設定成和各個頂點的法向量相同。當物體在世界座標系中有移動時,把移動的矩陣設定成貼圖座標轉換的矩陣就可以了。如果光源有轉動,則把光源轉動的矩陣做轉置,再乘到貼圖座標的轉換矩陣上。這樣 3D 模型的部分就算是準備好了。

接下來就是繪製結果了。首先是 ambient 部分,這部分很簡單,把光源的 ambient 顏色和物體的 ambient 顏色相乘就可以了。把算好的顏色放到貼圖常數中,和物體的貼圖相乘,就完成 ambient 的部分了。這部分得到的結果是這樣:

Ambient of the capsule

再來是 diffuse 部分。先把光源的 diffuse 顏色放到貼圖常數中,把貼圖座標轉換的功能開啟,Direct3D 就會把貼圖座標乘上之前設定好的轉換矩陣。因為之前已經把貼圖座標設定成和法向量一樣,所以當物體在轉動時,貼圖座標也要跟著「轉」。指定貼圖為之前產生好的 cubic environment map,再和物體的貼圖相乘,再乘上貼圖常數。最後,用 alpha blending 的方式,把 D3DRS_SRCBLENDD3DRS_DESTBLEND 都設為 D3DBLEND_ONE,就可以和之前的 ambient 結果加起來,得到最後的結果。整體來說,整個動作是:

STAGE 1: TFACTOR * COLOR_TEXTURE
STAGE 2: CUBIC_TEXTURE * COLOR_TEXTURE * TFACTOR

這裡使用的是 two pass 的方式。當然,它其實只用到兩個貼圖,所以,如果 3D 硬體支援適當的功能的話,這兩個動作是可以合在一起的,在 one pass 就可以完成了。


[Part 5]

用同樣的方法,其實還可以做到很多有趣的效果。因為,可以經由調整 cubic enviornment map 的計算法式,來調整打光的計算方式。

一個例子是做出 specular 的效果。在 Direct3D 和 OpenGL 中,計算 specular 的方式都是用 halfway vector 和 normal vector 的內積,再做 n 次方。經由調整 n 的值,可以產生相當不錯的結果。不過,傳統的打光方式,只在每個頂點上面做,所以效果很不好。

不過,前面所說明的方式,同樣可以用在 specular 上面,因為它和 diffuse 一樣,是某個向量和法向量的內積。問題在於,這個「某個向量」不再是光的方向,而是 halfway vector,它是光的向量和觀察者的向量相加後,再 normalize 後的結果。為了簡單起見,我們可以假設觀察者的位置是在無限遠的地方(即 infinite viewer),這樣一來,觀察者的向量就是常數。而光是平行光,也是常數。這樣一來,halfway vector 也就變成常數了。

和前面的例子一樣,先以 <0, 0, -1> 為 halfway vector,算出一個 cubic environment map。這個 cubic environment map 會隨著 n 而不同,所以對不同的 n 都要算出一個不同的貼圖。接著,再算出 halfway vector,把它轉回 <0, 0, -1>,再設定 texture stage,讓它和光的 specular 的顏色相乘。最後,畫出整個物體,用 blending 的方式,把它加到前面已經畫好的 ambient + diffuse 的結果上,就大功告成了。

要注意的是,specular 不受到物體的顏色影響,所以不需要把 specular 再乘上物體本身的貼圖。另外,我們也忽略了物體本身的 specular 顏色,因為這並不常用。這樣的計算所得到的結果如下圖所示:

Capsule with per pixel specular

在上圖中,為了讓 specular 的亮點更為明顯,所以 diffuse 的亮度稍微調低了一些。它所使用的 n 值為 160。

另一個有趣的例子,是做出類似卡通的效果。卡通的顏色比較平面化,往往使用同一個顏色來表示一整個漸層。而且,卡通的顏色通常比較亮一些,在光線照射不到的地方,也很少是全黑的。所以,可以把漸層的顏色改成用一些相同的顏色代替。如下圖所示:

Palette for cartoon rendering

上面的例子中,只使用三種層次來取代原先的 256 種層次。以這種方式來產生 diffuse 的 cubic environment map 之後,再套用到同樣的「膠囊」上面,就會變成:

Capsule with cartoon rendering

上圖中,為了讓效果更清楚,所以稍微改變了一下觀察者的位置。其它如光源的位置及模型等都沒有改變。

當然,要真的做到像卡通的效果,還需要為物體加上黑邊。不過,這已經超出了 per pixel lighting 的範圍了。將來有機會再來討論這一方面的技巧。


[Part 6]

在前面所提到的,利用 cubic environment map 來達到 per pixel lighting 的技巧,基本上只適用於平行光的情形。這是因為,在平行光的情形時,對任何 pixel 來說,光源的方向都是一致的。所以,才能夠把原來為雙向量函數的打光方程式,轉換成單向量函數。也因此,才能夠用一個 cubic environment map 來達成這樣的效果。這個方法的主要優點在於,因為它只利用到「查表」這個動作,所以打光函數的變化性很大,可以做出很多奇怪的效果,例如 anisotropic lighting。所謂的 anisotropic lighting 是指物體的表面的反光特性具有某種方向性。

如果光源的方向是隨著 pixel 的位置而變動,例如在點光源(point lighting)的情形下,那這個技巧就沒辦法用了。如果要用查表的方式,那將會需要一個 4D 的貼圖。這是不太可能做到的。當然,如果打光函數具有某些性質,可能可以加以 factorize,將一個 4D 的函數分為兩個 2D 函數的乘積(或是其它運算)。用這種方式,可以做到一些驚人的效果,像是某些 BRDF(Bi-directional Reflectance DistributiFunction)的模型。不過這部分相當複雜,如果以後有機會再討論這方面的東西。

如果光源的方向會隨著 pixel 的位置而改變,或是甚至物體本身的法向量就會改變(例如在 bump mapping 的情形),那就需要一個能在每個 pixel 上計算向量內積的方法,因為 diffuse lighting 就是用向量內積來計算的。在 DirectX 6 之後,Direct3D 開始支援一種新的 texture operation,稱為 DOT3_PRODUCT,它是把一個 pixel 的顏色(由 RGB 三個部分組成)視為一個 3D 向量,並計算兩個「顏色」的內積。不過,一般來說,顏色的各個 component 的範圍,傳統上是 0 ~ 1;但是在做向量內積時,向量的每個 component 的範圍通常需要是 -1 ~ +1。所以,在 DOT3_PRODUCT 這個 operation 中,把顏色的每個 components 經過 X * 2 - 1 這樣的運算,把它的範圍由 0 ~ 1 放大到 -1 ~ +1,進行內積運算後,再把結果切到 0 ~ 1 的範圍內,以符合 diffuse lighting 計算上的需要。

因此,利用 DOT3_PRODUCT 我們就可以在每個 pixel 上面做到向量內積的計算,這樣一來,就離 per-pixel lighting 不遠了。不過,要注意的是用 DOT3_PRODUCT 是沒辦法很容易做出 specular 的效果,這是因為 specular 的計算並不只是向量內積,通常還包含很多麻煩的計算。

不過,現在還有一個麻煩的問題。在 diffuse lighting 的公式中,雖然只是光源向量和法向量的內積,但是公式同時也要求這兩個向量必須是單位向量。而一般情形下,兩個單位向量的線性內插,並不一定會是單位向量,通常會變短。在某些極端的情形下,長度甚至會變成 0。而把一個向量 normalize 成單位向量,是非常複雜的動作,因為它需要先找出向量的長度(需要做平方根的計算),再把向量除以計算出來的長度。雖然一些方法可以簡化計算過程,例如直接計算出向量長度的倒數,再乘上原來的向量,即可避免除法。但是這樣的計算仍然不是可以在每個 pixel 上面進行的。

有些人可能會問,為什麼一定要做 normalization 呢?反正在大部分情形下,內插出來的向量,長度只是稍微短一些而已嘛!但是,實際上並不是這樣。如果稍微做一些計算,一定會發現一件驚人的事實:如果法向量內插後,沒有做 normalization 的話,那麼打光的結果將會和 per-vertex lighting 相差無幾!事實上,如果是在平行光的情形下,可以很容易證明,沒有做 normalization 的結果,會和 per-vertex lighting 完全相同。所以,如果不想做白工的話,要做出正確的 per-pixel lighting,做 normalization 是必要的。

幸運的是,我們並不需要真的去做 normalization,因為這些內量都是內插的結果。所以,只要事先計算出「內插並 normalize」的結果,就可以完全避開 normalization 的問題,把它轉換成查表的問題。這裡又會需要一個重要的貼圖方式,即前面已經用到多次的 cubic environment mapping。因為 cubic environment map 可以看成是一個「方向」的向量函數,即 <x, y, z> 和 <ax, ay, az> 會查到相同的值(如果 a 不是 0)。所以可以做出一個 cubic environment map,把任何非 0 向量 <x, y, z>,使其查到的顏色值,剛好是方向相同的單位向量。當然,顏色值需要做範圍放大的動作,讓範圍從 0 ~ 1 變成 -1 ~ +1。

點光源是另一個問題。如果要正確計算出光源的方向,就需要在每個 pixel 上面做向量的減法,再做 normalization。同樣的,這是不可能的。不過,如果光源的位置夠遠,而且三角面也都夠小的話,可以直接用內插的方式計算每個 pixel 的光源方向。這樣做並不正確,但是通常都夠接近了。

最後是點光源的衰減(attenuation)問題。點光源和平行光源有一個很大的不同,就是空間中的每個點,和光源都會有一個距離。所以,可以把點光源的強度,設定為會隨著距離而衰減。一般來說,要做到 per-pixel 的衰減是很困難的,因為衰減是距離的函數,通常是距離的某種函數的倒數或是類似的東西。這要做到 per-pixel 的計算並不容易。如果要用查表的方式也會有困難,因為空間中的距離是 3D 函數,所以這就表示要做到衰減的函數表,需要 3D 貼圖。這樣的成本就太高了,而且大部分的 3D 晶片也不支援 3D 貼圖。因此,一般來說,衰減只能以 per-vertex 的方式來做。

如果真的要只用 2D 貼圖做到 per-pixel 的衰減,也不是完全沒辦法,只是沒辦法做到完全正確。一個方法是把 3D 的座標投射到 2D 的座標上。這樣的效果會比只用 1D 座標來做,要稍微精確一些。不過,如果效果要好,投射的函式通常會是非線性的,這樣就不容易用硬體加速(一般的硬體 T&L 只能做四維的矩陣運算)。另一個方法,是試圖把衰減函數做 factorize,分解成兩個 2D 函數的乘積。這樣做的效果通常會比較好,但是成本極高,並不一定適用於所有的情形。

到這裡,大部分的問題都已經說得差不多了。接下來就是討論實作上的細節。


[Part 7]

現在我們來討論一個簡單但有效的點光源 per-pixel lighting 手法。

前面已經討論過,要在 per-pixel lighting 裡面做到 specular 並不容易,有許多麻煩的問題需要處理。而且,因為 specular lighting 需要比較複雜的計算(像是計算反射向量,和 n 次方的計算等),所以,要用一般的顯示硬體做到點光源的 specular lighting 並不容易。所以,這裡的討論就以 diffuse lighting 為主。

Diffuse lighting 的計算相當的簡單,因為它只是兩個向量的內積,並把結果限制在一個特定範圍內(0 ~ 1)而已。這正是 DOT_PRODUCT3 這個功能所要達到的動作。當然,這兩個向量都需要 normalize。不過,用前面已經討論過的方法,可以很容易用 cube map 達到 normalize 的動作。

所以,要做到點光源的 diffuse lighting,基本上就是把兩個 cube map 進行 DOT_PRODUCT3 的動作。其中一個 cube map 是代表 normalize 的法向量,另一個則是光源向量。

再來的問題是要產生適當的貼圖座標。對於法向量來說,這相當的容易,因為 Direct3D 8 和 OpenGL 的 ARB_texture_cube_map extension 都能把頂點的法向量轉換成貼圖座標,所以這並不是很困難的事情。光源向量就比較麻煩。不過,對於每個頂點來說,光源向量就是把光源位置向量減去頂點位置向量。因為光源位置對所有的頂點都是一樣的,所以,只要把頂點的位置轉換成貼圖座標,再用貼圖座標的 transformation 就可以做到光源位置減去頂點位置的向量。而 normalize 的動作交給 cube map 就可以了。

下圖是用這個方法所產生的結果:

Per-pixel point lighting without attenuation

圖中的白色方塊就是點光源的位置。

在做這樣的運算時,有幾個地方是要特別注意的。第一點是 texture filter 的選擇。因為這個 cube map 的主要目的是要做 normalization,而任何線性內插的動作,都會讓得到的向量稍微變短,而使得結果不精確。所以,在選擇 filter 時,應該要選擇 point sampling,也就是不做任何線性內插的動作。不管是放大或縮小的時候都是這樣。另外,根據經驗,使用 mipmap 的效果並不理想,所以也不應該使用 mipmap。

另外,因為使用 point sampling,所以 cube map 的大小要夠大。考慮到目前大部分的顯示晶片都只支援到 8 bits 的精確度,所以,256x256 的大小應該是夠了。當然 512x512 的 cube map 會更好,只是效率可能會稍差,特別是因為不使用 mipmap,會對效率有一定的影響。再更大的 cube map 可能就不會有明顯的幫助了。

至於 ambient lighting 則很容易,因為它是常數,所以可以直接用 per vertex 的方式來做。只不過要注意 ambient lighting 和 diffuse lighting 是用相加的方式,而不是相乘的方式。因為這很簡單,所以就不再特別舉出例子。

再來是要考慮到 attenuation(衰減)的問題,這是點光源和平行光源的一個很重大的不同處。所謂的 attenuation,就是讓光源的強度,隨著與光源距離拉長,而慢慢變弱的效果。在物理上,如果不考慮介質(空氣)吸收能量的效果的話,那點光源的衰減,應該是和距離的平方成反比。Direct3D 和 OpenGL 都是利用設定一個距離的二次方程式,再以其結果的反比,來處理點光源的衰減問題。

不過,如果要做到 per pixel 的衰減,那就沒辦法用這樣的公式了。因為距離平方反比是一個三維函數,所以,若要用查表法來做的話,就需要 3D 貼圖。目前支援 3D 貼圖的顯示晶片並不多,而且 3D 貼圖所需要的記憶空間實在是太大,用來做衰減實在是太不划算了。

另一個方法是放棄 per pixel 的衰減,而改以 per vertex 的衰減。這很容易做到,因為 per vertex 的衰減是本來就有提供的功能。但是它的效果並不是很理想,特別是在三角面很大的時候。所以,這並不算是一個很好的解決方式。不過,這樣做的效率應該是相當不錯的。

要用一般的 2D 貼圖做到 per pixel 的衰減,另一個方法就是設法把衰減函數分解成兩個分開的函數。但是距離平方反比是無法分解的。所以,只好另外尋找類似,但可以分解的函數。一個很好的例子是指數函數,如下例:

e-x2 - y2 - z2

上面的函數可以分解成兩個函數的乘積,也就是

e-x2 - y2 e- z2

再加上它的表現和一般的倒數函數很接近,所以很適合用來做 per pixel 的衰減。當然,它的衰減會比真正用倒數做的衰減要更快速。

要利用上面的方法來做到衰減,首先,要產生一個 2D 貼圖,可以查出 e-x2 - y2 的結果。不過,這裡有幾個小地方要注意:首先,這個函數的值是永遠大於 0 的。也就是說,不管 xy 的值有多大,函數的值總是比 0 大。不過,因為貼圖的精確度有限(通常是 8 bits),所以,實際情形是當 x2 + y2 大到某個程度時,這個函數的值就已經太小而無法表示了。以 8 bits 為例,當函數值小於 1/512 時,就已經無法表示。也就是說,當 x2 + y2 > 6.3 時,函數值實際上已經是 0。這時,只要配合貼圖座標的 clamp 就可以了。

但是,因為貼圖座標的範圍是 0 ~ 1,而這個函數是對原點對稱的,所以要先把原點移到 <0.5, 0.5> 這個位置,再配合貼圖座標的 clamp 才會得到正確的結果。另一個方法是在 OpenGL 下使用 ATI_texture_mirror_once 這個 extension 也可以達到對原點對稱的貼圖。不過支援這個 extension 的顯示晶片似乎並不多。

建好貼圖後,和前面的點光源一樣,利用同樣的方式,產生光源向量後,先用 <x, y> 查出第一個函數值,再用 <z, 0> 查出第二個函數值,把兩個值相乘(使用 modulate)就可以了。如果已經把原點移到 <0.5, 0.5>,那就需要移動一下,這並不太困難。另外,如果要調整衰減的程度,可以把貼圖座標各乘上一個常數,常數的大小和希望的衰減程度有關。最後,把衰減的結果,和前面的 diffuse 計算結果相乘,就可以了。當然這總共需要四個貼圖動作,所以,如果顯示晶片只支援兩個貼圖的話,就需要分兩個 pass 來畫。

下面是點光源衰減的例子:

Per pixel point lighting without attenuation Per pixel point lighting with attenuation
沒有衰減 有衰減

可以看到,靠右邊較遠的牆,在沒有衰減的情形下,顯得相當的亮。但是,在有衰減的情形下,則會變暗。很明顯的,有衰減的情形看起來比較真實。

最後是加上物體的貼圖。這部分不難,把貼圖和前面畫好的結果相乘,就得到最後的結果了。同樣的,如果顯示晶片不支援這麼多貼圖的話,就需要再用額外的 pass 來畫。下面是最後完成的例子:

Per pixel point lighting final rendering

測試程式可以在這裡下載。

返回顶部】 【打印本页】 【关闭窗口

关于我们 / 给我留言 / 版权举报 / 意见建议 / 网站编程QQ群   
Copyright ©2003- 2024 Lihuasoft.net webmaster(at)lihuasoft.net 加载时间 0.00371