• WebGL简易教程(十四):阴影


    1. 概述

    所谓阴影,就是物体在光照下向背光处投下影子的现象,使用阴影技术能提升图形渲染的真实感。实现阴影的思路很简单:

    1. 找出阴影的位置。
    2. 将阴影位置的图元调暗。

    很明显,关键还是在于如何去判断阴影的位置。阴影检测的算法当然可以自己去实现,但其实OpenGL/WebGL已经隐含了这种算法:假设摄像机在光源点,视线方向与光线一致,那么这个时候视图中看不到的地方肯定就是存在阴影的地方。这实际上是由光源与物体之间的距离(也就是光源坐标系下的深度Z值)决定的,深度较大的点为阴影点。如下图所示,同一条光线上的两个点P1和P2,P2的深度较大,所以P2为阴影点:

    image
    图1-1:通过深度来判断阴影

    当然,在实际进行图形渲染的时候,不会永远在光源处进行观察,这个时候可以把光源点观察的结果保存下来——使用上一篇教程《WebGL简易教程(十三):帧缓存对象(离屏渲染)》中介绍的帧缓冲对象(FBO),将深度信息保存为纹理图像,提供给实际图形渲染时判断阴影位置。这张纹理图像就被称为阴影贴图(shadow map),也就是生成阴影比较常用的ShadowMap算法。

    2. 示例

    在上一篇教程《WebGL简易教程(十三):帧缓存对象(离屏渲染)》中已经实现了帧缓冲对象的基本的框架,这里根据ShadowMap算法的原理稍微改进下即可,具体代码可参见文末的地址。

    2.1. 着色器部分

    同样的定义了两组着色器,一组绘制在帧缓存,一组绘制在颜色缓存。在需要的时候对两者进行切换。

    2.1.1. 帧缓存着色器

    绘制帧缓存的着色器如下:

    // 顶点着色器程序-绘制到帧缓存
    var FRAME_VSHADER_SOURCE =
      'attribute vec4 a_Position;
    ' +  //位置
      'attribute vec4 a_Color;
    ' + //颜色
      'uniform mat4 u_MvpMatrix;
    ' +
      'varying vec4 v_Color;
    ' +
      'void main() {
    ' +
      '  gl_Position = u_MvpMatrix * a_Position;
    ' + // 设置顶点坐标
      '  v_Color = a_Color;
    ' +
      '}
    ';
    
    // 片元着色器程序-绘制到帧缓存
    var FRAME_FSHADER_SOURCE =
      'precision mediump float;
    ' +
      'varying vec4 v_Color;
    ' +
      'void main() {
    ' +
      '  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);
    ' +
      '  vec4 rgbaDepth = fract(gl_FragCoord.z * bitShift);
    ' + // Calculate the value stored into each byte
      '  rgbaDepth -= rgbaDepth.gbaa * bitMask;
    ' + // Cut off the value which do not fit in 8 bits
      '  gl_FragColor = rgbaDepth;
    ' +   //将深度保存在FBO中
      '}
    ';
    

    其中,顶点着色器部分没有变化,主要是根据MVP矩阵算出合适的顶点坐标;在片元着色器中,将渲染的深度值保存为片元颜色。这个渲染的结果将作为纹理对象传递给颜色缓存的着色器。

    这里片元着色器中的深度rgbaDepth还经过一段复杂的计算。这其实是一个编码操作,将16位的深度值gl_FragCoord.z编码为4个8位的gl_FragColor,从而进一步提升精度,避免有的地方因为精度不够而产生马赫带现象。

    2.1.2. 颜色缓存着色器

    在颜色缓存中绘制的着色器代码如下:

    // 顶点着色器程序
    var VSHADER_SOURCE =
      'attribute vec4 a_Position;
    ' +  //位置
      'attribute vec4 a_Color;
    ' + //颜色
      'attribute vec4 a_Normal;
    ' + //法向量
      'uniform mat4 u_MvpMatrix;
    ' +     //界面绘制操作的MVP矩阵
      'uniform mat4 u_MvpMatrixFromLight;
    ' +      //光线方向的MVP矩阵
      'varying vec4 v_PositionFromLight;
    ' +
      'varying vec4 v_Color;
    ' +
      'varying vec4 v_Normal;
    ' +
      'void main() {
    ' +
      '  gl_Position = u_MvpMatrix * a_Position;
    ' +
      '  v_PositionFromLight = u_MvpMatrixFromLight * a_Position;
    ' +
      '  v_Color = a_Color;
    ' +
      '  v_Normal = a_Normal;
    ' +
      '}
    ';
    
    // 片元着色器程序
    var FSHADER_SOURCE =
      '#ifdef GL_ES
    ' +
      'precision mediump float;
    ' +
      '#endif
    ' +
      'uniform sampler2D u_Sampler;
    ' +  //阴影贴图
      'uniform vec3 u_DiffuseLight;
    ' + // 漫反射光颜色
      'uniform vec3 u_LightDirection;
    ' + // 漫反射光的方向
      'uniform vec3 u_AmbientLight;
    ' + // 环境光颜色
      'varying vec4 v_Color;
    ' +
      'varying vec4 v_Normal;
    ' +
      'varying vec4 v_PositionFromLight;
    ' +
      'float unpackDepth(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));
    ' +
      '  float depth = dot(rgbaDepth, bitShift);
    ' + // Use dot() since the calculations is same
      '  return depth;
    ' +
      '}
    ' +
      'void main() {
    ' +
      //通过深度判断阴影
      '  vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;
    ' +
      '  vec4 rgbaDepth = texture2D(u_Sampler, shadowCoord.xy);
    ' +
      '  float depth = unpackDepth(rgbaDepth);
    ' + // 将阴影贴图的RGBA解码成浮点型的深度值
      '  float visibility = (shadowCoord.z > depth + 0.0015) ? 0.7 : 1.0;
    ' +
      //获得反射光
      '  vec3 normal = normalize(v_Normal.xyz);
    ' +
      '  float nDotL = max(dot(u_LightDirection, normal), 0.0);
    ' +  //计算光线向量与法向量的点积  
      '  vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;
    ' +  //计算漫发射光的颜色   
      '  vec3 ambient = u_AmbientLight * v_Color.rgb;
    ' +  //计算环境光的颜色
      //'  gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
    ' +
      '  gl_FragColor = vec4((diffuse+ambient) * visibility, v_Color.a);
    ' +
      '}
    ';
    

    这段着色器绘制代码在教程《WebGL简易教程(十):光照》绘制颜色和光照的基础之上加入可阴影的绘制。顶点着色器中新加入了一个uniform变量u_MvpMatrixFromLight,这是在帧缓存中绘制的从光源处观察的MVP矩阵,传入到顶点着色器中,计算顶点在光源处观察的位置v_PositionFromLight。

    v_PositionFromLight又传入到片元着色器,变为该片元在光源坐标系下的坐标。这个坐标每个分量都是-1到1之间的值,将其归一化到0到1之间,赋值给变量shadowCoord,其Z分量shadowCoord.z就是从光源处观察时的深度了。与此同时,片元着色器接受了从帧缓冲对象传入的渲染结果u_Sampler,里面保存着帧缓冲对象的深度纹理。从深度纹理从取出深度值为rgbaDepth,这是之前介绍过的编码值,通过相应的解码函数unpackDepth(),解码成真正的深度depth,也就是在光源处观察的片元的深度。比较该片元从光源处观察的深度shadowCoord.z与从光源处观察得到的同一片元位置的渲染深度depth,如果shadowCoord.z较大,就说明为阴影位置。

    注意这里比较时有个0.0015的容差,因为编码解码的操作仍然有精度的限制。

    2.2. 绘制部分

    2.2.1. 整体结构

    主要的绘制代码如下:

    //绘制
    function DrawDEM(gl, canvas, fbo, frameProgram, drawProgram, terrain) {
      // 设置顶点位置
      var demBufferObject = initVertexBuffersForDrawDEM(gl, terrain);
      if (!demBufferObject) {
        console.log('Failed to set the positions of the vertices');
        return;
      }
    
      //获取光线:平行光
      var lightDirection = getLight();
    
      //预先给着色器传递一些不变的量
      {
        //使用帧缓冲区着色器
        gl.useProgram(frameProgram);
        //设置在帧缓存中绘制的MVP矩阵
        var MvpMatrixFromLight = setFrameMVPMatrix(gl, terrain.sphere, lightDirection, frameProgram);
    
        //使用颜色缓冲区着色器
        gl.useProgram(drawProgram);
        //设置在颜色缓冲区中绘制时光线的MVP矩阵
        gl.uniformMatrix4fv(drawProgram.u_MvpMatrixFromLight, false, MvpMatrixFromLight.elements);
        //设置光线的强度和方向
        gl.uniform3f(drawProgram.u_DiffuseLight, 1.0, 1.0, 1.0);    //设置漫反射光
        gl.uniform3fv(drawProgram.u_LightDirection, lightDirection.elements);   // 设置光线方向(世界坐标系下的)  
        gl.uniform3f(drawProgram.u_AmbientLight, 0.2, 0.2, 0.2);    //设置环境光
        //将绘制在帧缓冲区的纹理传递给颜色缓冲区着色器的0号纹理单元
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, fbo.texture);
        gl.uniform1i(drawProgram.u_Sampler, 0);
    
        gl.useProgram(null);
      }
    
      //开始绘制
      var tick = function () {
        //帧缓存绘制
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); //将绘制目标切换为帧缓冲区对象FBO
        gl.viewport(0, 0, OFFSCREEN_WIDTH, OFFSCREEN_HEIGHT); // 为FBO设置一个视口
    
        gl.clearColor(0.2, 0.2, 0.4, 1.0); // Set clear color (the color is slightly changed)
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear FBO
        gl.useProgram(frameProgram); //准备生成纹理贴图
    
        //分配缓冲区对象并开启连接
        initAttributeVariable(gl, frameProgram.a_Position, demBufferObject.vertexBuffer); // 顶点坐标
        initAttributeVariable(gl, frameProgram.a_Color, demBufferObject.colorBuffer); // 颜色
    
        //分配索引并绘制
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer);
        gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0);
    
        //颜色缓存绘制
        gl.bindFramebuffer(gl.FRAMEBUFFER, null); //将绘制目标切换为颜色缓冲区
        gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口为当前画布的大小
    
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the color buffer
        gl.useProgram(drawProgram); // 准备进行绘制
    
        //设置MVP矩阵
        setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram);
    
        //分配缓冲区对象并开启连接
        initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinates
        initAttributeVariable(gl, drawProgram.a_Color, demBufferObject.colorBuffer); // Texture coordinates
        initAttributeVariable(gl, drawProgram.a_Normal, demBufferObject.normalBuffer); // Texture coordinates
    
        //分配索引并绘制
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, demBufferObject.indexBuffer);
        gl.drawElements(gl.TRIANGLES, demBufferObject.numIndices, demBufferObject.indexBuffer.type, 0);
    
        window.requestAnimationFrame(tick, canvas);
      };
      tick();
    }
    

    这段代码的总体结构与上一篇的代码相比并没有太多的变化,首先仍然是调用initVertexBuffersForDrawDEM()初始化顶点数组,只是根据需要调整了下顶点数据的内容。然后传递非公用随帧不变的数据,主要是帧缓存着色器中光源处观察的MVP矩阵,颜色缓存着色器中光照的强度,以及帧缓存对象中的纹理对象。最后进行逐帧绘制:将光源处观察的结果渲染到帧缓存;利用帧缓存的结果绘制带阴影的结果到颜色缓存。

    2.2.2. 具体改动

    利用帧缓存绘制阴影的关键就在于绘制了两遍地形,一个是关于当前视图观察下的绘制,另一个是在光源处观察的绘制,一定要确保两者的绘制都是正确的,注意两者绘制时的MVP矩阵。

    2.2.2.1. 获取平行光

    这个实例模拟的是在太阳光也就是平行光下产生的阴影,因此需要先获取平行光方向。这里描述的是太阳高度角30度,太阳方位角315度下的平行光方向:

    //获取光线
    function getLight() {
      // 设置光线方向(世界坐标系下的)
      var solarAltitude = 30.0;
      var solarAzimuth = 315.0;
      var fAltitude = solarAltitude * Math.PI / 180; //光源高度角
      var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角
    
      var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);
      var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);
      var arrayvectorZ = Math.sin(fAltitude);
    
      var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);
      lightDirection.normalize(); // Normalize  
      return lightDirection;
    }
    

    2.2.2.2. 设置帧缓存的MVP矩阵

    对于点光源光对物体产生阴影,就像在点光源处用透视投影观察物体一样;与此对应,平行光对物体产生阴影就需要使用正射投影。虽然平行光在设置MVP矩阵的时候没有具体的光源位置,但其实只要确定其中一条光线就可以了。在帧缓存中绘制的MVP矩阵如下:

    //设置MVP矩阵
    function setFrameMVPMatrix(gl, sphere, lightDirection, frameProgram) {
      //模型矩阵
      var modelMatrix = new Matrix4();
      //modelMatrix.scale(curScale, curScale, curScale);
      //modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis 
      //modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis 
      modelMatrix.translate(-sphere.centerX, -sphere.centerY, -sphere.centerZ);
    
      //视图矩阵  
      var viewMatrix = new Matrix4();
      var r = sphere.radius + 10;
      viewMatrix.lookAt(lightDirection.elements[0] * r, lightDirection.elements[1] * r, lightDirection.elements[2] * r, 0, 0, 0, 0, 1, 0);
      //viewMatrix.lookAt(0, 0, r, 0, 0, 0, 0, 1, 0);
    
      //投影矩阵
      var projMatrix = new Matrix4();
      var diameter = sphere.radius * 2.1;
      var ratioWH = OFFSCREEN_WIDTH / OFFSCREEN_HEIGHT;
      var nearHeight = diameter;
      var nearWidth = nearHeight * ratioWH;
      projMatrix.setOrtho(-nearWidth / 2, nearWidth / 2, -nearHeight / 2, nearHeight / 2, 1, 10000);
    
      //MVP矩阵
      var mvpMatrix = new Matrix4();
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
    
      //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
      gl.uniformMatrix4fv(frameProgram.u_MvpMatrix, false, mvpMatrix.elements);
    
      return mvpMatrix;
    }
    

    这个MVP矩阵通过地形的包围球来设置,确定一条对准包围球中心得平行光方向,设置正射投影即可。在教程《WebGL简易教程(十二):包围球与投影》中论述了这个问题。

    2.2.2.3. 设置颜色缓存的MVP矩阵

    设置实际绘制的MVP矩阵就恢复成使用透视投影了,与之前的设置是一样的,同样在教程《WebGL简易教程(十二):包围球与投影》中有论述:

    //设置MVP矩阵
    function setMVPMatrix(gl, canvas, sphere, lightDirection, drawProgram) {
      //模型矩阵
      var modelMatrix = new Matrix4();
      modelMatrix.scale(curScale, curScale, curScale);
      modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis 
      modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis 
      modelMatrix.translate(-sphere.centerX, -sphere.centerY, -sphere.centerZ);
    
      //投影矩阵
      var fovy = 60;
      var projMatrix = new Matrix4();
      projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000);
    
      //计算lookAt()函数初始视点的高度
      var angle = fovy / 2 * Math.PI / 180.0;
      var eyeHight = (sphere.radius * 2 * 1.1) / 2.0 / angle;
    
      //视图矩阵  
      var viewMatrix = new Matrix4(); // View matrix   
      viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);
    
      /*
      //视图矩阵  
      var viewMatrix = new Matrix4();
      var r = sphere.radius + 10;
      viewMatrix.lookAt(lightDirection.elements[0] * r, lightDirection.elements[1] * r, lightDirection.elements[2] * r, 0, 0, 0, 0, 1, 0);
    
      //投影矩阵
      var projMatrix = new Matrix4();
      var diameter = sphere.radius * 2.1;
      var ratioWH = canvas.width / canvas.height;
      var nearHeight = diameter;
      var nearWidth = nearHeight * ratioWH;
      projMatrix.setOrtho(-nearWidth / 2, nearWidth / 2, -nearHeight / 2, nearHeight / 2, 1, 10000);*/
    
      //MVP矩阵
      var mvpMatrix = new Matrix4();
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
    
      //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
      gl.uniformMatrix4fv(drawProgram.u_MvpMatrix, false, mvpMatrix.elements);
    }
    

    3. 结果

    最后在浏览器运行的结果如下所示,阴影存在于一些光照强度较暗的地方:

    image
    图3-1:地形的阴影

    通过ShadowMap生成阴影并不是要自己去实现阴影检查算法,更像是对图形变换、帧缓冲对象、着色器切换的基础知识的综合运用。

    4. 参考

    本文部分代码和插图来自《WebGL编程指南》,源代码链接:地址 。会在此共享目录中持续更新后续的内容。

  • 相关阅读:
    Hubble.net 值得纪念的一天
    Hubble.net 0.5 版本开发完成
    计算任意数值的阶乘
    Hubble.net 建表语句 Create Table
    Linq to SQL 插入数据时的一个问题
    盘古分词中文人名识别算法介绍
    盘古分词功能简介
    最长不完全匹配子串频率计算eaglet 的解法
    背包分组问题的解法
    SharePoint 2010与2007的区别 配置基于表单的身份认证
  • 原文地址:https://www.cnblogs.com/charlee44/p/12001631.html
Copyright © 2020-2023  润新知