目录
任务
ShadowMap
PCF
PCSS
1.需要完善 phongFragment.glsl 中的 findBlocker(sampler2D shadowMap, vec2 uv, float zReceiver)。findBlocker 函数中需要完成对遮挡物平均深度的计算。
2.完善PCSS(sampler2D shadowMap, vec4 shadowCoord) 函数。
实现
ShadowMap
使用shadow map实现硬阴影,需要实现经典的 Two Pass Shadow Map 方法,第一次先以光源为视点位置,将光源能看见的所有物体进行光栅化,并将以深度值进行绘制的结果保存到帧缓冲区,帧缓冲区的内容写入纹理。第二次绘制,根据纹理和一个将世界坐标下的点转化到以光源为视点的坐标系的矩阵,判断是否被遮挡,从而实现硬阴影。
第一个任务是求世界坐标转化为光源坐标的矩阵。
在这里,采用glMatrix库的相关API。
以光源为视点,计算方法就是刚开始学图形学的时候学的MVP矩阵。对于模型矩阵modelMatrix,该函数传入了两个向量,一个是位移translate一个是缩放scale,使用这两个向量,计算出模型矩阵。对于视图矩阵,直接用该类自带的光源属性的值传入API得到。对于投影矩阵,使用正交投影,因为能够方便判断深度值。zNear值最好不要设置为0,因为zNear为0在后面的PCSS中会带来不必要的麻烦。
//DirectionalLight.js
CalcLightMVP(translate, scale) {
let lightMVP = mat4.create();
let modelMatrix = mat4.create();
let viewMatrix = mat4.create();
let projectionMatrix = mat4.create();
// Model transform
mat4.translate(modelMatrix, modelMatrix, translate);
mat4.scale(modelMatrix, modelMatrix, scale);
// View transform
mat4.lookAt(viewMatrix, this.lightPos, this.focalPoint, this.lightUp);
// Projection transform
mat4.ortho(projectionMatrix, -100, 100, -100, 100, 0.01, 500);
mat4.multiply(lightMVP, projectionMatrix, viewMatrix);
mat4.multiply(lightMVP, lightMVP, modelMatrix);
return lightMVP;
}
useShadowMap
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
shadowCoord = (shadowCoord + 1.0) / 2.0;
转换完后,[0,1]空间的xy坐标就能刚好对应贴图的uv坐标。在阴影贴图中使用uv坐标查询相应位置的深度。如果查到的深度比现在的点的深度小,说明现在的点被遮挡。
float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){
vec4 shadowColor = texture2D(shadowMap,shadowCoord.xy);
float depth = unpack(shadowColor);
float z = shadowCoord.z;
if(z > depth + EPS ){
return 0.0;
}
return 1.0;
}
PCF
第三个任务是实现PCF的功能。其实就是在原来ShadowMap的基础上,对周围进行采样,并将结果求平均。
先预定义一些常量以方便下面的PCF和PCSS的运算
#define SHADOW_MAP_SIZE 2048 //阴影贴图大小
#define NEAR 0.01 //之前在计算正交投影矩阵的时候的zNear
#define LIGHT_SIZE 10.0 //光源在世界的大小
#define LIGHT_UV_SIZE 0.15 //光源在贴图上的大小
开始先调用作业框架里自带的获取采样点的函数。获取对周围的随机采样方向,乘以采样半径大小再除以贴图大小即可求得采样点在贴图中的位置。在这里定义一个阻挡值来记录积累阻挡的数量。作业原代码,参数是没有sampleRadious的,这里是另外加上去的,方便控制采样区域大小。
float PCF(sampler2D shadowMap, vec4 coords ,float sampleRadious ) {
poissonDiskSamples(coords.xy);
float block = 0.0;
for(int i =0;i<NUM_SAMPLES;i++){
vec4 shadowColor = texture2D(shadowMap,coords.xy + poissonDisk[i] * sampleRadious/ float(SHADOW_MAP_SIZE) );
float depth = unpack(shadowColor);
float z = coords.z;
if(z > depth + EPS ){
block = block + 1.0;
}
}
return 1.0 - block / float(NUM_SAMPLES);
}
findBlocker
该函数实现的功能是计算遮挡物的平均深度。
要根据点到光源的距离来决定采样区域的大小,也就是,采用相似三角形来计算。得到采样区域的大小后,进行采样。因为是计算遮挡物的平均深度,所以没有遮挡物的话,返回-1处理,有遮挡物返回遮挡物的平均深度,而不是返回整个采样区域的平均深度。
float findBlocker( sampler2D shadowMap, vec2 uv, float zReceiver ) {
poissonDiskSamples(uv);
int blockCnt = 0;
float blockDepth = 0.0;
float sampleSize = LIGHT_UV_SIZE * (vPositionFromLight.z - NEAR ) / vPositionFromLight.z;
for(int i =0;i<NUM_SAMPLES;i++){
vec4 shadowColor = texture2D(shadowMap,uv + poissonDisk[i] * 10.0 / float(SHADOW_MAP_SIZE) );
float depth = unpack(shadowColor);
if(zReceiver > depth + EPS ){
blockCnt++;
blockDepth = blockDepth + depth;
}
}
if(blockCnt == 0)return -1.0;
return blockDepth /float(blockCnt);
}
PCSS
有了上面的findBlolcker后,实现起来就非常简单,使用上面findblocker的数据,调用PCF即可。注意当avgDepth为-1时,即无遮挡,为特殊情况,直接返回1.0。
float PCSS(sampler2D shadowMap, vec4 coords){
vec2 uv = coords.xy;
float zReceiver = coords.z;
// STEP 1: avgblocker depth
float avgDepth = findBlocker(shadowMap,uv,zReceiver);
if(avgDepth < 0.0)return 1.0;
// STEP 2: penumbra size
float penumbra = (zReceiver - avgDepth) / avgDepth * LIGHT_SIZE;
// STEP 3: filtering
return PCF(shadowMap,coords,penumbra);
}
void main(void) {
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
shadowCoord = (shadowCoord + 1.0) / 2.0;
float visibility;
//visibility = useShadowMap(uShadowMap, vec4(shadowCoord, 1.0));
//visibility = PCF(uShadowMap, vec4(shadowCoord, 1.0) , 10.0);
visibility = PCSS(uShadowMap, vec4(shadowCoord, 1.0));
vec3 phongColor = blinnPhong();
gl_FragColor = vec4(phongColor * visibility, 1.0);
//gl_FragColor = vec4(phongColor, 1.0);
}
结果
ShadowMap硬阴影
PCF实现软阴影
PCSS实现软阴影