Carved Marker

即時 3D 繪圖的陰影效果 [Part 1]

在目前的即時 3D 繪圖中,要做出真實的陰影效果,是很不容易的。因為陰影是因物體遮住光源所產生的,因此,要做出正確的陰影效果,就需要對整個場景做處理,這樣才能判斷出哪些物體被哪些物體遮住了。

不過,目前的 3D 硬體,並不容易進行這類的測試,因為資料量和工作量都太大了。不過,這並不表示使用現在的 3D 硬體就無法做出陰影。現在已經有很多方法是適合用在目前的 3D 硬體上面,可以產生效果不錯的陰影。本文會就一些常用的方法,做簡單的介紹。

目前常用的方法,幾乎都是把陰影看成是「物體投射到其它表面」來處理。在光源是平行光的時候(例如,太陽光),可以看成是物體把陰影「投射」到另一個表面上,如下圖所示:

Shadow Figure

如果場景中只有一個重要的光源(即最強的光源),那可以假設只有這個光源會產生明顯的陰影。以平行光源來說,要把陰影「投射」到一個平面上,就是一件相當容易的事。

設空間有一點 V,平行光源的方向是 L,要投射陰影的平面是 P,那麼,存在一常數 k 使

Shadow expression 1

成立,如下圖(VL、和 P 均為四維向量,表示一個三維的 homogeneous coordinate):

Shadow Projection Figure

解上式得

Shadow expression 2

空間中的點 V 投影到平面 P 的位置是 V+kP。對一個 3D model 的每個頂點都代入這個式子,就可以得到投影的結果了。不過,因為目前的 3D 硬體都是以 4×4 的矩陣來做變換,所以如果能把投影的動作寫成一個矩陣,就會方便很多。

V = <Vx, Vy, Vz, 1>L = <Lx, Ly, Lz, 0>P = <a, b, c, d>;展開前面的式子,會發現 k 有一個分母:

Shadow expression 3

因此這個式子會有點複雜。不過,因為在 OpenGL 中的向量都是 homogeneous coordinate,所以可以先把分母提出來,放到 w 中。對 Vx 展開得到:

Shadow expression 4

整理一下得到

Shadow expression 5

這樣就變成向量內積的形式,可以放到矩陣中。對 VyVz 做同樣的動作,再加上放到 w 的分母部分,就可以得到下面的矩陣:

Shadow expression 6

因此,理論上,要畫陰影時,把 MODELVIEW 矩陣設成上面的矩陣,畫出物體,就會是陰影的樣子。

上面討論的是以平行光源為主。如果光源是點光源,也可以用類似的方法來做。

這個方法可以對任何平面做出陰影的效果。如果有多個平面,可以分別對每個平面都做一次。不過,這個方法顯然是沒辦法把陰影投影到曲面上。所以,這個方法通常稱為 planar shadow。

理論的部分已經討論完了。不過,實作的時候,還有一些細節部分是需要特別注意的。

首先,如果真的直接用上面的方法來做,那做出來的結果可能會像這樣:

Shadow result without polygon offset

這是因為畫了圖中的地板之後,再畫黑色的陰影時,會和地板產生所謂的 Z fighting 現象,也就是陰影的一部分的 Z 值較地板的 Z 值小,所以會畫出來,但是有些部分的 Z 值則可能比地板的 Z 值大,所以就沒畫出來了。這種現象會使得陰影變得破碎不完整。

現在的 3D 硬體通常提供一個叫 polygon offset 的功能,用來解決 Z fighting 的問題。Polygon offset 的原理,是在畫一個物體時,要求把它的 Z 值進行一個小的調整。例如,在上面的例子中,我們可以把陰影的 Z 值減一個小的數字,這樣就可以避免 Z fighting 的現象。

處理掉 Z fighting 後,再來是第二個問題:

Shadow result without stencil buffer

上圖中,陰影跑到地板的外面去了。這裡需要一個方法,把陰影切到地板的範圍內。這個例子中,地板的邊界是直線,所以可以用 user defined clip planes 來做。不過,如果地板是奇怪的形狀,就需要別的方法。

而且,光是把陰影切到地板的範圍內是不夠的。通常陰影是用 blending 的方式畫上去的。如果一個物體的形狀比較複雜,那有些地方可能會 blend 兩次或更多次。這樣會讓陰影看起來不是同一個顏色,即有些地方顏色較深而有些地方較淺。

現在的 3D 硬體多半支援 stencil buffer。Stencil buffer 是一個用來「做記號」的 buffer。通常 stencil buffer 可以存放 1 bits 到 8 bits 不等的數字,不同的硬體支援的大小會不一樣。目前的 3D 硬體通常把 stencil buffer 和 Z buffer 放在一起。例如,它可能支援 15 bits 的 Z buffer 加上 1 bit 的 stencil buffer;或是支援 24 bits 的 Z buffer 加上 8 bit 的 stencil buffer。因此,通常 stencil buffer 的測試是和 Z test 一起做的,也就是在使用 Z buffer 時,stencil buffer 可說是「免費」的。

在我們的例子中,可以先把 stencil buffer 全清為 0。在畫地板之前,把 stencil buffer 設定為「當 Z test 通過時,把 stencil 的值設為 1」。這樣一來,畫完地板之後,地板所佔有的那些 pixels 的 stencil 值就都會是 1,其它的地方還是 0。現在,把 stencil buffer 設定成「當 Z test 通過時,若 stencil 的值為 1 才畫,且將 stencil 的值設為 0」。然後開始畫陰影。這樣畫出來的陰影,一定會在地板的範圍內,而且每個 pixel 只會被畫一次,不會出現 blend 兩次的情形。

如果有多個平面要投影,可以為每個平面指定不同的 stencil 值。不過,如果 stencil buffer 只有 1 bit,那就沒辦法了。這時,可能就需要在畫下一個平面之前,先把 stencil buffer 清掉,這樣會花很多時間。

下圖是一個完整的例子:

Complete shadow result

參考程式可以在這裡下載。這個程式需要至少 2 bits 的 stencil buffer 支援。

Planar shadow 就差不多是這個樣子了。Planar shadow 的好處是簡單、容易做,而且在投影面不多的時候,速度很快。但是,當投影面變多時,或是物體很複雜時,速度很快就會變得很慢,因為對每個平面都需要把投影的物體再畫一次。而且,它只能投影在平面上,對於不規則的表面則完全沒辦法。

還有一些別的方法可以產生陰影的效果。後面還會再討論一些產生 shadow 的方式,都可以投影在任何不規則的表面上。

[Part 1] [Part 2]

6/8/2000, Ping-Che Chen


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

Copyright© 2000 Ping-Che Chen