WebGL光照陰影映射


原文地址:WebGL光照陰影映射
經過前面的學習,webgl的基本功能都已經掌握了,我們不僅掌握了着色器的編寫,圖形的繪制,矩陣的變換,添加光照,還通過對webgl的基礎api封裝,編寫出了便利的工具庫. 是時候進一步深入學習webgl的高級功能了,我認為要做逼真的3D特效,陰影絕對是一個必不可少的環節.現在我們就在之前光照的基礎上添加陰影效果吧.

首先看一下陰影效果的實例:
陰影綜合(多物體高精度PCF)
點光源聚光燈陰影

內容大綱

我們以陰影綜合(多物體高精度PCF)為例, 開始學習陰影相關知識.

  1. 幀緩沖
  2. 陰影映射(shadow mapping)
  3. 提高陰影精度
  4. 抗鋸齒(PCF)

幀緩沖

我們實現陰影效果使用的是叫陰影映射的技術, 而實現陰影映射需要用到幀緩沖區。默認情況下,WebGL 在顏色緩沖區繪圖,使用隱藏面消除的話,還會用到深度緩沖區。即正常繪制的情況下包含:

  • 顏色緩沖區
  • 深度緩沖區

幀緩沖區對象 framebuffer object可以用來代替顏色緩沖區或深度緩沖區。繪制在幀緩沖區中的對象並不會直接顯示canvas上,可以先對幀緩沖區中的內容進行一些處理再顯示,或者直接用其中的內容作為紋理圖像。在幀緩沖區中進行繪制的過程又稱為離屏繪制 offscreen drawing。

繪制操作並不是直接發生在幀緩沖區中,而是發生在幀緩沖區所關聯的對象 attachment上,一個幀緩沖區有3個關聯對象:

  • 顏色關聯對象 color attachment,對應顏色緩沖區
  • 深度關聯對象 depth attachment,對應深度緩沖區
  • 模板關聯對象 stencil attachment,對應模板緩沖區。

而我們現在先有這個概念,來看看幀緩沖區的創建和配置:

  1. 創建幀緩沖區對象 gl.createFramebffer().
  2. 創建文理對象並設置其尺寸和參數 gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri().
  3. 創建渲染緩沖區對象 gl.createRenderbuffer().
  4. 綁定渲染緩沖區對象並設置其尺寸 gl.bindRenderBuffer()、gl.renderbufferStorage().
  5. 將幀緩沖區的顏色關聯對象指定為一個文理對象 gl.frambufferTexture2D().
  6. 將幀緩沖區的深度關聯對象指定為一個渲染緩沖區對象 gl.framebufferRenderbuffer().
  7. 檢查幀緩沖區是否正確配置 gl.checkFramebufferStatus().
  8. 在幀緩沖區中進行繪制 gl.bindFramebuffer().

它的創建和配置是一個非常繁瑣的過程,我們先熟悉了怎么使用,再慢慢研究它內部的原理,所以先把上面的步驟封裝成一個黑盒子,我這里就是createFramebuffer這個函數.

陰影映射(shadow mapping)

陰影映射的原理很簡單,首先從光的角度渲染場景,從光的角度看到的所有東西都被點亮了,而看不見的部分一定是在陰影里.。想象有一個盒子和它的光源照射下的地板,由於光源會看到這個盒子而它后面的地板部分是看不到的.那么當視線角度變化的時候,從光源角度照不到的那部分地板就渲染為陰影,原理如下圖

接着我們使用陰影映射的算法實現, 它要使用到前面介紹的幀緩沖區. 陰影映射要渲染兩遍:

  1. 從光源的角度渲染場景,同時把場景的深度值當成紋理渲染到幀緩沖區,也就是把它當作數據容器.
  2. 從眼睛的角度渲染場景,把物體真正渲染到畫布中,同時對比紋理的深度值,將陰影部分也渲染出來.

左邊的圖像是第一遍渲染的原理, 一個方向光源(所有的光線都是平行的)在立方體下面的表面投下陰影.我們通過用光源的視圖投影矩陣渲染場景(從光線的角度)來創建景深圖然后把它存儲到幀緩沖區中.

右邊的圖形是第二遍渲染的原理, 從眼睛的視圖投影矩陣渲染場景(從眼睛的角度), 光源角度下的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相關的東西才能解決,傷不起啊.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM