3D世界中的阴影有很多种实现方式,其中 ShadowMap 是比较常用的方案。 本文将介绍用 ShadowMap 生成阴影的原理、相关着色器代码以及阴影质量的逐步优化。
一. 阴影是如何产生的
在自然界中,一个不自发光的物体要被看见,是需要光源照射的。由于光是沿直线传播的,当光线被某些物体(图中橘色物体)遮挡后,那些本来有颜色的区域(点C)因为没有照射而变回黑色,这些区域就是阴影。
二. 如何用 ShadowMap 生成阴影
1. ShadowMap 原理
理论上,在绘制点的颜色时,只要判断该点有没有被“遮挡”,就知道是否要绘制成阴影。 而判断“遮挡”的方案有很多,最常用的就是 ShadowMap。 我们只要知道该点与光源的连线上,有没有比它离光源更近的点存在。其中点与光源的距离,在 ShadowMap 中就是深度。具体的做法是:
- (1) 生成深度纹理图:所谓深度纹理图,就是每个位置的最小深度。我们站在光源的位置,按照光线传播的视角,观察场景,计算场景中的物体距离光源的距离(也就是该视角下的深度),并记录各个位置上的最小值,从而获得一张深度纹理。
- (2) 使用深度纹理图:对于世界中的某个点 p,我们要先得到它在光源视角下的深度,再和深度纹理图中对应的深度进行比较,就可以判定它是否在阴影中了。
2. 着色器代码
(1) 生成深度纹理图
顶点着色器代码:
attribute vec4 a_Position;
uniform mat4 u_MvpMatrix; // 以光源为观察点的投影矩阵
void main() {
gl_Position = u_MvpMatrix * a_Position;
}
片元着色器代码:
precision mediump float; // 指定精度
void main() {
gl_FragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 0.0); // 将片元的深度值写入r值
}
(2) 使用深度纹理图
顶点着色器代码:
attribute vec4 a_Position;
attribute vec4 a_Color; // 物体被照射后显示的颜色
uniform mat4 u_MvpMatrix; // 以人为观察点的投影矩阵
uniform mat4 u_MvpMatrixFromLight; // 以光源为观察点的投影矩阵
varying vec4 v_PositionFromLight;
varying vec4 v_Color;
void main() {
gl_Position = u_MvpMatrix * a_Position;
v_PositionFromLight = u_MvpMatrixFromLight * a_Position; // 以光源为观察点的坐标
v_Color = a_Color;
}
片元着色器代码
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; // mvp矩阵处理完的坐标还会被自动转化成裁剪空间的坐标,范围在[0,1]区间,所以这里也要做归一化
vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy); // 拿到深度纹理中对应坐标存储的数据
float depth = rgbaDepth.r; // 拿到深度纹理中对应坐标存储的深度
float visibility = (shadowCoord.z > depth) ? 0.7 : 1.0; // 判断片元是否在阴影中
gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
}
3. 阴影效果
可以看到,虽然已经产生了阴影,但是阴影的质量很差,我们一一做优化。
三. ShadowMap 的缺陷和优化
1. Self-Shadowing && Shadow Bias
图中这些条状阴影的情况,就是 Self-Shadowing。因为我们需要把物体在光源视角下的深度作归一化和存储,所以必然会导致精度丢失,而精度丢失会导致深度误差。
比如空间中有一点 p,它在光源视角下的实际深度是 0.70001,也是光源视角下的最小深度,那么理论上不会被遮挡,应该显示白色。但我们是需要事先存储光源视角下的最小深度的,此时因为精度丢失,导致0.70001 -> 0.7000,那么在绘制点 p时,判断实际深度 0.70001 > 存储的最小深度 0.7000,表示被遮挡了,误绘成了黑色。当物体表面在灯光视图空间中的倾斜度越大时,误差也越大。
解决方案: Shadow Bias - 在实际绘制时,给从深度纹理拿到的存储深度加上一个阈值。
float visibility = (shadowCoord.z > depth + 0.15) ? 0.7 : 1.0; // 判断片元是否在阴影前加上一个阈值
gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
}
此时已经去除了条状阴影,但是阴影偏离的太严重了。这种情况称为 Peter Panning
2. Peter Panning
Peter Panning 的产生是因为我们的 Shadow Bias 加的太多,导致与它实际深度差别太大。
解决方案: 控制阈值大小。
float visibility = (shadowCoord.z > depth + 0.01) ? 0.7 : 1.0; // 控制阈值大小:0.15 -> 0.01
gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
}
3. 阴影边缘锯齿
(1) 提升分辨率
当深度纹理图太小(分辨率太低),会导致多个片元对应深度纹理中同一个像素的情况,从而引发锯齿。
以下是把纹理尺寸从 128 * 128 扩大成 1024 * 1024 的效果。
(2) Hard Shadow && PCF
在提升完深度纹理分辨率后,发现阴影仍存在锯齿。这种情况并不是阴影生成方式的问题,而是物体边缘本身就是有锯齿的。
我们既可以处理世界中各个物体的边缘锯齿,也可以采用一种更高效的方法,让阴影边缘本身变得平滑。
解决方案:PCF - Percentage Closer Filtering
PCF的核心思路是,不直接取当前点的阴影,而是通过周围的点加权平均得到。
具体的做法是对每个片元从 Shadow Map 中采样相邻的多个值,然后对每个值都进行深度比较。如果该片元处于阴影区就把比较结果记为0,否则记为1,最后把比较结果全部加起来除以采样点的个数就可以得到一个百分比p,表示其处在阴影区的可能性。若p为0代表该像素完全处于阴影区,若p为1表示完全不处于阴影区,最后根据p值设定混合系数即可。
着色器代码如下:其中样本的个数越多,平滑效果越好
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; // 归一化到[0,1]的纹理区间
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 > rgbaDepth.r + 0.01) ? 1.0 : 0.0;
}
}
shadows /= 16.0; // 4*4的样本
float visibility = min(opacity + (1.0 - shadows), 1.0);
gl_FragColor = vec4(v_Color.rgb * visibility, v_Color.a);
}
PCF 效果对比:
Shadow Map 整体优化效果对比:
四. 附录
- 相关代码:github.com/Zack921/vis…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!