原文地址:WebGL光照阴影映射

经过前面的学习,webgl的基本功能都已经掌握了,我们不仅掌握了着色器的编写,图形的绘制,矩阵的变换,添加光照,还通过对webgl的基础api封装,编写出了便利的工具库. 是时候进一步深入学习webgl的高级功能了,我认为要做逼真的3D特效,阴影绝对是一个必不可少的环节.现在我们就在之前光照的基础上添加阴影效果吧.

首先看一下阴影效果的实例:

阴影综合(多物体高精度PCF)

点光源聚光灯阴影

内容大纲

我们以阴影综合(多物体高精度PCF)为例, 开始学习阴影相关知识.

  1. 帧缓冲
  2. 阴影映射(shadow mapping)
  3. 提高阴影精度
  4. 抗锯齿(PCF)

帧缓冲

我们实现阴影效果使用的是叫阴影映射的技术, 而实现阴影映射需要用到帧缓冲区。默认情况下,WebGL 在颜色缓冲区绘图,使用隐藏面消除的话,还会用到深度缓冲区。即正常绘制的情况下包含:

  • 颜色缓冲区
  • 深度缓冲区

帧缓冲区对象 framebuffer object可以用来代替颜色缓冲区或深度缓冲区。绘制在帧缓冲区中的对象并不会直接显示canvas上,可以先对帧缓冲区中的内容进行一些处理再显示,或者直接用其中的内容作为纹理图像。在帧缓冲区中进行绘制的过程又称为离屏绘制 offscreen drawing。

绘制操作并不是直接发生在帧缓冲区中,而是发生在帧缓冲区所关联的对象 attachment上,一个帧缓冲区有3个关联对象:

  • 颜色关联对象 color attachment,对应颜色缓冲区
  • 深度关联对象 depth attachment,对应深度缓冲区
  • 模板关联对象 stencil attachment,对应模板缓冲区。

而我们现在先有这个概念,来看看帧缓冲区的创建和配置:

  1. 创建帧缓冲区对象 gl.createFramebffer().
  2. 创建文理对象并设置其尺寸和参数 gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri().
  3. 创建渲染缓冲区对象 gl.createRenderbuffer().
  4. 绑定渲染缓冲区对象并设置其尺寸 gl.bindRenderBuffer()、gl.renderbufferStorage().
  5. 将帧缓冲区的颜色关联对象指定为一个文理对象 gl.frambufferTexture2D().
  6. 将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象 gl.framebufferRenderbuffer().
  7. 检查帧缓冲区是否正确配置 gl.checkFramebufferStatus().
  8. 在帧缓冲区中进行绘制 gl.bindFramebuffer().

它的创建和配置是一个非常繁琐的过程,我们先熟悉了怎么使用,再慢慢研究它内部的原理,所以先把上面的步骤封装成一个黑盒子,我这里就是createFramebuffer这个函数.

阴影映射(shadow mapping)

阴影映射的原理很简单,首先从光的角度渲染场景,从光的角度看到的所有东西都被点亮了,而看不见的部分一定是在阴影里.。想象有一个盒子和它的光源照射下的地板,由于光源会看到这个盒子而它后面的地板部分是看不到的.那么当视线角度变化的时候,从光源角度照不到的那部分地板就渲染为阴影,原理如下图

接着我们使用阴影映射的算法实现, 它要使用到前面介绍的帧缓冲区. 阴影映射要渲染两遍:

  1. 从光源的角度渲染场景,同时把场景的深度值当成纹理渲染到帧缓冲区,也就是把它当作数据容器.
  2. 从眼睛的角度渲染场景,把物体真正渲染到画布中,同时对比纹理的深度值,将阴影部分也渲染出来.

左边的图像是第一遍渲染的原理, 一个方向光源(所有的光线都是平行的)在立方体下面的表面投下阴影.我们通过用光源的视图投影矩阵渲染场景(从光线的角度)来创建景深图然后把它存储到帧缓冲区中.

右边的图形是第二遍渲染的原理, 从眼睛的视图投影矩阵渲染场景(从眼睛的角度), 光源角度下的xy坐标相同的c点和p点,p深度值比c要大, 那么它一定处于阴影当中,那么p点就渲染为阴影.



来看实现以上功能的着色器代码,因为要渲染两遍,所以也就要建立两对的着色器(顶点/片段),顶点着色器比较简单,基本不涉及阴影映射,在此省略:

阴影片段着色器

#ifdef GL_ES
precision mediump float;
#endif
void main() {
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0); //将深度值z存放到第一个分量r中
}

正常片段着色器

