(一)基本光照
光照 —— 即根據場景中光源的分布及物體的形狀、朝向等信息,為物體"塗"上陰影、高光等一系列增加真實感的色彩。
為了給物體着色,我們需要一個"模型"—— 根據光源的情況和當前表面的參數,得到一個這個表面該有的顏色。
這么說可能不太好理解,那么來看一個最基本的光照模型:Phong光照模型。這種光照模型更像是一種"經驗模型",由於它在復雜場景中的真實性遠不足,在現在已經鮮有人使用,但是它仍然是一種非常重要的計算方法。(而且應該仍是各大學圖形學課程的必修內容)
我們知道,我們能看見一個自身不發光的物體是因為它反射了從其他地方的光。Phong光照模型把不發光物體所反射的光分成三種分別計算(如下圖):
下文中"表面"的解釋:
類似"dS"的感覺。把一個曲面分成一個個一個個細小的平面(多邊形),這些小平面足夠小,以致於在有限分辨率的屏幕前我們分辨不出它們。而它作為一個平面,自然有一個法向量代表它的朝向,也有它中心點的位置及一系列數據。而着色就是為這一個個小平面分別決定它們該有的顏色,最后組合成具有真實感的整個物體。
1) 環境光
在一個場景中,確定的光源並不是光唯一的來源。正如上面所講,不發光的物體會反射光源所發出的光,而這種反射大多是漫反射,這就導致了場景中有着無數"亂七八糟""橫沖直撞"的漫反射光。如果考慮這些光的存在,以當時計算機的計算能力,最頂級的計算機渲染出一幀(為了完成實時渲染,至少要在1/20秒內完成)可能都要花出比一天還多的運算時間;為了大幅度簡化計算,Phong光照模型把這些光視為各個方向上強度都相等的光,導致物體的各個表面都具有一個相同的,比較暗的"底色"。
2) 漫反射
它模擬了物體表面對光源發出的光產生的漫反射。它根據物體表面的朝向與光源相對於這個表面的位置給表面染上不同的顏色。當光源相對於該表面的位置與該表面的法向量夾角比較小時 —— 此時這個面朝向光源 —— 這個面就會被賦予一個比較亮的顏色。反之,這個面就會被賦予陰影的顏色。
3) 高光
它模擬了物體表面"規則"的部分,這部分遵循鏡面反射原則,進而可以在物體上產生光斑。由表面位置和光源位置可以確定出一道入射光;以這個平面的法線為反射平面的法線,可以求出對應的出射光。當這個出射光的朝向與觀察點相對於表面的朝向非常接近時,就給這個表面一個非常亮的顏色,進而在整個物體表面產生高光反射。
Phong模型把上述三種方式產生的顏色值按一定比例混合起來,就產生了最后的圖像。具體的計算方式限於篇幅原因不再敘述,這方面的網上應該有一大堆。大概是這樣的:使用夾角的余弦值來當作“強度”,並對高光部分的“強度”進行乘方來控制光斑大小。最后將這幾種顏色混合起來,就得到了最后的圖像。
(Phong光照模型的參考圖。P:小平面;V:觀察點;L:光源;N:法線;R:出射光)
(二)基本陰影
雖然上面的這種光照模型十分好用且經典,但還存在着非常多非常多的不足之處。其中比較大的是它無法體現物體之間或是物體自身對自身遮擋產生的陰影。為了解決這個問題,人們提出了一種稱為shadowMap的技術。
在此之前,需要先說明一下深度緩沖區的概念。計算機在渲染圖像時,需要保證離攝像機比較遠的物體被離攝像機較近的物體遮擋。但是很明顯,計算機不能在渲染之前先對物體進行一次離攝像機距離的排序再渲染,這是即耗時又容易“穿幫”的作法。為了解決這個問題,深度緩沖區的概念產生了。一般情況下,計算機在渲染圖像的過程中會同時產生一張深度圖,這是一張只有一個通道的圖像,代表了物體距離攝像機的距離。
上圖是一張典型的深度圖。越亮的地方代表離攝像機的距離越近,越暗的地方代表離攝像機的距離越遠。有了這張圖,在渲染物體時,先把即將輸出的像素的深度值(深度圖中這個像素的顏色)與當前深度緩沖區(當前深度圖)中的像素顏色進行比較,如果這里已經有一個比要渲染的像素還要離攝像機近的像素存在,就拋棄掉將要渲染的像素;反之就覆蓋掉原來的。這就是被稱為“深度測試”(depth test)的操作。
有了這張圖,我們其實不僅能處理遮擋,還能夠渲染陰影 —— 陰影就是從光源的位置看來,各個物體之間的遮擋關系;被遮擋的地方塗上陰影,沒被遮擋的地方該咋樣咋樣。也就是說,我們需要讓攝像機的位置朝向等與光源一致,然后渲染一張圖像,取出它的深度圖(事實上,我們只需要深度數據,所以我們可以只渲染深度來節約開銷)。然后把攝像機移回原來的位置,再進行渲染。在渲染時,對渲染的每個像素,找到它在光源處那張深度圖中的位置,並計算出它離光源的距離;然后把光源深度圖中的數據和它離光源的距離數據比較,就可以判斷出它是不是應該被塗上陰影了。
來看一組wikipedia上的連環畫(誤):
這是我們要渲染的場景,沒有陰影。
切換到光源視點,渲染了一張圖(由於是平行光,所以是正交投影)
得到的深度信息
“假想的”把這張深度圖投射到實際的物體表面的狀況
進行深度測試(需要塗上陰影的地方用白色標出)
最后貼上陰影的圖像。
大概就是這么一個過程。但是夢想是美好的,現實可未必如此;為了讓世界有最基本的陰影,還有一些需要解決的問題。再來看一下剛才的過程:
我們需要讓攝像機的位置朝向等與光源一致,然后渲染一張圖像,取出它的深度圖(事實上,我們只需要深度數據,所以我們可以只渲染深度來節約開銷)[我們該如何渲染一張圖像,不顯示它,並且在之后的渲染中使用它?如何獲取到它的深度信息?]。然后把攝像機移回原來的位置,再進行渲染。在渲染時,對渲染的每個像素,找到它在光源處那張深度圖中的位置[該如何找?],並計算出它離光源的距離[怎么算?];然后把光源深度圖中的數據和它離光源的距離數據比較[怎么比較?如何把顏色數據轉換為距離?],就可以判斷出它是不是應該被塗上陰影了。
好在這些問題都沒有那么困難。一個個來:
1)我們該如何渲染一張圖像,不顯示它,並且在之后的渲染中使用它? —— 渲染到紋理(Render To Texture)
在渲染過程中,我們可以使用被稱為“紋理”的圖像數據來幫助我們決定每個像素的顏色,比如每個模型的貼圖,法線貼圖,亂七八糟各種貼圖之類的。同樣,我們也有辦法在程序運行過程中動態的創建、修改它們。這里說起來太雜,而且網上也有許多現成的信息和教程(關鍵詞:“Render to texture”,“RTT”,“渲染到紋理”)。對於OGL/DX,可以使用諸如FrameBuffer,RenderTarget之類的東西;Unity可以用Unity提供的RenderTexture,並在場景中創建一個攝像機設置一下就行了。
2)如何獲取到它的深度信息?
首先,在渲染時告訴計算機“你需要給我一張深度圖”,然后去用就好了。
3)該如何找到某像素在光源深度圖中的位置?
我們需要對這個像素的位置進行變換。
首先,在vertex shader中向后傳一個世界坐標信息,這樣會方便之后的計算;並且要知道光源的變換矩陣(一個4x4的矩陣,代表了光源相對於世界空間的位置、朝向、透視(對於平行光來講是正交)的信息)
然后在fragment shader中取出這個坐標(就是這個像素在世界空間中的位置),把這個坐標矢量右乘矩陣,得到在光源“屏幕空間”中的坐標;這個坐標的x,y值就是對應於深度圖中的位置。
P.S. 對於平行光來講,由於沒有透視的問題,所以也可以用光源的位置加上光源局部坐標系下的至少兩個正交基,來確定相對於光源的位置。
4)怎么算離光源的距離?如何把顏色數據轉換為距離?
如何計算點到光源的距離這個問題沒什么好說的,光源也是一個點,兩個坐標矢量單純的相減即可。
接下來就是如何把顏色數據轉換為距離。
首先,對於深度圖中的顏色,我們有辦法用一個0.0~1.0之間的浮點數來代表它。通常直接讀取圖像數據就可以拿到這個浮點數;
然后我們需要把這個浮點數轉換為距離。把距離轉換為這個浮點數會相對好說一點,想要反着轉換回去只需要反着算就行。把這個距離轉換為0~1的浮點數事實上就是向深度緩沖區里寫入顏色信息的過程,這個過程大致如下:
先得到當前攝像機“屏幕空間”中的坐標pos(就是上一步中已經算好的坐標“深度圖中的位置”)。pos.z / pos.w的結果就是最終的顏色值。
對於正交攝像機來說,w是一個常數。此時深度圖中的顏色就是把0~1的浮點數線性地 “塗在” 近剪裁平面到遠剪裁平面之間。
然后我們就能算出這個點的“深度值”(一個0~1的浮點數),把它和光源深度圖中的顏色比較,得到這個點應不應該被覆蓋上陰影,進行后續的操作。
附一張極丑無比的圖,是我弄了棵樹然后強行丟了個shadowmap上去的結果:(
(真***丑啊...沒眼看了23333)
這種shadowmapping的方法十分基礎,也有着諸多的缺陷,例如分辨率不夠、沒法弄軟陰影之類的亂七八糟的不足。由上面這種最基本的方法衍生出了許許多多新的計算陰影的方式,諸如CSM等各種比較不錯的陰影處理算法,但它們都是上述方法的衍生產物。有時間的話再說吧(懶
P.S. 關於上面把離光源的位置轉換為0~1的浮點數的逆過程稱為“從深度重建位置信息”(或這一類的稱呼)。由於透視投影下這個變換不是線性的,離攝像機較遠的地方分辨率十分低下導致各種問題,也有一些比較好的改進算法來進行這個變換。還請善用搜索w~