games202:二,实时阴影Shadow Mapping、软阴影PCF、PCSS、VSSM、MSM、SDF + 作业1

一,Shadow Mapping

Shadow Mapping是很常用的阴影生成方法,塞尔达、玩具总动员都用它。games101里已经讲过原理,分为2pass:

  1. 先从光源视角得到各方向最浅深度shadow map。
  2. 再从相机视角根据shadow map渲染

1.1Shadow Mapping的问题

Shadow Mapping速度快但有锯齿化走样和自遮挡现象。

1.1.1自遮挡

下图是由数值精度造成的地板纹路,原因是光源视角的shadow map由于像素限制所记录的深度不连续,如右图一段段红色,在光线视角连续,在蓝色相机视角就不连续,形成自遮挡。在此前提下可以想象,光线视角与地面越垂直,纹路越少,与地面越平行,纹路越多。
在这里插入图片描述
在这里插入图片描述

1.1.2自遮挡解决办法

在shadow map两像素之间加一个bias(误差区间),如下图黄色段,在此区间内的最小深度不计算;并且bias根据光源角度是变化的。

  • 但此方法也会引入其他问题:阴影和物体断裂
    在这里插入图片描述在这里插入图片描述

工业界有许多解决办法,但都是找到一个更合适的bias,目前没有真正解决问题的方法。学术界有一种方法Second-depth shadow mapping:在生成 Shadow Map 时,同时保存最小深度和次小深度,比较的时候取两者平均值。但此方法工业界没什么人用,因为需要封闭模型而不能只是一个平面,并且计算量大一些。

1.1.3锯齿化走样

shadow map分辨率不够会出现锯齿,可以以效率为代价使用超采样或提高分辨率来改善,或者动态分辨率等方法。

「老师忠告:实时渲染不相信复杂度!常数一样很重要!----再好的方法超过1ms就基本不考虑了」

二,Shadow Mapping Math

微积分中的两个常用不等式:
在这里插入图片描述
实时渲染关注近似相等—把不等式当做约等式用:
在这里插入图片描述

  • 认为不等式近似相等的条件(满足其一即可):

    1. g(x)的积分域(support)很小
    2. g(x)的值足够光滑(变化不大)
  • 有了上述近似约等式后,可以把渲染方程简化,把 Visibility 项提取出来,剩下的g(x)就是shading的结果:
    在这里插入图片描述
    「Shadow Map的原理就是这样,先计算shading,再根据 Visibility 乘上系数」

  • 该不等式成立条件在渲染中的意义:

    1. g(x)的积分域很小----只有一个Visibility,即只有一个点光源或者一个方向光源
    2. g(x)的值足够光滑----radiance各处不变的面光源并且shading point是漫反射时(diffuse brdf)「如果brdf是glossy就是不光滑」

三,软阴影

Shadow Map 方法做出来的硬阴影在现实中不常见,因为现实中多是面光源对应的软阴影。下图是软硬阴影区别:
在这里插入图片描述在这里插入图片描述

3.1 PCF(Percentage Closer Filtering)

「该技术起初是用于抗锯齿\反走样,后来发现软阴影也可以用。」
「为什么不对shadow map做滤波?因为把shadow map模糊后,在深度判定完还是硬阴影(相当于二值化)。」

  • 原理是在阴影判定时做一个filtering----不仅计算当前着色点对应shadow map上的深度,还计算该点在shadow map上周围一圈(比如7x7)的深度判定结果(非0即1)并取平均,将平均值作为Visibility项。「如果滤波核比较大,可以在范围内随机采样固定个数」

3.2 PCSS(Percentage closer soft shadows)

  • 软阴影与物体远近关系:
    1. 投影平面上的阴影到物体的距离越远,阴影越软、滤波核越大。
    2. 投影平面离物体越近,阴影越硬、滤波核越小。「即下图W越大,阴影越软」

在这里插入图片描述在这里插入图片描述

这里决定软阴影大小的w通过下式计算(相似三角形),可以看到阴影越远、光源越大、物体离光源越近时阴影越软越模糊。由于实际物体是有形状的,深度不一样,我们这里的 d b l o c k e r d_{blocker} dblocker需要是一个average blocker depth(如果直接用shadow map的值会在陡峭处出现错误)。
在这里插入图片描述
在计算平均遮挡深度(average blocker depth)时有两种方式,一种固定范围计算,比如都用5X5计算;一种动态范围计算。在计算shadow map的时候把光源当做过相机,此时我们就把shadow map认为是近平面,然后将光源和要渲染的点相连,如下图,在shadow map覆盖的区域就是要计算平均遮挡距离的区域。
在这里插入图片描述

  • 根据PCF的原理,可以得到PCSS算法步骤:

    1. Blocker search,计算物体平均遮挡深度(在固定或动态范围内去遮挡区域计算)。
    2. Penumbra estimation,用平均遮挡深度确认该点滤波核大小。
    3. 应用PCF算法。
  • 步骤中有两次范围计算消耗性能,工业上一般用随机采样+图像降噪代替。

