欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

三维世界中的阴影--阴影图原理分析

最编程 2024-04-19 11:22:21
...

3D世界中的阴影有很多种实现方式,其中 ShadowMap 是比较常用的方案。 本文将介绍用 ShadowMap 生成阴影的原理、相关着色器代码以及阴影质量的逐步优化。

一. 阴影是如何产生的

image.png
在自然界中,一个不自发光的物体要被看见,是需要光源照射的。由于光是沿直线传播的,当光线被某些物体(图中橘色物体)遮挡后,那些本来有颜色的区域(点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. 阴影效果

image.png
可以看到,虽然已经产生了阴影,但是阴影的质量很差,我们一一做优化。

三. ShadowMap 的缺陷和优化

1. Self-Shadowing && Shadow Bias

image.png
图中这些条状阴影的情况,就是 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);
  }

image.png
此时已经去除了条状阴影,但是阴影偏离的太严重了。这种情况称为 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);
  }

image.png

3. 阴影边缘锯齿

image.png

(1) 提升分辨率

当深度纹理图太小(分辨率太低),会导致多个片元对应深度纹理中同一个像素的情况,从而引发锯齿。
以下是把纹理尺寸从 128 * 128 扩大成 1024 * 1024 的效果。

image.png

(2) Hard Shadow && PCF

在提升完深度纹理分辨率后,发现阴影仍存在锯齿。这种情况并不是阴影生成方式的问题,而是物体边缘本身就是有锯齿的。

image.png
我们既可以处理世界中各个物体的边缘锯齿,也可以采用一种更高效的方法,让阴影边缘本身变得平滑。
解决方案: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 效果对比:

image.png
Shadow Map 整体优化效果对比: image.png

四. 附录

  • 相关代码:github.com/Zack921/vis…

推荐阅读