目录
均匀圆盘采样 Stride=20, NUM_SAMPLES=100
泊松圆盘采样 Stride=20, NUM_SAMPLES=100
EPS=1e-2, NUM_SAMPLES=80, Stride=10
EPS=1e-3, NUM_SAMPLES=80, Stride=10
- 首先说明一下,由于闫老师建议我们不要直接贴出完整的代码,不利于独立完成作业。因此从202开始我也不会贴出全部的代码啦,涉及到作业的代码仅以截图的形式贴出每一步骤作为分享,希望大家一起讨论独立完成作业~!
- 详细了解作业1代码框架可戳:GAMES202作业1-万字分析代码框架&帮助更好理解框架内容
查看初始模型
首先打开查看一下初始模型:
很好!可以打开!另外,我最开始是直接从VS Code右键选择index.html进入的页面,会出现打开界面但不显示模型的情况,只有一个202,F12也没有任何报错,然后多刷新几次又行了:
保险起见每次从终端进入就行。
Shadow Map
要求
1 CalcLightMVP()
了解1-该方法如何参与到后续的环节
需要补全这个函数以得到光源方向的lightMVP,这个CalcLightMVP()方法通过以下途径参与到后面的环节中:
- 1-1 PhongMaterial.js的buildPhongMaterial()函数通过调用PhongMaterial
return new PhongMaterial(color, specular, light, translate, scale, vertexShader, fragmentShader);
在PhongMaterial中进行
let lightMVP = light.CalcLightMVP(translate, scale);
- 1-2 ShadowMaterial.js的buildShadowMaterial()函数通过调用ShadowMaterial
return new ShadowMaterial(light, translate, scale, vertexShader, fragmentShader);
在ShadowMaterial中进行
let lightMVP = light.CalcLightMVP(translate, scale);
- 2 并在loadOBJ中结合定义的transform和scale赋值传入material
let material, shadowMaterial;
let Translation = [transform.modelTransX, transform.modelTransY, transform.modelTransZ];
let Scale = [transform.modelScaleX, transform.modelScaleY, transform.modelScaleZ];
let light = renderer.lights[0].entity;
//根据材质,有两个材质
switch (objMaterial) {
case 'PhongMaterial':
material = buildPhongMaterial(colorMap, mat.specular.toArray(), light, Translation, Scale, "./src/shaders/phongShader/phongVertex.glsl", "./src/shaders/phongShader/phongFragment.glsl");
shadowMaterial = buildShadowMaterial(light, Translation, Scale, "./src/shaders/shadowShader/shadowVertex.glsl", "./src/shaders/shadowShader/shadowFragment.glsl");
break;
}
还需要知道的一点,传入的transform和scale是在engine中定义好的:
function setTransform(t_x, t_y, t_z, s_x, s_y, s_z) {
return {
modelTransX: t_x,
modelTransY: t_y,
modelTransZ: t_z,
modelScaleX: s_x,
modelScaleY: s_y,
modelScaleZ: s_z,
};
了解2-WebGL中glMatrix的使用
要想实现MVP变换矩阵,现在我们就不用像101一样自己写出变换矩阵了,借助WebGL的API就行!
还记得我们再index.html中从lib中加入了文件gl-matrix-min:
<script src="lib/gl-matrix-min.js" defer></script>
这就是加入了js用于矩阵处理的库glMatrix,可以用它来实现MVP矩阵以创建camera和model、view、projection变换的操作。
我们需要了解都有哪些跟矩阵相关的API,这些操作相关的我会在代码中给一点点注释,这里推荐我了解glMatrix参考的博客:
glMatrix — WebGL — Den's Website (dens.website)
WebGL学习06-投影,视图和模型矩阵 - 掘金 (juejin.cn)
学习WebGL之变换矩阵 - 简书 (jianshu.com)
补充代码(截图)
以上是了解CalcLightMVP()做的准备,下面是我分别对方法每一步实现做的注释:
- 创建4x4的单位矩阵以储存变换矩阵
//创建4x4的单位矩阵以储存变换矩阵
let lightMVP = mat4.create();
let modelMatrix = mat4.create();
let viewMatrix = mat4.create();
let projectionMatrix = mat4.create();
- Model transform
- View transform
- Projection transform
useShadowMap()
参数传入1 -> shadowMap
useShadowMap(sampler2D shadowMap, vec4 shadowCoord)
main()调用它时传入的参数为
visibility = useShadowMap(uShadowMap, vec4(shadowCoord, 1.0));
其中
- uShadowMap —— 提取来自方向光中创建的FBO中的深度信息
DirectialLight >>
this.fbo = new FBO(gl);//创建了一个帧缓冲区
...
PhongMaterial >>
//Phong
'uSampler': { type: 'texture', value: color }
// Shadow
'uShadowMap': { type: 'texture', value: light.fbo }
...
这里出现了新的关键字sampler2D,这是WeblGL在处理图片纹理时会声明的一个变量,它跟vec、float一样也表示一种数据类型,它表示的是一种取样器变量,从对应的纹理图片中提取像素值。
关于这个type为什么是texture?这个type其实是模型绘制入口MeshRender.js中为了绑定材质中参数所创建的字符串,除了'texture'外还有:'3fv' '1f'等等。
参数传入2 -> shadowCoord
- shadowCoord —— 纹理图片上像素对应的坐标,main()中有对应的归一化坐标计算
//实现了归一化
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
WebGL内置函数 -> texture2D
实现ShadowMap第一步就需要查询纹理图片对应坐标上的深度值,而实现深度值查询首先要查血对应的颜色,WebGL就提供了这样一个glsl的内建函数来查询对应位置纹理的颜色RGBA值——texture2D。
//第一个参数 -> 图片纹理
//第二个参数 -> 纹理坐标
vec4 texture2D(sampler2D sampler, vec2 coord)
关于WebGL其他的内置函数,我看了这篇文章总结了比较常用的,贴过来以供参考:WebGL内置函数 - 简书 (jianshu.com)
unpack()
这个函数在框架分析那个博客就提到过,用来将RGBA值转换成在范围[0,1]的float值。
转化成NDC坐标
main()中首先将像素坐标归一化了
// 归一化
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
也就是说,像素被缩放成了[-1,1]³(1指的只是一个unit,并非像素尺寸那个1),那么转换成[0,1]³的NDC坐标就先要进行一系列矫正:
(-1,-1,-1) -> (0,0,0) (1,1,1) -> (1,1,1) (-1,1,0) -> (0,1,0)
带入就能知道需要先+1再/2
// 需要转化到NDC,才能在纹理uv坐标中使用
shadowCoord.xyz=(shadowCoord.xyz+1.0)/2.0;
补充代码(截图)
-
1 查询最近深度 closest depth
- 2 获取当前深度值&进行比较
并在main()把以下被注释掉的useShadowMap和gl_FragColor放出来,以实现替换
// 应用shadowmap/PCF/PCSS
void main(void) {
// 归一化
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
// 需要转化到NDC,才能在纹理uv坐标中使用
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.);
}
硬阴影实现结果
优化->自适应Shadow Bias
可以发现上面实现出来的阴影是有问题的,由于自遮挡导致锯齿:
这个由于自遮挡产生锯齿的问题叫做阴影瑕疵(shadow acne),而shadow bias就是为了解决这个问题而提出的。
课上也提到,可以在进行深度判断时给个bias,在判断前先每个shading point深度往光照方向挪一挪,让由于自遮挡被判断处于阴影的点挪到有光照的地方,就能很大程度改善这一点。朝着光照方向挪动的距离即为bias,但是这个方法暂且是给每个shading point执行一个固定长度的bias。
自适应Shadow Bias算法 - 知乎 (zhihu.com)这篇文章中给出了更加清晰地数学模型,并给出了bias为什么产生的,感兴趣的话可以去看看,这里就不赘述。
根据上面参考文章最后给出的公式:
shadow bias 代码(截图)
添加一个函数getBias()以计算bias,给了一个调整量ctrl。
根据效果调整大小,最终定在了1.4效果最佳,useShadowMap()也需要加上计算bias的步骤
- 3 加上bias再进行比较
优化自遮挡后的结果
新问题->Peter Panning 阴影丢失
可以发现优化了acne后,有了新问题,阴影会“悬浮”
解决阴影丢失问题的对策是:采取基于物体斜度的bias,称为 slope scale based depth bias。这理就不再继续写代码了,具体步骤可参考:Unity基础6 Shadow Map 阴影实现 - RubbyZhang - 博客园 (cnblogs.com)
PCF
要求
采样方法X2
框架中给我们提供了两个采样方法,一个是泊松圆盘采样一个是均匀圆盘采样,二者都是根据
// Shadow map related variables
#define NUM_SAMPLES 20
#define BLOCKER_SEARCH_NUM_SAMPLES NUM_SAMPLES
#define PCF_NUM_SAMPLES NUM_SAMPLES
#define NUM_RINGS 10
定义的样本数量NUM_SAMPLES,这个过程会进行20次,得到的结果就是
vec2 poissonDisk[NUM_SAMPLES]; // NUM_SAMPLES = 20
Filtering
关于卷积可以参考文章:CNN 基础知识 - 卷积 (Convolution) 填充 (Padding) 步长 (Stride)-老唐笔记 (oldtang.com)
课上老师也提到过,PCF其实是基于shadow map做AA(Anti-Aliasing,即反走样)。
在进行shading point的深度与shadowmap比较时,不只比较一个方向的值,而是与周围像素做卷积,在周围采样多个点的深度值,逐一比较之后求平均值,就能得到一个[0,1]的连续分布,可以表示不同明暗程度的阴影,不再是硬阴影那样非0即1对比强烈的感觉,阴影就变得柔和起来,也就实现了人工软阴影化。
注意,PCF是实现的并不是:
- Filter深度(×)
- Filter shadow Map的结果 (×)
而是比较深度后再计算一个平均值。
filter size 卷积核大小?
PCF就是在做卷积,把卷积核也叫做过滤器,也就是filter。
- 作业1中filter的大小由采样数量决定,也就是一开始给定的NUM_SAMPLES,初始值设置成了20。
- filter的大小、个数一般都是先设定一个初始值,再根据实验效果进行调整。
- 在进行卷积时,在输出要求相同的情况下,filter越大参与计算的参数越多,那么对于作业1来说达到的阴影柔和的效果越明显。
采样偏移值 -> 与步长Sride关系
在卷积过程中,将每次卷积核滑动的行数/列数称为Stride(步长)。有时需要在卷积时通过设置的Stride来压缩一部分信息,成倍缩小尺寸。
对于作业1而言,由于PCF输入的坐标coords归一到了[0,1]的范围,那么给定采样点的偏移值poissonDiskSamples[i]也需要缩小一定范围以迎合coords坐标的尺寸,因此需要给定Stride以缩小尺寸。缩小比例当然是Stride/ShaodowMapSize,框架中ShadowMapSize=2048,Stride可以给定一个初始值1,根据效果进行调整。
代码实现(截图)
- 1 定义参数
-
2 泊松采样得到采样点
- 3 对每个点进行比较深度并累加
- 4 返回均值
- 5 并在main()实现PCF
PCF结果
通过给NUM_SAMPLES和Stride赋值,调整阴影的效果,可以发现:
- NUM_SAMPLES越小阴影边缘噪点越多
NUM_SAMPLES=20, Stride=10
NUM_SAMPLES=100, Stride=10
- Stride越大,边缘越模糊
Stride=2, NUM_SAMPLES=100
Stride=20, NUM_SAMPLES=100
-
对比两种采样方式的结果
均匀圆盘采样 Stride=20, NUM_SAMPLES=100
泊松圆盘采样 Stride=20, NUM_SAMPLES=100
可以看到泊松圆盘确实比均匀圆盘效果要好一点,且均匀圆盘采样消耗也会更多。
- EPS大小的影响
在调整的过程中还发现,EPS的大小虽然对地板上的阴影没啥影响,但是对模型上阴影判断效果有很大的影响,其实原因其实跟shadowmap的一样,shadow acne。EPS初始值给的是0.001,浅看一下0.001和0.01的区别:
EPS=1e-2, NUM_SAMPLES=80, Stride=10
EPS=1e-3, NUM_SAMPLES=80, Stride=10
PCSS
总算到了最后的PCSS环节了!
要求
半影范围
为半影范围,它其实就可以表示我们的阴影程度, 越大,我们的阴影越“软”,这一点也可以由相似三角形表示出来
- 光源Light越大,越大,阴影越软;
- 遮挡物Blocker越接近接受物Receiver,越小,阴影越硬;
- 遮挡物Blocker越接近光源Light,越大,阴影越软;
由相似三角形就能得到
1 Blocker Search
这一步其实就是在计算上面公式里的——Blocker depth。
首先给定基数BlockerNum和总的Block_depth。从当前的shading point连向方向光源light,方向上击中一点P,取点P周围的一个区域(利用到了泊松圆盘采样),判断区域里的点是否在阴影里,如果在则:BlockerNum++、Block_depth+=cur_depth;如果不在,则不纳入计算。
可以发现这个判断过程跟前面的shadowmap和PCF都是一样的,但是目的不同,这里是在求blocker的深度!
实现代码(截图)
2 Penumbra estimation
利用上面的公式计算
3 PCF
这里其实就是又进行了一次PCF,不过与PCF不同的是!这里考虑了阴影接受面与光源距离(dReceiver)对阴影软硬的影响, 这也是我们第二步计算得到半影范围的用途,使得到的阴影达到距离光源近的更硬,距离光源远的更软的效果,也更接近现实。
实现代码(截图)
最后在main()里给PCSS去掉注释即可。
PCSS结果
到这里作业1内容就做完了