深度值后面加了0.005,稍微大于1/256,即8位的表示范围(因为一个分量就是8位),这个是消除马赫带用的,不加这个值,画面会产生难看的条纹,具体的原理可查找马赫带,在此不细讲.

precision mediump float;
uniform sampler2D u_ShadowMap;
varying vec4 v_PositionFromLight;
varying vec4 v_color;
void main() {
// 获取纹理的坐标
vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0 + 0.5;
// 根据阴影xy坐标,获取纹理中对应的点,z值已经被之前的阴影着色器存放在该点的r分量中了,直接使用即可
vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);// 获取指定纹理坐标处的像素颜色rgba
float visibility = (shadowCoord.z > rgbaDepth.r + 0.005) ? 0.6 : 1.0;//大于阴影的z轴,说明在阴影中并显示为阴影*0.6,否则为正常颜色*1.0
gl_FragColor = vec4(v_color.rgb * visibility, v_color.a);
}

提高精度

完成了最简单的阴影效果,但是当你把光源与物体的距离拉远,问题出来了,怎么看不到阴影了?这是距离超过了8位的存储范围,溢出的缘故.之前我们只使用了一个分量来存储,现在我们把其他的分量也利用起来吧,rgba一共32位.

阴影片段着色器

这中间进行复杂的分解运算,并同时去除异常值,请看如下代码

/**
* 分解保存深度值
*/
vec4 pack (float depth) {
// 使用rgba 4字节共32位来存储z值,1个字节精度为1/256
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);
// gl_FragCoord:片元的坐标,fract():返回数值的小数部分
vec4 rgbaDepth = fract(depth * bitShift); //计算每个点的z值
rgbaDepth -= rgbaDepth.gbaa * bitMask; // Cut off the value which do not fit in 8 bits
return rgbaDepth;
}
void main() {
gl_FragColor = pack(gl_FragCoord.z);// 将z值分开存储到rgba分量中,阴影颜色的同时也是深度值z
}

正常片段着色器

这里对应就要解码出深度值

/**
* 释出深度值z
*/
float unpack(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));
return dot(rgbaDepth, bitShift);
}

抗锯齿(PCF)

解决了精度的问题,接着继续优化. 运行起来吧,阴影很粗糙有木有? 你看看下面左图,很严重的锯齿, 抗锯齿有很多种解决方案,我这里使用PCF, 也就是百分比渐近式过滤算法,因为它基于代码实现的,所以也叫软阴影.

PCF的原理也很简单, 采集当前点周围像素的阴影值,并将其深度与所有采集的样本进行比较,最后对结果进行平均,这样就得到光线和阴影之间更平滑的过渡效果.下面右图是经过PCF处理之后的阴影,效果要自然得多了.



我们看正常着色器的实现代码

vec3 shadowCoord = (v_positionFromLight.xyz/v_positionFromLight.w)/2.0 + 0.5;
float shadows =0.0;
float opacity=0.6;// 阴影alpha值, 值越小暗度越深
float texelSize=1.0/1024.0;// 阴影像素尺寸,值越小阴影越逼真
vec4 rgbaDepth;
// 消除阴影边缘的锯齿
for(float y=-1.5; y <= 1.5; y += 1.0){
for(float x=-1.5; x <=1.5; x += 1.0){
rgbaDepth = texture2D(u_shadowMap, shadowCoord.xy+vec2(x,y)*texelSize);
shadows += shadowCoord.z-bias > unpack(rgbaDepth) ? 1.0 : 0.0;
}
}
shadows/=16.0;// 4*4的样本
float visibility=min(opacity+(1.0-shadows),1.0);
specular=visibility < 1.0 ? vec3(0.0,0.0,0.0): specular;// 阴影处没有高光
gl_FragColor = vec4((diffuse + ambient + specular) * visibility, v_color.a);

总结

WebGL的阴影部分,涉及到了很多opengGL的底层,计算机图形学算法. 为了深入理解它,可真是花费了很多脑力,是到目前为止学习webgl的第一道坎,它里面的水很深.比如光是反锯齿部分就涉及到很多低层细节,算法的实现,显卡的性能问题等都是需要考虑的, 阴影部分后续还要慢慢查资料继续优化.

越是深入学习WebGL,就越觉得它相关的资料真是少,必须看openGL ES相关的东西才能解决,伤不起啊.

