基于静态局部立方体贴图的高效软阴影
概述:
现代移动GPU功能强大,支持丰富的图形特性,能够在保证高性能的同时提升游戏和应用的视觉效果。开发者在利用最新GPU特性时,需考虑两个关键因素:
设备兼容性:为了覆盖尽可能多的用户,游戏应使用较低级别的GPU特性,因为并非所有用户都拥有高端移动设备。
能耗管理:移动设备依赖电池供电,尽管ARM架构优化了能耗,但电池容量有限,需优化性能以延长续航时间。
本章聚焦于一种视觉效果——软阴影(Soft Shadows),介绍一种基于 Local Cubemap 的半动态软阴影渲染技术,兼顾高质量、性能和能耗。
引言:
在本文中,将学习一种基于Local Cubemap的新颖软阴影技术。(参见下图中的示例。)我们相信,通过这种方法,您将能够开发出高质量的局部环境半动态阴影,同时保持高性能并延长电池寿命。
它可以通过使用静态立方体贴图来节省大量带宽。立方体贴图的尺寸可以低于屏幕分辨率,并且质量仍然非常很好。此外,该贴图可以被压缩,例如使用ASTC,从而进一步减少带宽使用。
算法概述 :
算法分两步:
first step:生成静态局部立方体贴图纹理(建议离线生成以便压缩优化)。
second step:场景中应用阴影,主要在片元着色器中完成。通过光源位置和包围体(bounding volume)计算光线向量,求与包围体的交点,利用该交点构造向量从立方体贴图采样,采样的alpha通道决定阴影强度。
What is a Local CubeMap ?
在深入讲解该技术之前,理解“局部立方体贴图”这一概念非常重要。局部立方体贴图不仅仅是GPU普遍支持的立方体贴图纹理类型。如今几乎所有GPU都支持立方体贴图纹理类型,甚至早于着色器出现之前的OpenGL版本就已支持。
局部立方体贴图技术在业界已经存在一段时间 [Bjork 04, Czuba 10, Lagarde and Zanuttini 12, Lopez 14]。
如果我们在局部环境中渲染一个立方体贴图,并打算用传统方式使用它,结果往往不会如预期。比如在 上图 中,摄像机沿向量 V 方向观察星星。如果直接用该向量从立方体贴图中采样,得到的将是笑脸纹理而非星星。
为了获得星星而非笑脸,我们需要使用一个新的向量 V',即从立方体贴图中心 C 指向视线 V 与包围体边界的交点 (P) 的向量。由于包围体可以是任意形状,为简化交点计算,我们使用代理几何体,最简单的代理几何体是包围盒。
(bounding volume)包围体描述了局部环境的边界,即立方体贴图有效且阴影应被应用的空间范围。换句话说,包围体是局部环境的几何近似,必须尽可能贴合真实几何的边界。例如,在一个有四面墙、地板和天花板的房间中,包围盒可以很好地描述该几何体。如果是墙面不平整的环境,比如洞穴,艺术家需要对起伏的墙面进行折中,创建一个近似的立方体包围盒。
因此,局部立方体贴图的概念包括:
一个立方体贴图纹理;
两个额外数据:立方体贴图的位置和包围体(包围盒);
局部矢量校正过程。
从局部立方体贴图中采样纹理的函数与普通立方体贴图采样函数相同。唯一的区别是,用于确定采样纹理的向量需要根据立方体贴图生成位置和包围体进行局部校正。
创建阴影立方体贴图
为了生成局部立方体贴图,我们需要选择一个合适的采样点,通常是局部环境的中心点。然后,从该点出发,沿着六个正交方向(对应立方体的六个面)渲染场景,生成六张纹理图,合成一个完整的立方体贴图。
在渲染过程中,我们将场景中所有几何体的透明度信息存储在立方体贴图的 alpha 通道中。这个 alpha 通道的值代表了光线穿过该方向时的遮挡程度,或者说是光线的透射率。通过这种方式,立方体贴图不仅包含颜色信息,还包含了光线被遮挡的概率,从而为后续的阴影计算提供依据。
此外,颜色通道可以用来存储光线颜色的变化信息,比如彩色玻璃投射的彩色阴影效果。这样,阴影不仅仅是黑暗的阴影,而是带有颜色调制的软阴影。
生成的立方体贴图可以离线完成,这样可以利用纹理压缩技术(如ASTC)对纹理进行压缩,减少内存占用和带宽需求,同时保证阴影质量。
应用阴影
一旦你创建了局部阴影立方体贴图,就可以开始将阴影应用到你的局部环境中了。应用阴影相当简单直接。它需要使用片元到光源的向量从立方体贴图中采样一个纹素(texel),并根据该纹素的 alpha 通道值来应用阴影强度。
渲染阴影的过程对于点光源和方向光源基本相同。
对于点光源,片元到光源的向量可以直接在片元着色器中构建,也可以通过顶点到光源向量的插值获得。
对于方向光源,只需用光照方向向量替换顶点到光源向量(或片元到光源向量)。
无论是方向光还是点光,得到光线方向向量后,都需要对该向量进行局部校正,之后才能从立方体贴图中采样纹素。向量校正可以按如下方式进行(参见下图):
输入参数:
EnviCubeMapPos — 立方体贴图的原点位置
BBoxMax — 环境的包围体(包围盒)最大坐标
BBoxMin — 环境的包围体(包围盒)最小坐标
V — 世界空间中的顶点/片元位置
L — 世界空间中归一化的顶点到光源向量
输出值:
Lp — 校正后的顶点到光源向量,用于从阴影立方体贴图中采样纹素。
校正向量的方法可能有很多种。我们使用的方法列在如下代码中。
// Working in the world coordinate system
vec3 intersectMaxPointPlanes = ( BBoxMax - V) / L;
vec3 intersectMinPointPlanes = ( BBoxMin - V) / L;
// Find only intersections in the forward direction of the ray .
vec3 largestRayParams = max ( intersectMaxPointPlanes ,
intersectMinPointPlanes );
// The smallest value of the ray parameters is the distance
// to the intersection point .
float dist = min ( min ( largestRayParams .x , largestRayParams . y) ,
largestRayParams .z) ;
// Find the position of the intersection point .
vec3 intersectPositionWS = V + L * dist ;
// Get the local corrected vector .
Lp = intersectPositionWS - EnviCubeMapPos ;//矢量局部校正。
其中所有代码都在片元着色器中执行。现在,利用从片元到光源位置的校正向量 Lp,你可以像hlsl 中获取纹素 使用 float shadow = texture(cubemap, Lp); 所示那样从立方体贴图中采样纹素。
纹素的 alpha 通道表示当前处理的片元应应用多少阴影(阴影强度)。此时,一旦完成该阶段并运行程序,你可以将光源位置设置在任意位置,并观察半动态阴影随光源位置变化而变化。
场景中可以有多个光源,但你需要对每个光源单独实现向量校正。不过,纹素采样应来自同一个立方体贴图。关于该技术的其他改进,请参见后续章节。
平滑度(Smoothness)
本节是本章中最有趣的部分,因为一旦你实现了这里描述的内容,你的阴影效果将变得非常出色!在现实世界中,你可以观察到非均匀的阴影半影(penumbra)现象。投射阴影的物体距离越远,阴影边缘越模糊,阴影的强度也越弱。导致这一现象的因素有很多,但最主要的因素是光源的面积大小。这也间接反映了现实世界中的光线反弹(辐射度,radiosity)效应。该技术允许你在场景中实现类似甚至相同的阴影效果。
此外,由于柔和效果依赖于较低的 mipmap 级别,这会减少带宽需求,从而提升性能。
你需要做的就是确保对立方体贴图(cubemap)启用了三线性过滤(trilinear filtering)。然后,在渲染过程中,需要计算从片元位置到光线与包围体(bounding volume)交点的距离。这个距离已经在局部校正过程中计算过,你可以在这里复用它。利用该距离从立方体贴图中采样对应的纹素(texel)。距离应当被归一化到局部环境中的最大距离范围以及立方体贴图的 mipmap 级别数。
不过,还有一种更简单的方法:你可以暴露一个浮点参数作为距离的乘数,用于微调阴影效果的质量,以适应具体的局部环境。
以下代码逐步展示了实现该效果所需的代码:
float texLod = length ( IntersectPositionWS - V );
//图元到交点的距离
texLod *= distanceNormalizer ;
//将单位话的距离为立方体贴图的详细程度。
shadow = textureLod ( cubemap , Lp , texLod ).a;
//获取shadow值
完成上述步骤后,你应该能够在项目中看到非常动态且平滑的阴影效果,如图
与其他阴影技术的结合
如前所述,包围体(bounding volume)需要对局部环境进行近似定义。包围体与真实几何体的贴合度越高,该技术产生误差的可能性就越小。该技术适用于位于局部环境(包围体)边界附近的几何体。例如,图 17.6 中的房间是该技术最适合的环境,因为每面墙都定义在包围体的边界上。
动态物体可能会带来问题。使用该技术生成动态物体的阴影并不合适,因为这需要每帧更新立方体贴图的六个面,开销极大。然而,动态物体仍然可以接收来自立方体贴图的阴影,但无法投射阴影。如果需要动态物体投射阴影,建议采用其他阴影技术。
该技术的核心思想是使立方体贴图保持静态,预先烘焙(pre-bake)所有静态几何体及其周围环境的光照强度。场景中的其他物体,如动态物体,则需要使用另一种技术(例如实例阴影贴图,instance shadowmap)进行渲染。
无论选择哪种技术来渲染动态物体的阴影,合并两者结果的数学计算都相对简单,如图所示,可以将静态阴影和动态阴影合成到最终的阴影结果中。
性能与质量
我们的软阴影技术,与其他阴影技术不同,不需要在渲染到深度纹理、模板缓冲区或帧缓冲对象的颜色缓冲区时进行写入操作。
相比之下,当使用阴影贴图时,通常(如果不是每帧的话)需要更新深度纹理。在更新深度纹理时,数据必须从 GPU 刷新到内存。然后,在应用阴影时,数据又需要从主内存传回 GPU 作为纹理使用。此外,更新深度纹理通常还需要额外的 CPU 端工作,比如剔除和重新提交遮挡几何体以渲染阴影贴图。使用阴影贴图的另一个缺点是无法使用 ASTC 纹理压缩,这种压缩技术旨在减少带宽流量。
该技术每帧需要读取内存的次数与光源数量成正比。更重要的是,当使用较低的 mipmap 级别来实现柔和效果时,传输的数据量更少。因此,该技术不会产生“纹素抖动”伪影。这是因为立方体贴图是静态的,内容在帧间不发生变化,而使用阴影贴图时则不然。(见图 )
未来工作
正如您可能已经注意到的,本章主要聚焦于单一环境下使用单个局部立方体贴图的情况。我们尚未涉及如何处理更复杂的环境,这类环境可能需要使用多个立方体贴图来实现阴影渲染。
尽管我们尚未在这方面进行深入研究,但我们相当有信心,通过融合多个立方体贴图阴影,可以构建出更加复杂且精细的场景阴影系统。
另一个需要特别指出的重要点是,本章假设光源始终位于包围体(bounding volume)之外,以便在片元着色器中以最高效的方式进行计算。对于光源从包围体外部移动到内部的情况,我们尚未在本章中讨论其处理方法,这将是未来研究的一个方向。
结论:
本节所述技术的真正亮点在于,尽管它能够生成高质量的阴影并显著节省带宽开销,但并不依赖任何额外的 GPU 特性。该方法仅需支持至少 OpenGL ES 2.0 的图形管线,几乎可以在市面上所有主流平台上实现。
该技术存在一些限制(前文已提及),例如可能不适用于某些特定的渲染场景或项目需求,但在许多其他应用场景中依然具有良好的适用性和实用价值。
最初提出该方法时,我们认为它仅适合特定的用例,甚至怀疑其是否能应用于“冰洞”项目。然而,实际应用结果表明,尽管洞穴中不规则的几何形状在某些区域与近似的包围体(bounding volume)存在较大偏差,该技术依然能够稳定且高效地生成阴影,表现出良好的鲁棒性和实用性。