先放上公式:
后面的积分项是我们在作业0中就做好的blinnphong项,我们要求的就是积分项前,等号后的可见项。
最终体现在代码中便是
这部分代码在homework1\src\shaders\phongShader\phongFragment.glsl中
第一部分是使用Shadow Map的方法来渲染阴影,也就是渲染硬阴影,经典的Two Pass Shadow Map方法。
简单来说,shadow map主要分为两步操作:
先看src\renderers\WebGLRenderer.js,两次Pass计算阴影的主要代码:
首先是src/lights/DirectionalLight.js的矩阵变换部分,该矩阵参与了第一步从光源处渲染场景从而构造 ShadowMap 的过程。
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,1e-2,400);
mat4.multiply(lightMVP, projectionMatrix, viewMatrix);
mat4.multiply(lightMVP, lightMVP, modelMatrix);
return lightMVP;
}
完成该矩阵的输出后,我们就可以在片元着色器中获取到Shadow Map:
src\shaders\phongShader\phongFragment.glsl
在代码里发现是调用了useShadowMap方法,
float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){
float mapDepth = unpack(texture2D(shadowMap,shadowCoord.xy));//shadow map中各点的最小深度,unpack将RGBA值转换成[0,1]的float
float shadingDepth = shadowCoord.z; //当前着色点的深度
float visibility1 = ((mapDepth + EPS) < shadingDepth) ? 0.0 : 1.0;
return visibility1;
}
放大之后会发现会有很多锯齿,因为发生了自遮挡现象:
其一是处理器的数值精度的限制
还有一个原因是因为shadow map本身保存的值是离散值,也就是说shadow map上每个采样点都代表着一块范围内图元的深度值,因此在第二个pass比较深度的时候,shadow map中的深度可能会略低于物体表面的深度,部分片元就会被误计算为阴影,导致自遮挡。该现象在光源与平面趋于平行时(掠射)尤为严重。
为了解决这个问题,课上也说了,使用bias(偏移值)方法。
在shadow map中引入一个偏移值(bias),使得每次在比较深度大小的时候,都将一定区间内的shadow map深度认作与屏幕空间深度相等,强行减弱阴影判定。
但这样做又会引入一个新的问题——detached shadow,或者说,peter panning(阴影悬浮)——即丢失部分原本可能发生遮挡的阴影
那来看一下怎么做这个bias,还是在src\shaders\phongShader\phongFragment.glsl中直接添加一个Bias方法:
放两个,其实没多大区别,差别在于这个阴影的出现程度,第二个的阴影区域会比第一个小:
float Bias(float CDepth){
vec3 lightDir1 = normalize(uLightPos);
vec3 normal1 = normalize(vNormal);
float m = 200.0 / 2048.0 / 2.0; // 正交矩阵宽高/shadowmap分辨率/2
float bias1 = max(m * (1.0-dot(normal1,lightDir1)),m) * CDepth;
return bias1;
}
float Bias1(){
vec3 lightDir1 = normalize(uLightPos);
vec3 normal1 = normalize(vNormal);
float bias1 = max(0.08 * (1.0-dot(normal1,lightDir1)),0.08);
return bias1;
}
然后修改useShadowMap方法:
float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){
float mapDepth = unpack(texture2D(shadowMap,shadowCoord.xy));//shadow map中各点的最小深度,unpack将RGBA值转换成[0,1]的float
float shadingDepth = shadowCoord.z; //当前着色点的深度
//float visibility1 = ((mapDepth + EPS) < shadingDepth) ? 0.0 : 1.0;
float bias = Bias(1.4);
float visibility1 = ((mapDepth + EPS) <= (shadingDepth - bias)) ? 0.2 : 0.9;
return visibility1;
}
放大之后,虽然不会产生严重的锯齿现象,但还是有锯齿:
但很明显,腿部的阴影没有了,因为发生了我们上面说的阴影悬浮现象,这就可以通过修改bias方法来改善,但不可避免。
完成了硬阴影的two pass shadow map方法后,在实际生活中我们更希望我们得到的是软阴影,接下来就是写软阴影部分的代码,软阴影又分为PCF和PCSS两种。
我们通过SM生成了一个硬阴影,但是在实际生活中我们希望我们得到的是软阴影,而在我们刚才的硬阴影的计算中我们得到的visibility项非0即1.如果我们着色点周围的一圈像素进行一个加权平均,我们就可以得到一个相对来说较软的阴影——visibility项不再是非0即1。
请注意,这个过程发生在采样过程中:
作业中建议用泊松圆盘采样和均匀圆盘采样,代码里都给出来了:
实话说没看懂哈,会用就行了,要是有小伙伴感兴趣的可以看下面的链接:
课上老师也提到过,PCF其实是基于shadow map做AA(Anti-Aliasing,即反走样)。PCF就是在做卷积,把卷积核也叫做过滤器,也就是filter。
卷积原理看这个:卷积 (Convolution) 填充 (Padding) 步长 (Stride)
在进行shading point的深度与shadowmap比较时,不只比较一个方向的值,而是与周围像素做卷积,在周围采样多个点的深度值,逐一比较之后求平均值,就能得到一个[0,1]的连续分布,可以表示不同明暗程度的阴影,不再是硬阴影那样非0即1对比强烈的感觉,阴影就变得柔和起来,也就实现了人工软阴影化。
float PCF(sampler2D shadowMap, vec4 coords) {
float stride = 2.0; //定义步长
float shadowMapSize = 2048.0; //shadowmap分辨率
float visibility1 = 0.0; //初始可见项
float cur_depth = coords.z; //卷积范围内当前点的深度
float filterRange = stride / shadowMapSize; //滤波窗口的范围
//泊松圆盘采样得到采样点
poissonDiskSamples(coords.xy);
//均匀圆盘采样得到采样点
//uniformDiskSamples(coords.xy);
//对每个点进行比较深度值并累加
for(int i = 0; i < NUM_SAMPLES; i++){
float shadow_depth = unpack(texture2D(shadowMap,coords.xy + poissonDisk[i] * filterRange));
float res = (cur_depth < shadow_depth + EPS) ? 1.0 : 0.0;
visibility1 += res;
}
//返回均值
float avgVisibility = visibility1 / float(NUM_SAMPLES);
return avgVisibility;
}
用泊松圆盘采样的结果:
NUM_SAMPLES=80,stride=10:
要是更改一下参数:NUM_SAMPLES=80,stride=2:
用均匀圆盘采样的结果:
NUM_SAMPLES=80,stride=2:
然后就是最后的PCSS方法了。为了达到前实后虚的软阴影效果,就可以采用PCSS(Percentage Closer Soft Shadow),通过计算投影平面与遮挡物之间的距离,来确定滤波范围的大小(自适应的filter size)。
算法的整体思路是:
这里有个问题,filter size可以按上述方法确定了,那么计算filter size时需要用到的d(blocker)同样需要在一定范围内做平均,这个范围又怎么确定呢?我们可以认为规定一个固定的大小,如4 * 4,16 * 16等,但这么做绝对不是最优解,更好的方法是在光源处设置一个视锥,将shadow map置于近平面上,接着连接着色点和光源,以其在shadow map上所截得的范围作为样本,来计算平均深度。
这么做有一个非常大的好处,就是计算d(blocker)也采用了自适应的方法,离光源越远,遮挡物越多,计算blocker所用的样本范围就越小;而离光源越近,遮挡物越少,计算blocker所用的样本空间就越大,非常合理。
由相似三角形就能得到:
所以PCSS的具体步骤为:
那作业需要我们完成findBlocker(sampler2D shadowMap,vec2 uv, float zReceiver) 和 PCSS(sampler2D shadowMap, vec4 shadowCoord)函数。
看一下findBlocker函数,需要完成对遮挡物平均深度的计算,也就是上面的d(Blocker)。
float findBlocker( sampler2D shadowMap, vec2 uv, float zReceiver ) {
int blockerNum = 0; //着色点对应到shadow map中后,周围一圈邻域内为blocker的点的数量
float blocker_depth = 0.0; //blocker的深度
float shadowMapSize = 2048.0; //shadow map的分辨率
float stride = 50.0; //采样的步长
float filterRange = stride / shadowMapSize; //滤波窗口的范围
//泊松圆盘采样得到采样点
poissonDiskSamples(uv);
//均匀圆盘采样得到采样点
//uniformDiskSamples(uv);
//判断着色点对应到shadow map中后,邻域中的点是否为blocker,如果是就累加
for(int i = 0; i < NUM_SAMPLES; i++){
float shadow_depth = unpack(texture2D(shadowMap,uv+ poissonDisk[i] * filterRange));
if(zReceiver > shadow_depth + 0.01){
blockerNum++;
blocker_depth += shadow_depth;
}
}
if(blockerNum == 0){
return 1.0;
}
blocker_depth = blocker_depth / float(blockerNum);
return blocker_depth;
}
float PCSS(sampler2D shadowMap, vec4 coords){
// STEP 1: avgblocker depth
float avgBlocker_depth = findBlocker(shadowMap,coords.xy,coords.z); //在这步里我们已经做好了采样,后面就能直接调用数据
float wLight = 1.0; //光源大小
float dReceiver = coords.z;
// STEP 2: penumbra size
float wPenumbra = wLight * (dReceiver - avgBlocker_depth) / avgBlocker_depth;
// STEP 3: filtering 就是做PCF,不过加入了wPenumra的影响
//首先定义变量
float stride = 10.0;
float shadowMapSize = 2048.0;
float visibility1 = 0.0;
float cur_depth = coords.z;
float filterRange = stride / shadowMapSize;
//做采样,前面已经做好了
//poissonDiskSamples(coords.xy);
//然后循环比较
for(int i = 0; i < NUM_SAMPLES; i++){
float shadow_depth = unpack(texture2D(shadowMap,coords.xy + poissonDisk[i] * filterRange * wPenumbra));
float res = cur_depth < shadow_depth+0.01 ? 1.0 : 0.0;
visibility1 += res;
}
//求平均
visibility1 /= float(NUM_SAMPLES);
return visibility1;
}
最后main函数改一下记得,结果图:
下面这个是NUM_SAMPLES=80
这个是让cur_depth < shadow_depth+EPS,NUM_SAMPLES=20的结果:
也可以加bias,但是得调…