原文地址:WebGL光照陰影映射
經過前面的學習,webgl的基本功能都已經掌握了,我們不僅掌握了着色器的編寫,圖形的繪制,矩陣的變換,添加光照,還通過對webgl的基礎api封裝,編寫出了便利的工具庫. 是時候進一步深入學習webgl的高級功能了,我認為要做逼真的3D特效,陰影絕對是一個必不可少的環節.現在我們就在之前光照的基礎上添加陰影效果吧.
首先看一下陰影效果的實例:
陰影綜合(多物體高精度PCF)
點光源聚光燈陰影
內容大綱
我們以陰影綜合(多物體高精度PCF)為例, 開始學習陰影相關知識.
- 幀緩沖
- 陰影映射(shadow mapping)
- 提高陰影精度
- 抗鋸齒(PCF)
幀緩沖
我們實現陰影效果使用的是叫陰影映射的技術, 而實現陰影映射需要用到幀緩沖區。默認情況下,WebGL 在顏色緩沖區繪圖,使用隱藏面消除的話,還會用到深度緩沖區。即正常繪制的情況下包含:
- 顏色緩沖區
- 深度緩沖區
幀緩沖區對象 framebuffer object可以用來代替顏色緩沖區或深度緩沖區。繪制在幀緩沖區中的對象並不會直接顯示canvas上,可以先對幀緩沖區中的內容進行一些處理再顯示,或者直接用其中的內容作為紋理圖像。在幀緩沖區中進行繪制的過程又稱為離屏繪制 offscreen drawing。
繪制操作並不是直接發生在幀緩沖區中,而是發生在幀緩沖區所關聯的對象 attachment上,一個幀緩沖區有3個關聯對象:
- 顏色關聯對象 color attachment,對應顏色緩沖區
- 深度關聯對象 depth attachment,對應深度緩沖區
- 模板關聯對象 stencil attachment,對應模板緩沖區。
而我們現在先有這個概念,來看看幀緩沖區的創建和配置:
- 創建幀緩沖區對象 gl.createFramebffer().
- 創建文理對象並設置其尺寸和參數 gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri().
- 創建渲染緩沖區對象 gl.createRenderbuffer().
- 綁定渲染緩沖區對象並設置其尺寸 gl.bindRenderBuffer()、gl.renderbufferStorage().
- 將幀緩沖區的顏色關聯對象指定為一個文理對象 gl.frambufferTexture2D().
- 將幀緩沖區的深度關聯對象指定為一個渲染緩沖區對象 gl.framebufferRenderbuffer().
- 檢查幀緩沖區是否正確配置 gl.checkFramebufferStatus().
- 在幀緩沖區中進行繪制 gl.bindFramebuffer().
它的創建和配置是一個非常繁瑣的過程,我們先熟悉了怎么使用,再慢慢研究它內部的原理,所以先把上面的步驟封裝成一個黑盒子,我這里就是createFramebuffer這個函數.
陰影映射(shadow mapping)
陰影映射的原理很簡單,首先從光的角度渲染場景,從光的角度看到的所有東西都被點亮了,而看不見的部分一定是在陰影里.。想象有一個盒子和它的光源照射下的地板,由於光源會看到這個盒子而它后面的地板部分是看不到的.那么當視線角度變化的時候,從光源角度照不到的那部分地板就渲染為陰影,原理如下圖
接着我們使用陰影映射的算法實現, 它要使用到前面介紹的幀緩沖區. 陰影映射要渲染兩遍:
- 從光源的角度渲染場景,同時把場景的深度值當成紋理渲染到幀緩沖區,也就是把它當作數據容器.
- 從眼睛的角度渲染場景,把物體真正渲染到畫布中,同時對比紋理的深度值,將陰影部分也渲染出來.
左邊的圖像是第一遍渲染的原理, 一個方向光源(所有的光線都是平行的)在立方體下面的表面投下陰影.我們通過用光源的視圖投影矩陣渲染場景(從光線的角度)來創建景深圖然后把它存儲到幀緩沖區中.
右邊的圖形是第二遍渲染的原理, 從眼睛的視圖投影矩陣渲染場景(從眼睛的角度), 光源角度下的xy坐標相同的c點和p點,p深度值比c要大, 那么它一定處於陰影當中,那么p點就渲染為陰影.
來看實現以上功能的着色器代碼,因為要渲染兩遍,所以也就要建立兩對的着色器(頂點/片段),頂點着色器比較簡單,基本不涉及陰影映射,在此省略:
陰影片段着色器
#ifdef GL_ES
precision mediump float;
#endif
void main() {
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0); //將深度值z存放到第一個分量r中
}
正常片段着色器
深度值后面加了0.005,稍微大於1/256,即8位的表示范圍(因為一個分量就是8位),這個是消除馬赫帶用的,不加這個值,畫面會產生難看的條紋,具體的原理可查找馬赫帶,在此不細講.
precision mediump float;
uniform sampler2D u_ShadowMap;
varying vec4 v_PositionFromLight;
varying vec4 v_color;
void main() {
// 獲取紋理的坐標
vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;
// 根據陰影xy坐標,獲取紋理中對應的點,z值已經被之前的陰影着色器存放在該點的r分量中了,直接使用即可
vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);// 獲取指定紋理坐標處的像素顏色rgba
float visibility = (shadowCoord.z > rgbaDepth.r + 0.005) ? 0.6 : 1.0;//大於陰影的z軸,說明在陰影中並顯示為陰影*0.6,否則為正常顏色*1.0
gl_FragColor = vec4(v_color.rgb * visibility, v_color.a);
}
提高精度
完成了最簡單的陰影效果,但是當你把光源與物體的距離拉遠,問題出來了,怎么看不到陰影了?這是距離超過了8位的存儲范圍,溢出的緣故.之前我們只使用了一個分量來存儲,現在我們把其他的分量也利用起來吧,rgba一共32位.
陰影片段着色器
這中間進行復雜的分解運算,並同時去除異常值,請看如下代碼
/**
* 分解保存深度值
*/
vec4 pack (float depth) {
// 使用rgba 4字節共32位來存儲z值,1個字節精度為1/256
const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
// gl_FragCoord:片元的坐標,fract():返回數值的小數部分
vec4 rgbaDepth = fract(depth * bitShift); //計算每個點的z值
rgbaDepth -= rgbaDepth.gbaa * bitMask; // Cut off the value which do not fit in 8 bits
return rgbaDepth;
}
void main() {
gl_FragColor = pack(gl_FragCoord.z);// 將z值分開存儲到rgba分量中,陰影顏色的同時也是深度值z
}
正常片段着色器
這里對應就要解碼出深度值
/**
* 釋出深度值z
*/
float unpack(const in vec4 rgbaDepth) {
const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));
return dot(rgbaDepth, bitShift);
}
抗鋸齒(PCF)
解決了精度的問題,接着繼續優化. 運行起來吧,陰影很粗糙有木有? 你看看下面左圖,很嚴重的鋸齒, 抗鋸齒有很多種解決方案,我這里使用PCF, 也就是百分比漸近式過濾算法,因為它基於代碼實現的,所以也叫軟陰影.
PCF的原理也很簡單, 采集當前點周圍像素的陰影值,並將其深度與所有采集的樣本進行比較,最后對結果進行平均,這樣就得到光線和陰影之間更平滑的過渡效果.下面右圖是經過PCF處理之后的陰影,效果要自然得多了.
我們看正常着色器的實現代碼
vec3 shadowCoord = (v_positionFromLight.xyz/v_positionFromLight.w)/2.0 + 0.5;
float shadows =0.0;
float opacity=0.6;// 陰影alpha值, 值越小暗度越深
float texelSize=1.0/1024.0;// 陰影像素尺寸,值越小陰影越逼真
vec4 rgbaDepth;
// 消除陰影邊緣的鋸齒
for(float y=-1.5; y <= 1.5; y += 1.0){
for(float x=-1.5; x <=1.5; x += 1.0){
rgbaDepth = texture2D(u_shadowMap, shadowCoord.xy+vec2(x,y)*texelSize);
shadows += shadowCoord.z-bias > unpack(rgbaDepth) ? 1.0 : 0.0;
}
}
shadows/=16.0;// 4*4的樣本
float visibility=min(opacity+(1.0-shadows),1.0);
specular=visibility < 1.0 ? vec3(0.0,0.0,0.0): specular;// 陰影處沒有高光
gl_FragColor = vec4((diffuse + ambient + specular) * visibility, v_color.a);
總結
WebGL的陰影部分,涉及到了很多opengGL的底層,計算機圖形學算法. 為了深入理解它,可真是花費了很多腦力,是到目前為止學習webgl的第一道坎,它里面的水很深.比如光是反鋸齒部分就涉及到很多低層細節,算法的實現,顯卡的性能問題等都是需要考慮的, 陰影部分后續還要慢慢查資料繼續優化.
越是深入學習WebGL,就越覺得它相關的資料真是少,必須看openGL ES相關的東西才能解決,傷不起啊.