内容参考闫令琪课程《games202-高质量实时渲染及作业》、花桑博客
知识点
- 阴影原理
- PCF(percentage close filter)原理
- PCSS(percentage close soft shadow)原理
实现效果
原理
阴影的原理
物体之间有遮挡,在被遮挡物的表现上形成阴影
最基本的阴影基于shadow map实现,需要绘制两遍
- 第一遍绘制
将camera放在光源处,绘制一遍,生成深度图
- 第二遍绘制
回到正常的camera位置绘制,每个点着色时,先从深度图里取值,判断当前点是否被遮挡,如果被遮挡则绘制成阴影
PCF
只用shadow map生成的阴影称之为"硬阴影",有两个问题
- 在边缘处存在锯齿
- 阴影边缘处生硬
shadow map方式计算阴影时,与深度图的值比大小,非0即1。PCF在此基础上,每次会采样多个深度值,求平均值,形成渐变的soft shadow,采样点越多,阴影越软。代码里采样使用泊松采样,为圆形采样。
//phongFragment.glsl
float PCF(sampler2D shadowMap, vec4 coords, float biasC, float filterRadiusUV) {
//uniformDiskSamples(coords.xy);
poissonDiskSamples(coords.xy); //使用xy坐标作为随机种子生成
float visibility = 0.0;
for(int i = 0; i < NUM_SAMPLES; i++){
vec2 offset = poissonDisk[i] * filterRadiusUV;
float shadowDepth = useShadowMap(shadowMap, coords + vec4(offset, 0., 0.), biasC, filterRadiusUV);
if(coords.z > shadowDepth + EPS){
visibility++;
}
}
return 1.0 - visibility / float(NUM_SAMPLES);
}
PCSS
仔细观察阴影,会发现物体越接近地面影子越清晰,离地面越高形成的影子越"软"
准确的说,假设光源有一定宽度,光源高度固定,遮挡物离光源越近,阴影越软,或者说半遮挡范围越大
- W L i g h t W_{Light} WLight:光源的宽
- d B l o c k e r d_{Blocker} dBlocker:遮挡物离光源的距离
- d R e c e i v e r d_{Receiver} dReceiver:地面(阴影接受面)离光源的距离
- W p e n u m b r a W_{penumbra} Wpenumbra:半影宽(软阴影的宽)
PCF对每个点采样多个深度求均值形成软阴影,PCSS在PCF上根据 W p e n u m b r a W_{penumbra} Wpenumbra值,动态计算采样半径,半径越大,阴影就越淡
实际在上面的公式中, d B l o c k e r d_{Blocker} dBlocker是未知的,必须先估算出来,看下图,连接着色点和光源边界,得到shadow map上的一个矩形,求矩形里像素的平均值(深度的平均)。
阴影自遮挡、悬浮问题
自遮挡是因为深度图的精度,第二遍绘制图像时,多个点对应深度图中的一个像素值,
自遮挡可以通过增加偏移解决,偏移的副作用就是悬浮,相当于把整个阴影的深度往后挪了一个偏移值
解决思路:
- 前面剔除,要求物体是有厚度的,即前后面不同
- 自适应偏移,根据视锥大小、camera与法线夹角动态计算偏移,夹角越大表示越倾斜,则偏移值越大
代码说明
shadow map
绘制两遍生成阴影。
注意:对比learnopengl教程中,opengl 3.0的API已经支持生成深度图,Webgl 2.0 通过在fragment shader中取z坐标为深度
//WebGLRenderer.js
// Shadow pass
if (this.lights[l].entity.hasShadowMap == true) {
for (let i = 0; i < this.shadowMeshes.length; i++) {
this.shadowMeshes[i].draw(this.camera);
}
}
// Camera pass
for (let i = 0; i < this.meshes.length; i++) {
this.gl.useProgram(this.meshes[i].shader.program.glShaderProgram);
this.gl.uniform3fv(this.meshes[i].shader.program.uniforms.uLightPos, this.lights[l].entity.lightPos);
this.meshes[i].draw(this.camera);
}
生成深度,用RGBA 4通道来存储深度值,提升精度。
EncodeFloatRGBA算法原理参考理解EncodeFloatRGBA与DecodeFloatRGBA
//shadowFragment.glsl
#ifdef GL_ES
precision mediump float;
#endif
uniform vec3 uLightPos;
uniform vec3 uCameraPos;
varying highp vec3 vNormal;
varying highp vec2 vTextureCoord;
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 = vec4( 1.0, 0.0, 0.0, gl_FragCoord.z);
gl_FragColor = pack(gl_FragCoord.z);
}
阴影visibility的计算
//phongFragment.glsl
void main(void) {
//vPositionFromLight为光源空间下投影的裁剪坐标,除以w结果为NDC坐标
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
//把[-1,1]的NDC坐标转换为[0,1]的坐标
shadowCoord.xyz = (shadowCoord.xyz + 1.0) / 2.0;
float visibility;
visibility = useShadowMap(uShadowMap, vec4(shadowCoord, 1.0));
//visibility = PCF(uShadowMap, vec4(shadowCoord, 1.0));
//visibility = PCSS(uShadowMap, vec4(shadowCoord, 1.0));
vec3 phongColor = blinnPhong();
gl_FragColor = vec4(phongColor * visibility, 1.0);
//gl_FragColor = vec4(phongColor, 1.0);
}
pcss实现
//phongFragment.glsl
float PCSS(sampler2D shadowMap, vec4 coords, float biasC){
float zReceiver = coords.z;
// STEP 1: avgblocker depth
float avgBlockerDepth = findBlocker(shadowMap, coords.xy, zReceiver);
if(avgBlockerDepth < -EPS)
return 1.0;
// STEP 2: penumbra size
float penumbra = (zReceiver - avgBlockerDepth) * LIGHT_SIZE_UV / avgBlockerDepth;
float filterRadiusUV = penumbra;
// STEP 3: filtering
return PCF(shadowMap, coords, biasC, filterRadiusUV);
}