游戏中的阴影实现

作者:TC130 2020-03-03


阴影对于提高游戏真实感非常总要,简单总结下游戏中的阴影实现.

先来看下阴影的组成部分


阴影的组成部分


1.平面映射

最简单的情况就是物体在一个平面上投射阴影,这种情况下只是需要通过矩阵把产生阴影点面投射到平面上.


从v点映射到p点:  

令  

推导后写成矩阵的形式:



如果receiver不是一个无穷大的平面,需要通过stencil buffer标记出需要接受阴影的部分.

同样需要注意避免这种情况,这时候不应该绘制出阴影.


右边的情形下不应该绘制出阴影


如果想要Soft Shadow的效果,可以在光源周围使用多个点进行投射阴影,然后取出一个平均值,但是这样产生的阴影性能消耗很大,一般需要几百次采样平均才能得到较好的效果.

另外一种方法是先得到阴影贴图,然后做一个blur处理.这样比较快,缺点就是不符合实际阴影近处清晰远处模糊的特点.

2.Projector实现

和上面的方法非常类似,区别在于先把阴影从光源的视角先渲染occluder得到阴影,再投射到receiver上,这样就可以把阴影作用到不平坦的面上.

这两种方法的缺点就是需要明确地知道occluder和receiver,只能适用于简单的场景.但是性能消耗相对较小,所以在手游上仍然能看到这些方法的应用.

甚至在手游上可以直接将阴影做成一张固定的贴图,以decal的形式贴到地面上,虽然是很简单的形式,也能极大地增强真实感.

3.Shadow Volumes <参考GPU Gems 3 Chapter11>

一种非常过时的渲染阴影的方法,但是其思想很值得学习借鉴.

1.shadow volume:

就是从光源沿着模型边缘拉伸至无限远处加上前盖后盖形成的形状.


shadow volume


2. z-pass算法:

shadow volume阴影的思想就是取一条从视点到目标点的线,每次进入shdow volume,计数加一,每次离开计数减一,这样计数为0的部分就是无阴影的地方.

通常使用stencil buffer来实现,从视点渲染shadow volume集合体,开启z-test,正面部分+1,背面部分-1,stencil buffer为0的部分就是无阴影部分.


z-pass


3. z-fail算法:

z-pass算法有个缺陷,当视点在shadow volume中的时候,会产生错误的结果.

所以就有了z-fail的算法,z-fail算法其实就是从物体背面计数,在z-test fail的几何体部分,在进入shdow volume时计数-1,离开时计数+1,这样就可以规避这个缺陷.


z-fail


不过z-fail算法普遍要比z-pass要慢,因为从背面渲染shadow volume,通常会覆盖更多的像素点,其次从上图可以看出,使用z-fail时必须渲染shadow volume的capping部分(前盖后盖).因此在渲染前可以做一个摄影机是否在shdow volume中的简单判断,来决定使用z-pass或者z-fail算法.

4. 生成阴影体的步骤:

一种最常见的一种生成shadow volume的方法,不过这种方法要求目标模型是封闭的多边形网格(没有空洞,裂隙,自相交).

分为三部分: front capping 前盖-> back capping 后盖-> silhouette 轮廓拉伸成的侧面

front capping就是取模型中面向光源的三角面,方向判断可以通过判断面法线和光源方向的乘积的正负值来判断.

back capping 取模型中背向光源的面,沿光源方向拉伸到无穷远处.

silhouette 判断两个临接面与光源方向不同的边,认为是轮廓边,将每条边扩展拉伸到无穷远处形成一个四边形面.

5. 在无穷远出的渲染:

如何表示无穷远处的点?使用齐次坐标将w分量置为0,xyz表示方向即可.

如何避免图元在摄影机far clip plane外被裁剪掉?

一种方法是使用 GL_DEPTH_CLAMP_NV 扩展, 将far plane外的点clamp到裁剪空间中.不过这个方法好像是只适用于OpenGL 和 NVIDIA显卡.

另外一种方法是稍微修改下摄影机的裁剪矩阵,将far plane设置为无穷远


普通摄影机矩阵


变成下面这样:


远裁面在无穷远处的摄影机矩阵


当然精度或有微乎其微的减少.

6. 适用于非封闭模型的方法:

把模型分成两部分,一部分是面向光源的面,一部分是背向光源的面,分别进行拉伸生成shadow volume,就可以支持非封闭模型.缺点是原来的轮廓边相当于生成了两次,造成性能浪费.


左边是面向光源面,右边是背向光源面,两个加在一起形成正确的结果


7. 使用Geometry shader生成Shadow volume

使用GS可以将生成shadow volume的工作移交给GPU,不过必须用TRIANGLE_STRIP的方式来输入模型.

使用GL_TRINGLES_ADJACENCY_EXT模式来向GS中输入三角形图元,就可以获取三角形的邻接面,以此在GS中进行轮廓边判断,输出Shdow volume等操作.


Geometry Shader中输入的顶点


4. Shadow Map