四,VSSM(Variance Soft Shadow Mapping)

PCSS中的1、3步计算范围内像素数值效率太低,于是有了VSSM。

4.1 加速PCSS-step3

  • 前提:pcss步骤三其实就是计算shadow map的一个区域内能被相机看到的像素的百分比(涉及到比例,我们就可以拟合它),也可以比作 已知自己的成绩求自己排第几 的问题。

  • 中心思想:为了拟合上述比例,我们假设着色点附近的深度是符合概率分布的(正态分布),因此快速计算出均值( μ μ μ)和方差( σ 2 σ^2 σ2)后,就可以求出该着色点的深度在附近排前百分之多少。

  • 步骤

    1. 计算均值:基于硬件的MipMap(时间几乎可以忽略但不准----但没关系,实时渲染就喜欢快)或 Summed Area Tables(SAT区域求和表) 方法获得期望,O(1)。
    2. 计算方差:生成储存深度平方的square-depth map(texture多一个通道即可满足)得到平方值期望,再根据概率论公式 V a r ( X ) = E ( X 2 ) − E 2 ( X ) Var(X) = E(X^2) - E^2(X) Var(X)=E(X2)E2(X)计算得到方差,O(1) 。
    3. 计算排名:计算CDF(积分分布函数) of PDF(概率密度函数),如下图,实际vssm使用切比雪夫不等式,O(1)。
      在这里插入图片描述
  • 对于高斯分布的CDF没有解析解,但可以把数值解的值打成一张表(表就叫Error Function,在c++里有对应函数erf),但这样比较麻烦。因此VSSM这里使用了切比雪夫不等式
    P ( x > t ) ≤ σ 2 σ 2 + ( t − μ ) 2 = > P ( x ≤ t ) ≈ 1 − σ 2 σ 2 + ( t − μ ) 2 P(x>t) ≤ \frac{σ^2}{σ^2 + (t-μ)^2} => P(x≤t) ≈ 1- \frac{σ^2}{σ^2 + (t-μ)^2} P(x>t)σ2+(tμ)2σ2=>P(xt)1σ2+(tμ)2σ2
    「此处切比雪夫不等式成立条件为 t大于均值(不管分布啥样),但实际实时渲染中问题不大」

4.1.1 MIPMAP

mipmap做的是快速近似方形查询,其中用到了差值,有误差,个别有长方形用各向异性过滤,具体内容101讲过了,不懂回去看下。

4.1.2 SAT-Summed Area Tables

mipmap局限于正方形或特殊情况长方形,想随意取矩形计算就不划算。Summed Area Table每个像素的值为原纹理从(0,0)到该像素组成的矩形中所有像素值的和,是100%准确的,如下图:
在这里插入图片描述
在这里插入图片描述

对任意矩形内的平均值可通过下式四次加减即可求得。
s = t [ x m a x , y m a x ] − t [ x m a x , y m i n ] − t [ x m i n , y m a x ] + t [ x m i n , y m i n ] s = t[x_max,y_max] - t[x_max,y_min] - t[x_min,y_max] + t[x_min,y_min] s=t[xmax,ymax]t[xmax,ymin]t[xmin,ymax]+t[xmin,ymin]

查询很快但构建需要点时间,实时渲染不喜欢。(如何构建应用SAT可看参考链接3。加速构建可以行列分开并行计算)

4.2 加速PCSS-step1

在这里插入图片描述
第一步中要获得对应遮挡区域的平均深度,如上图图蓝色区域平均深度,记为 Z o c c Z_{occ} Zocc,而我们通过mipmap已知全部区域的平均深度 Z a v g Z_{avg} Zavg,红色非遮挡区域的平均深度记为 Z u n o c c Z_{unocc} Zunocc,数量为N。有下式成立:
Z a v g = N u n o c c N Z u n o c c + N o c c N Z o c c Z_{avg} = \frac{N_{unocc}}{N} Z_{unocc} + \frac{N_{occ}}{N} Z_{occ} Zavg=NNunoccZunocc+NNoccZocc N u n o c c N + N o c c N = 1 \frac{N_{unocc}}{N} + \frac{N_{occ}}{N} = 1 NNunocc+NNocc=1 N o c c N = P ( x > t ) \frac{N_{occ}}{N} = P(x>t) NNocc=P(x>t)
到这里只有 Z u n o c c Z_{unocc} Zunocc是未知量,因此VSSM大胆假设 Z u n o c c = t Z_{unocc} = t Zunocc=t,即假设没有遮挡的着色点平均深度就是该着色点的深度,即上图红色值全部都是7。

  • 这么假设的前提是大部分阴影接受面是平面,但接受面如果是曲面或光源不平行时就会有问题。
  • tip:但现在主流方法还是PCSS,因为随着降噪技术发展,有噪声越来越不是问题了。但VSSM的大胆假设思想值得学习。