WebGL光照阴影映射的更多相关文章

  1. DirectX11 With Windows SDK--31 阴影映射

    前言 阴影既暗示着光源相对于观察者的位置关系,也从侧面传达了场景中各物体之间的相对位置.本章将起底最基础的阴影映射算法,而像复杂如级联阴影映射这样的技术,也是在阴影映射的基础上发展而来的. 学习目标: ...

  2. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

  3. webGL 光照

    1.着色(shading) 在三维图形学术语“着色”的真正含义就是,根据光照条件重建“物体各表面明暗不一的效果”的过程.明白着色过程,需要考虑两件事:    1.发出光线的光源类型.    2.物体表 ...

  4. 推荐一款基于XNA的开源游戏引擎《Engine Nine》

    一.前沿导读 XNA是微软基于.Net部署的下一代3D/2D游戏开发框架,其实XNA严格来说类似下一代的DirectX,当然不是说XNA会取代DirectX,但是基于XNA我们对于面向XBOX360, ...

  5. WebGL学习笔记(九):阴影

    3D中实现实时阴影技术中比较常见的方式是阴影映射(Shadow Mapping),我们这里也以这种技术来实现实时阴影. 阴影映射背后的思路非常简单:我们先以光的位置为视角进行渲染,我们能看到的东西都将 ...

  6. WebGL 与 WebGPU比对[5] - 渲染计算的过程

    目录 1. WebGL 1.1. 使用 WebGLProgram 表示一个计算过程 1.2. WebGL 没有通道 API 2. WebGPU 2.1. 使用 Pipeline 组装管线中各个阶段 2 ...

  7. WebGL 高级技术

    1.如何实现雾化 实现雾化的方式由多种,这里使用最简单的一种:线性雾化(linear fog).在线性雾化中,某一点的雾化程度取决于它与视点之间的距离,距离越远雾化程度越高.线性雾化有起点和终点,起点 ...

  8. 游戏里的动态阴影-ShadowMap实现原理

    ShadowMap是比较流行的实时阴影实现方案,原理比较简单,但真正实现起来还是会遇到很多问题的,我这里主要记录下实现方式 先看效果 凹凸地形上也有阴影 实现原理 ShadowMap技术是从灯光空间用 ...

  9. Unity shader学习之阴影

    Unity阴影采用的是 shadow map 的技术,即把摄像机放到光源位置上,看不到的地方就有阴影. 前向渲染中,若一光源开启了阴影,Unity会计算它的阴影映射纹理(shadow map),它其实 ...

随机推荐

  1. Java Web Session设置

    一.前言 在做 java web项目时,我们很多时候都要用到 Session,那么我就简单的写一下 Session 的写法. 二.代码实现 Servlet Session 的设置 package co ...

  2. spring boot https --restful接口篇

    我们写的接口默认都是http形式的,不过我们的接口很容易被人抓包,而且一抓全是明文的挺尴尬的 spring boot配置https生成证书大的方向有3种: 1.利用keytool自己生成证书 2.从免 ...

  3. AI 系列 总目录

    AI 系列 答应了园区大牛 张善友 要写AI 的系列博客,所以开始了AI 系列之旅. 一.四大平台系列(百度AI.阿里ET.腾讯.讯飞) 1.百度篇 (1) 百度OCR文字识别-身份证识别 (2) 基 ...

  4. 58、js扩展

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理. 一.js的作用域 任何程序设计语言都有作用域的概念,简单的说,作用 ...

  5. The Movie db (TMDB)的API申请

    在共享API TMDB中申请时,一只报错Application summary please elaborate on how you plan to use our API,我是用汉字描述的,开始以 ...

  6. iOS 电脑新装的系统, 使用sourceTree 创建本地仓库的时候, 总是提示, 无效路径

    把qq聊天记录分享出来: 我电脑新装的系统, 使用sourceTree 创建本地仓库的时候, 总是提示, 无效路径请问哪位遇到过求指教群里有产品经理没有? ssh 配制的不对重装系统过后,重新生成一下 ...

  7. iOS 多线程 简单学习NSThread NSOperation GCD

    1:首先简单介绍什么叫线程 可并发执行的,拥有最小系统资源,共享进程资源的基本调度单位. 共用堆,自有栈(官方资料说明iOS主线程栈大小为1M,其它线程为512K). 并发执行进度不可控,对非原子操作 ...

  8. ArcGIS API for JavaScript 4.2学习笔记[28] 可视域分析【使用Geoprocessor类】

    想知道可视域分析是什么,就得知道可视域是什么 我们站在某个地方,原地不动转一圈能看到的所有事物就叫可视域.当然平地就没什么所谓的可视域. 如果在山区呢?可视范围就会被山体挡住了.这个分析对军事上有十分 ...

  9. Pashmak and Flowers

    Pashmak decided to give Parmida a pair of flowers from the garden. There are nflowers in the garden ...

  10. canvas学习api

    1.canvas.getContext():获取渲染上下文和绘画功能: 一.绘制矩形 2.ctx.fillRect(x,y,width,height):绘制矩形: 3.ctx.strokeRect(x ...