当下应用最广泛最常见的方法,从光源处出发向光照的方向渲染需要产生阴影的物体,得到保存最近处物体的深度值的shdow map.

对于directional light使用一个足够大的orthographic projection包住所有需要渲染的物体, spot light使用一个和自己光照范围相当的frustrum, omini light沿六个方向生成类似于 cubic environment mapping的 omnidirectional shadow maps.

渲染物体光照时,将像素点代入到光照的矩阵中,和shadow map中该点的深度值比较,如果深度值大于shadow map中深度值,说明该点在阴影中.



因为Shadow map的分辨率限制,可能会出现 self-shadowing,因此需要加上一个小的bias偏移量.


self-shadowing


5. Shadow Map 增强

1.Cascaded Shadow Maps(CSM)
参考文献
通常在渲染视角附近的物体时需要更高的shadow map精度,而直接生成的shadow map往往不符合这个条件,所以将frustum分割成数个部分,提高视角附近shadow map的精度.


CSM




Unity中的CSM,不同的颜色代表不同的CSM区域


2. Percentage-Closer Filtering (PCF)

参考文献
在采样点附近周围选取一些点,分别进行depth-test,将测试结果进行平均.

现在的硬件大多提供周围四点采样的加权PCF深度测试(OpenGL中的sampler2DShadow, DirectX中的 SampleCmp)

再向外的PCF就需要手动偏移采样点,简单的方法是使用N*N的Grid方式采样,高级的方法是在一个disk中用预计算好的Possion分布(或者其他带抖动的分布方式)的点采样,然后每个像素采样时对disk进行旋转,产生soft shadow的效果.


从左到右是 4*4 Grid采样, 12点Possion采样, 12点Possion采样 + 旋转, Possion分布图


3. Percentage-Closer Soft Shadows (PCSS)

参考文献
根据光源到目标点距离和Occluder到光源距离,计算PCF的软阴影程度值,再进行PCF处理,得到近处锐利远处模糊的阴影.




PCSS的阴影效果


4. Linearized Depth
参考文献
普通的阴影可能会出现在远处精度不足的情况,因为一般的阴影深度z值不是线性的,在近处精度大,远处精度小.所以有线性阴影的做法.

改变普通的FS代码,大致写成这样:

  1. float4 vPos = mul(Input.Pos,worldViewProj); vPos.z = vPos.z * vPos.w / Far; Output.Pos = vPos;
复制代码

先将深度值除以Far远平面的值,得到0-1的线性阴影深度值,再乘以w值,这样在光栅化时得到深度值vPos.z / vPos.w,自然是我们得到的0-1的线性阴影深度值.

5. Variance Shadow Map(VSM)

参考文献

用PCF产生软阴影时,每计算一次深度值,需要采样很多个点的深度进行比较然后求和.

VSM是一种Filtered Shadow Map,可以对Shadow map进行blur或者mipmap,每次计算阴影时,不需要采样目标点周围的很多个点,节省性能.

生成两张Shadow Map,一张是普通的深度值,另外一张存储深度值的平方.

对两张Shadow map进行blur,每个新的像素点的值是原来点周围点值的加权平均.


两张ShadowMap 中值的含义,可以得到方差值


如果求得的目标点的深度值小于ShadowMap中的 E(x)值,认为该点被完全点亮,不渲染阴影,这里和普通的阴影渲染一样.

当目标点深度值大于E(x)值时,根据Chebyshev不等式推导出该点周围点深度值大于目标点深度值的概率:



根据这个概率值,就可以来计算软阴影的程度.

6. Exponential Shadow Map ESM

参考文献

ESM也是一种类似VSM的Filtered Shadow Map

假设d代表shadow map中的深度值,z代表目标点的深度值,得到阴影函数f(d, z) = 0 (当d > z) / 1 ( 当d<=z),f(d,z)叫做Heaviside step function.

ESM就是用一个指数函数来模拟f(d,z):

图中可以看出指数函数和Heaviside step function很相近,而且c值越大,近似效果越好.



Shadow Map中存储exp(c*d)的值,可以进行blur,来产生软阴影.

普通的ESM会有精度限制的问题,会限制c的值不能太大,所以有改进版的ESM,具体比较可以参考这个切换到ESM - KlayGE游戏引擎以及上面的参考链接.

7. Pack To RGBA

某些移动平台不支持浮点数纹理,需要将shadow map的深度值pack到RGBA贴图中
  1. //Pack:
  2. vec4 comp = fract(depth * vec4(255.0 * 255.0 * 255.0, 255.0 * 255.0, 255.0, 1.0));
  3. comp -= comp.xxyz * vec4(0.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
  4. //UnPack:
  5. float depth = dot(texture((m_tex), (m_uv)), vec4(1.0 / (255.0 * 255.0 * 255.0), 1.0 / (255.0 * 255.0), 1.0 / 255.0, 1.0))
复制代码

也有使用256替换255的版本,但是用255比256是要好的.在某些硬件上用256会有表现不一致情况,而且精度略低些.

作者:TC130专栏地址:https://zhuanlan.zhihu.com/p/104687855



最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多