五,MSM(Moment矩 shadow mapping)

VSSM为了解决PCSS的问题,但它自己也有问题----假设约等太多了。

  • 深度分布复杂时假设为正态分布问题不大,反而场景简单时就可能不成立了,实际可能是多峰分布,会造成light leaking漏光现象如下图。
  • 非平面接收面或光线不平行可能导致artifact。
  • 切比雪夫不等式 t大于均值的条件有可能不满足。
    在这里插入图片描述在这里插入图片描述
  • 以上问题的根本原因都是 深度估计不准,因此引入“使用更高阶的矩来估计分布”—MSM。(这里的矩指 随机变量的指数 x 2 、 x 3 、 x 4 x^2、x^3、x^4 x2x3x4,VSMM相当于使用了2阶矩来估计)「本质相当于某种展开,矩越多越准」

「随着temporal filter的发展,PCSS又流行起来,MSM因为实现麻烦现在用得少了。」

  • MSM思路:用前m阶矩来拟合CDF(会有 m 2 \frac{m}{2} 2m个阶梯,如图),对于深度估计取m为4就可以。(在blocker search和PCF步两步中计算CDF)在这里插入图片描述

六,SDF (Signed Distance Field) soft shadows

  • 有向距离场SDF记录了空间中任何一点到定义该场的物体之间的最小距离,并用正负号表示在物体内外(0表示在物体上)。
  • 不同物体的距离场还可以用插值将不同形状混合。
  • SDF相关知识:最优传输Optimal Transport(顾险峰)
    在这里插入图片描述

6.1 SDF应用

6.1.1 Ray Marching

解决光线和SDF的求交问题,将SDF距离作为安全距离,判断光线是否与物体相交(sphere tarcing)(第一次取的安全距离作为步长,在光线方向取第二次,以此类推,直到安全距离小于一定值或者在该光线上走了很远,然后取最小值)。「运动刚体可以用此方法,但形变物体不行」
Ray Marching

6.1.2 生成距离场软阴影

估计大概的percentage of occlusion(实际上不准但符合人们观察)。下图左模拟人眼看向面光源的阴影距离场计算,对光线方向计算不会被物体遮挡的最小安全角度(光线上每个step都要计算取最小,如下右图),安全角度越小,阴影越硬。
在这里插入图片描述在这里插入图片描述

  • 计算角度做切线用arcsin就能算出,但是在实时渲染中不喜欢开销大的反三角函数,而我们这里只需要获得角度相对大小和Visibility的关系,因此可以用以下近似替代(k的大小是控制阴影的软硬程度,k越大阴影越硬):

a r c s i n S D F ( p ) p − o = m i n { k ⋅ S D F ( p ) p − o , 1.0 } arcsin \frac{SDF(p)}{p-o} = min\{ \frac{k \cdot SDF(p)}{p-o} , 1.0 \} arcsinpoSDF(p)=min{pokSDF(p),1.0}

  • SDF优缺点:
    1. SDF可以快速生成高质量软阴影(在shader里就可以做ray marching),SDF预计算生成后调用shadow map又快又好。
    2. 存储上消耗比较大,需要预计算,生成SDF的时间也长,如果是动态物体消耗就高了。还有artifact,表面参数不好获得,不容易uv贴图。

七,作业1

7.1 硬阴影

  • lights\DirectionalLight.js中完成MVP矩阵的变换得到lightMVP(这部分完全忘了,抄完别人作业以后想起来一点,希望以后别是个坑),上代码:
// 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,-150,150,-150,150,1e-2,400);
  • 获得了正确的lightMVP后,shaders\phongShader\phongFragment.glsl中的vPositionFromLight就是正确的了,用它采样shadowmap后与自身深度对比获得可见性(注意vPositionFromLight需要先转换到NDC 标准空间 [0,1])
  • 做完法线头发身上好多锯齿,出现了1.1中的自遮挡现象,于是在深度判断时加了一个bias,但头发那块bias值很大了还有遮挡但脚部的阴影却没了,出现1.2的典型情况。怎么解决老师也没讲,可能要动态bias。
vec3 shadowCoord = vPositionFromLight.rgb*0.5+0.5;


float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){
  vec4 rgbaDepth = texture2D(shadowMap, shadowCoord.xy);
  float depthlight = unpack(rgbaDepth);

  float ret = shadowCoord.z < depthlight + 0.01 ? 1.0:0.0;//+ 0.01为了避免自遮挡
  return ret;
}

7.2 PCF

