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 οnlοad="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,这样就又能够正确的绘制出阴影了

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: WebGL 是一种基于 JavaScript 和 OpenGL 的 3D 图形库,它可以在 Web 浏览器中渲染 3D 图形,从而实现浏览器中的 3D 视觉效果实现阴影效果可以使用 WebGL 中的阴影映射技术,简单来说,就是在场景中添加光源,然后从光源的角度渲染场景,生成一张深度图,再将深度图应用到场景中的物体上,就可以实现阴影效果了。同时,还可以使用其他技术,例如环境映射和法线映射,来增强场景的真实感和深度感。总之,WebGL 提供了丰富的工具和技术,使得在 Web 浏览器中实现 3D 阴影效果成为可能。 ### 回答2: WebGL是一种基于Web的图形渲染技术,可以实现高性能的3D图形渲染。要实现阴影效果,可以借助WebGL的功能来完成。 首先,要实现阴影效果,需要一个光源。在WebGL中,可以通过设置光源的位置和方向来控制光的照射效果。 其次,还需要一个或多个需要投射阴影的物体。通过在渲染过程中为这些物体添加一个额外的"shadow"属性,可以标识它们需要投射阴影。 然后,在渲染阴影之前,需要创建一个用于渲染阴影的额外场景。在该场景中,只包含需要投射阴影的物体。这个场景可以使用一个特殊的着色器来渲染,这个着色器和其他场景使用的着色器可能稍有不同,因为它只关注渲染阴影而不需要考虑光照。 接下来,在渲染主场景之前,需要对每个需要投射阴影的物体进行阴影计算。这可以通过使用光源的位置和方向来确定每个像素是否在阴影之中。可以使用阴影映射技术(Shadow Mapping)来完成这个计算过程,这个过程涉及渲染一个特殊的深度贴图来表示场景中的深度信息。 最后,在主场景渲染过程中,可以使用计算得到的阴影信息来对物体进行阴影投射。可以使用对阴影贴图进行采样的方式,来判断像素是否位于阴影之中。 综上所述,WebGL通过设置光源、创建阴影场景、进行阴影计算和使用阴影信息投射,可以实现3D场景中的阴影效果。 ### 回答3: WebGL是一种在网页浏览器上运行3D图形的技术,它使用JavaScript和OpenGL的子集来实现。要实现阴影效果,我们可以借助WebGL的着色器程序和纹理功能。 首先,为了产生阴影,我们需要一个光源。可以使用WebGL的光照功能来定义光源的位置和类型,例如平行光或点光源。 接下来,我们需要为阴影接收器(即产生阴影的物体)和阴影投射器(即光源)创建阴影贴图。阴影贴图是一个与场景大小相同的二维纹理,它记录了物体与光源之间的相对位置关系。 然后,我们需要通过在阴影投射器和阴影接收器之间进行渲染来生成阴影贴图。我们可以使用投射阴影的算法,如阴影映射或体积光,来计算每个像素的阴影强度。 在渲染阶段,我们可以在顶点着色器和片段着色器中应用阴影贴图。顶点着色器将物体的顶点位置转换为世界坐标和光源坐标,并将它们传递给片段着色器。片段着色器使用阴影贴图来计算每个像素的阴影强度,并将其与物体的颜色进行混合。 最后,我们可以通过在片段着色器中使用深度测试来控制阴影的渲染顺序,并使用透明度来调整阴影的强度和透明度。 总体来说,WebGL通过使用光照、阴影贴图和着色器程序来实现阴影效果。这样可以使3D场景更加逼真和生动,并增强用户对物体之间关系的认知。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值