54 WebGL實現陰影效果
案例檢視地址:點選這裡
實現陰影的基本思想是:太陽看不見陰影。如果在光源處放置以為觀察者,其視線方向與光線一致,那麼觀察者也看不到陰影。他看到的每一處都在光的照射下,而那些背後的,他沒有看到的物體則處在陰影中。這裡,我們需要用到光源與物體之間的距離(實際上也就是物體在光源座標系下的深度z值)來決定物體是否可見。如圖所示,同一條光線上有兩個點P1和P2,由於P2的z值大於P1,所以P2在陰影中。
我們需要使用兩對著色器以實現陰影:[1]一對著色器用來計算光源到物體的距離,[2]另一對著色器根據[1]中計算出的距離繪製場景。使用一張紋理影象把[1]的結果傳入[2]中,這張紋理影象就被稱為陰影貼圖(shadow map),而通過陰影貼圖實現陰影的方法就被稱為陰影對映(shadow mapping)。陰影對映的過程包括以下兩步:
(1)將視點移動到光源的位置處,並執行[1]中的著色器。這時,那些“將要被繪出”的片元都是被光照射到的,即落在這個畫素上的各個片元中最前面的。我們並不實際地繪製出片元的顏色,而是將片元的z值寫入到陰影貼圖中。
(2)將視點移回原來的位置,執行[2]中的著色器繪製場景。此時,我們計算出每個片元在光源座標系(即[1]中的視點座標系)下的座標,並與陰影貼圖中記錄的z值比較,如果前者大於後者,就說明當前片元處在陰影之中,用較深暗的顏色繪製。
案例程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Title</title> <style> body { margin: 0; overflow: hidden; } #canvas { margin: 0; display: block; } </style> </head> <body onload="main()"> <canvas id="canvas" height="800" width="800"></canvas> </body> <script src="lib/webgl-utils.js"></script> <script src="lib/webgl-debug.js"></script> <script src="lib/cuon-utils.js"></script> <script src="lib/cuon-matrix.js"></script> <script> //設定WebGL全屏顯示 var canvas = document.getElementById("canvas"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; //設定陰影貼圖頂點著色器 var shadowVertexShaderSource = "" + "attribute vec4 a_Position;\n" + "uniform mat4 u_MvpMatrix;\n" + "void main(){\n" + " gl_Position = u_MvpMatrix * a_Position;\n" + //計算出在燈源視點下各個座標的位置 "}\n"; //設定陰影貼圖的片元著色器 var shadowFragmentShaderSource = "" + "#ifdef GL_ES\n" + "precision mediump float;\n" + "#endif\n" + "void main(){\n" + " gl_FragColor = vec4( 0.0, 0.0, 0.0,gl_FragCoord.z);\n" + //將燈源視點下的每個頂點的深度值存入繪製的顏色內 "}\n"; //正常繪製的頂點著色器 var vertexShaderSource = "" + "attribute vec4 a_Position;\n" + "attribute vec4 a_Color;\n" + "uniform mat4 u_MvpMatrix;\n" + //頂點的模型投影矩陣 "uniform mat4 u_MvpMatrixFromLight;\n" + //頂點基於光源的模型投影矩陣 "varying vec4 v_PositionFromLight;\n" + //將基於光源的頂點位置傳遞給片元著色器 "varying vec4 v_Color;\n" + //將顏色傳遞給片元著色器 "void main(){\n" + " gl_Position = u_MvpMatrix * a_Position;\n" + //計算並設定頂點的位置 " v_PositionFromLight = u_MvpMatrixFromLight * a_Position;\n" + //計算基於光源的頂點位置 " v_Color = a_Color;\n" + "}\n"; //正常繪製的片元著色器 var fragmentShaderSource = "" + "#ifdef GL_ES\n" + "precision mediump float;\n" + "#endif\n" + "uniform sampler2D u_ShadowMap;\n" + //紋理的儲存變數 "varying vec4 v_PositionFromLight;\n" + //從頂點著色器傳過來的基於光源的頂點座標 "varying vec4 v_Color;\n" + //頂點的顏色 "void main(){\n" + " vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" + " vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" + " float depth = rgbaDepth.a;\n" + " float visibility = (shadowCoord.z > depth + 0.005) ? 0.5 : 1.0;\n" + " gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" + "}\n"; //生成的紋理的解析度,紋理必須是標準的尺寸 256*256 1024*1024 2048*2048 var resolution = 256; var offset_width = resolution; var offset_height = resolution; //燈光的位置 var light_x = 0.0; var light_y = 7.0; var light_z = 2.0; function main() { var canvas = document.getElementById("canvas"); var gl = getWebGLContext(canvas); if(!gl){ console.log("無法獲取WebGL的上下文"); return; } //初始化陰影著色器,並獲得陰影程式物件,相關變數的儲存位置 var shadowProgram = createProgram(gl, shadowVertexShaderSource, shadowFragmentShaderSource); shadowProgram.a_Position = gl.getAttribLocation(shadowProgram, "a_Position"); shadowProgram.u_MvpMatrix = gl.getUniformLocation(shadowProgram, "u_MvpMatrix"); if(shadowProgram.a_Position < 0 || !shadowProgram.u_MvpMatrix ){ console.log("無法獲取到陰影著色器的相關變數"); return; } //初始化正常繪製著色器,獲取到程式物件並獲取相關變數的儲存位置 var normalProgram = createProgram(gl, vertexShaderSource, fragmentShaderSource); normalProgram.a_Position = gl.getAttribLocation(normalProgram, "a_Position"); normalProgram.a_Color = gl.getAttribLocation(normalProgram, "a_Color"); normalProgram.u_MvpMatrix = gl.getUniformLocation(normalProgram, "u_MvpMatrix"); normalProgram.u_MvpMatrixFromLight = gl.getUniformLocation(normalProgram, "u_MvpMatrixFromLight"); normalProgram.u_ShadowMap = gl.getUniformLocation(normalProgram, "u_ShadowMap"); if(normalProgram.a_Position < 0 || normalProgram.a_Color < 0 || !normalProgram.u_MvpMatrix || !normalProgram.u_MvpMatrixFromLight || !normalProgram.u_ShadowMap){ console.log("無法獲取到正常繪製著色器的相關變數"); return; } //設定相關資料,並存入緩衝區內 var triangle = initVertexBuffersForTriangle(gl); var plane = initVertexBuffersForPlane(gl); if(!triangle || !plane){ console.log("無法設定相關頂點的資訊"); return; } //設定幀緩衝區物件 var fbo = initFramebufferObject(gl); if(!fbo){ console.log("無法設定幀緩衝區物件"); return; } //開啟0號紋理緩衝區並繫結幀緩衝區物件的紋理 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, fbo.texture); //設定背景設並開啟隱藏面消除功能 gl.clearColor(0.0,0.0,0.0,1.0); gl.enable(gl.DEPTH_TEST); //宣告一個光源的變換矩陣 var viewProjectMatrixFromLight = new Matrix4(); viewProjectMatrixFromLight.setPerspective(70.0, offset_width/offset_height, 1.0, 100.0); viewProjectMatrixFromLight.lookAt(light_x, light_y, light_z,0.0,0.0,0.0,0.0,1.0,0.0); //為常規繪圖準備檢視投影矩陣 var viewProjectMatrix = new Matrix4(); viewProjectMatrix.setPerspective(45.0, canvas.width/canvas.height, 1.0, 100.0); viewProjectMatrix.lookAt(0.0,7.0,9.0,0.0,0.0,0.0,0.0,1.0,0.0); var currentAngle = 0.0; //聲明當前旋轉角度的變數 var mvpMatrixFromLight_t = new Matrix4(); //光源(三角形)的模型投影矩陣 var mvpMatrixFromLight_p = new Matrix4(); //光源(平面)的模型投影矩陣 (function tick() { currentAngle = animate(currentAngle); //切換繪製場景為幀緩衝區 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.viewport(0.0,0.0,offset_height,offset_height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(shadowProgram); //使用陰影程式物件繪製陰影紋理 //繪製三角形和平面(用於生成陰影貼圖) drawTriangle(gl, shadowProgram, triangle, currentAngle, viewProjectMatrixFromLight); mvpMatrixFromLight_t.set(g_mvpMatrix); //稍後使用 drawPlane(gl, shadowProgram, plane, viewProjectMatrixFromLight); mvpMatrixFromLight_p.set(g_mvpMatrix); //稍後使用 //解除幀緩衝區的繫結,繪製正常顏色緩衝區 gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0.0, 0.0, canvas.width, canvas.height); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //切換為正常的程式物件並繪製 gl.useProgram(normalProgram); gl.uniform1i(normalProgram.u_ShadowMap, 0.0); //繪製三角形和平面(正常繪製的圖形) gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatrixFromLight_t.elements); drawTriangle(gl, normalProgram, triangle, currentAngle, viewProjectMatrix); gl.uniformMatrix4fv(normalProgram.u_MvpMatrixFromLight, false, mvpMatrixFromLight_p.elements); drawPlane(gl, normalProgram, plane, viewProjectMatrix); requestAnimationFrame(tick); })(); } //宣告座標轉換矩陣 var g_modelMatrix = new Matrix4(); var g_mvpMatrix = new Matrix4(); function drawTriangle(gl,program,triangle,angle,viewProjectMatrix) { //設定三角形圖形的旋轉角度,並繪製圖形 g_modelMatrix.setRotate(angle, 0.0, 1.0, 0.0); draw(gl, program, triangle, viewProjectMatrix); } function drawPlane(gl, program, plane, viewProjectMatrix) { //設定平面圖形的旋轉角度並繪製 g_modelMatrix.setRotate(-45.0, 0.0, 1.0, 1.0); draw(gl, program, plane, viewProjectMatrix); } function draw(gl, program, obj, viewProjectMatrix) { initAttributeVariable(gl, program.a_Position, obj.vertexBuffer); //判斷程式物件上面是否設定了a_Color值,如果有,就設定顏色緩衝區 if(program.a_Color != undefined){ initAttributeVariable(gl, program.a_Color, obj.colorBuffer); } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexBuffer); //設定模板檢視投影矩陣,並賦值給u_MvpMatrix g_mvpMatrix.set(viewProjectMatrix); g_mvpMatrix.multiply(g_modelMatrix); gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements); gl.drawElements(gl.TRIANGLES, obj.numIndices, gl.UNSIGNED_BYTE, 0); } function initAttributeVariable(gl, a_attribute, buffer) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0); gl.enableVertexAttribArray(a_attribute); } var angle_step = 30; var last = +new Date(); function animate(angle) { var now = +new Date(); var elapsed = now - last; last = now; var newAngle = angle + (angle_step*elapsed)/1000.0; return newAngle%360; } function initFramebufferObject(gl) { var framebuffer, texture, depthBuffer; //定義錯誤函式 function error() { if(framebuffer) gl.deleteFramebuffer(framebuffer); if(texture) gl.deleteFramebuffer(texture); if(depthBuffer) gl.deleteFramebuffer(depthBuffer); return null; } //建立幀緩衝區物件 framebuffer = gl.createFramebuffer(); if(!framebuffer){ console.log("無法建立幀緩衝區物件"); return error(); } //建立紋理物件並設定其尺寸和引數 texture = gl.createTexture(); if(!texture){ console.log("無法建立紋理物件"); return error(); } gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, offset_width, offset_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); framebuffer.texture = texture;//將紋理物件存入framebuffer //建立渲染緩衝區物件並設定其尺寸和引數 depthBuffer = gl.createRenderbuffer(); if(!depthBuffer){ console.log("無法建立渲染緩衝區物件"); return error(); } gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, offset_width, offset_height); //將紋理和渲染緩衝區物件關聯到幀緩衝區物件上 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER,depthBuffer); //檢查幀緩衝區物件是否被正確設定 var e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); if(gl.FRAMEBUFFER_COMPLETE !== e){ console.log("渲染緩衝區設定錯誤"+e.toString()); return error(); } //取消當前的focus物件 gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); return framebuffer; } function initVertexBuffersForPlane(gl) { // 建立一個面 // v1------v0 // | | // | | // | | // v2------v3 // 頂點的座標 var vertices = new Float32Array([ 3.0, -1.7, 2.5, -3.0, -1.7, 2.5, -3.0, -1.7, -2.5, 3.0, -1.7, -2.5 // v0-v1-v2-v3 ]); // 顏色的座標 var colors = new Float32Array([ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]); // 頂點的索引 var indices = new Uint8Array([0, 1, 2, 0, 2, 3]); //將頂點的資訊寫入緩衝區物件 var obj = {}; obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT); obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT); obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE); if(!obj.vertexBuffer || !obj.colorBuffer || !obj.indexBuffer) return null; obj.numIndices = indices.length; gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); return obj; } function initVertexBuffersForTriangle(gl) { // Create a triangle // v2 // / | // / | // / | // v0----v1 // 頂點的座標 var vertices = new Float32Array([-0.8, 3.5, 0.0, 0.8, 3.5, 0.0, 0.0, 3.5, 1.8]); // 顏色的座標 var colors = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0, 1.0, 0.0, 0.0]); // 頂點的索引 var indices = new Uint8Array([0, 1, 2]); //建立一個物件儲存資料 var obj = {}; //將頂點資訊寫入緩衝區物件 obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT); obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT); obj.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE); if(!obj.vertexBuffer || !obj.colorBuffer || !obj.indexBuffer) return null; obj.numIndices = indices.length; gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); return obj; } function initArrayBufferForLaterUse(gl, data, num, type) { var buffer = gl.createBuffer(); if(!buffer){ console.log("無法建立緩衝區物件"); return null; } gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); buffer.num = num; buffer.type = type; return buffer; } function initElementArrayBufferForLaterUse(gl, data, type) { var buffer = gl.createBuffer(); if(!buffer){ console.log("無法建立著色器"); return null; } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); buffer.type = type; return buffer; } </script> </html>
前兩個帶shadow字首的著色器負責生產陰影貼圖。我們需要將繪製目標切換到幀緩衝區物件,把視點在光源處的模型檢視投影矩陣傳給u_MvpMatrix變數,並執行著色器。著色器會將每個片元的z值寫入幀緩衝區關聯的陰影貼圖中。頂點著色器的任務很簡單,將頂點座標乘以模型檢視投影矩陣,而片元著色器略複雜一些,它將片元的z值寫入了紋理貼圖中。為此,我們使用了片元著色器的內建變數gl_FragCoord。
gl_FragCoord的內建變數是vec4型別的,用來表示片元的座標。gl_FragCoord.x和gl_FragCoord.y是片元在螢幕上的座標,而gl_FragCoord.z是深度值。它們是通過(gl_Position.xyz/gl_Position.w)/2.0+0.5 計算出來的,都被歸一化到[0.0,1.0]區間。如果gl_FragCoord.z是0.0,則表示該片元在近裁剪面上,如果是1.0, 則表示片元在遠才見面上。我們將該值寫入到陰影貼圖的R分量重。當然,你也可以使用其他分量。
" gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0);\n" +
這樣,著色器就將視點位於光源時每個片元的z值儲存在陰影貼圖中。陰影貼圖將被作為紋理物件傳給另一對著色器中的u_shadowMap變數。正常繪製的頂點著色器和片元著色器實現了第2步。將繪製目標切換回顏色緩衝區,把視點移回原位,開始真正地繪製場景。此時,我們需要比較片元在光源座標系下的z值和陰影貼圖中對應的值來決定當前片元是否處在陰影之中。u_MvpMatrix變數是視點在原處的模型檢視投影矩陣,而u_MvpMatrixFromLight變數是第1步中視點位於光源處時的模型檢視投影矩陣。頂點著色器計算每個頂點在光源座標系(即第1步中的檢視座標系)中的座標v_PositionFromLight(等價於第1不重的gl_Position),並傳入片元著色器。
片元著色器的任務是根據片元在光源座標系中的座標v_PositionFromLight 計算出可以與陰影貼圖相比較的z值。前面說過,陰影貼圖中的z值是通過(gl_Position.z/gl_Position.w)/2.0+0.5 計算出來的,為使這裡的結果能夠與之比較,我們也需要通過(v_PositionFromLight.z / v_PositionFromLight.w) / 2.0 + 0.5 來進行歸一化。然後,為了將z值與陰影貼圖中的相應紋素值比較,需要通過 v_PositionFromLight 的 x 和 y 座標從陰影貼圖中獲取紋素。但我們知道,WebGL中的x和y座標都是在[-1.0, 1.0]區間中的, 而紋理座標s和t是在[0.0, 1.0]的區間中的。所以我們還需要將x和y座標轉化為s和t座標:
s = (v_PositionFromLight.x / v_PositionFromLight.w) / 2.0 + 0.5
t = (v_PositionFromLight.y / v_PositionFromLight.w) / 2.0 + 0.5
其歸一化的方式與z值的歸一化的方式一致。所以我們在一行程式碼中完成xyz的歸一化(74行),計算出shadowCoord變數,其x和y分量為當前片元在陰影貼圖中對應的紋素的紋理座標,而z分量表示當前片元在光源座標系中的歸一化z值,可與陰影貼圖中的紋素值比較。 "void main(){\n" +
" vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" +
" vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" +
" float depth = rgbaDepth.r;\n" +
" float visibility = (shadowCoord.z > depth + 0.005) ? 0.5 : 1.0;\n" +
" gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" +
"}\n";
然後,我們通過shadowCoord.xy從陰影貼圖中抽取出紋素(75,76行),你應該還記得,這並非單純的抽取紋理的紋素,而涉及內插過程。由於之前z值被寫在了R分量中(48行),所以這裡也只需提取R分量,並儲存在depth變數中。接著,我們通過比較shadowCoord.z和depth來決定片元是否是在陰影中,如果前者比較大,說明當前片元在陰影中,就為visibility變數賦值為0.7,否則就賦為1.0.改變數參與了計算片元的最終顏色的過程,如果其為0.7,那麼片元就會深暗一些,以表示在陰影中。個人心得:
上面是原搬的書中的解釋,但是對實現的原理的解釋過於原理,而且裡面摻雜著相關變數,變得更加難懂。博主本來上一節的在幀緩衝區繪製紋理就看得不怎麼懂,沒想到在這一節還竟然用的這種方法,所以,個人感覺在這裡解釋一波比較好。
(1)其實WebGL繪製模型就和作畫一樣,在2D介面上展現3D的效果。與其說是3D模型,不如說有3D效果的2D圖。動畫,顧名思義,就是動起來的畫,切換的速度快了,就感覺不出來是2D平面的效果。
(2)如果能理解(1)所說的東西了,下一步就好理解了。所以,這一節的兩個著色器繪製了兩張圖片而已,fbo是啥,就是能夠不直接在畫布上繪製,而是直接憑空繪製完成,還放到紋理快取區。它繪製出來了啥,就是繪製出來了所有能夠被光照射到的地方。
(3)繪製第一對著色器儲存了繪製的每個點與光源的距離,這個是幹嘛用的,如果再繪製別的地方的時候,繪製的點比這個距離大的話,就肯定處於陰影當中了。(教程中是把這個z值賦值給了RGBA中的R變數,但是如果紋理設定的解析度太低的話,圖形的邊緣也會出現顯示出是貼圖的效果。這是因為片元著色器獲取紋理時的一個內插的效果,所以我改成了A變數,效果比之前好了很多。)
(4)第二對著色器繪製時幹了啥,首先就是計算出了繪製的當前點在第一對著色器中時這個點的xyz的值,就是計算出來這個點距離光源的距離z,然後和放到紋理中的這個z值進行對比。如果距離比紋理中的能證明處於亮面的距離遠的話,就能判斷出來是處在陰影當中,就讓RGBA乘以0.7,比不處於亮面的顏色顯得黑,就做出來了陰影。
馬赫帶:
在77行中,我們對比的時候給depth添加了0.005的偏移量。如果將這個偏移量刪除掉的話,再執行程式,就會發現會出現和上圖一樣的馬赫帶(Mach band)。
偏移量0.005的作用是消除馬赫帶。出現馬赫帶的原因雖然有點複雜,但三維圖形學中經常會出現類似的問題,這個問題值得弄明白。我們知道,紋理影象RGBA分量中,每個分量都是8位,那麼儲存在陰影貼圖中的z值精度也只有8位,而與陰影貼圖進行比較的值shadowCoord.z是float型別的,有16位。比如說z值是0.1234567,8位的浮點數的精度是1/256,也就是0.00390625。根據:0.1234567/(1/256) = 31.6049152 在8位精度下,0.1234567實際上是31個1/256,即0.12109375 。同理,在16位精度下,0.1234567實際上是8090個1/65536,即0.12344360 。前者比後者小。這意味著,即使是完全相同的座標,在陰影貼圖中的z值可能會比shadowCoord.z中的值小,這就造成了矩形平面的某些區域被誤認為是陰影了。我們再進行比較時,為陰影貼圖添加了一個偏移量0.005,就可以避免產生馬赫帶。注意,偏移量應當略大於精度,比如這裡的0.005就略大於1/256 。
提高精度:
雖然我們已經成功地實現了場景中的陰影效果,但這僅僅使用與光源距離物體很近的情況。如果我們將光源拿遠一些,比如將其y座標改為40:
86行
//燈光的位置
var light_x = 0.0;
var light_y = 40.0;
var light_z = 2.0;
再次執行,就會發現陰影就會消失掉。陰影消失的原因是,隨著光源與照射物體間的距離變遠,gl_FragCoord的值也會增大,當光源足夠遠時,gl_FragCoord.z就大到無法儲存在只有8位的R分量中了。簡單的解決方法是,使用陰影貼圖中的R、G、B、A這四個分量,用4個位元組共32位來儲存z值。實際上,已經有列行的方法來完成這項任務了,讓我們來看看示例程式修改:
將設定陰影貼圖的片元著色器修改為:
var shadowFragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"void main(){\n" +
" const vec4 bitShift = vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0);\n" +
" const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);\n" +
" vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);\n" +
" rgbaDepth -= rgbaDepth.gbaa * bitMask;\n" +
" gl_FragColor = rgbaDepth;\n" + //將燈源視點下的每個頂點的深度值存入繪製的顏色內
"}\n";
將正常繪製的片元著色器修改為: var fragmentShaderSource = "" +
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_ShadowMap;\n" + //紋理的儲存變數
"varying vec4 v_PositionFromLight;\n" + //從頂點著色器傳過來的基於光源的頂點座標
"varying vec4 v_Color;\n" + //頂點的顏色
//從rgba這4個分量中重新計算出z值的函式
"float unpackDepth(const in vec4 rgbaDepth){\n" +
" const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/(256.0*256.0), 1.0/(256.0*256.0*256.0));\n" +
" float depth = dot(rgbaDepth, bitShift);\n" +
" return depth;\n" +
"}\n" +
"void main(){\n" +
" vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;\n" +
" vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);\n" +
" float depth = unpackDepth(rgbaDepth);\n" + //重新計算出z值
" float visibility = (shadowCoord.z > depth + 0.0015) ? 0.5 : 1.0;\n" +
" gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);\n" +
"}\n";
片元著色器shadowFragmentShaderSource將gl_FragCoord.z拆為了4個位元組R、G、B、A 。因為1個位元組的精度是1/256,所以我們將大於1/256的部分儲存在R分量中,將1/256到1/(256*256)的部分儲存在G分量中,將1/(256*256)到1/(256*256*256)儲存在B分量中,並將小於1/(256*256*256)的部分儲存在A分量中。我們使用內建函式fract()來計算上述分量的值,改函式捨棄引數的整數部分,返回小數部分。此外,由於rgbaDepth是vec4型別的,精度高於8位,還需要將多餘的部分砍掉。最後,將rbgaDepth賦值給gl_FragColor,這樣就將z值儲存在陰影貼圖的4個分量中,獲得了更高的精度。
片元著色器fragmentShaderSource呼叫unpackDepth()函式獲取z值。該函式是自定義函式,該函式根據如下公式從RGBA分量中還原出高精度的原始z值。此外,由於該公式與點積公式的形式一樣,所以我們結果了dot()函式完成了計算。
這樣,我們還原了原始的z值,並將它與shadowCoord.z相比較。我們仍然添加了一個偏移量0.0015來消除馬赫帶,因為此時z值得精度已經提高到了float,在medium精度下,精度為2-10=0.000976563,這樣就又能夠正確的繪製出陰影了