阴影
阴影贴图
实现阴影的方法有很多,本节介绍的方法叫做 阴影贴图,
阴影贴图(shadow map) 也称 深度贴图(depth map)
如何实现阴影
使用两对着色器以实现阴影,
设第一对着色器为S1,第二对着色器为S2,
光源为O,物体上存在一点P1,P1在阴影上的位置时P2
- S1计算出OP1
- 使用一张纹理图像将S1的计算结果传入S2中
- 这张纹理图像就是 阴影贴图(shadow map)
- 即将视点移到光源位置处
- 将每个像素最前面的z值写入到阴影贴图中
- 如p1的z值
- 将每个像素最前面的z值写入到阴影贴图中
- 将视点移回原来的位置
- 计算出每个片元在光源坐标系下的坐标,与阴影贴图中记录的z值比较
- 如p2的z值与p1的z值
- 如果前者大于后者,就说明当前片元处在阴影之中,用较深颜色绘制
- p2.z > p1.z ,因此p2处于阴影之中
- 计算出每个片元在光源坐标系下的坐标,与阴影贴图中记录的z值比较
Shadow
1 | // 阴影vertex shader |
1 | // 帧缓冲区渲染尺寸 |
1 | let g_modelMatrix = new Matrix4() |
1 | // 写入attribute数据 |
1 | // 创建帧缓冲区对象 |
步骤分解
步骤1:获取阴影贴图
- 绘制目标: 帧缓冲区对象
- 绘制视点: 光源
- 绘制着色器:
- 顶点着色器:负责将顶点坐标切换到光源坐标系下
- 片元着色器:将片元的z值写入纹理贴图中
1 | gl_FragColor = vec4(gl_FragCoord.z. 0.0, 0.0, 0.0); |
实际上存入阴影贴图的值也就是每个片元的 gl_FragCoord.z 的值
gl_FragCoord
gl_FragCoord 是 vec4 类型的
- gl_FragCoord.xy:片元在屏幕上的坐标
- gl_FragCoord.z: 片元的深度值
gl_FragCoord是如何被计算出来的?
答: gl_FragCoord是由 gl_Position归一化 得到的。
从 [-1.0, 1.0] 归一化到 [0.0, 1.0]
- gl_FragCoord.z = 0.0
- 表示片元在 近裁剪面 上
- gl_FragCoord.z = 1.0
- 表示片元在 远裁剪面 上
归一化公式
1 gl_FragCoord.xyz = (gl_Position.xyz/gl_Position.w)/2.0+0.5
步骤2:使用阴影贴图
- 绘制目标: 颜色缓冲区
- 绘制视点: 原视点
每一个片元都有两个值:
- 光照坐标系下的z值(v_PositionFromLight.z)
- 该片元对应阴影贴图中存储的z值(shadowMap)
需要将这两个值拿来比较,那么问题就来了:
问题1:怎么获取片元在光照坐标系下的z值呢?
答: 只需要
- a_Position 顶点坐标
- u_MvpMatrixFromLight 光源模型视图投影矩阵
计算光源MVP矩阵下的顶点坐标即可:
1 | v_PositionFromLight = u_MvpMatrixFromLight * a_Position; |
问题2:怎么获取片元对应shadowMap中存储的对应值呢?
需要两步:
- 根据上一步计算出来的v_PositionFromLight转化为纹素坐标
- 根据纹素坐标从阴影贴图中抽取对应纹素
同样需要对顶点坐标进行 纹素归一化处理,
这是由于顶点坐标和纹素坐标的区间不同:
- 顶点坐标:[-1.0, 1.0]
- 纹素坐标: [0.0, 1.0]
归一化处理与gl_Position到gl_FragCoord归一化的过程相似
1
2 s = (v_PositionFromLight.x/v_PositionFromLight.w)/2.0+0.5;
t = (v_PositionFromLight.y/v_PositionFromLight.w)/2.0+0.5;
将归一化得到的纹素坐标放到shadowCoord中去
1 vec3 shadowCoord = (v_PositionFromLight.xyz/v_PositionFromLight.w)/2.0+0.5;shadowCoord包含了很多信息:
- shadowCoord.xy: 该片元对应的shadowMap坐标
- shadowCoord.z:当前片元在光源坐标系中的归一化z值
既然计算出shadowCoord,剩下的工作就比较简单了,
可以通过 shadowCoord.xy 从 shadowMap 中抽取纹素,
这里别忘了 内插过程
depth里最终放着阴影贴图中对应的z值
1
2 vec4 rgbaDepth = texture2D(u_ShadowMap, shadowCoord.xy);
float depth = rgbaDepth.r;
从 问题2 中,我们获得了用于比较的两个值:
- shadowCoord.z 光照坐标系下被归一化的片元的z值
- depth 同一片元对应的阴影贴图中的值
接下来只需要将这两个值拿来比较就可以了:
1 | float visibility = (shadowCoord.z > depth +0.005)?0.7:1.0; |
为什么在比较z值和阴影贴图值的时候要多给阴影贴图加上 0.005?
如果把这0.005拿掉会怎么样?
1 | float visibility = (shadowCoord.z > depth)?0.7:1.0; |
为什么会出现马赫带?
答: 由于用于比较的两个值存在 精度偏差
- shadowCoord.z
- 类型:RGBA分量
- 位数:8位
- 精度:1/256
- depth
- 类型:float
- 位数:16位
- 精度:1/65536
如何消除马赫带
答: 给较大精度的值加上一个偏移量。
注意: 偏移量应当略大于精度
如:N > M ?
N: 8位 精度1/256
M:16位 精度1/65536
因为M精度值较大,所以应该加上一个略大于1/256的值(0.005)
∴ N > M + 0.005
Step1 program
准备绘制的着色器程序
Step2 buffer
准备绘制图形的顶点数据
Step3 frameBuffer
创建帧缓冲区对象
Step4 bindTexture
将0号纹理绑定到帧缓冲区的纹理对象上
Step5 count shadowMap
切换光源视角,计算出阴影贴图
Step6 draw by shadowMap
切换屏幕视角,使用阴影贴图开始绘图
提高精度
Shadow.js存在哪些问题?
Shadow.js存在精度缺陷问题:
近距离光源
1 | let LIGHT_X = 0 |
远距离光源
1 | let LIGHT_X = 0 |
随着光源与照射物间距的变大,
gl_FragCoord.z的值也会随之变大,
最终8位的R分量无法存储下gl_FragCoord.z,导致精度缺陷
光源位置:
(0, 40, 4)
8位精度
提高精度
1 | let SHADOW_FSHADER_SOURCE = ` |
1 | let FSHADER_SOURCE = ` |
假设有这样一个片元坐标z值:
gl_FragCoord.z = 2.135793057816414332378301
如果只使用 gl_FragColor.r 来存储它的话
实际存储的结果是
gl_FragCoord.z * 2^(-8) * 2^(-8) = 2.1328125
既然gl_FragColor.r放不下精度这么大的值,
那就把gl_FragColor的 rgba 四个8位值全用上
R: (2^-8, 2^0)
G: (2^-16, 2^-8)
B: (2^-24, 2^-16)
A: (0, 2^-24)
一个位数提取的问题
如何在不破坏一个二进制数的其他位的情况下,将其中需要的位数提取出来?
如:有一个二进制数 0b0001_0001_0000_1000
我要如何将它的高8位和低8位分别提取出来?
fract函数(模糊)
作用: 舍弃参数的整数部分,返回小数部分。
消除马赫带偏移量
1 | float visibility = ( shadowCoord.z > depth + 0.0015 )?0.7:1.0; |
之所以选0.0015作为偏移量是因为:
z精度已经提高到了float
在mediump精度下,精度为2^-10
2^-10 = 0.000976563