1. 程式人生 > >54 WebGL實現陰影效果

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,這樣就又能夠正確的繪製出陰影了