Carved Marker

Per Pixel Lighting [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

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

[Part 1] [Part 2] [Part 3] [Part 4] [Part 5] [Part 6] [Part 7]

7/10/2001, Ping-Che Chen


Sorry, Traditional Chinese only. This page is encoded in UTF-8.

Copyright© 2000, 2001 Ping-Che Chen