文章目录

  • 1. 概述
  • 2. 示例
    • 2.1. 着色器部分
      • 2.1.1. 帧缓存着色器
      • 2.1.2. 颜色缓存着色器
    • 2.2. 绘制部分
      • 2.2.1. 整体结构
      • 2.2.2. 具体改动
        • 2.2.2.1. 获取平行光
        • 2.2.2.2. 设置帧缓存的MVP矩阵
        • 2.2.2.3. 设置颜色缓存的MVP矩阵
  • 3. 结果
  • 4. 参考

1. 概述

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

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

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

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

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

2. 示例

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

2.1. 着色器部分

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

2.1.1. 帧缓存着色器

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

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

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

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

2.1.2. 颜色缓存着色器

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

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

这段着色器绘制代码在教程《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); //将绘制目标切换为帧缓冲区对象FBOgl.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 FBOgl.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 buffergl.useProgram(drawProgram); // 准备进行绘制//设置MVP矩阵setMVPMatrix(gl, canvas, terrain.sphere, lightDirection, drawProgram);//分配缓冲区对象并开启连接initAttributeVariable(gl, drawProgram.a_Position, demBufferObject.vertexBuffer); // Vertex coordinatesinitAttributeVariable(gl, drawProgram.a_Color, demBufferObject.colorBuffer); // Texture coordinatesinitAttributeVariable(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_MvpMatrixgl.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_MvpMatrixgl.uniformMatrix4fv(drawProgram.u_MvpMatrix, false, mvpMatrix.elements);
}

3. 结果

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

图3-1:地形的阴影

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

4. 参考

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

WebGL简易教程(十四):阴影相关推荐

  1. WebGL简易教程(十):光照

    目录 1. 概述 2. 原理 2.1. 光源类型 2.2. 反射类型 2.2.1. 环境反射(enviroment/ambient reflection) 2.2.2. 漫反射(diffuse ref ...

  2. WebGL简易教程(十五):加载gltf模型

    文章目录 1. 概述 2. 实例 2.1. 数据 2.2. 程序 2.2.1. 文件读取 2.2.2. glTF格式解析 2.2.2.1. 场景节点 2.2.2.2. 网格 2.2.2.3. 缓冲,缓 ...

  3. WebGL简易教程——目录

    文章目录 1. 绪论 2. 目录 3. 资源 1. 绪论 最近研究WebGL,看了<WebGL编程指南>这本书,结合自己的专业知识写的一系列教程.之前在看OpenGL/WebGL的时候总是 ...

  4. Cesium教程(十四):简易三维模型的可视化

    Cesium教程(十四):简易三维模型的可视化 效果预览 1.高效三维数据格式:3D Tiles 3D Tiles是Cesium提出的处理三维地理大数据的数据格式,目前已是OGC数据标准之一,并在We ...

  5. 【Visual C++】游戏开发笔记四十六 浅墨DirectX教程十四 模板测试与镜面特效专场

    本系列文章由zhmxy555(毛星云)编写,转载请注明出处.   文章链接: http://blog.csdn.net/zhmxy555/article/details/8632184 作者:毛星云( ...

  6. Wix 安装部署教程(十四) -- 多语言安装包之用户许可协议

    Wix 安装部署教程(十四) -- 多语言安装包之用户许可协议 原文:Wix 安装部署教程(十四) -- 多语言安装包之用户许可协议 在上一篇中,留下了许可协议的问题,目前已经解决.感谢网友武全的指点 ...

  7. WebGL简易教程(十一):纹理

    文章目录 1. 概述 2. 实例 2.1. 准备纹理 2.2. 配置纹理 2.3. 使用纹理 3. 结果 4. 参考 1. 概述 在之前的之前的教程<WebGL简易教程(九):综合实例:地形的绘 ...

  8. akka学习教程(十四) akka分布式实战

    akka系列文章目录 akka学习教程(十四) akka分布式实战 akka学习教程(十三) akka分布式 akka学习教程(十二) Spring与Akka的集成 akka学习教程(十一) akka ...

  9. Ocelot简易教程(四)之请求聚合以及服务发现

    上篇文章给大家讲解了Ocelot的一些特性并对路由进行了详细的介绍,今天呢就大家一起来学习下Ocelot的请求聚合以及服务发现功能.希望能对大家有所帮助. 作者:依乐祝 原文地址:https://ww ...

最新文章

  1. 【深度学习的数学】为什么用二次代价函数,误差越大,参数调整幅度越小?
  2. 计算机应用基础考证理论知识,《计算机应用基础》考试大纲
  3. DISCUZ中判断当前页是否是门户首页
  4. 齐博php百度编辑器上传图片_php版百度编辑器ueditor怎样给上传图片自动添加水印?...
  5. Redis学习一Redis的介绍与安装部署
  6. 百度SEO站群织梦响应式博客资讯网模板
  7. 并行程序设计模式--Master-Worker模式
  8. css sprites css精灵
  9. loadrunner error 27796 Failed to connect to server
  10. html如何让英文自动换行,HTML如何让英文自动换行不断词
  11. linux内核的reciprocal_value结构体
  12. java二级大题分值_2017年9月全国计算机等级考试各级别题型分值
  13. INFOR ERP LN 创建表
  14. 去掉whatsns问答系统页面底部隐藏的官网链接
  15. hdu 5296 Annoying problem(LCA)
  16. 前端技术搭建飞机大战小游戏(内含源码)
  17. 电商宝打单发货-菜鸟面单回收
  18. 呼吸机氧电池的工作原理及性能检测
  19. Homekit智能家居DIY一WIFI智能插座
  20. 共模(Common Mode)差模(Differential Mode)共模抑制(Common Mode Rejection)

热门文章

  1. 二进制开关理解和实现
  2. idea提示非法字符
  3. 什么是shell?linux常用的shell有哪些
  4. 阿里云TSDB时空数据库实战(一):数据入库与导出
  5. 【JSP课程设计】实现动态网页及后台搭建--食味坊
  6. Windows XP 激活之修改注册表
  7. 魔兽地图编辑器插件YDWE的使用与基本设置5 触发编辑器4 技能特效
  8. 山寨版tftp客户端
  9. 抖音参与福袋显示服务器异常,抖音dou福袋怎么提现 抖音dou福袋提现方法
  10. 三星超级本530u3c重装系统。。