在前面所提到的,利用 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 函數的乘積。這樣做的效果通常會比較好,但是成本極高,並不一定適用於所有的情形。
到這裡,大部分的問題都已經說得差不多了。接下來就是討論實作上的細節。