HOOOS

百个动态光源怎么办?延迟渲染之外的高效方案与性能权衡

0 5 引擎探索者 游戏引擎延迟渲染动态光源
Apple

你好!很高兴看到你对游戏引擎原理有这么深入的思考。你提出的问题——如何高效处理上百个动态光源,特别是在延迟渲染的背景下,并且关注内存和GPU开销,这绝对是现代图形渲染中的一个核心挑战,也是很多引擎都在努力优化的方向。

你观察到不同光源数量对性能影响很大,尤其在延迟渲染(Deferred Shading)中,这是非常准确的。传统的延迟渲染在处理少量复杂光源时效率很高,因为它将几何体和光照分离处理,避免了同一个像素多次运行复杂的光照计算。但当光源数量激增时,它面临两个主要瓶颈:

  1. G-Buffer尺寸和写入带宽:G-Buffer需要存储每个像素的法线、颜色、深度等信息。光源越多,G-Buffer本身并不会变大,但每个光源都需要对G-Buffer进行采样,并累加其光照贡献。当有大量光源时,虽然光照计算被延迟了,但每次光照计算都需要访问G-Buffer,大量的随机读写会导致内存带宽成为瓶颈。
  2. 光照计算的空耗:在传统延迟渲染中,即使一个光源只影响屏幕上很小一部分区域,或者被几何体遮挡,它的光照计算通常仍然会在整个屏幕(或至少是一个较大的包围盒)上进行。对于上百个光源,这意味着大量的像素会进行不必要的计算,导致GPU资源浪费。

那么,除了传统的延迟渲染,有没有更高效处理上百个动态光源的方案呢?答案是肯定的,主要有两种主流的优化方案:平铺延迟渲染(Tiled Deferred Shading)簇延迟渲染(Clustered Deferred Shading),以及与它们有异曲同工之妙的 前向+渲染(Forward+ Rendering)

1. 平铺延迟渲染 (Tiled Deferred Shading)

平铺延迟渲染是延迟渲染的一种优化,它旨在解决传统延迟渲染中光照计算空耗的问题。

基本原理:

  1. G-Buffer阶段:与传统延迟渲染一样,首先将场景的几何信息(法线、颜色、深度等)渲染到G-Buffer。
  2. 屏幕空间分块:将屏幕空间划分为固定大小的二维“瓦片”(Tiles),例如16x16或32x32像素。
  3. 光源剔除与列表构建:对于每个瓦片,利用其深度信息(G-Buffer中的深度范围)以及场景中所有光源的包围盒,在GPU上对光源进行视锥体剔除。这样,每个瓦片就能得到一个只影响该瓦片区域的光源列表。这个过程通常在计算着色器(Compute Shader)中完成。
  4. 光照计算:对于每个瓦片内的像素,只对影响该瓦片的光源列表中的光源进行光照计算,并将结果累加。

内存和GPU开销权衡:

  • 优势:
    • GPU效率提升:显著减少了每个像素需要计算的光源数量。对于一个只被几个光源影响的瓦片,它不需要处理场景中的所有光源,大大减少了不必要的计算。
    • 内存局部性:每个瓦片的光源列表是存储在GPU共享内存(Shared Memory / LDS)中的,这提高了数据访问的局部性,减少了对全局显存的访问。
  • 劣势:
    • 额外内存:需要额外的内存来存储每个瓦片的光源列表。不过,由于列表通常很短,并且可以使用紧凑的数据结构,这部分开销通常可控。
    • 剔除计算开销:在CPU或GPU上执行瓦片-光源剔除操作会带来额外的计算开销。然而,对于大量光源的场景,这项开销通常远小于传统延迟渲染中重复光照计算的开销。
    • G-Buffer带宽依然是瓶颈:G-Buffer的读写带宽问题没有从根本上解决。

2. 簇延迟渲染 (Clustered Deferred Shading)

簇延迟渲染是平铺延迟渲染的进一步扩展,它在空间上将屏幕空间划分为三维的“簇”(Clusters),不仅考虑了屏幕的X、Y轴,还考虑了深度(Z轴)。

基本原理:

  1. G-Buffer阶段:同上。
  2. 三维空间分块:将视锥体空间划分为一系列三维的“簇”,例如X轴方向16个、Y轴方向16个、Z轴方向16个,形成一个16x16x16的簇网格。Z轴的分块通常是非线性的,以更好地匹配透视投影的深度分布。
  3. 光源剔除与列表构建:在CPU或计算着色器中,对每个簇进行光源剔除,构建每个簇中影响的光源列表。由于是三维剔除,比二维瓦片剔除更精确。
  4. 光照计算:像素的深度值可以确定它属于哪个簇,然后只对该簇的光源列表中的光源进行光照计算。

