作业1主要是对GAMES202-Lecture3中的知识进行应用,涉及到了ShadowMap生成、基本硬阴影绘制、PCF、PCSS等知识

ShadowMap生成

作业中对于ShadowMap的生成,仅要求书写模型空间到光源空间的MVP矩阵的书写

对于modelMatrix,我们能获得的参数有模型的Scale和Translate,没有旋转相关参数,我们按照模型变换顺序先进行Scale的矩阵变换然后进行TransLate的矩阵变换,得到modelMatrix。对于viewMatrix,我们能获得的参数有观察点和光源位置以及光源的Up向量,根据观察点和光源位置我们可以确定view的朝向,然后将光源的Up向量与朝向进行正交化并叉乘得到第三个基向量,将三个基向量正交化并对组成的矩阵取逆得到viewMatrix(由于z轴正方向和摄像机正反向是反的,viewMatrix需要对朝向向量取反)。对于projectionMatrix,我们采用正交变换,但这边没有参数,只能是根据给到的参数进行推测,根据正交变换实际是一次平移加上缩放获得变换矩阵。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
let lightMVP = mat4.create();
let modelMatrix = mat4.create();
let viewMatrix = mat4.create();
let projectionMatrix = mat4.create();
// Model transform
let s = mat4.create();
let t = mat4.create();
s[0] = scale[0]
s[5] = scale[1]
s[10] = scale[2]
mat4.multiply(modelMatrix, s, modelMatrix);
t[12] = translate[0]
t[13] = translate[1]
t[14] = translate[2]
mat4.multiply(modelMatrix, t, modelMatrix);
// View transform
let vt = mat4.create();
let vr = mat4.create();
vt[12] = -1 * this.lightPos[0]
vt[13] = -1 * this.lightPos[1]
vt[14] = -1 * this.lightPos[2]
mat4.multiply(viewMatrix, vt,viewMatrix);
let t_up = normalize(this.lightUp);
let g = normalize(vecMinus(this.focalPoint,this.lightPos));
//对t_up进行正交化
t_up = normalize(vecMinus(t_up , vecChen(vecDot(g, t_up) , g)));
let gxt = cross(g,t_up);
vr[0] = gxt[0]
vr[1] = t_up[0]
vr[2] = -1.0 * g[0]
vr[4] = gxt[1]
vr[5] = t_up[1]
vr[6] = -1.0 * g[1]
vr[8] = gxt[2]
vr[9] = t_up[2]
vr[10] = -1.0 * g[2]
mat4.multiply(viewMatrix, vr, viewMatrix);
// Projection transform
//realnear = - near ,realfar = -far
let aspect = 1
let size = 100
let near = 1
let far = 500
projectionMatrix[0] = 1 / (aspect * size)
projectionMatrix[5] = 1 / size
projectionMatrix[10] = -2 / (far - near)
projectionMatrix[14] = -1 * (far + near) / (far - near)
mat4.multiply(lightMVP, projectionMatrix, viewMatrix);
mat4.multiply(lightMVP, lightMVP, modelMatrix);

硬阴影绘制以及自遮挡问题的解决

将顶点坐标通过上述光源空间的MVP矩阵转换到光源空间坐标后进行归一化获得NDC坐标,再得到0-1的UV坐标,采样ShadowMap中数值并转换为深度,与当前深度对比获得visibility值并与shading结果相乘得到硬阴影效果

按照物体法线和光线的角度计算Bias后

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
float getBias() {

return 0.005 + 0.003* (1.0 - dot(normalize(vNormal) , normalize(uLightPos - vFragPos)));

}



float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){

vec4 depth = texture2D(shadowMap,shadowCoord.xy);

float real_depth = unpack(depth);

if(real_depth + getBias() < shadowCoord.z) return 0.0;

return 1.0;

}

PCF

PCF是在硬阴影的基础上进行ShadowMap的多次采样并取结果平均值作为visibility值达到一点的软化效果

笔者对正方形采样、以及闫大神提供的泊松圆盘随机采样和均匀圆盘采样都进行了测试

正方形

泊松圆盘

均匀圆盘


PCSS

在PCF的基础上,我们将FileSize即采样区域设置为动态的,依据Lecture3得到的采样区域由penumbra大小决定,其公式为penumbraSize = lightWidth * (ReceiveDepth - avgblockerDepth) / avgblockerDepth,我们设光源为的宽度为1,ReceiveDepth是已知的,需要求遮挡深度的平均值,这边的遮挡深度采样点由点与光源距离决定,距离越远,采样区域越大,将这三步合并我们得到以下过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
float PCSS(sampler2D shadowMap, vec4 coords){

float lightWidth = 1.0;

float mapSize = 2048.0;

float blockSize = 5.0;

// STEP 1: avgblocker depth

float sizeX = length(uLightPos - vFragPos) / 100.0;

float avgblockerDepth = GetAvgblockerDepth(shadowMap,coords,blockSize,coords.z,mapSize * sizeX);

if(avgblockerDepth == 1.0) {//非遮挡区域直接按照返回1.0

return 1.0;

}

// STEP 2: penumbra size

float penumbraSize = lightWidth * (coords.z - avgblockerDepth) / avgblockerDepth;

// STEP 3: filtering

// penumbraSize 越大采样区域越大,filtersize越大,即mapSize越小,阴影越软

mapSize = mapSize / (penumbraSize * 20.0);

return GetFileterRitaByUniform(shadowMap,coords,coords.z,mapSize);

//return GetFileterRitaByPoss(shadowMap,coords,coords.z,mapSize);

//return GetFileterRita(shadowMap,coords,3.0,coords.z,mapSize);

}

随机采样为均匀圆盘随机采样的效果图如下,可以看到随着距离增大,阴影有明显的变软效果


后续改进

泊松和均匀圆盘采样的效果一直很不好,后面小伙伴告知需要调大采样个数,测试后发现效果确实好了不少