高质量实时渲染 Real-Time 【Shadow】Shadow Mapping and Pertenage Closer Soft Shadows
Shadow Mapping
先放games202的手写笔记,再次感谢闫神的无私奉献
Shadow Mapping 基本知识
2 Pass 的算法
- Light Pass 渲染 Shadow Mapping
- Render from light
- Camera Pass 使用 Shadow Mapping 检测可见性
- Render from eye
- 要检测一个点是否被光照照到,去检测 Shadow Mapping 里面该点对应的UV处的深度和此处的深度,如果检测到SM里面结果比这个此处离light更*,说明前面有东西,此处被阴影遮挡
- 只要深度即可 深度 0 最* 1 最远
- 深度比较时保证两者的含义一致即可,要么都在投影后z值0~1范围内,要么都在实际的坐标下面
渲染一个点光源在场景中投射出的阴影,我们要用shadow mapping技术,
shadow mapping,它是一个2-pass的算法,也就是我们会对场景渲染两次
1-pass我们从Light处看向场景并输出一个从light处看向场景所生成的深度图,也就是所谓的shadow map.
2-pass我们从camera处看向场景渲染一遍将,并参考1-pass生成的shadow map去判断物体是否在阴影中.
shadow mapping是一个完全在图像空间中的算法
优点:
- 一旦shadow Map已经生成,就可以利用shadow map来获取场景中的几何表示而不是场景中的几何.
缺点:
- 会产生自遮挡和走样现象.
步骤:
1.从light处出发看向场景生成一张记录每个像素中最*物体深度的一张图,如图:
图中有很多像素,记录下每个像素中他们各自看到的最浅的深度 or 最*的物体他们的位置在哪,存下来从而得到一张texture.
2.我们从camera(眼睛)出发,在渲染一遍场景.
渲染中的每个像素中都要来判断是否能被light照到,
如果被照到则不在阴影中,反之,则存在于阴影中.
对于这个点来说,如果将这点连向light处会发现在1-pass得到的shadow map中,
这一点的最浅深度 = 2-pass中从点连到light处的深度
(2-pass中得到的点是可以投影回light处求出距离的),
因此这一点是可见的,不在阴影中.
而对于这个点来说,仍然连向light处会发现:
1-pass的shadow map中这一点的最浅深度 < 2-pass中从点连到light处的深度,
因此这一点是被遮挡的,在阴影中.
从这图可以看到,light在左上方,场景中的遮挡关系和阴影表示出的效果是很不错的.
有阴影 VS 无阴影
从Light处看向场景生成的是shadow map,并不是Shading的结果,而是在light的pass中生成一张深度的buffer,
如图中,颜色深的表示值比较小,也就是离light*,颜色浅的就是离light远,值大.
1-pass生成的深度buffer
2-pass生成的深度buffer
在Opengl中,
1-pass真就是在light处放置一个相机,然后往某一方向看去,定义framebuffer写到某个texture上,然后在fragment shader中定义写的是一个深度而非shading的结果,
2-pass中则只需要用1-pass得到的texture即可.
在经过投影变换之后得到的Z其实不是实际上几何上的点到Light的距离,
因此再真正生成阴影时比较两个pass中的depth时需要一致,
也就是要么都用投影后的Z值比较,要么通过两点的位置得一向量算实际距离.
Shadow Mapping的弊端:
自遮挡 self occlusion,aliasing
原因:Shadow Mapping 每个像素记录一块区域的深度,但是这块区域里面任意部分的实际深度如果比记录的更大,那就会判断成被遮挡
地板上的东西会感觉像摩尔纹,但并不是,它是由数值精度造成的一种现象.
Shadow map肯定有自己的分辨率,其每个像素要记录它所看见的最浅深度,可以理解为,每个像素内部他的深度是一个常数
那么从Light处看向场景时,沿某一像素看过去我们看到的位置就认为是像素所代表的深度,也就是认为这个场景在像素覆盖区域内都是一个常数的深度.
在Shadow map看来,场景被离散化为一系列红色小片形成的场景而不是直接的*面.
因为每个红色小片所代表的深度不一样,因此在2-pass,也就是从camera处看向场景时,后面的红色小片在连接light时,会被误认为被前面的红色小片遮挡住,从而产生了错误的阴影,这个现象在light与*面趋于*行时候最严重.
解决方法:
- Adding a (variable) bias 来减少发生的情况
- bias可以根据角度变化
- Con: Detached Shadow (彼得潘?):导致原本可能真的在遮挡的地方被判断成非遮挡
- 工业界并没有试图从根本解决这一问题,而是 "找一个合适的bias"
- 学术界:Second-depth Shadow Mapping
- 最小深度和次小深度的*均 作为后续使用的深度
- 实际没人用
- 要求 watertight,有正面就得有反面,面片也得做成box
- 开销太大,可能并不值得,虽然O(n),但是GPU里面并行处理下会爆炸
只需要把在黄色区域内的遮挡关系给舍弃掉就可以了,而且是有技巧的,当Light处垂直于场景时,我们可以让这个遮挡区域尽可能小一点,当light趋*于*行场景时,我们让这个区域尽可能大一点,我们可以引入一个bias的概念(黄色区域),来降低自遮挡情况,bias是根据角度调整的,并非常数.
具体方式就是当一个点深度大于记录深度的值超过一个阈值时,我们才认为这个点在阴影内。这也是工业界使用较多的一个办法。
引出了另一个问题:
detach shadow
从图中我们可以看到,有一段的阴影被舍弃了,因为当Bias调的过大时,我们会丢失原本应该存在的阴影.
解决方案:
Second-depth shadow mapping
此时我们就舍弃biasd的概念,而是在渲染时不仅存最小的深度,我们还要存第二小的深度,然后我们用最小深度和第二小深度中间的深度来作比较.
(个人理解为,我们在渲染时生成两张深度图,一张存储的是最浅,一张存储的是第二浅,之后将两张图*均从而得到最后的深度图,然后用这张*均得来的深度图和物体算遮挡关系)
以这个为例:
假设一根光线照过来,我们不用最小深度来比较,而是用由最小和第二小深度所得到的红色线来做后续的阴影,此处就没有Bias的事情了.
假设图中是人物的鞋,鞋底是接着一个*面的,我们以右边最极端的部分为例,当一根光线从右边鞋头部分打向鞋底,就算它底部紧贴着*面,我们能得到一个明显的遮挡关系.
然后实际中并没有人去使用这个技术,因为场景内的物体必须都是watertight(非面片),还有就是算的复杂,开销太大,实时渲染不相信复杂度。
只相信绝对的速度!
Shadow mapping的第二个问题就是
走样 Aliasing
原因:使用时的采样频率和生成时的渲染频率不匹配
shadow map本身就存在分辨率,当分辨率不够大自然会看到锯齿,因为shadow map上每一个像素都可以看为小片,那么投出来的阴影自然会存在锯齿.
The math behind shadow mapping Shadow Mapping 背后的数学
在微积分中有很多有用的不等式
微积分有很多不等式
Jensen不等式
Holder不等式
Schwarz不等式
Minkowski不等式
……
如图中的两个不等式为例:
但是在实时渲染中,只关心*似约等:把不等式拿来在某些条件下面当*似等式
实时渲染中重要的一个约等式
两个函数的乘积,想把他们的乘积积分起来,你可以将其拆出来,也就是:
两个函数乘积的积分 ≈ 两个函数积分的乘积,
首先为什么右边第一个函数多了个分母,分母这一项的作用是为了保证左右能量相同而做的归一化操作。
归一化操作 例子:假设f(x)是一个常值函数,也就是f(x) = 2,积分域恒为0-3,
约等式左边,把f(x) = 2代入,则可以提出来变为2倍的g(x)积分
而等式右侧第一个函数代入f(x)的积分是2 * 3 =6,分母的积分是3,结果也正好是2.正好也是2倍的g(x)积分.
右半部分就是Shading,左半部分就是Visilibity
使用条件:(使用Shadow Mapping大致准确)
- small support
- 点光源或者方向光源 → !
- smooth
- 物体是diffuse bsdf 或者区域光源的radiance比较均一
意味着 glossy 情况下 或者 光源比较怪
还会在AO里面用到
在什么情况下约等式结果更加准确:
一般需要两个条件:
- g(x)积分的support较小。这里的support我们可以暂时理解为积分域。
- 2.g(x)在积分域上足够光滑(变化不大)。
把rendering equation代入这个约等式中:
我们把visibility看作是f(x),提取出来并作归一化处理:
红色区域部分时visibility,
g(x)部分为shading的结果.
因此其表示的意义就是,我们计算每个点的shading,然后去乘这个点的visibality得到的就是最后的渲染结果。
这也就是shadow mapping的基本思想。
什么时候这个约等式比较正确呢?
- 控制积分域足够小,也就是说我们只有一个点光源或者方向光源。
- 保证shading部分足够光滑,也就是说brdf的部分变化足够小,那么这个brdf部分是diffuse的。
- 还要保证光源各处的radience变化也不大,类似于一个面光源。
Pertenage Closer Soft Shadows (PCSS)and ertenage Closer Filtering (PCF)
上为硬阴影、下为软阴影
软阴影的效果要更加真实也更自然,强于硬阴影的效果
为了实现软阴影的效果,首先会用一个工具PCF------percentage closer filtering:
PCF最初是为了阴影的反走样/抗锯齿(解决上面的忍者阴影出现的锯齿状)
后来才用在软阴影上:通过把shadow结果求一个加权*均(或者叫filtering).
强调:
不能拿到走样的结果再想着filter,信息缺失
为什么不filter SM?因为没意义,比较完还是binary的visibility
- PCF不是直接在最后生成的结果上模糊,而是在你做阴影判断时进行filtering.如忍者那张图,并不是在他的基础上去进行模糊处理.
在有锯齿的结果上进行filtering
就跟在反走样时一样,不能先得到一个走样的结果再去做在这个走样的结果上进行模糊.
- 也不是Filter the shadow map
如果直接在shadow map上filtering就会造成阴影和物体交界直接糊起来,而且在第二个pass上做深度测试还是非0即1的结果,最后得到的仍然是硬阴影。
我们之前在做点是否在阴影中时,把shading point连向light然后跟Shadow map对应的这一点深度比较判断是否在阴影内,之前我们是做一次比较,这里的区别是,对于这个shading point我们仍要判断是否在阴影内,但是我们把其投影到light之后不再只找其对应的单个像素,而是找其周围一圈的像素,把周围像素深度比较的结果加起来*均一下,就得到一个0-1之间的数,就得到了一个模糊的结果。
PCF方法:(Reeves, SIGGRAPH 87)
- 对每个像素,对它在SM上对应的点以及这个点周围的点进行多次(比如7x7)深度比较,然后进行*均
- 每次都是 0/1, *均之后的visibility值就是 [0,1]
- 可以加权
效果:可以抗锯齿
开销:纹理采样次数成倍增长
filtering size(1x1,3x3,...)重要吗?越小越锐利,越大越柔和
如果不用来抗锯齿,而是用来生成软阴影?不同地方的filtering size不应该一样。
- 观察:遮挡物离阴影越远,越“软”
- 结论:和遮挡物与阴影距离相关
数学:卷积
如图,蓝点是本来应该找的单个像素,现在我们对其周围3 * 3个像素的范围进行比较,由于是在Shadow map上,因此每个像素都代表一个深度,我们让在shadow map上范围内的每个像素都与shading point的实际深度进行一下比较,如果shadow map上范围内的像素深度小于shading point的实际深度,则输出1,否则输出0.
从而得到9个非0即1的值:
最终我们用得到的加权*均值0.667作为shading point的可见性。在计算阴影的时候我们就拿这个作系数来绘制阴影。
可以看到抗锯齿效果很好
我们会发现如果filter size越大,阴影本身越软,所以这个方法也就可以去绘制软阴影,也就是pcss技术。
PCSS算法:
核心:adaptive filtering size
(面光源按点光源的方式来生成SM)
-
对每个像素,对它在SM上对应的点以及这个点周围的点进行多次(比如5x5)深度比较
-
第一步当中的filtering size怎么选? 可以是个固定值,也可以由光照区域大小和接受点离光照的距离来共同决定:把SM放到Light的**面上,获取接受点和面光源的连线对应SM上的区域大小。
-
-
选择那些遮住的点,获取它们*均的深度,估算半影的大小,以此选择filtering size
-
然后再PCF,最终算出半影
数学上:依然是卷积
是符号函数 0 or 1
开销巨大!第一步和第三步涉及多次纹理采样
应用的例子:Dying Light
优化:稀疏采样→噪声→降噪
多光源也要多次重新处理
在这幅图中我们可以看见,笔尖的阴影十分锐利(硬),因为我们可以认为
阴影接受物与阴影投射物的距离越小,阴影越锐利.
因此要解决的一个问题是我们如何决定一个软阴影的半影区。换句话说,就是filter size 有多大的问题:
我们可以看到左下和右上两个黄色虚线形成的三角是两个相似三角形。
如果我们将blocker的位置移动一下,比如越靠*receiver,我们会发现()也就会越小.
用数学来表示半影区():
这里的 和
的大小我们是知道的,所以我们只需要拿到blocker的深度即可。
所以,在PPT中写道: filster size < - > blocker distance
但是这里又有一个问题了,如何确定一个blocker距离光源的位置?
不能直接使用shadow map中对应单个点的深度来代表blcoker距离,因为如果该点的深度与周围点的深度差距较大(遮挡物的表面陡峭或者对应点正好有一个孔洞),将会产生一个错误的效果,我们选择使用*均遮挡距离来代替,所以*常我们指的blocker depth其实是Average blocker depth.
blocker上的每个点距离光源的距离是不同的,深度也是不一样的。这里我们采用取*均深度的方式来表示blocker的深度。
求blocker距离的方法如下:
首先,我们把目标shading point 转换到light space 找到shading point在shadow map上对应的像素。
如果shading point的深度大于这个shadow map上点对应的深度,则说明shadow map上的点就是一个Blocker,然后我们取shadow map上这个点(像素)周围的一些像素,找出能够挡住shading point的点的像素,并求出他们的深度*均值作为blocker的深度。
这个方法还是有一个问题,这个问题就是我们虽然找出了filter size的大小。但是我们需要知道寻找blocker之一步骤中,我们需要找到周围的一些像素,那这个范围又是多少呢?一般我们有两种方法可以解决这个问题。
第一种,就是自己规定一个,比如 4 * 4, 16 * 16,比较简单但不实用.
第二种,是通过计算得到一个范围大小:
我们计算shadow map的时候在光源处设置过相机,如图所示,我们把shadow map放在由相机看向场景形成的视锥中的*截面上,然后将光源shading point相连,在shadow map上截出来的面就是要查询计算*均遮挡距离的部分.这部分的深度求一个均值,就是Blocker到光源的*均遮挡距离。
离光源越远,遮挡物也会更多,所以需要在Shadow map上的一个小区域内查找blocker.
离光源越*,遮挡物会少,所以需要在Shadow map上的一个大区域内查找blocker.
这样我们就得到了PCSS的三个步骤:
- 寻找blocker,并计算*均深度。
- 通过blocker 深度计算filter size。
- 按照PCF方式绘制软阴影。
PCSS本质上就是求出了阴影中需要做PCF的半影部分后再进行PCF的计算,这样动态调节了半影范围,也就是动态设置了PCF的搜索范围,这样我们的硬阴影部分清晰,软阴影部分模糊,动态的实现了不错的软阴影效果。
基础 filtering 技术
Variance Soft Shadow Mapping (VSSM | VSM)
针对性地解决 PCSS 第一步(blocker search)和第三步(filtering) 慢的问题
为什么慢:多次采样
为什么要多次采样?想知道当前点的深度在附*部分的排名(percentage)
VSSM的核心思想:快速计算深度在某一个区域内的均值和方差,估计其排名(利用chebychev不等式),减少采样次数
如何计算Percentage?
- 精准:统计的直方图(通过sample,费)就是PCSS的方法
- *似:猜测是正态分布
- 只要快速能拿到均值+方差这两个信息就可以生成一个正态分布,就可以估计当前点的深度在附*部分的排名
- 均值求法:
- Hardware MipMaping
- Summed Area Tables (SAT)
- 方差求法:
-
排名:即概率论中的 CDF(x) Cumulative Distribution Function
-
对于 高斯分布(Gaussian PDF,包括正态),其CDF称为error function(误差函数)没有解析解,只有数值解 (C++ erf)http://www.cplusplus.com/reference/cmath/erf/ 也可以打表 是精确解
-
但是不一定要精确,由切比雪夫不等式(下),可以通过分布的均值和方差估算排名(percent)(不等式看作约等于)前提条件:
总结:
- 性能上:
- SM 生成:需要一张深度*方的贴图,和深度图一起生成,可并行 复杂度 #pixels
- 均值:Mipmap 或者 SAT
- 运行时:均值 和 方差 都是 O(1) ,切比雪夫的估算 也是O(1) → O(1) 求排名(有xx%的texel挡住这个地方)visibility直接获得,第三步 完美解决
- (动态光照或者动态物体下,SM每一帧都要重新生成)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