内存和GPU开销权衡:

  • 优势:
    • 更精确的光源剔除:通过三维空间划分,簇延迟渲染可以比平铺延迟渲染更精确地剔除光源。一个像素所属的簇通常只包含极少数相关光源,进一步减少了不必要的光照计算。
    • 对于深度复杂的场景效果更佳:在深度变化大的场景中,簇延迟渲染能更好地组织光源。
  • 劣势:
    • 更高的内存开销:需要存储每个簇的光源列表,由于簇的数量更多(XYZ),这部分的内存开销会高于平铺延迟渲染,但可以通过限制每个簇存储的光源数量或使用间接索引来优化。
    • 更复杂的剔除逻辑:三维空间划分和剔除逻辑比二维瓦片更复杂。
    • G-Buffer带宽依然是瓶颈:同样未根本解决G-Buffer带宽问题。

3. 前向+渲染 (Forward+ Rendering)

前向+渲染是传统前向渲染(Forward Rendering)的优化,它巧妙地借鉴了平铺/簇延迟渲染的光源剔除思想,但避免了G-Buffer的创建和访问。

基本原理:

  1. 深度预通道:首先渲染一次场景,只写入深度信息。这被称为深度预通道(Depth Pre-pass),用于生成一个屏幕空间的深度纹理。
  2. 屏幕空间分块与光源剔除:与平铺延迟渲染类似,将屏幕划分为瓦片,并利用深度预通道生成的深度信息对每个瓦片进行光源剔除,生成每个瓦片的光源列表。这一步通常在计算着色器中完成。
  3. 最终渲染通道:正常进行前向渲染,为每个几何体绘制。在像素着色器中,根据像素的屏幕坐标找到其所属的瓦片,获取该瓦片的光源列表,然后只对列表中的光源进行光照计算。

内存和GPU开销权衡:

  • 优势:
    • 无需G-Buffer:这是最大的优势。它避免了G-Buffer巨大的内存开销和带宽瓶颈,特别是在高分辨率下。
    • 灵活性:由于没有G-Buffer,它可以更容易地处理透明物体、多重材质等传统延迟渲染难以处理的特效。
    • 光照计算效率高:与平铺延迟渲染类似,通过精确的光源剔除,大大减少了每个像素的光照计算量。
  • 劣势:
    • 额外的深度预通道:需要两次几何体渲染(一次深度,一次最终渲染),这会增加顶点处理的开销。对于顶点复杂度高的场景,这可能是一个负担。
    • 光源列表内存:与平铺延迟渲染类似,需要内存来存储瓦片-光源列表。
    • 深度预通道的GPU开销:虽然只写深度,但渲染整个场景的几何体依然有GPU开销。

总结与选择建议

特性/方案 传统延迟渲染 平铺延迟渲染 簇延迟渲染 前向+渲染
光源数量 少量 (<~50) 中等 (<~200) 大量 (>200) 中等到大量 (>100)
G-Buffer 必需,大开销 必需,大开销 必需,大开销 无需,仅需深度图
内存开销 G-Buffer大 G-Buffer+瓦片列表(中等) G-Buffer+簇列表(较大) 深度图+瓦片列表(最小)
GPU光照开销 高(空耗多) 中等(瓦片剔除) 低(精确剔除) 低(瓦片剔除)
剔除精度 屏幕空间二维 视锥体空间三维 屏幕空间二维
透明物体支持
几何渲染次数 1 (G-Buffer) + 1 (光照) 1 (G-Buffer) + 1 (光照) 1 (G-Buffer) + 1 (光照) 1 (深度) + 1 (最终渲染)
适用场景 静态复杂光照,少量动态光 复杂动态光照,场景深度变化不大 复杂动态光照,场景深度变化大 复杂动态光照,特别关注内存和灵活性

给你的建议:

如果你处理的是“上百个动态光源”的场景,那么传统的延迟渲染肯定会遇到瓶颈。

  • 平铺延迟渲染 是一个非常好的起点,它相对容易实现,并且能显著提升性能。如果你的场景深度变化不是特别剧烈,或者光源分布相对均匀,它会表现得很好。
  • 如果你的场景有非常复杂的几何结构和深度变化(例如室内环境,或者多层楼的场景),并且光源数量非常庞大,簇延迟渲染 可能会提供更好的性能,因为它在深度维度上做了更精细的剔除。
  • 如果你的项目对内存开销非常敏感,或者需要处理大量的透明物体,并且可以接受额外的几何体渲染通道,那么 前向+渲染 可能是最合适的选择。它通过牺牲一些顶点处理性能来换取光照计算效率和内存占用。

在实际开发中,很多现代游戏引擎可能会根据场景的复杂度和需求,混合使用这些技术。例如,对于重要的主光源,可能使用更精确的渲染路径;对于大量的次要动态光源,则采用平铺/簇渲染来优化。

希望这些解释能帮助你更好地理解和选择适合你项目的渲染方案!探索这些原理本身就是一件非常有趣的事情。

点评评价

captcha
健康