[白话图程]是笔者尝试简明介绍图形技术点的的栏目,既是梳理知识加深记忆,也许能和一同学习的伙伴互相交流,也希望给gameplay向开发者在性能和表现力方面有所裨益。
如果错误疏漏也请各路大佬斧正补充。
0. 现实中的阴影
将一盏灯泡视为一个点光源,光沿直线传播,最先接触到的表面被照亮,射线上之后的表面则被遮挡而呈现出阴影。
所以图形学中也是类似,只给被照亮的表面计算光照,其余则是阴影。回到游戏,寻找每根光线的第一个接触表面属于之后才出现的光追渲染管线,对于传统的光栅化管线更常用的做法是shadow map(或shadow texture)。
1. shadow map
笔者倾向于把还在三维空间中具有深度的像素称为片元(frag),而最终输出的颜色称为像素(pixel)。各位应该了解渲染引擎在输出颜色图时,通常还包括一张记录每个像素距离相机距离的深度图(depth image),由于经过遮挡判断,深度图其实记录了占据同一像素位置所有片元的最小深度,这和“记录光线的第一个接触表面”正好契合。
于是shadow map的思路被提出,首先光源处放置一个摄像机计算得到深度图,渲染时场景时任何片元只需要计算相对光源的深度,与shadow map对应位置比较则可判定处在阴影或光照中。
2. 游戏中的应用
对于不同光源,平行光可以使用正交投影很容易将整个场景投影到一个矩形纹理;对于光线方向四散的点光源选择透视投影和六个面的cube map包裹住光源即可,不过具体还是看实际使用,比如一个房间天花板上的点光源用一个能覆盖天花板的单面纹理也足够了。
除此之外还有许多优化方式,比如对于静态光源+静态场景的情况下,可以直接把是否光照烘焙到模型材质中无需任何深度计算;静态光源+静态场景+少量的动态物体(比如npc和玩家)只需要计算动态模型深度无需更新shadow map;但对于动态光源则需要每帧就整个场景重新绘制shadow map,所以出于性能考虑很少引入会产生大范围动态阴影的高速动态光源(许多游戏只有太阳会产生全场景范围的动态阴影)。
事实上总体来说阴影计算是特别耗费性能的,特别是场景模型多,光源多的时候。所以目前游戏中大多数阴影都是烘焙好的,实时动态阴影个数很有限,要么限制会产生阴影的范围,要么限制会产生阴影的光源数量。以及笔者最近才意识到许多游戏中的光源其实只负责把附近一些片元打上颜色而不考虑阴影,光照影响范围和强度小点画面看上去也不会违和。
3. 有限的分辨率
shadow map很巧妙,但作为实际在引擎中应用的技术可能会遇到许多问题。最显著的即分辨率有限,各位应该都见过游戏中阴影边缘的锯齿,很多时候就是shadow map分辨率不足导致的,可以用AA或大力出奇迹增加分辨率的方法缓解。
但开放世界游戏提出了大场景大视距的阴影实现问题,根据上面的分析,要想正确绘制阴影shadow map要把玩家相机可见范围都涵盖进去,小场景尚可,场景规模越来越大怎么办?比如一个户外场景:
如果方块是建筑物那shadow map像素可能比模型三角形都大了,要包裹住整个区域再大分辨率可能都不够。于是分层级绘制的方法被提出,分割多个小区域来处理,提高分辨率利用率:
对分层方法有兴趣的同学可以看下笔者研究原神cascaded shadow map的文章: [摸着原神学图形]平行光的cascade和软阴影
硬堆分辨率当然也是一种做法,但当场景更复杂时大纹理的读取很容易成为性能瓶颈,于是原神采用了shadow map压缩的方法,将2k×2k原始8MB的纹理缩小到274.4KB,笔者也尝试研究了一下相关技术点,有兴趣的同学可以考虑参考: [摸着原神学图形]静态shadow texture压缩