GAMES202作业1-实现过程详细步骤

目录

查看初始模型

Shadow Map

要求

1 CalcLightMVP()

了解1-该方法如何参与到后续的环节

了解2-WebGL中glMatrix的使用

补充代码(截图)

useShadowMap()

参数传入1 -> shadowMap

参数传入2 -> shadowCoord

WebGL内置函数 -> texture2D

unpack()

转化成NDC坐标

补充代码(截图)

硬阴影实现结果

优化->自适应Shadow Bias

shadow bias 代码(截图)

优化自遮挡后的结果

新问题->Peter Panning 阴影丢失

PCF

要求

采样方法X2

Filtering

filter size 卷积核大小?

采样偏移值 -> 与步长Sride关系

代码实现(截图)

PCF结果

NUM_SAMPLES=20, Stride=10

NUM_SAMPLES=100, Stride=10

Stride=2, NUM_SAMPLES=100

Stride=20, NUM_SAMPLES=100

均匀圆盘采样 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

PCSS

要求

半影范围 ​编辑

1 Blocker Search

实现代码(截图)

2 Penumbra estimation

3 PCF

实现代码(截图)

PCSS结果


  • 首先说明一下,由于闫老师建议我们不要直接贴出完整的代码,不利于独立完成作业。因此从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环节了!

要求

半影范围 w_{Penumbra}

 

w_{Penumbra} 为半影范围,它其实就可以表示我们的阴影程度,w_{Penumbra} 越大,我们的阴影越“软”,这一点也可以由相似三角形表示出来

  • 光源Light越大,w_{Penumbra}越大,阴影越软;
  • 遮挡物Blocker越接近接受物Receiver,w_{Penumbra}越小,阴影越硬;
  • 遮挡物Blocker越接近光源Light,w_{Penumbra}越大,阴影越软;

由相似三角形就能得到

w_{Penumbra}=w_{Light}*(d_{Receiver}-d_{Blocker})/d_{Blocker}

1 Blocker Search

这一步其实就是在计算上面公式里的d_{Blocker}——Blocker depth

首先给定基数BlockerNum和总的Block_depth。从当前的shading point连向方向光源light,方向上击中一点P,取点P周围的一个区域(利用到了泊松圆盘采样),判断区域里的点是否在阴影里,如果在则:BlockerNum++、Block_depth+=cur_depth;如果不在,则不纳入计算。

可以发现这个判断过程跟前面的shadowmap和PCF都是一样的,但是目的不同,这里是在求blocker的深度!

实现代码(截图)

2 Penumbra estimation

利用上面的公式计算w_{Penumbra}

3 PCF

这里其实就是又进行了一次PCF,不过与PCF不同的是!这里考虑了阴影接受面与光源距离(dReceiver)对阴影软硬的影响, 这也是我们第二步计算得到半影范围的用途,使得到的阴影达到距离光源近的更硬,距离光源远的更软的效果,也更接近现实。

实现代码(截图)

最后在main()里给PCSS去掉注释即可。

PCSS结果


到这里作业1内容就做完了

猜你喜欢

转载自blog.csdn.net/qq_41835314/article/details/125726339#comments_27604576