WebGL光照阴影映射
原文地址:WebGL光照阴影映射
经过前面的学习,webgl的基本功能都已经掌握了,我们不仅掌握了着色器的编写,图形的绘制,矩阵的变换,添加光照,还通过对webgl的基础api封装,编写出了便利的工具库. 是时候进一步深入学习webgl的高级功能了,我认为要做逼真的3D特效,阴影绝对是一个必不可少的环节.现在我们就在之前光照的基础上添加阴影效果吧.
首先看一下阴影效果的实例:
阴影综合(多物体高精度PCF)
点光源聚光灯阴影
内容大纲
我们以阴影综合(多物体高精度PCF)为例, 开始学习阴影相关知识.
- 帧缓冲
- 阴影映射(shadow mapping)
- 提高阴影精度
- 抗锯齿(PCF)
帧缓冲
我们实现阴影效果使用的是叫阴影映射的技术, 而实现阴影映射需要用到帧缓冲区。默认情况下,WebGL 在颜色缓冲区绘图,使用隐藏面消除的话,还会用到深度缓冲区。即正常绘制的情况下包含:
- 颜色缓冲区
- 深度缓冲区
帧缓冲区对象 framebuffer object可以用来代替颜色缓冲区或深度缓冲区。绘制在帧缓冲区中的对象并不会直接显示canvas上,可以先对帧缓冲区中的内容进行一些处理再显示,或者直接用其中的内容作为纹理图像。在帧缓冲区中进行绘制的过程又称为离屏绘制 offscreen drawing。
绘制操作并不是直接发生在帧缓冲区中,而是发生在帧缓冲区所关联的对象 attachment上,一个帧缓冲区有3个关联对象:
- 颜色关联对象 color attachment,对应颜色缓冲区
- 深度关联对象 depth attachment,对应深度缓冲区
- 模板关联对象 stencil attachment,对应模板缓冲区。
而我们现在先有这个概念,来看看帧缓冲区的创建和配置:
- 创建帧缓冲区对象 gl.createFramebffer().
- 创建文理对象并设置其尺寸和参数 gl.createTexture()、gl.bindTexture()、gl.texImage2D()、gl.Parameteri().
- 创建渲染缓冲区对象 gl.createRenderbuffer().
- 绑定渲染缓冲区对象并设置其尺寸 gl.bindRenderBuffer()、gl.renderbufferStorage().
- 将帧缓冲区的颜色关联对象指定为一个文理对象 gl.frambufferTexture2D().
- 将帧缓冲区的深度关联对象指定为一个渲染缓冲区对象 gl.framebufferRenderbuffer().
- 检查帧缓冲区是否正确配置 gl.checkFramebufferStatus().
- 在帧缓冲区中进行绘制 gl.bindFramebuffer().
它的创建和配置是一个非常繁琐的过程,我们先熟悉了怎么使用,再慢慢研究它内部的原理,所以先把上面的步骤封装成一个黑盒子,我这里就是createFramebuffer这个函数.
阴影映射(shadow mapping)
阴影映射的原理很简单,首先从光的角度渲染场景,从光的角度看到的所有东西都被点亮了,而看不见的部分一定是在阴影里.。想象有一个盒子和它的光源照射下的地板,由于光源会看到这个盒子而它后面的地板部分是看不到的.那么当视线角度变化的时候,从光源角度照不到的那部分地板就渲染为阴影,原理如下图
接着我们使用阴影映射的算法实现, 它要使用到前面介绍的帧缓冲区. 阴影映射要渲染两遍:
- 从光源的角度渲染场景,同时把场景的深度值当成纹理渲染到帧缓冲区,也就是把它当作数据容器.
- 从眼睛的角度渲染场景,把物体真正渲染到画布中,同时对比纹理的深度值,将阴影部分也渲染出来.
左边的图像是第一遍渲染的原理, 一个方向光源(所有的光线都是平行的)在立方体下面的表面投下阴影.我们通过用光源的视图投影矩阵渲染场景(从光线的角度)来创建景深图然后把它存储到帧缓冲区中.
右边的图形是第二遍渲染的原理, 从眼睛的视图投影矩阵渲染场景(从眼睛的角度), 光源角度下的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光照阴影映射的更多相关文章
- DirectX11 With Windows SDK--31 阴影映射
前言 阴影既暗示着光源相对于观察者的位置关系,也从侧面传达了场景中各物体之间的相对位置.本章将起底最基础的阴影映射算法,而像复杂如级联阴影映射这样的技术,也是在阴影映射的基础上发展而来的. 学习目标: ...
- DirectX11 With Windows SDK--38 级联阴影映射(CSM)
前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...
- webGL 光照
1.着色(shading) 在三维图形学术语“着色”的真正含义就是,根据光照条件重建“物体各表面明暗不一的效果”的过程.明白着色过程,需要考虑两件事: 1.发出光线的光源类型. 2.物体表 ...
- 推荐一款基于XNA的开源游戏引擎《Engine Nine》
一.前沿导读 XNA是微软基于.Net部署的下一代3D/2D游戏开发框架,其实XNA严格来说类似下一代的DirectX,当然不是说XNA会取代DirectX,但是基于XNA我们对于面向XBOX360, ...
- WebGL学习笔记(九):阴影
3D中实现实时阴影技术中比较常见的方式是阴影映射(Shadow Mapping),我们这里也以这种技术来实现实时阴影. 阴影映射背后的思路非常简单:我们先以光的位置为视角进行渲染,我们能看到的东西都将 ...
- WebGL 与 WebGPU比对[5] - 渲染计算的过程
目录 1. WebGL 1.1. 使用 WebGLProgram 表示一个计算过程 1.2. WebGL 没有通道 API 2. WebGPU 2.1. 使用 Pipeline 组装管线中各个阶段 2 ...
- WebGL 高级技术
1.如何实现雾化 实现雾化的方式由多种,这里使用最简单的一种:线性雾化(linear fog).在线性雾化中,某一点的雾化程度取决于它与视点之间的距离,距离越远雾化程度越高.线性雾化有起点和终点,起点 ...
- 游戏里的动态阴影-ShadowMap实现原理
ShadowMap是比较流行的实时阴影实现方案,原理比较简单,但真正实现起来还是会遇到很多问题的,我这里主要记录下实现方式 先看效果 凹凸地形上也有阴影 实现原理 ShadowMap技术是从灯光空间用 ...
- Unity shader学习之阴影
Unity阴影采用的是 shadow map 的技术,即把摄像机放到光源位置上,看不到的地方就有阴影. 前向渲染中,若一光源开启了阴影,Unity会计算它的阴影映射纹理(shadow map),它其实 ...
随机推荐
- MyBatis学习总结(一)简单入门案例
MyBatis学习总结(一)简单入门案例 主要内容:本文主要通过对数据库中的use表进行增删改查总结mybatis的环境搭建和基本入门使用 一.需要的jar包: 1.核心包 2.依赖包 3.jdbc数 ...
- CSS3渐变相关
背景渐变 background: -moz-linear-gradient( top,#f24652,#da2c3c); background: -o-linear-gradient(top,#f24 ...
- 【ANT】taskdef class org.programmerplanet.ant.taskdefs.jmeter.JMeterTask cannot be found using the classloader AntClassLoader[]解决办法
把文件apache-jmeter-3.1\extras\ant-jmeter-1.1.1.jar复制到apache-ant-1.10.1\lib目录下即可.
- 小白的Python之路 day4 迭代器
迭代器 学习前,我们回想一下可以直接作用于for循环的数据类型有以下几种: 1.集合数据类型,如list.tuple.dict.set.str等: 2.是generator,包括生成器和带yield的 ...
- Spark源码剖析(五):Master原理与源码剖析(下)
一. 状态改变机制源码分析 在剖析Master核心的资源调度算法之前,让我们先来看看Master的状态改变机制. Driver状态改变 可以看出,一旦Driver状态发生改变,基本没有好事情,后果要 ...
- 我是如何确认线上CLOSE_WAIT产生的原因及如何解决的。
1.阐述 内部架构:Tomcat应用程序---> nginx ---> 其他Tomcat应用程序,内部Tomcat应用通过nginx调用其他应用. HTTP插件:HttpClient 4. ...
- ELK日志检索并邮件微信通知
简介 脚本为通过api检索日志内容,并通过邮件或者微信发送出来. 脚本 index检索脚本 #!/usr/bin/env python # coding:utf-8 from elasticsearc ...
- Android测试:Building Local Unit Tests
原文:https://developer.android.com/training/testing/unit-testing/local-unit-tests.html 如果你的单元测试没有依赖或者只 ...
- python学习中的一些“坑”
一.交互列表元素时,需要注意的坑. 例如: array=[4,5,9,8,10,8,4,0,3,4] 最大的值与第一个元素交换,最小的值与最后一个元素交换 # -*- coding: UTF-8 - ...
- Android java.io.FileNotFoundException:*** (Permission denied)
自从离开上海来到杭州之后,陆续接触了Android,Python,C++,OpenCV,最为一个新人来说,一路上遇到了很多坑,幸运的是 这互联网的强大(大傻子我还是蛮喜欢的哈),一路填填补补总算走了下 ...