这里注意看作业文档中的建议,使用一个圆盘滤波进行随机采样,老师给了两个圆盘滤波,怕我看不懂居然还给了可视化网站(呜呜呜谢谢)!

然后就是采样NUM_SAMPLES个点后取可见性的平均值,注意采样完后还需要设置一个软边缘半径,根据engine.js中可知图像大小为2048,半径自己调就行了。

float PCF(sampler2D shadowMap, vec4 coords) {
  poissonDiskSamples(coords.xy);
  // uniformDiskSamples(coords.xy);

  float visibility = 0.0;

  for( int i = 0; i < NUM_SAMPLES; i ++ ) {
    vec4 rgbaDepth = texture2D(shadowMap, coords.xy + poissonDisk[i]*10.0/2048.0);
    float depthlight = unpack(rgbaDepth);
    float ret = coords.z < depthlight + 0.01 ? 1.0:0.0;
    visibility += ret;
  }
  
  return visibility/float(NUM_SAMPLES);
}
  • 这里泊松圆盘采样和均匀圆盘采样的结果是不一样的,我设置NUM_SAMPLES为100,在泊松采样下能获得比较自然的软阴影(下图左),而均匀采样就噪点很多还有明显分层(下图右):
    在这里插入图片描述在这里插入图片描述

7.3 PCSS

float findBlocker( sampler2D shadowMap,  vec2 uv, float zReceiver ) {
  float averagedepth = 0.0;
  int count = 0;

  for( int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; i ++ ) {
    vec4 rgbaDepth = texture2D(shadowMap, uv + poissonDisk[i]*10.0/150.0);
    float depthlight = unpack(rgbaDepth);
    if(depthlight - 0.01 < zReceiver)
    {
      averagedepth += depthlight;
      count += 1;
    }
  }
	return averagedepth/float(count);
}

float PCSS(sampler2D shadowMap, vec4 coords){
  poissonDiskSamples(coords.xy);
  // STEP 1: avgblocker depth
  float averagedepth = findBlocker(shadowMap,coords.xy,coords.z);

  // STEP 2: penumbra size
  float penumbrasize = (coords.z - averagedepth) * 20.0/averagedepth;

  // STEP 3: filtering
  float visibility = 0.0;

  for( int i = 0; i < NUM_SAMPLES; i ++ ) {
    vec4 rgbaDepth = texture2D(shadowMap, coords.xy + poissonDisk[i]*penumbrasize/2048.0);
    float depthlight = unpack(rgbaDepth);
    float ret = coords.z < depthlight + 0.01 ? 1.0:0.0;
    visibility += ret;
  }
  
  return visibility/float(NUM_SAMPLES);
}

在这里插入图片描述

7.4 多光源

搞了半天也没找到WebGL的变暗blend怎么设置,最后勉强搞了个各自0.5透明度叠加的,还有js和Three.js都不会,学习太难了!

找了两个资源mark下吧:
WebGL中文学习网
Three.js学习网

  • 先是loadOBJ.js里添加多光源的meshRender和shadowMeshRender:
let light = renderer.lights[0].entity;
let light2 = renderer.lights[1].entity;
switch (objMaterial) {
	case 'PhongMaterial':
		material = buildPhongMaterial(colorMap, mat.specular.toArray(), light, Translation, Scale, "./src/shaders/phongShader/phongVertex.glsl", "./src/shaders/phongShader/phongFragment.glsl");
		material2 = buildPhongMaterial(colorMap, mat.specular.toArray(), light2, 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");
		shadowMaterial2 = buildShadowMaterial(light2, Translation, Scale, "./src/shaders/shadowShader/shadowVertex.glsl", "./src/shaders/shadowShader/shadowFragment.glsl");
		break;
}

material.then((data) => {
	let meshRender = new MeshRender(renderer.gl, mesh, data);
	renderer.addMeshRender(meshRender);
});
shadowMaterial.then((data) => {
	let shadowMeshRender = new MeshRender(renderer.gl, mesh, data);
	renderer.addShadowMeshRender(shadowMeshRender);
});

material2.then((data) => {
	let meshRender = new MeshRender(renderer.gl, mesh, data);
	renderer.addMeshRender(meshRender);
});
shadowMaterial2.then((data) => {
	let shadowMeshRender = new MeshRender(renderer.gl, mesh, data);
	renderer.addShadowMeshRender(shadowMeshRender);
});
  • 再在WebGLRenderer.js里添加以下代码使两个光源的渲染结果blend:
gl.enable(gl.BLEND);
gl.blendFunc(768, 769);

结果:
在这里插入图片描述

参考链接

  1. https://www.jianshu.com/p/55b66f65ba41
  2. https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
  3. http://www.shaderwrangler.com/publications/sat/SAT_EG2005.pdf
  4. https://zhuanlan.zhihu.com/p/396924653
  5. https://blog.csdn.net/qq_58622402/article/details/124276196
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值