A Deep Dive into Nanite Virtualized Geometry

A Deep Dive into Nanite Virtualized Geometry

Nanite 虚拟几何技术深入解析: 讲座介绍与核心愿景

1. Nanite 的宏大目标:几何领域的“虚拟纹理”

讲座开篇点明了 Nanite 的核心定位和愿景,其目标远不止是技术优化,而是旨在引发一场美术创作流程的革命。

  • 核心定义: Nanite 是 UE5 的 虚拟几何系统 (Virtualized Geometry System)
  • 核心愿景: 从根本上改变美术资产的创作管线 (Fundamentally change the art asset creation pipeline)
  • 灵感来源与类比: 其设计哲学深受 虚拟纹理 (Virtual Texturing) 系统的启发。虚拟纹理技术通过流式加载,让美术师可以几乎无视纹理内存预算,自由使用高分辨率贴图。Nanite 的目标就是在几何体上实现同样甚至更伟大的解放。

2. 旨在解决的传统渲染三大痛点

Nanite 的出现,旨在彻底消除长期困扰游戏开发者的三大传统性能瓶颈。

  • 痛点一:多边形数量预算 (Poly Counts)
    • 传统流程中,美术师必须时刻关注模型的面数,以确保性能达标。
  • 痛点二:绘制调用 (Draw Calls)
    • 场景中的每一个独立物体都会产生 Draw Call,数量过多会造成 CPU 瓶颈。
  • 痛点三:几何体内存限制 (Geometry Memory Budgets)
    • 显存和内存容量有限,无法容纳无限精细和数量庞大的几何体。

核心观点: Nanite 的目标是让美术师和关卡设计师在创作时,不再需要为以上三个限制而妥协

3. 为艺术家和开发者带来的变革

通过解决上述痛点,Nanite 将为内容创作带来两大革命性变化:

  • 对于资产创作 (For Asset Creation):

    • 直接使用影视级资产: 允许将ZBrush雕刻、摄影扫描等产生的数百万乃至数亿面的高精度模型直接导入引擎使用,无需任何手动减面或优化。
    • 终结繁琐的优化工作: 开发者可以告别耗时耗力的手动拓扑、LOD (Level of Detail) 创建、以及为弥补细节而进行的法线贴图烘焙等传统流程。这极大地节省了美术制作的时间和成本。
  • 对于场景构建 (For Scene Building):

    • 无限的场景搭建自由度 (Set Dressing): 美术师可以随心所欲地在场景中放置任意数量的实例,无论是巨石还是螺丝钉,都无需担心性能会因物体数量过多而崩溃。

4. 核心原则与挑战

在实现宏大目标的同时,Nanite 坚守着一个不容妥协的原则,并指出了其面临的根本性挑战。

  • 核心原则: 无损的视觉质量 (No Loss in Quality)。Nanite 的所有优化和效率提升,都必须建立在最终渲染效果与艺术家创作的源资产在视觉上毫无二致的基础上。如果为了速度而牺牲了品质,那就失去了意义。

  • 核心挑战: 这不仅仅是一个内存管理问题

    • 与虚拟纹理主要解决数据(纹理)的存储和流式加载不同,虚拟几何体的问题要复杂得多。
    • 几何体的细节直接影响渲染本身的复杂度,包括光栅化、着色计算等。因此,Nanite 不仅要解决数据流送问题,还必须从根本上重构渲染管线处理几何体的方式。

处理超高精度几何体的挑战

这部分内容深入探讨了在实时渲染中处理超高细节几何体所面临的根本性难题,并分析了为何像体素(Voxels)这样的流行方案在实践中会遇到瓶颈。

1. 几何体 vs. 纹理:一个更棘手的问题

讲座首先将几何体细节管理与大家熟悉的虚拟纹理(Virtual Texturing)进行对比,指出几何体处理的复杂性远超纹理。

  • 核心观点: 几何体细节不仅仅是一个内存管理问题,它直接且深刻地影响着渲染成本
  • 关键差异:
    • 渲染成本关联性: 更多的几何体(如三角形)会直接增加GPU的绘制负担(Rasterization, Shading等)。而纹理主要是内存带宽和存储问题,其渲染成本相对固定。
    • 可滤波性: 纹理可以被简单地滤波(例如通过 Mipmapping),在远处平滑地过渡到低分辨率版本。而几何体不存在类似的、通用的、无损的“滤波”方法,LOD切换往往很突兀或实现复杂。

2. 探索解决方案:体素化 (Voxelization) 与符号距离场 (SDF)

为了解决传统网格(Mesh)的局限性,业界最常讨论的方向是使用体素或隐式表面(如符号距离场 SDF)。然而,这种方法并非银弹。

  • 核心观点: 将传统网格转换为体素或SDF,本质上是一种有损的统一重采样 (Uniform Resampling),这会引发一系列严重问题。
  • 关键术语:
    • 体素 (Voxels): 3D空间中的像素,将连续的几何空间离散化为一个个小方块。
    • 符号距离场 (Signed Distance Field, SDF): 一种隐式表面表示法,存储空间中每个点到最近表面的距离(带符号,内正外负或反之)。
    • 稀疏存储 (Sparsity): 一种优化手段,只存储包含几何表面信息的体素,跳过完全空白或完全实心的区域。
体素化的问题根源

讲座通过一个实例揭示了体素化方案的致命缺陷:

  • 实例分析: 一个200万面的精细模型,即使在采用了稀疏存储优化后,被重采样为1300万体素的窄带SDF,其数据量膨胀了6倍,但视觉效果却变得模糊不清、像一团“Blob”

  • 根本原因:统一重采样 (Uniform Resampling)

    • 类比: 这个过程非常类似于将矢量图形(Vector Graphics)转换为像素图形(Pixel Graphics)。矢量图形无论如何放大都保持清晰,而像素图在分辨率不足时则会失真、产生锯齿。网格就像矢量图,而体素就像像素图。
    • 对不同类型资产的影响:
      • 有机表面 (Organic surfaces): 如生物皮肤,表面细节频率相对均匀,重采样的破坏性较小。
      • 硬表面 (Hard surfaces): 如机械、建筑,包含大量锐利的边缘和精确的平面,统一重采样会极具破坏性,完全摧毁艺术家精心制作的细节。

3. 理想几何体数据结构的核心要求

基于体素化方案的种种弊端,讲座总结了未来理想的几何体解决方案必须满足的几个苛刻条件:

  1. 数据大小与性能的平衡:

    • 必须实现极致的稀疏度 (Maximum Sparsity) 来控制数据量。
    • 但这种稀疏表示不能以牺牲光线投射性能 (Raycasting Performance) 为代价。
  2. 高度自适应性 (Super Adaptive):

    • 数据结构需要在锐利边缘处存储极高密度的采样点,以保持形状。
    • 同时在平滑区域使用非常稀疏的采样,避免浪费存储空间。
  3. 重采样的保真度困境:

    • 一个无法回避的事实是:任何重采样方案都无法100%还原原始创作的几何数据
    • 这就引出一个关键问题:我们是否仍然需要在足够近的距离上,切换回并渲染原始网格 (Original Mesh) 来保证最终的视觉保真度?

4. 现实约束:必须兼容现有内容创作流程 (Content Creation Workflow)

最后,讲座提出了一个决定方案能否在业界被采纳的现实问题:技术方案必须服务于内容创作,而不是颠覆它。

  • 核心观点: 我们不能期望改变整个CG行业的工作流程,新的几何体方案必须能够无缝接入现有的生产管线
  • 具体要求:
    • 必须支持导入外部创作的网格 (Importing of Meshes),无论它们来自哪个DCC软件(Maya, Blender, ZBrush等)。
    • 必须完整支持并利用艺术家在网格上制作的UV坐标
    • 必须兼容基于UV的平铺细节贴图 (Tiling Detail Maps) 等传统美术技术。

几何表示方案的探索与权衡


核心议题:寻找传统多边形网格的替代方案

讲座的这一部分深入探讨了取代传统多边形网格(Meshes)的几种常见技术方案。其核心前提是,我们的目标是替换几何体本身,而不是颠覆整个美术创作管线。这意味着新的方案必须能与现有的材质(Materials)纹理(Textures) 以及尤其是UV映射(UV Mapping) 等工具链协同工作。


一、体素 (Voxels) 的局限

尽管体素提供了一种全新的几何表示方式,但在完全替代传统网格方面,存在多个难以逾越的障碍。

  • 核心观点: 直接用体素替换美术师创作的带有UV的网格是不可行的,因为它会引入大量目前无法有效解决的技术难题。

  • 关键挑战点:

    • UV接缝 (UV Seams): 如何在离散的体素数据上处理和重建连续的UV坐标,并解决接缝问题,是一个巨大的难题。
    • 细小特征丢失 (Thin Feature Problem): 在使用有向距离场 (Signed Distance Fields, SDF) 来表示体素表面时,任何比单个体素还要薄的几何特征(如纸片、铁丝)都会被“抹掉”或消失。
    • 属性泄漏 (Attribute Leaking): 当两个表面在空间中非常接近(相隔几个体素的距离)时,一侧的表面属性(如颜色、材质ID)可能会“泄漏”或影响到另一侧。
    • 动画 (Animation): 如何对体素几何体进行高效且可控的动画(如骨骼蒙皮、变形)是一个极其复杂且远未成熟的研究领域。
  • 结论: 彻底用体素取代现有成熟的网格方案,需要多年基础研究和行业经验的积累,目前时机远未成熟


二、细分曲面 (Subdivision Surfaces) 的问题

细分曲面能够产生无限平滑的表面,但在应用于需要LOD(Level of Detail)的游戏渲染中时,其根本特性带来了问题。

  • 核心观点: 细分曲面的本质是**“放大”(Amplification)** ,它只能在基础网格上增加细节,而不能简化基础网格。

  • 关键挑战点:

    • 无法简化基础网格 (Base Cage): 渲染的最低复杂度被艺术家的 “基础控制笼” (Base Cage) 的复杂度所限制。细分技术无法生成比这个基础笼更简单的模型。
    • 高昂的初始成本: 在影视制作流程中,为了便于雕刻和控制,这个基础笼的复杂度通常已经远超游戏引擎中典型的低模(Low-Poly)标准。这意味着渲染的起始成本(Baseline Cost)非常高
    • 违背核心原则: 这直接违背了 “艺术家创作与渲染成本解耦” 的核心目标。艺术家的创作选择(基础笼的疏密)直接锁死了渲染的最低性能开销。

三、位移贴图 (Displacement Maps) 的拓扑限制

使用位移贴图,特别是矢量位移贴图 (Vector Displacement),可以在极低的多边形模型上增加惊人的表面细节。但它存在一个根本性的数学限制。

  • 核心观点: 位移贴图无法改变表面的拓扑结构 (Topology),特别是无法增加表面的亏格 (Genus)

  • 关键挑战点:

    • 无法改变拓扑: 亏格 (Genus) 是一个描述物体“洞”的数量的拓扑学概念(例如,球体的亏格为0,甜甜圈的亏格为1)。位移只能在表面法线方向(或任意矢量方向)上移动顶点,但无法“凭空”在一个平面上“凿出”一个洞来。
    • 实例说明: 你无法通过对一个简单的平面(Plane)进行位移操作,来生成一个链条(Chain)的形状。因为链条的每一个环都具有亏格为1的拓扑结构,而平面是亏格为0的。
  • 结论: 位移贴图是增加表面细节的强大工具,但不能用于生成具有不同拓扑结构的复杂几何体。


几何表示方法的权衡与选择

1. 探索其他几何表示方法 (Exploring Alternatives)

在决定使用三角形作为核心渲染图元之前,讲座探讨了其他几种流行方案的优缺点,并解释了为何它们不适用于像 Nanite 这样通用、高质量的渲染系统。

1.1 Displacement Mapping (位移贴图)的局限性

位移贴图虽然能增加表面细节,但其作为一种通用的几何表示或简化方案,存在根本性限制。

  • 核心观点: 位移贴图无法改变模型的拓扑结构
  • 关键术语: Genus (亏格)
    • 这是一个拓扑学概念,通俗地讲,它描述了一个物体表面上有多少个“洞”或“环柄”。
    • 例如,你无法通过位移贴图将一个球体(Genus 0)变成一个甜甜圈(Torus, Genus 1)。位移只能在法线方向上“推”或“拉”顶点,无法创造出新的洞。
  • 适用场景与问题:
    • 优点: 将高模信息烘焙到位移或法线贴图上是一种均匀重采样 (Uniform Resampling),对于拓扑均匀的有机表面(如皮肤、岩石)效果很好。
    • 缺点: 对于硬表面 (Hard Surface) 模型,这种方法是毁灭性的。它会破坏艺术家精心制作的锐利边缘、倒角和轮廓,除非艺术家进行极其细致的手动控制。
  • 结论: 位移贴图是很好的近距离细节放大 (Amplification) 技术,但不是一个合格的通用几何简化 (Simplification) 方案
1.2 Point-Based Rendering (点云渲染)的挑战

点云渲染速度极快,但要达到高质量渲染,面临着难以解决的问题。

  • 核心观点: 点云渲染的核心缺陷在于缺少几何连接性信息,导致无法可靠地重建表面。
  • 优点:
    • 渲染速度极快: 将点“喷射”到屏幕上非常高效。
  • 缺点:
    • 过绘制 (Overdraw): 如果不进行任何处理,会导致大量的重复着色。
    • 空洞填充 (Hole Filling): 为了解决点与点之间的缝隙,需要填充算法。但最大的问题是,算法无法区分“本该存在的缝隙”和“需要被填充的空洞”
  • 根本原因:
    • 要准确判断是否需要填充,必须拥有连接性数据 (Connectivity Data)
    • 在传统渲染中,这些数据就是索引缓冲 (Index Buffer),它定义了顶点如何连接成三角形。没有这些信息,点云本质上是一盘散沙。

2. 最终选择:为什么是三角形?

在对多种方案进行长期探索后,团队得出结论,对于他们的需求而言,三角形依然是最佳选择。

  • 核心观点: 三角形在质量和性能之间提供了最佳的平衡,并且是构建通用渲染系统的最可靠基础。
  • 关键决策依据:
    • 通用性: 其他表示方法(如点云、体素)通常带有强烈的视觉风格。如果一个游戏可以围绕这种美学风格构建,并接受其优缺点,那么这些方法非常有价值。
    • 引擎的责任: Unreal Engine 作为一个通用引擎,不能强加一种特定的艺术风格。它必须能够支持从写实到卡通的各种风格,而三角形是实现这种灵活性的最强大图元。
    • 结论: 因此,三角形是 Nanite 的核心

3. 构建先进的三角形渲染管线

既然确定了使用三角形,下一步就是构建一个当前最先进的(State-of-the-art)三角形渲染管线。这里介绍了 Unreal Engine 渲染架构的演进。

  • 核心观点: Unreal Engine 的现代渲染架构基于保留模式 (Retained Mode) 设计,为高效处理海量数据奠定了基础。
  • 关键术语: Retained Mode (保留模式)
    • 场景数据常驻显存: 将场景的完整几何数据存储在 VRAM 中,并在帧与帧之间持久化 (Persists across frames)
    • 稀疏更新 (Sparsely Updated): 无需每帧都重新上传所有数据。系统只在场景发生变化时(如物体移动、变形),才更新那一小部分数据。
    • 这种设计避免了传统立即模式(Immediate Mode)每帧提交大量渲染命令和数据的开销,是处理大规模场景的关键。

GPU 驱动的剔除管线与剔除技术

本节深入探讨了 Nanite 实现大规模场景渲染的核心技术之一:一个完全在 GPU 上运行的高效剔除管线。其关键在于如何从海量的几何数据中,快速找出当前帧真正需要绘制的部分。

一、GPU 驱动管线的基石

为了实现完全由 GPU 主导的渲染流程,Nanite 在数据组织和管线设计上做了几个关键的预设。

  • 持久化、稀疏更新的场景数据 (Persistent, Sparsely Updated Scene Data):

    • 核心观点: 整个场景的几何体数据被视为一个巨大的资源,常驻于显存中,并且在帧与帧之间保持不变,只在需要时(例如物体移动或变形)进行稀疏、局部的更新。这避免了传统渲染管线中每帧都需要从 CPU 向 GPU 大量传输数据的开销。
  • 统一的大型资源管理 (Unified Large Resource Management):

    • 核心观点: 所有 Nanite 网格数据都存储在单个、巨大的资源中。这种设计使得 GPU 可以在一次操作中访问到任意网格的任意部分,而无需依赖 Bindless Resources 这类现代图形 API 的特性,从而获得了更好的兼容性和底层控制。
  • 全流程 GPU 驱动 (Fully GPU-Driven Process):

    • 基于以上数据结构,整个渲染流程可以被高度整合。
    • 实例可见性判断: 仅用一个 Compute Shader Dispatch 就可以确定当前视口中所有可见的物体实例。
    • 三角形光栅化: 在仅绘制深度的预阶段(Depth Pre-pass),所有可见的三角形都可以通过一个 Draw Indirect 命令一次性提交给硬件进行光栅化。

二、核心问题:剔除不可见的三角形

尽管 GPU 驱动的管线极大地提升了提交(Submission)效率,但我们仍然可能在处理大量最终对画面没有任何贡献的三角形(例如被遮挡或在视锥外的三角形)。为此,Nanite 引入了更细粒度的剔除。

  • 引入三角面集群剔除 (Triangle Cluster Culling):

    • 核心观点: 将剔除粒度从单个三角形提升到三角形集群(Cluster)。我们将一组邻近的三角形打包成一个集群,并为每个集群计算一个包围盒(Bounding Box)。后续的剔除操作都以集群为单位进行,这极大地降低了需要处理的剔除单元数量。
  • 集群剔除的具体方法 (Specific Cluster Culling Methods):

    1. 视锥剔除 (Frustum Culling): 基于集群的包围盒,剔除掉完全位于视锥体之外的集群。
    2. 遮挡剔除 (Occlusion Culling): 基于集群的包围盒,判断其是否被场景中其他物体完全遮挡。
    3. 背向剔除 (Backface Culling): 讲座中特别提到,基于锥体(Cone-based)的背向剔除通常是不值得的。因为绝大多数背向的集群,也同样会被遮挡剔除算法所剔除,所以单独进行背向剔除的收益很小。

三、遮挡剔除的实现:层级Z缓冲 (Hierarchical Z-Buffer)

遮挡剔除是整个流程中至关重要的一环,其效率直接决定了 Nanite 的性能。

  • 关键术语: 层级Z缓冲 (Hierarchical Z-Buffer, HZB)

    • HZB 是一种 Z-Buffer 的 Mipmap 版本,它存储了屏幕空间中一块区域内最远的深度值。通过查询 HZB 的低分辨率 Mip 层,我们可以非常快速地判断一个物体的包围盒是否有可能被遮挡。
  • HZB 遮挡测试流程:

    1. 将集群包围盒投影到屏幕空间,计算出其覆盖的屏幕矩形(Screen Rect)
    2. 在 HZB 的 Mipmap 链中,找到一个合适的 Mip 层,使得该屏幕矩形在该 Mip 层上的大小约为 4x4 像素
    3. 将包围盒的最近深度值与该 4x4 区域在 HZB 中存储的最远深度值进行比较。如果包围盒比 HZB 中的所有像素都远,那么它就被认为是被遮挡的(Occluded)

四、遮挡物数据的来源:一个“先有鸡还是先有蛋”的问题

HZB 遮挡剔除非常高效,但它引出了一个根本问题:在我们渲染任何东西之前,用来进行遮挡测试的 HZB 从何而来?

  • 传统方法的局限性:上一帧深度的重投影 (Reprojecting the Previous Frame's Z-Buffer)

    • 一种常见的思路是利用前一帧渲染完成的深度缓冲(Z-Buffer),通过重投影技术将其转换到当前帧的摄像机视角下,并以此构建 HZB。
    • 主要缺陷: 这种方法是近似的(Approximate)非保守的(Not Conservative)。由于摄像机移动和物体运动,重投影会产生空洞和错位,可能导致本应可见的物体被错误地剔除,从而引发闪烁等视觉瑕疵。
  • 核心思想的转变:从重投影“结果”到重投影“原因”

    • 根本假设: 渲染具有很强的时间连贯性(Temporal Coherence)。也就是说,上一帧可见的物体,在当前帧很大概率仍然是可见的
    • 思想转变: 与其重投影上一帧的深度图(渲染结果),不如直接重投影上一帧可见的几何体本身(产生结果的原因)。这种方式可以提供更精确、更保守的遮挡信息,为当前帧的剔除提供一个稳定可靠的基准。这是 Nanite 管线设计的精髓所在,也为下一部分的讲解埋下了伏笔。

从高效遮挡剔除到解耦可见性与着色

本节内容承接上一部分关于可见性剔除的讨论,提出了一种更先进的遮挡剔除方法,并以此为基础,探讨了渲染管线从纯深度(Depth-Only)向支持完整材质(Materials)演进时所面临的核心挑战与架构选择。


一、更进一步的遮挡剔除:双通道遮挡剔除(Two-Pass Occlusion Culling)

在前一部分讨论了基于深度缓存(Depth Buffer)的剔除技术后,这里提出一个更高效的思路:与其复用(Reproject)上一帧的深度缓存,不如直接复用上一帧被判定为可见的几何体本身。这个思路催生了双通道遮挡剔除算法。

  • 核心观点:利用帧间连贯性,直接将上一帧的可见物体作为本帧遮挡测试的“初始遮挡物”,从而快速剔除大量被遮挡的物体。

  • 算法流程

    1. 渲染旧的可见集:首先,渲染上一帧(Previous Frame) 被判定为可见的几何体集合,生成一个基础的深度缓存。
    2. 构建HZB:基于上一步的结果,构建一个层次化Z缓存(Hierarchical Z-Buffer, HZB)。
    3. 测试并渲染新增物体:使用这个HZB来测试本帧中所有物体的可见性。对于那些通过了测试不属于上一帧可见集的“新”可见物体,才将它们真正渲染出来。

通过这个流程,渲染管线在处理静态或缓慢变化的场景时,可以极大地减少需要测试和绘制的物体数量,是一种非常高效的遮挡剔除方案。


二、超越深度:解耦可见性与材质(Decoupling Visibility from Materials)

当渲染管线完成了高效的可见性判断(即知道哪个像素属于哪个三角形)之后,下一步自然是进行着色(Shading),也就是应用材质。为了构建一个高性能、高扩展性的现代渲染管线,一个核心的设计哲学被提出来:将可见性判断与材质计算解耦

  • 核心观点:像素的可见性归属(哪个物体最终显示在该像素上)的确定过程,应该与该物体的材质计算(它是什么颜色、什么质感)过程分离开来。

  • 解耦的目标(Why Decouple?)

    • 避免着色器切换:在传统的前向渲染中,绘制不同材质的物体需要频繁切换Shader和相关资源,解耦可以避免这种高昂的开销。
    • 消除材质计算的Overdraw:如果一个像素被多个物体覆盖,传统的做法会对被遮挡的物体也进行完整的(且最终被丢弃的)材质计算。解耦后,可以确保每个像素只对最终可见的物体执行一次材质计算。
    • 避免深度预处理通道(Depth Pre-Pass):虽然Depth Pre-Pass是解决材质Overdraw的常用手段,但它本身就意味着要将所有几何体再完整绘制一遍,增加了额外的Draw Call和顶点处理开销。
    • 解决像素四边形效率问题(Pixel Quad Inefficiencies):对于非常密集的网格(Dense Meshes),GPU的光栅化以2x2的像素四边形(Pixel Quad)为单位进行。即使只有一个像素被三角形覆盖,整个Quad也可能被激活,导致不必要的导数计算等操作。解耦的架构可以更好地应对此问题。

三、解耦方案的探索与选择

为了实现可见性与材质的解耦,业界主要有两种技术路线:

1. 物体空间着色(Object-Space Shading)
  • 基本思路:在物体自身的空间(例如UV空间或3D纹理空间)中进行着色,代表技术有光线追踪(Ray Tracing)或纹理空间着色(Texture-Space Shading)。
  • 主要缺点过度着色(Over-shading)。这种方法通常会计算物体表面所有点的着色结果,而不管它们最终是否在屏幕上可见或只占了很少的像素。讲座中提到,这可能导致4倍甚至更多的无效着色计算
  • 局限性:虽然可以通过缓存(Caching)来缓解Over-shading问题,但这种缓存机制对于现代渲染中普遍存在的视图相关效果(View-dependent)动画(Animation) 以及 非UV材质(Non-UV-based materials) 支持非常不理想,因此不适合作为渲染管线的基础。
2. 延迟材质(Deferred Materials)
  • 基本思路:这是讲座中选择的方案。它将渲染分为两个主要阶段:
    1. 几何阶段:只处理几何体,确定每个像素最终由哪个图元(Primitive)覆盖。
    2. 着色阶段:根据几何阶段的结果,对屏幕上所有可见的像素进行一次性的材质计算和着色。
  • 具体实现:讲座中特别提到了 Visibility Buffer(可见性缓冲区) 模式。
  • 关键术语与原则
    • Visibility Buffer:与传统的G-Buffer(存储法线、颜色、粗糙度等)不同,Visibility Buffer的目标是写入最少量的几何信息到屏幕空间的缓冲区中。通常,这个信息可能仅仅是物体的实例ID(Instance ID)图元ID(Primitive ID)。然后,在后续的着色阶段,根据这些ID反向查找所需的全部几何和材质数据,再进行光照计算。这是实现解耦的极致和高效的方式。

可见性缓冲(Visibility Buffer)驱动的渲染管线

本节核心讨论了一种替代传统光栅化和G-Buffer填充的几何体处理方法——可见性缓冲(Visibility Buffer)。其核心目标是解耦几何体的可见性判断与材质的着色计算,并最大限度地利用GPU驱动的渲染流程来降低CPU开销。

一、 可见性缓冲(Visibility Buffer)的核心思想

  • 核心观点: 这是一种两阶段渲染方法。第一阶段(Visibility Pass)的目标是以极低的成本确定每个像素“看到”的是哪个三角形,而不是立即进行完整的着色计算。

  • 关键数据: 在第一阶段,我们只向一个屏幕大小的缓冲区(即Visibility Buffer)写入最少量的几何信息,足以唯一标识一个三角形:

    • 深度(Depth)
    • 实例ID(Instance ID)
    • 三角形ID(Triangle ID)
  • 关键术语:Visibility Buffer 一个屏幕空间的缓冲区,其每个像素存储了足以唯一标识可见几何图元(三角形)的最小信息集合。它本质上是场景可见性的一个高度压缩的索引。

二、 工作流程:从可见性到最终着色

该方法将传统的单次光栅化着色流程拆分为两个主要阶段:

  1. 阶段一:可见性/ID预渲染(Visibility Pass)

    • 使用一个完全由GPU驱动的流程,通过单一绘制调用(Single Draw Call) 来提交所有不透明物体。
    • 光栅化阶段的输出极其简单,仅将上述的深度、实例ID和三角形ID写入到Visibility Buffer中。
  2. 阶段二:材质着色与G-Buffer生成(Material Shading Pass)

    • 这是一个全屏的计算着色(或像素着色)过程,每个像素独立执行。
    • 像素着色器的工作流程:
      1. 从Visibility Buffer中加载当前像素的可见性数据(深度、实例ID、三角形ID)。
      2. 使用ID从全局缓冲区中获取该三角形的三个顶点数据(位置、UV、法线等)。
      3. 将这三个顶点的位置重新变换到屏幕空间
      4. 利用这三个屏幕空间顶点位置和当前像素坐标,计算出重心坐标(Barycentric Coordinates)
      5. 使用算出的重心坐标,对从顶点数据中获取的顶点属性进行手动插值
    • 与讲座中的渲染器集成:
      • 讲座中提到,他们并没有直接在此阶段完成最终光照着色。
      • 而是利用插值后的属性(如世界位置、法线、反照率等)来填充G-Buffer
      • 这样做是为了能无缝集成到他们现有的延迟着色(Deferred Shading)渲染管线中。

三、 该方法的优势

  • GPU驱动与性能解耦:

    • CPU开销与场景对象数量无关。可以轻松处理数百万个实例,因为它们是通过单个Draw Call提交的。
    • 材质着色阶段按着色器(Shader) 进行批处理,其数量远少于物体数量。
  • 消除重复光栅化:

    • 每个视图只需要光栅化一次三角形。无需像Z-Prepass(深度预处理)等技术那样,为了减少Overdraw而进行多次光栅化。
  • 极致的Overdraw优化:

    • 真正的着色计算(材质求值)只发生在最终可见的像素上,完全没有无效的着色开销(Overdraw)
    • 由于后续的材质求值是基于ID索引进行数据访问,因此具有非常高的缓存命中率,且没有传统光栅化中的像素四边形(Pixel Quad)效率问题。

四、 瓶颈与挑战:线性缩放问题

  • 核心观点: 尽管比传统方法快得多,但该方法在实例数量三角形数量上仍然是线性扩展(Linearly Scaling) 的。

  • 实例扩展 (Instance Scaling):

    • 在一定范围内(如一百万个实例)是可以接受的。对于 typical 关卡规模,这个问题不大。
  • 三角形扩展 (Triangle Scaling):

    • 这是不可接受的瓶颈。渲染性能与场景总三角形数成正比,这违背了渲染器“无论你扔进多少数据都能正常工作(just works)”的终极目标。
  • 未来方向的暗示:

    • 讲座暗示了线性扩展的瓶颈无法满足终极目标,即使是 光线追踪的对数级扩展(log n) 可能也还不够。
    • 同时,海量场景数据还面临 内存限制 的问题,即使能渲染,也可能无法全部加载到内存中。这为后续章节要讨论的更先进的解决方案埋下了伏笔。

从 LOD 到虚拟几何:应对无限细节的渲染策略

本节内容探讨了在面对远超硬件承载能力的超大规模几何场景时,传统渲染方法遇到的瓶颈,并引出了解决这一问题的核心思想——渲染开销应与屏幕分辨率而非场景复杂度挂钩,以及如何通过层级细节(LOD)数据虚拟化来实现这一目标。


一、 传统方法的两大瓶颈

即使我们拥有了像 Nanite Clusters 这样的高效几何体组织方式,当场景复杂度达到极限时,依然会面临两个无法回避的根本性问题:

  1. 内存瓶颈 (Memory Bottleneck)

    • 核心观点: 高精度场景(如电影级资产、扫描数据)的几何数据量极其庞大,完全无法一次性全部加载到 GPU 或 CPU 内存中。
  2. 性能瓶瓶颈 (Performance Bottleneck)

    • 核心观点: 即使数据能够奇迹般地装入内存,实时渲染海量的几何体(例如,每帧处理几十亿个三角形)对于现有硬件来说也是不现实的。传统的加速结构(如 BVH)虽然能优化查询,但其性能扩展性(通常为 log N)仍不足以应对如此庞大的数据规模。

二、 核心理念转变:开销应与屏幕分辨率挂钩

要解决上述问题,必须从根本上改变渲染成本的衡量方式。

  • 核心观点: 渲染几何体的开销,应该与屏幕分辨率成正比,而不是与场景的几何复杂度成正比。
  • 关键洞察: 屏幕上的像素数量是有限的。当一个物体或一个几何细节在屏幕上的投影面积小于一个像素时,继续渲染其完整的、高精度的三角形细节是毫无意义的巨大浪费。
  • 目标: 无论场景中有多少物体、几何密度有多高,我们希望每一帧渲染的几何簇(Clusters)数量大致保持恒定。这本质上要求渲染性能在场景复杂度方面达到恒定时间(Constant Time)

三、 解决方案:基于层级细节(LOD)的恒定开销策略

实现上述目标的关键技术是层级细节(Level of Detail, LOD)

  1. 数据结构:簇的层级树(Cluster Hierarchy)

    • 核心观点: 将几何簇组织成一个层级树(Hierarchy Tree)。在这个树状结构中,每个父节点都是其所有子节点的简化版本(Simplified Version)。叶子节点代表最高精度的几何细节,而根节点则代表整个模型最粗糙的简化。
  2. 运行时决策:动态“切分”(The Cut)

    • 核心观点: 在运行时,渲染系统会根据当前视点,动态地在层级树中找到一个“切分”(Cut)。这个“切分”定义了当前帧需要被渲染的节点集合。
    • 特性:
      • 视图依赖(View-Dependent): “切分”的位置是根据当前摄像机位置和朝向动态计算的。
      • 连续细节: 同一个模型的不同部分可以处于不同的LOD层级。离镜头近的部分使用树中更深的节点(更高细节),而远处的部分则使用更靠近根的节点(更低细节)。
  3. 决策依据:屏幕空间投影误差(Screen-Space Projected Error)

    • 核心观点: 系统通过计算屏幕空间投影误差来决定是否可以用父节点替代其子节点进行渲染。
    • 工作原理: 如果一个父节点(简化模型)与其所有子节点(精细模型)在当前视角下渲染出来的视觉差异小于一个像素,或者说人眼无法分辨其区别时,系统就会选择渲染这个更简单、开销更低的父节点。

四、 迈向真正的“虚拟化”:按需加载(On-Demand Streaming)

这个基于层级树的 LOD 系统,正是实现虚拟几何(Virtualized Geometry) 的基石。

  • 核心观点: 我们无需将完整的LOD层级树一次性加载到内存中。这使得处理远超内存容量的几何数据成为可能。
  • 类比虚拟纹理(Virtual Texturing): 这个机制与虚拟纹理的工作原理高度相似。虚拟纹理按需加载和卸载 Mipmap 等级的纹理数据,而虚拟几何则是按需加载和卸载不同层级的几何簇数据。
  • 工作流程:
    1. 内存中仅保留当前“切分”所需的节点以及它们的父节点。
    2. 当摄像机移动,需要渲染更高细节时(即需要访问当前“切分”中某个叶节点的子节点),系统会检查这些子节点数据是否在内存中。
    3. 如果数据不在内存(not resident),系统会向磁盘(Disc)发起一个异步加载请求
    4. 在数据加载完成前,系统会继续渲染当前可用的最低LOD层级(即那个叶节点本身)。加载完成后,再切换到更高细节进行渲染。

处理集群化LOD中的接缝(Cracks)问题

1. 集群独立决策带来的问题:接缝(Cracks)

在上一部分我们提出了一个高层概念:基于请求动态加载和剔除几何体集群(Cluster)。然而,这个看似简单的想法在实践中会遇到一个严重的问题——接缝(Cracks)

  • 核心观点: 当系统允许独立的几何体集群做出各自独立的LOD(Level of Detail)决策时,相邻集群的边界就会出现不匹配,从而产生视觉上的裂缝或破洞。

  • 问题根源:

    • 独立LOD决策 (Independent LOD Decisions): 每个集群根据自身与视点的距离等因素,独立选择要渲染的LOD层级。
    • 边界不匹配 (Boundary Mismatch): 如果一个集群选择了LOD 1,而它旁边的集群选择了精细度更低的LOD 2,那么它们共享的边界顶点和边将无法对齐,因为LOD 2的边界已经被简化(坍缩)了,而LOD 1的没有。

2. 初始解决方案(及其缺陷):锁定边界边

为了解决接缝问题,一个直观的“朴素”方案是强制锁定集群间的共享边界。

  • 核心观点: 通过在几何体简化(Simplification)过程中禁止修改(锁定)集群间的共享边界边,来确保无论集群如何选择LOD,它们的边界始终能够完美匹配。

  • 方法 (The Method):

    1. 在生成LOD层级的简化过程中,识别出所有位于集群边界上的边。
    2. 将这些边标记为“不可坍缩”(locked)。
    3. 这样一来,无论简化进行到哪个层级,原始的集群边界形状都会被保留下来。
  • 致命缺陷 (The Fatal Flaw):

    • 三角形废料 (Triangle Cruft) 的堆积: 由于边界从不被简化,大量高密度的三角形会堆积在这些固定的边界上。这些边界就像无法清除的“疤痕”一样贯穿整个LOD层级结构。
    • 简化效率急剧下降: 想象在一个平衡树结构中,从根节点到叶子节点可以画出一条不跨越任何边的直线。这条线就是一条贯穿所有LOD层级的、被完全锁定的边界。这意味着这条边界区域的复杂度永远无法降低,LOD系统的核心优势——“按需降低复杂度”——在这些区域完全失效,最终导致整个系统崩溃。

3. 改进方案:集群分组与同步LOD决策

既然问题的根源是“独立的LOD决策”,那么解决方案就是打破这种独立性。

  • 核心观点: 通过将相邻的集群分组(Group Clusters),并强制组内所有集群在同一时刻做出完全相同的LOD决策,可以从根本上消除产生接缝的可能性。

  • 实现机制 (Mechanism):

    1. 构建时分析 (Build-time Analysis): 在离线构建几何体层级结构时,系统可以检测到哪些集群之间共享边界,并可能产生接缝问题。
    2. 集群分组 (Grouping): 将这些相互关联的集群定义为一个“决策组”。
    3. 同步决策 (Synchronized Decisions): 在运行时,该组内的所有集群不再独立决策,而是作为一个整体共同切换LOD层级。
  • 带来的优势:

    • 消除接缝: 因为组内所有集群的LOD层级永远保持一致,所以它们之间的LOD差异为零,接缝问题自然就消失了。

      No difference in level means no possibility for cracks.

    • 解锁并简化边界边 (Unlock and Simplify Boundary Edges): 既然组内的边界两侧LOD总是同步的,我们就不再需要锁定这些边界了。这些原先的“共享边”现在可以被当作“内部边”一样,在简化过程中自由地被坍缩,从而解决了“三角形废料”堆积的问题。

4. 方案的权衡与限制

虽然集群分组是解决接缝问题的有效方法,但它也引入了新的权衡。

  • 核心观点: 我们不能无限制地扩大分组,否则将失去LOD系统的粒度优势。

  • 限制: 如果我们将一个LOD层级中的所有集群都分到同一个组里,这就等同于回到了传统的、非集群化的离散LOD(Discrete LOD) 方案——即整个模型要么是LOD 0,要么是LOD 1,失去了根据视点位置进行精细化、局部化细节调整的能力。因此,如何智能地进行分组,是在LOD粒度和接缝问题之间取得平衡的关键。


层级LOD构建 - 动态群集分组与简化

本节深入探讨了为实现无缝的层级细节(LOD)过渡,系统是如何在预计算阶段构建几何体群集(Clusters)的层级结构的。核心在于一种巧妙的、逐级交替的分组简化策略,以避免产生传统LOD的接缝问题,并防止简化误差在边界上累积。

一、 核心思想:交替变化的群组边界 (Alternating Group Boundaries)

为了避免一次性处理所有群集(这会退化成传统的离散LOD),系统采用了一种选择性地分组(Selective Grouping) 策略。其精髓在于,用于简化的群组边界在不同LOD层级之间是动态变化的。

  • 核心观点: 在一个LOD层级中作为群组边界(Group Boundary) 的边缘,在下一个更高的LOD层级中,将被包含在新的、更大的群组内部(Interior)
  • 技术优势:
    1. 避免误差累积: 在简化过程中,群组边界上的边缘会被锁定(Locked Edges) 以防止与邻近未分组的群集产生裂缝。由于边界是交替变化的,一条边只会在某个特定的LOD层级被锁定,而在下一层级它就位于群组内部,可以被自由地简化。
    2. 防止边界退化: 这种机制确保了边缘不会因为在多个LOD层级中持续被锁定而累积大量不可简化的“密集网格”(dense craft),从而保证了整体的简化质量。

二、 构建流程:自下而上的群集简化 (Bottom-up Cluster Simplification)

整个层级结构是通过一个自下而上(从最精细到最粗糙)的迭代过程构建的。

  1. 初始化:构建叶子群集 (Leaf Clusters)

    • 将原始模型网格划分为最底层的、最小的单元,即叶子群集
    • 在Nanite的实现中,每个叶子群集大约包含 128个三角形
  2. 迭代循环 (LOD Level Generation)

    • 对每一个LOD层级,重复以下步骤,直到整个模型只剩下一个根群集(Root Cluster):
    • a. 分组 (Group): 选取若干个相邻的群集,形成一个临时群组。分组的主要目的是为了“清理”它们之间的共享边界。
    • b. 合并 (Merge): 将该群组内所有群集的三角形合并到一个共享的列表中。
    • c. 简化 (Simplify): 对合并后的三角形列表进行网格简化,通常是将其三角形数量减少一半
      • 关键约束: 在简化过程中,该临时群组对外的整体边界上的顶点和边是锁定的,以此保证与外部其他群集之间无缝连接。
    • d. 拆分 (Split): 将简化后的网格重新拆分成新的、更粗糙的父群集。例如,4个子群集合并简化后,可以拆分成2个新的父群集。
    • e. 重复 (Repeat): 进入下一个LOD层级的构建。

三、 数据结构与图示:有向无环图 (DAG) 的形成

这个自下而上的构建过程自然地形成了一个有向无环图(Directed Acyclic Graph, DAG) 的数据结构,用以描述不同LOD层级群集之间的父子关系。

  • DAG构建过程:
    1. 起始: 假设我们选择了4个相邻的子群集(它们是DAG中的子节点)。
    2. 合并与简化: 将这4个群集合并、简化,生成一个包含更少三角形的新网格。
    3. 拆分与链接: 将这个新网格拆分成2个新的父群集。
    4. 建立父子关系: 在DAG中,让原来的4个子节点全部指向新生成的这2个父节点。这意味着,在运行时,当需要显示更低细节的版本时,这4个子群集可以被它们的父群集所替代。

图示:四个子群集(底部节点)经过合并、简化和拆分后,生成了两个新的父群集。在DAG中,这四个子节点共同指向了它们的父节点。整个过程保证了与外部群集的边界是无缝的(locked boundary)。


构建集群DAG与图划分策略

这一部分深入探讨了 Nanite 构建其核心数据结构——有向无环图(DAG)的具体流程,并详细解释了其集群分组(Grouping)背后的核心算法思想。


一、 DAG 的构建流程:合并与分裂 (Merging and Splitting)

与传统的自底向上构建树状结构(Tree)不同,Nanite 采用了一种包含“合并”与“分裂”的迭代过程来构建一个更灵活的 DAG (有向无环图)

  • 核心观点: DAG的构建不是简单的聚合,而是一个不断选择、合并、再分裂的迭代优化过程,直至形成单一的根节点。

  • 基本流程:

    1. 初始化: 从一个包含所有基础集群(例如 4x4 三角形组成的集群)的“池子”开始。
    2. 选择与分组: 从池子中选择最优的集群进行分组。
    3. 合并与分裂: 当两个父集群被合并时,它们的子集群会被重新连接到两个新的父集群上。这个操作可以想象成:旧的父节点分裂成两个新的、更大的父节点,而所有的子节点同时与这两个新父节点保持连接。
    4. 迭代: 这个过程会减少池子中集群的总数。将新生成的集群放回池中,重复上述步骤,直到所有集群最终被归纳到一个根(Root)集群下。
  • 关键优势:为何选择 DAG 而非树结构?

    • 核心目的: 避免“锁边” (Locked Edges) 的产生
    • 在传统的树状 LOD 结构中,如果两个集群在很低的层次上合并,它们之间的边界(我们称之为“锁边”)就会被固定下来。这条边界会一直存在于更高层次的 LOD 中,无法在后续的简化过程中被优化掉,从而导致 冗余细节的累积 (collect cruft)
    • DAG 结构通过允许一个子节点拥有多个父节点,打破了这种严格的层级锁定。这意味着一条边界不会被“锁定”在一路向上的简化路径中,它有机会在不同的合并路径中被消除,从而实现更高效的几何简化。

二、 分组策略:基于图划分的边界最小化

选择哪些集群进行合并是整个构建过程的核心决策,这直接影响到最终的简化质量。Nanite 将这个问题转化为一个经典的计算机科学问题——图划分 (Graph Partitioning)

  • 核心观点: 优先组合那些拥有最多共享边界边的集群。共享边界越长,合并后产生的“新”边界就越短,这等同于最小化了潜在的“锁边”数量,为后续的简化提供了更大的自由度。

  • 图划分问题的映射:

    • 图的节点 (Graph Nodes): 代表每一个独立的 集群 (Cluster)
    • 图的边 (Graph Edges): 连接两个在几何上 相邻(即有直接相连的三角形)的集群。
    • 边的权重 (Graph Edge Weights): 两个集群之间 共享的三角形边 的数量。权重越高,代表它们连接得越紧密。
  • 优化目标:

    • 最小化边切割代价 (Minimum Edge Cut Cost)
    • 边切割代价 (Edge Cut Cost) 是一个图论术语,指将图划分为不同分区时,所有横跨分区的边的权重之和。
    • 在 Nanite 的场景下,将集群进行分组就等同于在图上进行一次“切割”。最小化切割代价 意味着我们尽可能地将权重高(即共享边多)的边保留在同一个分区内,也就是将连接紧密的集群优先组合在一起。
  • 特殊处理:

    • 为了防止出现拓扑上不相连的“孤岛 (islands)”被随意分组,算法会为那些空间上邻近但拓扑上不相连的集群之间额外添加一些图的边。这确保了空间上的邻近性也会被纳入分组决策的考量范围,使得分组结果更加合理。

网格聚类与图划分 (Mesh Clustering & Graph Partitioning)

本节聚焦于如何为层次化结构(如DAG)构建最底层的叶子节点 (Leaf Clusters)。核心思想是将这个几何问题转化为一个图论问题,并通过图划分算法来求解。

一、 核心思想:将网格聚类视为图划分问题

为了构建一个高效的层次结构,我们需要将原始网格分割成许多小的三角形簇。如何分割直接影响后续处理(如简化和剔除)的效率。

  • 核心问题: 如何对簇进行分组,才能使得簇之间的连接最少?
  • 解决方案: 将其抽象为图划分 (Graph Partitioning) 问题。
    • 在这个图中,每个三角形簇是一个节点。
    • 如果两个簇相邻,则它们之间有一条边。
  • 优化目标: 求解图的最小边切割 (Minimum Edge Cut)
    • 最小边切割 意味着找到一种划分方式,使得被切断的边(即跨越不同簇边界的边)数量最少。
    • 在图形学中,这直接对应于最小化簇之间的锁定边 (locked edges) 数量。
  • 关键优势: 为后续的网格简化器 (Simplifier) 提供最大程度的自由。当簇内部的“解锁”边越多,简化算法(如边折叠)能执行的操作就越多,从而获得更高质量的简化结果。

二、 叶子节点的构建:一个多维优化难题

创建最底层的叶子节点(Leaf Clusters)是一个需要权衡多个目标的多维优化问题 (multi-dimensional optimization problem)。理想的叶子簇需要同时满足以下几个条件:

  1. 空间范围 (Bounds Extent): 簇的包围盒应尽可能小,以提升剔除效率 (Culling Efficiency)
  2. 三角形数量 (Triangle Count): 每个簇的三角形数量需要严格控制,通常是接近但不超过128个,以完美适配现代光栅化器 (Rasterizer) 的工作负载。
  3. 顶点数量 (Vertex Count): 簇的顶点数量不能超过某个上限,以兼容图元着色器 (Primitive Shaders) 的处理能力。
  4. 边界边数量 (Boundary Edges): 最小化簇之间的共享边界边,这一点与上一节提到的为简化器提供自由度的目标一致。

策略与权衡:

  • 同时优化所有维度是不现实的。
  • 讲座中采用的策略是优先优化两个最相关的维度
    1. 最小化边界边数量
    2. 控制每个簇的三角形数量 (≤128)
  • 选择这两个目标,并期望其他维度(如空间范围和顶点数)因为数据的相关性而自然得到较好的结果。

三、 具体实现与挑战

  • 图的表示: 在进行初始聚类时,操作的图是网格的对偶图 (dual of the mesh)

    • 在对偶图中,每个三角形是一个图节点
    • 如果两个三角形在原始网格中共享一条边,则它们对应的图节点之间存在一条边。
    • 在这个图上进行划分,最小化切割边就是最小化簇之间的共享边。
  • 技术选型: 使用了业界流行的图划分库 METIS 来解决这个复杂的优化问题。

  • 实践中的挑战:

    • 标准的图划分算法通常旨在平衡分区的大小,但不保证严格的尺寸上限(例如,严格小于等于128个三角形)。
    • 讲座中的实现通过引入一定的冗余量 (slack) 和设计回退机制 (fallbacks),强制算法满足这个严格的上限约束,尽管在极少数情况下可能会失败。
  • 统一的划分过程:

    • 一个重要的发现是:从整个网格生成初始叶子簇的过程,与后续在层级结构中将一个大簇分裂 (split step) 成几个小簇的过程,本质上是完全相同的算法。这表明该聚类方法具有良好的一致性和可复用性。

网格简化与误差度量

在离线构建(Baking)过程的最后一步,我们需要对已经分好簇(Cluster)的网格进行简化,以生成多层次细节(LOD)。这一步的核心在于如何高质量且高效地进行简化,并产出一个精确的误差度量,为运行时的 LOD 选择提供依据。


一、 网格简化 (Mesh Simplification)

1. 核心方法:边折叠 (Edge Collapsing)

Nanite 采用的是业界非常成熟和典型的网格简化算法:边折叠抽取 (Edge Collapsing Decimation)

  • 基本原理: 通过迭代地将网格中的一条边(Edge)折叠成一个顶点(Vertex),从而减少三角形数量,达到简化网格的目的。
  • 工程实践: Epic Games 对此算法的实现进行了高度优化和精炼,据讲者所言,其在质量和速度上超越了市面上任何商业化的解决方案。
2. 误差度量:二次误差度量 (Quadratic Error Metric - QEM)

为了决定“应该折叠哪条边”以及“新顶点应该放在哪里”,Nanite 使用了经典的 二次误差度量 (QEM) 算法。

  • 优化目标: 算法的目标是寻找能够最小化误差的边折叠方案。这个误差不仅包括新顶点的几何位置 (positions) 偏差,还包括其他顶点属性 (attributes)(如 UV、法线、顶点色等)的偏差。
  • 产出: 简化过程完成后,算法会返回一个因简化而引入的误差估算值 (estimate of the error)。这个估算值是整个 Nanite 系统质量和效率的基石。

二、 误差度量的挑战与感知启发 (The Challenge of Error Metrics)

如何设计一个优秀的误差度量,是整个简化流程中最具挑战性的部分,Epic 在此投入了巨大的精力(据估算约一个“人年”)。

1. 核心目标:可用于运行时决策的像素误差

这个在离线时计算出的误差值,最终会在运行时被使用。

  • 运行时换算: 它会被投影到屏幕空间,换算成一个像素误差 (pixels of error) 的数值。
  • LOD 选择: 引擎根据这个像素误差来决定具体应该选用哪一个层级的 LOD 进行渲染。
  • 设计要求: 因此,这个误差度量必须是一种感知启发式 (Perceptual Heuristic) 的度量,它不仅需要能在运行时被高效地评估 (highly efficient evaluate form),还需要在离线简化时成为一个可以被直接优化 (directly optimized for) 的目标。
2. 一项近乎不可能的任务 (An Almost Impossible Task)

在离线进行网格简化时,我们无法预知它在最终场景中会被如何使用,这使得设计一个完美的、基于感知的误差度量变得极其困难。我们缺乏以下上下文信息

  • 材质未知: 无法预知网格最终会使用什么材质。
    • 法线误差 (Normal Error): 不知道物体表面有多光滑/镜面 (shiny),因此难以评估法线变化对高光的影响。
    • UV 误差 (UV Error): 不知道会应用什么纹理 (texture),因此无法判断 UV 扭曲的视觉影响程度。
  • 顶点色用途未知: 不知道顶点色的具体用途(例如,是作为遮罩还是直接显示颜色),因此也无法评估其变化带来的视觉影响。
3. 当前方案与未来展望
  • 现状: 目前的方案并非完美,在极少数情况下可能会出错(但讲者表示这些错误通常难以被察觉)。
  • 未来: 理论上,可以通过引入更多关于人类视觉感知 (human perception) 的因素,让误差评估变得更加激进,从而在不牺牲视觉质量的前提下进一步提升性能。

三、 运行时部分 (Runtime Portion) - 初探

讲座内容从离线构建部分过渡到运行时。

  • 核心任务: 每一帧,系统需要根据当前的视点 (View),从预先构建好的簇层级结构 (Cluster Hierarchy) 中,动态地选择需要被渲染的簇。
  • 一个有趣的伏笔: 讲者提到,在离线构建的每一次迭代中,实际上都产出了两套 (two sets) 数据。这为接下来的内容埋下了伏笔。

Nanite 运行时 LOD 选择与并行化

本节深入探讨了 Nanite 在运行时如何根据视图动态选择合适的细节层次(LOD),以及如何在 GPU 上高效地并行化这一决策过程。

一、 可替换集群:无缝 LOD 的基石

这是 Nanite 实现平滑、无裂缝 LOD 切换的核心机制。

  • 核心观点: 在网格构建(Build)的每次迭代中,系统会生成两组集群(一组简化前,一组简化后)。这两组集群虽然三角形数量不同,但共享完全相同的边界
  • 关键特性: 可替换性 (Interchangeability)。由于边界一致,这两组集群可以在原始网格中互相替换,而不会产生任何视觉上的裂缝 (Cracks)。这正是整个 LOD 系统的精髓。

二、 运行时决策:基于屏幕空间误差

在运行时,引擎需要决定在原始集群和简化集群之间如何选择。这个决策完全基于视觉误差。

  • 核心观点: 引擎通过估算每个集群在屏幕上产生的屏幕空间误差 (Screen Space Error) 来决定是否使用它。
  • 误差计算流程:
    1. 预计算几何误差: 在离线构建阶段,简化器(Simplifier)会为每次简化操作计算一个几何误差值。
    2. 实时投影到屏幕: 在运行时,这个预计算的几何误差会被投影到屏幕空间。这个投影过程会考虑:
      • 距离 (Distance): 物体离摄像机越远,同样的几何误差在屏幕上显得越小。
      • 投影角度畸变 (Projection Angle Distortion): 当表面与视线方向接近平行时,投影畸变会更大。
    3. 最坏情况分析: 为了保证视觉质量,误差投影计算是在集群的包围球 (Bounding Sphere) 内,能够产生最大投影误差的点上进行的。这是一种保守且稳健的策略。

三、 并行化挑战:确保集群组决策一致

对于被分组(Grouped)的集群,它们必须作为一个整体做出相同的 LOD 决策,以避免出现一部分显示高模、一部分显示低模的“边界断层”。

  • 核心问题: 如何在没有昂贵的通信开销(如 one-to-many expansion)的情况下,让一个组内的所有集群在并行环境中做出完全一致的决定?
  • 解决方案: 相同输入,相同输出 (Same Input, Same Output)。这是一个非常简洁且高效的并行计算原则。
    • 具体实现:
      • 一个集群组内的所有集群,都存储完全相同的并集误差值 (Union Error Value)
      • 它们也使用完全相同的包围球边界 (Sphere Bounds) 来进行误差投影计算。
    • 结果: 由于决策所需的所有输入数据都完全相同,每个集群独立计算的结果也必然完全相同,从而实现了无需通信的并行一致性。

四、 LOD 选择的本质:寻找层级图的“切割”

仅仅判断一个集群的误差是否足够低是不够的。因为在同一片区域,可能会有很多不同细节层次的集群都满足误差要求。我们只想绘制其中最合适的一层。

  • 核心观点: LOD 选择的本质是在整个集群层级结构中,寻找一个视图相关的切割 (View-dependent Cut)。这个“切割”所包含的集群集合,就是当前帧需要渲染的几何体。
  • 关键术语: 这个层级结构并非简单的树,而是一个有向无环图 (Directed Acyclic Graph - DAG)
  • 最终挑战: 如何在 GPU 上高效地并行计算出这个“切割”?

五、 并行化“切割”算法:本地化的父子节点决策

讲座揭示了确定“切割”位置的算法,其关键在于决策的本地性 (Locality),这使其极度适合大规模并行化。

  • 核心观点: “切割”发生在层级结构中,当一个父节点的误差对于当前视图来说过高,而其子节点的误差又恰好足够低的位置。
  • 决策规则 (The Cut Rule): 一个集群节点被选中进行渲染(即成为“切割”的一部分),当且仅当:
    • 它的 父节点的误差过高 (Parent's error is too high),无法满足当前视图的精度要求。(父节点说:“我不行”)
    • 并且
    • 自身的误差足够小 (Its own error is small enough),可以被渲染。(子节点说:“我可以”)
  • 并行化优势: 这个 “父拒绝,子接受” (Parent says no, Child says yes) 的判断逻辑是完全本地化的。它只依赖于节点自身和其直接父节点的信息,不依赖于到根节点的完整路径。因此,这个判断可以为层级图中的所有节点同时并行评估,实现了在 GPU 上的高效切割计算。

Nanite: 并行化选择、误差控制与平滑过渡

这部分内容深入探讨了 Nanite 如何实现高效、并行的集群(Cluster)选择,以及如何解决由此产生的视觉瑕疵(如 LOD 切换跳变)。核心思想是围绕一个精确且单调的误差函数来构建整个系统。

一、 并行化选择的关键:唯一的切割点 (Unique Cut)

为了实现大规模并行化的集群选择,评估过程必须是完全局部 (entirely local) 的,即对一个节点的选择不依赖于其祖先节点的状态。这样,GPU 上的每个线程都可以独立判断一个集群是否应该被渲染。

  • 核心前提: 要实现局部评估,必须保证在从根节点到任意叶子节点的路径上,存在一个唯一的切割点 (Unique Cut)
  • “唯一切割点”的定义: 在一条路径上,选择函数的状态从“不选择”(No)变为“选择”(Yes)只会发生一次,并且之后不会再变回“不选择”。
  • 工作流程:
    1. GPU 并行评估所有集群。
    2. 对于每个集群,根据其视图相关的误差函数和阈值,计算出一个“选择/不选择”的结果。
    3. 由于“唯一切割点”的存在,我们可以确保当一个子节点被选择时,其父节点一定不会被选择,反之亦然,从而避免了渲染层级的混乱和重复。

二、 实现唯一切割点:单调误差函数 (Monotonic Error Function)

要保证“唯一切割点”的存在,选择函数(基于视图相关的误差函数)沿路径必须是单调 (monotonic) 的。

  • 核心观点: 父节点的误差必须大于或等于其所有子节点的误差
    • Error(Parent) >= Error(Child)
  • 实现方式: 这个约束是在离线 DAG 构建 (offline DAG building) 过程中强制执行的。
  • 具体操作:
    1. 修改父节点存储的误差: 在计算父节点的误差时,会取其自身计算的误差和其所有子节点误差中的最大值。
    2. 扩展父节点的包围体: 同样,父节点的包围体(Bounds)会被扩展,以完全包含其所有子节点的包围体。
  • 效果: 通过这两个步骤,可以保证在任何视角下,父节点投影到屏幕上的误差永远不会小于其任何一个子节点的投影误差,从而实现了误差函数的单调性。

三、 解决 LOD 切换的“跳变” (Popping) 问题

当系统在父节点和子节点之间切换时,一个经典的问题就是视觉上的“跳变”(Popping)。Nanite 采用了一种非常巧妙且高效的方式来解决这个问题。

  • 传统方案(被 Nanite 舍弃):
    • 几何渐变 (Geomorphing): 在顶点级别平滑地过渡几何体,运行时开销大。
    • 交叉淡入淡出 (Cross-fading): 同时渲染两个 LOD 并进行混合,需要额外数据和渲染开销。
  • Nanite 的解决方案:亚像素误差切换
    • 核心观点: 如果LOD切换发生在误差小于一个像素 (less than one pixel of error) 的时刻,那么父集群和子集群在视觉上是几乎无法区分 (imperceptibly different) 的。
    • TAA 的妙用: 时间抗锯齿 (Temporal Anti-Aliasing, TAA) 在这里扮演了关键角色。
      • TAA 的设计目标就是为了混合和稳定时间上的亚像素差异 (subpixel differences)
      • 当 Nanite 在亚像素误差的阈值下进行LOD切换时,任何微小的视觉变化都会被 TAA 自然地平滑掉,相当于免费获得了高质量的过渡效果
    • 重要推论: 这也反过来证明了为什么拥有一个精确的误差估算 (accurate error estimate) 至关重要。整个系统的视觉稳定性和无缝过渡都建立在误差计算的准确性之上。

四、 并行选择的局限性与优化方向

尽管基于单调误差的并行选择在理论上是完美的,但在处理大规模场景时,它暴露出了效率问题。

  • 问题: 纯粹的并行评估是极其浪费 (extremely wasteful) 的。
  • 原因: 对于一个庞大的场景,GPU 会并行检查所有的集群,但其中绝大多数 (the vast majority) 的集群都因为过于精细(误差远小于阈值)而不会被选中。这导致了大量的无效计算。
  • 优化方向: 为了解决这个问题,我们需要一种机制来进行快速剔除 (quick rejection),避免对那些明显过于精细的集群进行不必要的评估。这自然地引出了下一步的优化:层级结构 (Hierarchy) 的引入。

LOD 选择的加速结构与并行遍历挑战

1. 为何需要独立的加速结构?

讲座的这一部分聚焦于如何高效地剔除(Cull)大量不应被渲染的几何体簇(Clusters)。对数百万个 Cluster 逐一进行 LOD 测试是完全不可行的,因此必须引入层级结构进行快速剔除。

  • 核心观点: 直接使用 Nanite 数据本身的 DAG(有向无环图) 结构作为加速层级并不是一个好选择。

  • 关键原因:

    • 遍历复杂性: DAG 中一个子节点可以有多个父节点,这种多对一的关系使得在 GPU 上进行高效、简单的并行遍历(Parallel Traversal)变得非常困难。
    • 性能要求: LOD 选择过程需要极高的并行度,而复杂的图遍历逻辑会成为性能瓶颈。
  • 解决方案的基石:

    • 局部决策(Local Decision): LOD 的选择测试(一个 Cluster 是否需要被细分)可以仅凭其自身的误差(Cluster Error)和其父节点的误差(Parent Error) 在局部完成。
    • 解耦(Decouple): 这个特性意味着我们无需遍历原始的 DAG 结构来进行 LOD 判断。我们可以构建任何适合并行计算的、独立的加速数据结构来完成这个任务。

2. 基于“父节点误差”构建 BVH

既然可以构建任意数据结构,讲座选择了 BVH (Bounding Volume Hierarchy)。但这是一个为 LOD 剔除任务特殊设计的 BVH。

  • 核心观点: 用于加速 LOD 剔除的层级结构,其组织依据应该是父节点误差(Parent Error),而不是 Cluster 自身的误差。

  • 设计逻辑:

    1. 剔除条件: 当一个 Cluster 的父节点误差已经足够小,满足当前屏幕空间的精度要求时,这个 Cluster 和它所有的子孙节点就都不需要再被考虑细分了,可以被安全地剔除。
    2. 层级组织: 因此,在 BVH 的一个父节点中,我们需要存储一个保守的边界,这个边界不仅包含其所有子孙节点的空间范围(例如 AABB),还必须包含它们父节点误差的最大值
    3. 遍历测试: 在遍历 BVH 时,如果一个 BVH 节点记录的“最大父节点误差”已经满足了屏幕空间的 LOD 阈值,那么整个 BVH 节点及其下的所有内容都可以被一次性剔除,无需再向下遍历。
  • BVH 结构细节:

    • 节点内容: BVH 的父节点保守地包围其子节点,包围信息中除了常规的空间包围盒,还关键性地包含了父节点误差的包围(例如,存储该节点下所有 Clusters 的最大 Parent Error)。
    • 叶子节点: BVH 的叶子节点并非单个 Cluster,而是一个固定大小的 Cluster 列表(Group-sized lists of clusters)。这样做是为了匹配 GPU 的 SIMD/Wavefront 架构,提高处理效率。

3. GPU 并行遍历的挑战与朴素实现

在 GPU 上遍历这个为 LOD 定制的 BVH 是一个经典的并行扩展工作调度问题 (Parallel Expansion Work Scheduling Problem)

  • 朴素实现(Naive Implementation): 采用多趟(Multi-Pass)渲染管线的方式。

    1. Pass 1: 在一个 Compute Shader Dispatch 中,处理 BVH 的第 0 层(根节点)。
    2. 写入 Buffer: 将所有通过测试的子节点(第 1 层节点)的索引写入一个 Buffer 中。
    3. Pass 2: 启动下一个 Dispatch,从 Buffer 中读取数据,处理第 1 层的节点。
    4. 循环往复: 重复此过程,每一层 BVH 的遍历都对应一次独立的 Dispatch。
  • 朴素实现的致命缺陷:

    • GPU 闲置(GPU Drain): 每个 Pass 都依赖于上一个 Pass 的输出结果。这意味着在两次 Dispatch 之间,GPU 必须等待前一个 Pass 完全执行完毕并写入内存,造成同步点(Synchronization Point)。这会导致 GPU 管线出现大量“气泡”(Bubbles),严重降低硬件利用率。
    • 无效的调度(Empty Dispatches): CPU 在发起 GPU 工作时,并不知道这次遍历需要递归多少层。为了处理最坏情况,CPU 必须发起足够多的 Dispatch 次数(例如,假设 BVH 最大深度为 32,就发起 32 次 Dispatch)。如果某次遍历只深入了 5 层,那么后续的 27 次 Dispatch 将会是完全空的,不执行任何有效工作,造成了额外的 CPU 和 GPU 开销。
    • 缓解措施与代价: 提高 BVH 的分支因子(Branch Factor)(例如,从二叉树变为八叉树)可以降低树的深度,从而减少 Pass 的数量。但这也会导致 BVH 节点的包围盒不够紧凑,反而降低了剔除效率,是一种治标不治本的权衡。

讲座在此处引出了问题,暗示这种朴素的多趟实现方案性能极差,后续会提出更优的、单次调度的(Single-Pass)解决方案。


GPU驱动的层级剔除优化:持久化线程 (Persistent Threads)

一、 传统层级剔除的瓶颈

传统的层级剔除(Hierarchical Culling)通常采用逐层处理的方式,即处理完当前层级的所有节点后,再统一派发(Dispatch)下一层级节点的处理任务。这种模式存在明显的效率问题。

  • 核心观点: 逐层(Level-by-Level)处理的同步开销是主要瓶颈。必须等待当前层级的所有节点(无论其是否通过测试)全部处理完毕,才能开始处理下一层级的节点,这导致了严重的GPU闲置。
  • 具体表现:
    • GPU资源浪费: 每次派发之间,GPU都需要被“排空”(drained),造成了不必要的等待和同步开札。
    • 延迟高: 某个节点的子节点明明可以被立即处理,却必须等待同级的其他所有节点完成,拖慢了整个剔除流程。
    • 利用率低: 尤其是在BVH(包围盒层次结构)遍历的深层,活跃节点的数量可能远小于GPU的并行宽度,导致大量线程被浪费。

二、 引入持久化线程 (Persistent Threads)

为了解决上述问题,我们不再为每一层级都发起新的调度,而是采用一种更高效的工作分发模式。

  • 核心观点: “持久化线程”技术通过在GPU上实现一个微型作业系统(Mini Job System),将多轮调度合并为单次调度,从而实现线程复用和持续工作。
  • 基本思想:
    1. 一次性派发: 在开始时,仅发起一次大规模的Compute Shader派发,启动足够多的线程(通常足以占满整个GPU)。
    2. 线程复用: 这些线程不会在完成单个任务后就退出,而是会持续存在(Persistent),直到没有更多任务可做。
    3. 作业队列: 线程们会从一个共享的作业队列(Job Queue) 中主动拉取(Pop)工作任务(例如,一个待处理的节点)。

三、 算法工作流程

基于持久化线程的层级剔除,其工作流程转变为一个动态的、自驱动的循环。

  • 核心观点: 线程不断地从队列中取出节点进行处理,并将“存活”的子节点重新推入队列,直至队列为空。
  • 步骤分解:
    1. 初始化: 将根节点(或初始节点集)推入(Push)作业队列。
    2. 工作循环: 每个持久化线程执行以下循环:
      • 从队列中原子地(atomically)弹出一个节点。
      • 对该节点执行剔除测试(如视锥剔除、遮挡剔除等)。
      • 如果节点通过测试,则将其所有子节点推回(Push back)到作业队列中。
    3. 终止: 当所有线程都发现作业队列为空时,整个剔除过程结束。

四、 核心优势

这种方法带来了显著的性能和灵活性提升。

  • 单一调度(Single Dispatch): 消除了多次调度的CPU开销和GPU同步延迟。
  • 消除深度限制(No Depth Limits): 算法的递归性体现在数据(队列)而非代码调用上,因此不再受限于渲染API的调度层级或硬件限制。
  • 提高GPU利用率(Improved GPU Utilization): GPU始终保持繁忙状态,只要队列中有工作,线程就会立即投入处理,最大化了并行效率。
  • 性能提升: 根据讲座中的数据,该方法比朴素的逐层调度方法平均快 25%

五、 重要提醒:未定义行为与实际应用

尽管持久化线程非常高效,但它依赖于一个图形API规范中未明确定义的行为。

  • 关键术语: 阻塞算法(Blocking Algorithms)线程调度行为(Scheduling Behavior)
  • 核心风险: 此算法依赖一个关键假设:一旦一个线程组(Thread Group)开始执行并获取了锁(例如,为了安全地访问作业队列),调度器就不会使其被无限期地“饿死”(starved indefinitely)。换言之,它必须能被持续调度以完成任务并释放锁,从而保证系统的“向前推进”(Forward Progress)。
  • 实践结论:
    • 这种行为在 Direct3D或HLSL规范中并未得到保证,理论上属于“未定义行为”(Undefined Behavior)。
    • 然而,在实践中,这种方法在所有经过测试的主流GPU上都能可靠地工作。这是一种业界常用的、基于实践经验的优化技巧。开发者在使用时需要意识到其背后的风险和依赖。

剔除管线优化与遮挡剔除的整合

本节内容深入探讨了 Nanite 渲染管线中两个关键的优化点:如何通过混合不同粒度的剔除任务来提升 GPU 利用率,以及如何将经典的双通道遮挡剔除技术与动态 LOD 和流式加载系统进行整合。


一、 混合 BVH 与 Cluster 剔除以提升 GPU 利用率

核心观点

在 BVH(层次包围盒)遍历的后期,待处理的节点数量会急剧减少,导致 GPU 出现大量空闲。为了解决这个问题,系统将粒度更细的 Cluster 剔除任务与 BVH 剔除任务混合执行,从而填补 GPU 的空闲时隙,最大化硬件利用率。

关键问题与解决方案
  • 问题:GPU 利用率不足

    • 在进行 BVH 剔除时,树的遍历初期(靠近根节点)工作量很大,可以充分利用 GPU 的并行计算能力。
    • 但随着遍历深入到树的末端,活跃的节点(Active Nodes)数量会远小于 GPU 的宽度(Wavefront/Warp Size),导致大量线程闲置。
    • 此时,少数几个深度遍历的延迟会成为主导 (Latency Dominated),拖慢整个剔除阶段的执行时间。
  • 解决方案:混合剔除 (Hybrid Culling)

    • Cluster 剔除整合进持久化层级剔除着色器 (Persistent Hierarchy Culling Shader) 中。
    • 这意味着当 BVH 遍历的工作量不足以喂饱 GPU 时,系统可以立即开始处理已经找到的、更细粒度的 Cluster 的剔除工作。
  • 实现细节

    • 双工作队列 (Dual Work Queues):
      1. 节点队列 (Node Queue): 存放待处理的 BVH 节点。
      2. Cluster 队列 (Cluster Queue): 存放已经通过 BVH 遍历找到的、待进一步剔除的 Cluster。
    • 工作流: 当一个工作线程(Worker)在节点队列中等待新的 BVH 节点时,它可以转而从 Cluster 队列中获取任务进行处理,从而避免了停顿。
    • 避免线程发散 (Divergence): 为了维持高效率,对 Cluster 的处理是分批次 (in batches) 进行的,这有助于减少因任务类型不同而导致的线程发散问题。

二、 动态LOD与流式系统下的双通道遮挡剔除

核心观点

传统的双通道遮挡剔除 (Two-Pass Occlusion Culling) 依赖于追踪前一帧的可见物体集。然而,在 Nanite 这种具有动态 LOD 和流式加载的系统中,这种追踪变得极其复杂。因此,系统采用了一种反向测试 (Reverse Testing) 的思想:用上一帧的遮挡信息去测试当前帧的物体,而非追踪上一帧的物体。

关键挑战与解决方案
  • 核心挑战:追踪前一帧可见集 (Tracking Previously Visible Set) 失效

    • 动态LOD选择: 同一个物体在两帧之间的 LOD (Level of Detail) 选择可能完全不同,导致其代表的 Cluster 集合也不同。
    • 数据流式加载 (Streaming): 上一帧可见的 Cluster,在这一帧可能因为被移出视锥或距离玩家过远而被卸载出内存,无法进行追踪。
  • 解决方案:反向测试

    • 基本思路:不再问“上一帧的可见物体现在是否还可见?”,而是问“当前帧选出的 Cluster 在上一帧是否本应可见?
    • 具体做法:获取当前帧选定的 Cluster,使用它们上一帧的变换矩阵 (Previous Transforms),将其包围盒投影到上一帧的层级剔除缓冲 (HCB, Hierarchical Cull Buffer) 上进行可见性测试。
改进后的双通道剔除流程

这是一个多阶段的过程,旨在利用时序连贯性来提前剔除大部分被遮挡的物体:

  1. 第一轮剔除与绘制 (Pass 1 - Culling & Initial Draw)

    • 使用上一帧完整的 HCB上一帧的变换,来测试当前帧所有LOD选择后的Cluster。
    • 将测试后可见的物体直接绘制出来。
    • 将被判断为遮挡的物体暂存起来,留待后续处理。
  2. 生成初始 HCB (Initial HCB Generation)

    • 基于第一轮绘制出的可见物体所生成的深度缓冲 (Depth Buffer),构建一个当前帧的初始 HCB。这个 HCB 只包含了确定可见的物体,是不完整的。
  3. 第二轮剔除与绘制 (Pass 2 - Re-Culling & Final Draw)

    • 使用这个当前帧的初始 HCB,再次测试在第一轮中被判断为遮挡的物体。
    • 绘制那些在这一轮测试中变为可见的物体(即之前被错误遮挡的物体)。
  4. 生成最终 HCB (Final HCB Generation)

    • 此时,深度缓冲已经包含了所有可见物体的信息。基于这个完整的深度缓冲,生成一个完整的 HCB,以供下一帧使用。
性能代价
  • 这种复杂的剔除与验证流程,其代价是巨大的。
  • 最终结果是几乎将整个 Nanite 渲染管线运行了两次:一次是针对上一帧可见性数据的快速路径,另一次是针对本帧新生成数据的验证路径。这是一种用计算量换取更高剔除效率的典型权衡。


Nanite 渲染管线深入:处理动态遮挡的双通道剔除策略

本节深入探讨了 Nanite 为了处理动态场景和相机移动所引入的一个核心机制——双通道剔除(Two-Pass Culling)。这个策略旨在解决利用上一帧数据进行遮挡剔除时,因物体或相机移动导致的 “disocclusion”(解除遮挡) 区域渲染错误的问题。

一、 双通道剔除的核心思想与性能考量

  • 核心问题: 当依赖上一帧的遮挡信息(HCB) 进行剔除时,当前帧新暴露出来的区域(disoccluded regions)会被错误地剔除掉,导致画面出现空洞或闪烁。
  • 解决方案: Nanite 实际上会运行两次剔除管线
    • 第一通道 (主通道): 使用上一帧的变换信息上一帧的 HCB (Hierarchical Culling Buffer) 来快速剔除绝大部分被遮挡的物体。这是一个基于历史数据的、非常高效的剔除过程。
    • 第二通道 (修正通道): 专门用于“清理”第一通道中可能被错误剔除的区域。它将那些在第一通道中被判断为“遮挡”的物体,用当前帧的变换信息当前帧的 HCB 再次进行测试,从而“找回”那些在新帧中变得可见的物体。
  • 性能优化:
    • 虽然听起来是双倍开销,但第二通道的计算量通常远小于主通道,因为它只处理那些在第一通道中被剔除的候选对象,这通常只是总量的很小一部分。
    • 与遮挡无关的剔除,如视锥剔除(frustum culling)细节层次剔除(LOD culling)只需要在第一通道执行一次即可,无需在第二通道重复,这极大地降低了额外开销。

二、 完整的剔除与渲染流程拆解

结合双通道策略,Nanite 的完整剔除流程如下:

  1. 第一通道 (Pass 1 - 基于历史数据)

    • 输入: 上一帧的变换矩阵 (Transforms) 和 上一帧的层级剔除缓冲 (HCB)。
    • 步骤:
      1. 在 GPU 场景中,对所有实例 (Instance) 进行可见性测试。
      2. 通过测试的实例被送入持久化线程 (Persistent Threads) 进行层级簇剔除 (Hierarchical Cluster Culling),此阶段同时处理 LOD 选择遮挡可见性
      3. 最终可见的簇 (Cluster) 被光栅化到当前帧的可见性缓冲 (Visibility Buffer) 中。
    • 中间产物: 基于第一通道的结果,为当前帧生成一个临时的 HCB
  2. 第二通道 (Pass 2 - 修正解除遮挡区域)

    • 输入: 在第一通道中被判断为“被遮挡”的实例、节点和簇。
    • 步骤:
      1. 使用当前帧的 HCB当前帧的变换矩阵,对这些候选对象重新进行可见性测试
      2. 将新通过测试的物体(即被解除遮挡的物体)也光栅化到可见性缓冲 (Visibility Buffer) 中。
  3. 后续步骤

    • 生成下一帧的 HCB: 此时,我们拥有了当前帧完整且准确的可见性缓冲。基于这个最终结果,生成用于下一帧剔除的 HCB。
    • 延迟材质渲染: 在所有可见性信息都确定后,执行延迟材质通道 (Deferred Material Passes),完成最终着色。

三、 LOD 的目标与现实:像素级细节的代价

讲座最后讨论了 Nanite 这种精细化 LOD 系统所追求的目标及其面临的物理限制。

  • 核心目标: 实现与屏幕分辨率成正比的渲染开销,并达到零感知的细节损失 (zero perceptual loss of detail)
  • LOD 衡量标准: 几何误差需要小于一个像素尺寸(error is less than a pixel big),这样在理论上就达到了“无损”效果。
  • 一个关键问题: 我们能否用比像素更大的三角形来实现这个目标?
    • 在很多情况下,可以。因为三角形是自适应的 (adaptive),可以在平坦区域用大三角形,在细节复杂的区域用小三角形,高效地分配几何资源。
    • 但从根本上说,不可以。当需要表现像素大小的特征 (pixel-sized features) 时,例如精细的雕刻纹理或锐利的边缘,唯一能精确表示它们的方法就是使用像素大小的三角形 (pixel-sized triangles)。这是基于三角形光栅化渲染的本质限制。


微三角形渲染:硬件光栅化的瓶颈与软件方案

一、 硬件光栅化处理微三角形的困境 (The Dilemma of Hardware Rasterization for Micropolygons)

这部分内容的核心在于解释为什么现代GPU的硬件光栅化器在处理海量微小三角形(Micropolygons)时效率低下。

  • 核心观点:为了在渲染中精确表示像素尺寸的细节 (pixel-size features),理论上需要使用同样是像素大小的三角形 (pixel-sized triangles)。然而,这种做法对于传统的硬件光栅化管线来说是一场性能灾难。

  • 关键瓶颈分析

    • 并行模型错配 (Parallelism Mismatch)

      • 硬件光栅化器被设计为在像素上高度并行 (highly parallel in pixels),因为其典型工作负载是处理覆盖大量像素的大三角形。
      • 处理微三角形时,每个三角形只覆盖少量像素。此时,我们真正需要的是在三角形上高度并行 (run wide over the triangles),而硬件架构并非为此优化。
    • 吞吐量限制 (Throughput Limitation)

      • 现代GPU的三角形设置单元 (triangle setup) 存在硬性限制,例如,每时钟周期最多处理 4 个三角形 (four triangles per clock max)
      • 当需要输出用于 Visibility Bufferprimitive ID 时,这个瓶颈会更加严重。
      • 即使是较新的 Primitive ShadersMesh Shaders,虽然有所改进,但依然存在瓶颈,并非为处理这种极端情况而设计。

二、 软件光栅化:一种更优的解法 (Software Rasterization: A Better Solution)

讲座提出了一个反直觉但有效的观点:在特定场景下,软件光栅化可以击败高度优化的硬件。

  • 核心观点:通过在通用计算单元(Compute Units)上实现的软件光栅化器 (software rasterizer),可以显著超越硬件光栅化器处理微三角形的性能。

    • 性能对比:平均比最快的 Primitive Shader 实现快 3 倍。在纯微多边形(micro-poly)场景下,优势更为明显。
  • 根本原因:并行维度的转变 (The Root Cause: A Shift in Parallelism Dimension)

    • 硬件光栅化器:为大三角形优化。其工作模式可以理解为“一个三角形进来,广播给大量像素并行处理”。其并行策略是 在像素上“跑得宽” (run wide over pixels)
    • 软件光栅化器:为海量小三角形优化。其工作模式是“一次性分发海量三角形,让大量计算单元并行处理各自的三角形”。其并行策略是 在三角形上“跑得宽” (run wide over triangles)
    • 这解释了为什么软件方案更快:它采用了与问题规模(海量三角形)相匹配的并行策略,避免了硬件前端的瓶颈。
  • 关于硬件设计的思考

    • 讲座提出,与其为这种特殊场景设计专门的硬件单元,不如提供更多的通用计算单元 (general compute units)。开发者可以利用这些通用单元为微多边形光栅化或其他任何计算密集型任务编写高度优化的代码,这提供了更大的灵活性。

三、 软件方案的代价与挑战 (The Costs and Challenges of a Software Approach)

转向软件方案并非没有代价,它引入了新的、必须在软件层面解决的复杂问题。

  • 核心观点:放弃硬件光栅化意味着我们同时失去了硬件提供的ROP (光栅操作处理单元) 以及与之配套的高效硬件深度测试 (hardware depth test) 功能。

  • 需要软件解决的关键问题

    • Z-buffer 管理:我们依然需要 Z-buffer 来进行深度剔除,现在必须在软件中实现它。
    • 并发写入冲突 (Concurrent Write Hazard):在并行处理大量三角形时,多个线程(Work Item)可能会同时尝试写入屏幕上同一个图块(Tile)甚至同一个像素的深度值。
    • 避免锁操作:传统的软件光栅化器可能会通过锁住图块 (lock tiles) 来解决写入冲突,但这会严重扼杀并行性,导致性能大幅下降。因此,必须设计一种高效的、无锁或少锁的机制来处理深度写入。

核心主题:利用原子操作实现的无锁软件光栅化

本节深入探讨了一种专为可见性缓冲区(Visibility Buffer)设计的软件光栅化方案。其核心在于放弃传统的锁定机制,通过精巧的64位原子操作来高效、并行地处理大量微多边形(Micro-Polygon)的写入,从而实现一个高性能、无锁的光栅化管线。


核心技术:基于64位原子操作的深度与数据写入

在硬件光栅化中,大量三角形同时写入同一图块(Tile)甚至同一像素是常态。若在软件实现中采用锁(Lock)机制,将导致严重的性能瓶颈。此方案通过原子操作巧妙地规避了这个问题。

  • 核心问题: 如何在没有锁的情况下,并发地、正确地将最近的三角形信息写入到可见性缓冲区?

  • 解决方案: 使用 64位原子操作,具体为针对全局图像的 InterlockedMax (原子取大)操作。

  • 数据封装结构: 将深度值和载荷(Payload)打包到一个 64位整数 中,这是整个技术的关键。

    +--------------------------------+----------------------------------+
    |         高32位 (High Bits)      |          低32位 (Low Bits)        |
    +--------------------------------+----------------------------------+
    |             深度值 (Depth)      |         载荷 (Payload)           |
    +--------------------------------+----------------------------------+
    
    • 高位:深度值 (Depth)

      • 当执行 InterlockedMax 时,该操作会比较两个64位整数的大小。由于深度值位于高位,这个比较天然地实现了深度测试。只有当新像素的64位值更大时(意味着其深度值更符合测试要求),写入才会成功。
    • 低位:载荷 (Payload)

      • 存储真正需要写入可见性缓冲区的数据。
      • 在本方案中,载荷是 可见簇索引 (Visible Cluster Index)三角形索引 (Triangle Index)
      • 关键约束: 载荷必须足够小,能够被压缩到 34位或更少 的空间内。这是实现快速软件光栅化的前提。如果没有这个约束,就无法将深度和载荷打包进一个64位的原子变量中。

微多边形软件光栅器 (Micro-Polygon Software Rasterizer)

这套方案采用了一个精简但高度优化的软件光栅器,其设计哲学是“去繁就简”。

  • 设计特点:
    • 抛弃复杂结构: 摒弃了现代硬件中常见的分层分块(Hierarchical Tiling)图章(Stamps) 等高级优化,回归本源。
    • 基础算法: 本质上是一个经过指令级微优化 (instruction-level micro-optimized)基础半空间光栅器 (basic half-space rasterizer)
    • 与Mesh Shader的相似性:
      • 其结构与现代的 网格着色器 (Mesh Shaders) 思想类似。
      • 通过将顶点处理和图元处理放在同一个线程组中,实现了顶点处理工作的共享 (shared vertex work)
      • 这种设计无需后变换缓存 (Post-Transform Cache),因为变换后的顶点直接存储在共享内存中供后续阶段使用。

两阶段执行流程 (Two-Phase Execution Flow)

该软件光栅器在单个计算着色器(Compute Shader)中分两个阶段执行,线程组大小 (Thread Group Size) 为128。

  • 第一阶段:顶点处理 (Vertex Processing)

    1. 线程映射: 一个线程负责处理一个顶点(Thread -> Vertex)。
    2. 工作内容:
      • 从簇的顶点缓冲区中读取顶点位置。
      • 对顶点进行坐标变换。
      • 将变换后的结果存入线程组共享内存 (Group Shared Memory)
    3. 处理能力: 每个线程组最多可处理 256个顶点(128个线程可以分两轮处理)。
  • 第二阶段:三角形处理与光栅化 (Triangle Processing & Rasterization)

    1. 线程映射: 切换模式,一个线程负责处理一个三角形(Thread -> Triangle)。
    2. 工作内容:
      • 读取当前三角形的索引。
      • 利用索引从共享内存中获取已变换的顶点位置。
      • 计算该三角形的边方程 (Edge Equations)深度梯度 (Depth Gradient)
      • 遍历该三角形的矩形包围盒 (Rect Bounding Box) 内的所有像素。
      • 对每个像素进行内外测试(判断是否在三角形内)。
      • 如果像素在三角形内,则执行前述的 64位原子写入操作 到可见性缓冲区。
    3. 处理能力: 每个线程组最多可处理 128个三角形

混合光栅化策略:软件与硬件的协同


1. 核心思想:混合光栅化方案

在处理海量三角形时,单一的光栅化方法无法在所有情况下都达到最优。本次讲座提出了一种混合光栅化(Hybrid Rasterization) 方案,根据三角形的尺寸和特性,动态选择最高效的处理路径:软件实现或硬件实现。

  • 软件光栅化 (Software Rasterization): 用于处理小三角形
  • 硬件光栅化 (Hardware Rasterization): 用于处理大三角形以及裁剪等复杂情况。

决策是在 Cluster(簇) 级别上进行的,系统会评估一个簇使用哪种方式更快,然后将整个簇调度到对应的管线。在实际演示中,绝大多数的几何体都通过软件路径进行光栅化。


2. 软件光栅化:针对微小三角形

对于尺寸非常小的三角形,采用一种简单直接的软件实现方式,运行在 Compute Shader 中。

  • 核心算法:

    1. 遍历三角形包围盒(Bounding Box) 内的每一个像素。
    2. 对每个像素中心点,进行三次边缘函数测试(Edge Function Test),判断其是否在三角形内部。
    3. 如果像素在内部,则将深度(Depth)负载(Payload,如材质ID等) 打包。
    4. 使用 atomicMax 原子操作,将打包好的数据写入屏幕空间的 UAV (Unordered Access View) 中。atomicMax 天然地完成了深度测试。
  • 设计理念:

    • 低固定开销: 对于微小三角形,其包围盒内的像素总数很少。因此,这种简单循环的效率足够高,避免引入更复杂算法(如扫描线)所带来的固定开销。

3. 硬件光栅化:统一输出与避免裂缝

当三角形尺寸过大或需要裁剪时,利用 GPU 内置的固定功能硬件光栅器(Hardware Rasterizer) 是最高效的选择。但如何将其与软件路径无缝集成是关键挑战。

  • 挑战 1:像素裂缝 (Pixel Cracks)

    • 问题: 软件和硬件光栅化规则的微小差异可能导致在两者渲染的簇边界产生可见的像素裂缝。
    • 解决方案: 在软件光栅器中严格遵循并精确匹配 DirectX 的光栅化规范。这确保了软件和硬件对于同一个三角形会产生完全一致的像素覆盖结果,从而杜绝裂缝。
  • 挑战 2:管线同步与合并

    • 问题: 软件路径写入 UAV,而传统的硬件路径写入 RTV/DSV(渲染目标和深度模板视图)。合并这两条路径的结果会导致同步点,无法实现异步重叠执行(Async Overlap)
    • 解决方案: 让硬件路径也写入同一个 UAV。
      • 实现方式: 在执行硬件光栅化时,不绑定任何颜色或深度渲染目标
      • 在像素着色器(Pixel Shader)中,执行与软件路径完全相同的 atomicMax 操作,将深度和负载写入到同一个 UAV 缓冲中。
      • 这样,两条路径的输出目标和写入方式完全统一,可以并行执行而无需同步。

4. 性能权衡与优化空间

  • 软硬件的临界点

    • 一个令人意外的发现是,软件光栅器的性能远超预期,即使对于远大于微多边形(Micro-polygon)的三角形,它依然比硬件更快。
    • 当前的启发式规则是:当簇内三角形的边长小于 32 像素时,采用软件光栅化。
  • 简单软件光栅化的瓶颈

    • 当三角形接近 32 像素的阈值时,简单的包围盒遍历法开始变得低效。
    • 一个三角形最多只能覆盖其包围盒面积的 50%。这意味着大量的像素测试被浪费在三角形外部的空白区域,造成了着色器级别的过度绘制(Overdraw)
  • 未来的优化方向:探索中间地带

    • 讲座暗示,在“极其简单”的包围盒遍历和“完全切换到硬件”这两种极端情况之间,存在一个优化空间。
    • 对于中等大小的三角形,是否可以采用更高效的软件光栅化算法(如传统的扫描线/梯形算法)来进一步提升性能?这是接下来将要探讨的主题。

Nanite 软件光栅化:扫描线优化与硬件对比


这部分内容深入探讨了 Nanite 软件光栅化器的一个关键性能优化,并将其能力与传统的硬件光栅化管线进行了详细比较,揭示了其设计哲学和性能权衡。

一、 扫描线光栅化:优化内循环 (Scanline Rasterization: Optimizing the Inner Loop)

传统的软件光栅化通常是在三角形的2D包围盒(Bounding Box)内,对每一个像素进行覆盖测试(is_inside_triangle),这种方法对于细长的三角形效率很低。Nanite 采用了一种更智能的方法。

  • 核心问题: 在三角形的包围盒内逐像素测试覆盖率,会因大量无效测试而浪费计算资源,尤其是在处理狭长的三角形时。

  • 核心思想: 变“测试”为“求解”。与其在内循环中被动地测试每个像素 (x, y) 是否在三角形内,不如直接为当前扫描线(scanline, 即固定的 y 值)解算出三角形所覆盖的 x 坐标区间 [x_start, x_end]

  • 具体实现:

    • 传统方法:
    for y from y_min to y_max:
        for x from x_min to x_max:
            if is_pixel_in_triangle(x, y):
                process_pixel()
    
    • 扫描线方法:
    for y from y_min to y_max:
        (x_start, x_end) = solve_for_horizontal_span(y)
        for x from x_start to x_end:
            process_pixel()
    

    这样,内循环就只迭代确定会被覆盖的像素,极大地减少了不必要的判断。

  • 技术权衡:

    • 计算开销: 求解 x 区间涉及除法运算,这意味着它不再是纯粹的定点数数学(fixed-point math),但实践证明其带来的精度影响可以忽略不计。
    • 性能启发式(Heuristic): 这个优化并非总是最优。Nanite 采用一个启发式规则:仅当一个计算波次(Wave)中,存在水平像素跨度大于 4 的三角形时,才启用扫描线光栅化模式。对于非常小的三角形,求解区间的开销可能比直接暴力测试更大。

二、 Nanite 软件光栅器 vs. 传统硬件光栅器

将 Nanite 的软件光栅化与 GPU 硬件光栅化进行对比,可以清晰地看出其设计上的取舍和适用场景的限制。

  • 无材质着色 (No Material Shaders)

    • 在此光栅化阶段,不运行任何与材质相关的着色器。它的唯一目标是确定几何体的可见性并生成深度信息(Visibility/Depth-Only Pass),这与传统渲染管线中的 Z-Prepass 阶段非常相似。
  • 微多边形成本模型 (Micro-Polygon Cost Model)

    • 在处理微多边形(micro-poly)时,其成本模型发生了根本性变化。因为没有昂贵的像素着色器,“三角形的设置和光栅化开销”本身就变成了“像素级别的开销”(triangles are like pixel work)。性能瓶颈从像素填充率转移到了三角形的吞吐量上。
  • 缺失硬件 Early-Z 功能

    • 关键限制: Nanite 没有逐三角形的剔除(per-triangle culling),这在功能上等同于缺少硬件的 Early-Z 测试
    • 影响: 硬件 Early-Z 可以在三角形进入光栅化阶段前,通过查询深度缓冲区(Z-Buffer)来判断其是否被完全遮挡,从而避免后续所有开销。Nanite 无法做到这一点,如果一个 Cluster 可见,其内部所有三角形都必须被处理,即使它们最终被深度测试丢弃,也会产生 Overdraw
    • 问题爆发点:
      1. 表面紧密重叠: 当多个表面在空间上靠得非常近(距离小于一个 Cluster 包围盒的尺寸)时,无法有效剔除被遮挡的内层表面。
      2. 带孔洞的聚合几何体 (Aggregate Geometry with Holes): 这是最典型的负面案例,例如树叶(leaves)和草(grass)。这些模型充满了微小的透明或镂空区域,导致遮挡效率极低。这也是 Nanite 不擅长处理植被等资产的根本原因之一。
  • 缺失硬件 Hi-Z 功能

    • 关键限制: Nanite 没有硬件的 Hi-Z(Hierarchical Z-Buffer) 遮挡剔除功能。Hi-Z 允许 GPU 以图块(Tile)为单位,快速拒绝掉一整块被完全遮挡的像素区域。
    • Nanite 的替代方案: Nanite 拥有一个在更粗粒度上的等价物——HCB(Hierarchical Cluster Bounding)剔除。它可以高效地剔除整个 Cluster,但无法在 Cluster 内部进行更细粒度的遮挡剔除。对于密集的网格,Cluster 的大小和屏幕上的像素块大小趋于一致,HCB 在某种程度上起到了类似 Hi-Z 的作用。

剔除粒度的挑战与局限性

本节内容深入探讨了 Nanite 系统在处理两种极端情况时遇到的性能瓶颈:巨大的三角形微小的实例。这揭示了其基于簇(Cluster)的剔除机制的粒度限制。

一、 三角形剔除粒度:大三角形带来的问题

Nanite 的工作提交和剔除是在簇 (Cluster) 的粒度上进行的。这在处理常规密集网格时非常高效,但在某些情况下会成为瓶颈。

  • 理想情况:密集网格 (Dense Meshes)

    • 对于多边形密度很高的网格,一个簇 (Cluster) 在屏幕上覆盖的区域大小,大致与硬件光栅化器中的剔除瓦片 (HCB tiles) 相当。
    • 在这种情况下,可以把 Nanite 的簇剔除看作是一种高效的、仅用于剔除工作负载的软件版 Hi-Z (Hierarchical Z-Buffer)
  • 问题所在:稀疏网格与大三角形 (Sparse Meshes & Large Triangles)

    • 当网格不密集,包含巨大的三角形时,一个簇可能会因为这一个大三角形而覆盖广阔的屏幕空间。
    • 这导致了剔除粒度变得非常粗糙。即使这个大簇只有一小部分像素是可见的,整个簇也必须被提交处理,因为它无法被更精细地剔除。
    • 最终结果是像素透支 (Pixel Overdraw) 急剧增加,因为大量被遮挡的像素区域仍然需要执行昂贵的像素着色计算。
  • 一个反直觉的结论

    • 在某些情况下,绘制更多的三角形反而更快
    • 这是因为用更多、更小的三角形来替代一个大三角形,可以将它们划分到更小、更紧凑的簇中。这些小簇可以被剔除系统更精确地处理,从而显著减少整体的像素透支,最终提升性能。

二、 实例剔除粒度:微小实例的挑战

Nanite 优化了微小三角形,但当整个实例(Mesh Instance)在屏幕上只覆盖几个像素时,又出现了新的问题。

  • LOD 的下限:根簇 (The Lower Bound of LOD)

    • Nanite 的簇层级结构 (Cluster Hierarchy) 并非无限精细。它有其最粗糙的一层,即一个包含 128 个三角形的根簇
    • 当一个实例小到可以被这一个根簇完全容纳时,Nanite 的 LOD 机制就达到了下限。
  • 成本缩放失效 (Failure of Cost Scaling)

    • 一旦达到这个根簇的粒度,渲染成本便不再随分辨率(或实例在屏幕上的大小)缩放
    • 无论这个微小实例覆盖 10 个像素还是 2 个像素,系统处理它的开销是基本固定的(处理这一个根簇的开销),这违背了 Nanite 核心的可伸缩性设计理念。
  • 无法简单剔除的原因

    • 一个直接的想法是当实例变得极小时直接剔除它们。
    • 但这在实践中是不可行的,特别是对于结构性构建块 (Structural Building Blocks),例如远处建筑的一块墙壁。
    • 剔除这些微小的结构性实例,会导致整个建筑或大型结构突然消失,造成严重的视觉跳变问题。
  • 新的性能瓶颈:“实例是新的三角形”

    • 这是一个非常形象的说法,总结了艺术家使用 Nanite 后的行为变化。他们开始像过去堆积多边形一样,大量地堆积实例数量。
    • 这使得微小实例的处理从一个边缘情况变成了亟待解决的核心问题。

三、 未来的方向:合并与层级化

讲座指出,要根本解决微小实例的问题,必须超越当前基于单个实例的 LOD 框架。

  • 最终方案:合并与代理 (Merging & Proxy)

    • 远处的物体(无论是实例还是网格)最终需要被流式地替换 (Stream Out) 为一个成本更低的代理模型 (Cheaper Proxy)
    • 这意味着需要一个机制来将大量的微小实例合并 (Merge) 成一个单一的、更简单的几何体。
  • 双重瓶颈:渲染与内存

    • 即使能够完美地缩放实例的渲染成本,也无法回避内存瓶颈
    • 存储海量实例的变换矩阵 (Transforms) 本身就会消耗巨大的内存,在某个时刻会变得无法承受。
  • 展望:层级化实例 (Hierarchical Instances)

    • 未来的解决方案寄希望于支持层级化的实例结构,这能够从根本上解决渲染和内存的双重问题,实现更大规模场景的渲染。


远距离几何体代理与材质处理

这一部分承接之前的内容,深入探讨了 Nanite 如何处理远距离的几何体,特别是当实例化达到瓶颈时所面临的挑战,并引出了当前的解决方案——可见性缓冲区代理体(Imposters),最后过渡到材质的渲染流程。


一、 远距离几何体的挑战与目标

当场景规模变得极其庞大时,即使是高效的实例化(Instancing)也会因为需要处理天量的变换矩阵(Transforms)而达到性能瓶颈。为了在极远的距离外继续维持性能,必须采用更激进的聚合策略,但这带来了新的问题。

  • 核心挑战:数据量爆炸

    • 实例化瓶颈 (Instancing Bottleneck): 仅依赖实例化的方式,当实例数量过多时,处理和存储每个实例的变换信息本身就会成为巨大的负担。讲座中提到未来希望支持层次化实例化 (Hierarchical Instancing) 来缓解这个问题,但这并非通用解。
    • 唯一数据的合并代价 (Unique Data Merging Cost): 一种通用的远距离聚合方法是几何体合并(Merging),但这会产生大量高分辨率的唯一数据 (Unique Data),管理这些数据会迅速失控,类似于过去业界试图解决但最终受限的 MegaTextureMegaGeometry 方案。
  • 理想目标:无缝的视觉保真度

    • 终极目标是在任何距离下都能实现像素级的完美几何细节 (Pixel Perfect Geometry),并且没有任何可感知的视觉损失。
    • 这意味着,任何形式的几何体代理(Proxy)或合并(Merge)都应该被推到尽可能远的地方,以至于玩家无法察觉到切换。

二、 当前解决方案:可见性缓冲区代理体 (Visibility Buffer Imposters)

在达到需要进行几何合并的距离之前,Nanite 采用了一种巧妙的中间方案来应对海量远景物体。

  • 核心概念:可见性缓冲区代理体 (Visibility Buffer Imposters)

    • 这是一种经过特殊改造的静态代理体 (Static Imposters)
    • 与传统 Imposter 存储颜色、法线等 G-Buffer 属性不同,它只存储两个关键信息:
      1. 深度 (Depth)
      2. 来自原始根簇的三角形ID (Triangle ID from the root cluster)
    • 本质上,它是一个预烘焙的、能够代表原始高精度模型的、极度轻量化的几何体描述。
  • 工作原理

    • 在渲染时,这些代理体所存储的深度和三角形ID信息,可以直接被注入 (Injected) 到屏幕空间的可见性缓冲区 (Visibility Buffer) 中。
    • 这意味着,从后续渲染管线的角度看,这些代理体与经过光栅化的真实 Nanite 几何体没有区别,它们都以相同的数据格式存在于 Visibility Buffer 中。
  • 优势

    • 与Nanite管线完美兼容: 由于它最终写入的是标准的 Visibility Buffer 数据,因此它天生支持 Nanite 的所有高级功能,例如:
      • 材质重映射 (Remapping Materials)
      • 非均匀缩放 (Non-uniform Scale)
      • 其他 Nanite 支持的特性
  • 局限性与权衡

    • 可能产生视觉瑕疵 (Popping): 在某些情况下,从真实 Nanite 模型切换到 Imposter 时会产生质量损失,导致可被察觉的跳变 (Pop)
    • 问题高发场景: 当大量相同的网格(如重复的墙体)并排摆放时,这种跳变尤为明显。因为多个物体同时发生变化,且旁边有未切换的物体作为参照物,使得变化非常容易被注意到。
    • 未来展望: 尽管在大多数情况下效果不错,但这并非终极方案。未来的目标是寻找一种速度相当、内存占用更低、并且真正无缝 (Seamless) 的替代方案。

三、 材质处理流程的承接

在讨论完几何体的可见性判定和代理方案后,话题自然过渡到如何为这些可见的像素进行着色。

  • 核心流程:基于可见性缓冲区的材质求值
    • 可见性缓冲区 (Visibility Buffer) 被解码后,渲染器就拥有了为一个像素着色所需的所有信息(类似于传统延迟渲染中的 G-Buffer),例如物体的ID、三角形ID等。
    • 接下来的步骤非常直接:
      1. 绘制一个覆盖全屏的四边形 (Fullscreen Quad)。
      2. 在 Pixel Shader 中,根据当前像素坐标,解码可见性缓冲区
      3. 获取到材质信息,并执行完整的材质着色计算

从可见性缓冲区到材质渲染

在 Nanite 的虚拟化几何渲染管线中,当完成可见性判断并生成可见性缓冲区 (Visibility Buffer) 后,下一步就是如何为屏幕上的每个像素应用正确的、由美术师创建的复杂材质。这一部分探讨了实现这一目标所面临的挑战和最终采用的巧妙解决方案。

一、 核心挑战:解耦的几何与材质渲染

Nanite 的设计中,几何体的光栅化与最终的材质着色是分离的。

  • 几何渲染阶段:顶点变换等过程可能由固定管线处理,其主要产物是可见性缓冲区,记录了每个像素对应的图元(例如三角形ID)和深度信息。
  • 材质渲染阶段:需要支持完全可编程的、由美术师创建的像素着色器 (Pixel Shader)。这意味着不能用一个统一的材质来渲染所有像素。

核心问题:如何在像素着色阶段,高效地为每一个像素找到并执行其对应的材质代码?

二、 初步探索:几种着色方案及其局限性

讲座中提到了几种看似可行但存在效率问题的方案。

  1. 方案一:Callable Shaders

    • 思路:理论上,可以使用诸如 Callable Shaders (DXR 中的概念) 这样的现代 GPU 特性,在一个渲染通道 (Pass) 内,根据可见性缓冲区解码出的材质信息,动态调用所有需要的材质代码。
    • 局限性:这种方法存在实现上的复杂性和潜在的性能瓶颈,因此未被采纳。
  2. 方案二:逐材质全屏绘制与像素剔除

    • 思路:为场景中每一种唯一的材质 (Unique Material) 都提交一个覆盖全屏的四边形 (Quad) 绘制调用。在像素着色器中:
      1. 读取当前像素在可见性缓冲区中记录的材质ID (Material ID)
      2. 判断该 ID 是否与当前绘制通道所绑定的材质 ID 相匹配。
      3. 如果不匹配,则 discard 该像素。
    • 致命缺陷CPU-GPU 效率极低。由于 Nanite 的剔除完全是GPU驱动 (GPU-driven) 的,CPU 层面并不知道当前帧究竟有哪些材质是可见的。因此,CPU 必须为场景中的 所有材质 都提交一次全屏绘制调用,即使该材质在屏幕上完全不可见。这会导致大量的冗余 draw call 和对每个像素进行海量的无效判断,效率极差。

三、 核心技巧:利用深度测试硬件加速材质分发

为了解决上述问题,Nanite 采用了一种极为聪明的技巧,将材质分发问题转化为一个可以由硬件高效解决的深度测试问题

1. 准备阶段:生成材质深度缓冲区

在正式的材质渲染前,一个计算着色器 (Compute Shader) 会处理可见性信息,并输出两个关键的缓冲区:

  • 材质深度缓冲区 (Material Depth Buffer):这是一个特殊的“深度”缓冲,但它存储的不是几何体的 Z 值,而是每个像素对应的 Material ID
  • 标准深度缓冲区 (Standard Depth Buffer):这才是我们传统意义上的 Z-Buffer,用于后续的深度测试(如透明物体、阴影等)。
  • Hi-Z / HTile 生成:同时,GPU 会为这个材质深度缓冲区生成相应的层级深度信息(Hi-Z 或称 HTile),这是后续实现硬件加速剔除的关键。

实现细节: 在主机平台 (Console) 上,可以通过内存别名 (Aliasing Memory) 等技巧,让一个 Compute Shader 高效地同时输出材质深度和标准深度,并确保内存布局最优。

2. 渲染阶段:利用深度测试进行材质匹配

准备工作完成后,材质渲染阶段按以下流程进行:

  • 遍历所有材质: 和方案二类似,CPU 依然需要遍历场景中的所有唯一材质并发起绘制。
  • 设置特殊的绘制状态: 对于每一种材质:
    1. 绘制一个全屏四边形 (Full-screen Quad)
    2. 将该四边形的Z值设置为一个常量,这个常量就是当前正在绘制的材质的 ID
    3. 将硬件深度测试函数 (Depth Test) 设置为 等于 (EQUAL)
3. 工作原理与优势
  • 硬件加速剔除:当这个全屏四边形被渲染时,GPU 的光栅器会对每个像素进行深度测试。它会比较四边形的 Z 值(即当前材质ID)与材质深度缓冲区中对应像素的值(即该像素实际的材质ID)。
  • 高效筛选:由于深度测试函数是 EQUAL,只有当两者相等时,测试才会通过,像素着色器才会被执行。对于所有不匹配的像素,它们会在硬件层面被极速剔除,根本不会进入像素着色阶段。
  • Hi-Z 加速:借助预先生成的 Hi-Z,GPU 可以一次性剔除掉大块的像素区域,如果一个区域内没有任何像素的材质ID与当前ID匹配,整个区域的测试都会被跳过,效率远高于逐像素判断。

四、 优化与展望:超越全屏四边形

尽管利用深度测试已经极大地提升了效率,但对于那些只覆盖屏幕一小部分区域的材质,绘制一个全屏四边形仍然存在一定的浪费。讲座在此处留下了悬念,暗示还有比绘制全屏四边形更优化的方法,这将在后续内容中揭晓。


Nanite Shading 与纹理过滤优化

本节内容深入探讨了 Nanite 在完成可见性判断和深度写入后,如何高效地进行材质着色,并重点解决在此过程中遇到的纹理过滤(Texture Filtering)挑战。

一、 从全屏渲染到分块光栅化 (Tiled Rasterization)

为了提升着色阶段的效率,系统摒弃了传统的全屏四边形 (Fullscreen Quad) 渲染方案,转而采用一种更精细的优化策略。

  • 核心思想: 将屏幕划分为一个瓦片网格 (Grid of Tiles) 进行绘制。
  • 剔除机制: 利用在材质深度(Material Depth)阶段生成的 32位掩码 (32-bit mask),可以高效地剔除掉没有任何像素需要着色的瓦片。这意味着 GPU 只需为真正有内容的屏幕区域执行 Pixel Shader。
  • 未来方向: 讲者提到,这一部分目前在主机上是这样实现的,但正在被积极重构,未来极有可能完全迁移到 Compute Shader 流程中。

二、 纹理过滤的挑战:跨边界的像素四边形 (Pixel Quads)

在传统光栅化中,GPU 通过计算相邻像素(组成一个 2x2 的像素四边形 - Pixel Quad)之间纹理坐标 (UV) 的变化率(即导数/梯度),来决定使用哪个层级的 Mipmap。这个过程依赖于有限差分 (Finite Differences) 方法。Nanite 的渲染方式保留了这一基础,但也带来了新的问题。

  • 优点: 在 Nanite 的渲染管线中,像素四边形不再局限于单个三角形。这对于渲染微小三角形 (Tiny Triangles) 极为有利,因为它显著减少了因像素四边形跨越多个微小三角形而导致的四边形过绘制 (Quad Overdraw) 问题。

  • 缺点与挑战:

    • 跨越不连续性: 像素四边形现在可能同时覆盖深度不连续 (Depth Discontinuities)UV接缝 (UV Seams),甚至分属不同物体 (Different Objects) 的像素。
    • 错误的导数: 在这些不连续的边界上计算有限差分会得到毫无意义且数值巨大的导数。
    • 视觉瑕疵: 巨大的导数会让纹理采样器误以为表面变化剧烈,从而选择一个非常低分辨率的 Mipmap 层级,导致纹理采样的结果异常模糊,产生明显的视觉瑕疵。

三、 解决方案:解析微分 (Analytic Derivatives)

为了解决上述问题,系统采用了一种更为精确和健壮的导数计算方法。

  • 核心方法: 放弃在像素间计算有限差分,转而在三角形内部计算属性的解析微分 (Analytic Derivatives)。这意味着导数是基于三角形表面的数学属性精确推导出来的,而不是通过相邻像素采样值的差异来估算。

  • 自动传播:

    • 这个微分计算过程是自动化的。系统会利用链式法则 (Chain Rule),将导数通过艺术家创建的材质节点图 (Material Node Graph) 进行逐节点传播。
    • 例如,如果一个 UV 坐标经过了乘法、加法等节点操作,它的导数也会相应地通过链式法则进行变换。
  • 鲁棒性与回退机制:

    • 如果材质图中遇到了一个无法进行解析微分的数学操作(例如某些复杂的程序化噪声函数),系统会回退到使用传统的有限差分方法来计算这一步的导数。
    • 虽然更完美的方案可能是采用光线微分 (Ray Differentials),但目前的混合方案在实践中并未遇到显著问题。

四、 性能与实现

将整个材质系统的纹理采样导数计算切换到解析微分,听起来计算成本会很高,但实际情况并非如此。

  • 实现细节:

    • 所有的纹理采样函数调用(如 Sample)都被替换为 SampleGrad
    • SampleGrad 是一个 HLSL 内置函数,它允许开发者手动传入 x 和 y 方向的导数(梯度),而不是让硬件自动计算。
  • 性能开销:

    • 经过测量,引入解析微分的额外性能开销低于 2%
    • 开销低的原因:
      1. 额外的计算工作仅限于影响纹理采样的操作,对于材质图中不影响最终 UV 坐标的计算部分,没有额外开销。
      2. 引擎的虚拟纹理 (Virtual Texturing) 系统为了正确处理纹理图块边界的 Mipmap 选择,原本就已经在使用 SampleGrad。因此,这次改动只是将导数的来源从有限差分换成了更精确的解析微分,边际成本非常小。

Nanite 性能总结与阴影渲染的挑战

一、 Nanite 渲染管线性能回顾

这部分内容总结了 Nanite 在主视图(Primary View)渲染管线中的惊人性能表现,并与传统渲染路径进行了直接对比。

1.1 与传统渲染管线的对比
  • 传统渲染 (UE4 Standard):

    • 如果使用标准渲染路径,渲染示例场景一帧需要光栅化的三角形数量超过 10 亿个。
  • Nanite 渲染:

    • 实际光栅化的三角形数量仅为 2500 万个。
    • 核心优势: 这得益于 Nanite 高效且智能的 LOD(层次细节)剔除 (Culling) 系统。
    • 关键特性: 2500万 这个数字在整个演示中保持了高度稳定,不受场景几何复杂度的影响,展示了其强大的可扩展性。
1.2 帧渲染成本分析 (Frame Cost Breakdown)

以下数据基于最终输出到 4K 分辨率(使用动态分辨率时间性上采样技术),内部平均渲染分辨率约为 1400p

  • 阶段一:场景剔除与光栅化 (Culling & Rasterization)

    • 任务: 从 GPU 场景数据和视点信息开始,到最终生成完整的可见性缓冲(Viz Buffer)。
    • GPU 耗时: 平均 2.5 毫秒
    • CPU 成本: 可忽略不计 (negligible)。这是因为整个过程是完全由 GPU 驱动 (GPU-driven) 的,几乎不占用 CPU 资源。
  • 阶段二:材质应用 (Material Application)

    • 任务:Viz Buffer 中的可见性信息转换为最终的 G-Buffer
    • GPU 耗时: 平均 2.0 毫秒
    • CPU 成本: 很小。每个材质对应一个 Draw Call,对于一个 60Hz 的游戏来说,这个开销完全在预算之内。

二、 阴影渲染的挑战

主视图渲染只是整个渲染流程的一部分,阴影(Shadows)作为次级视图(Secondary Views)同样至关重要,并带来了新的挑战。

2.1 次级视图的细节需求
  • 核心问题: 像阴影这类次级视图,是否也需要 Nanite 提供的微多边形(micro-poly)级别的几何细节?
  • 核心观点: 阴影渲染确实需要高精度几何体
    • 对于间接光照(Indirect Lighting)可能不需要,但对于直接阴影,细节至关重要。
    • 真实几何体与法线贴图(Normal Maps)之间最大的视觉差异,往往来自于精细的自遮挡阴影 (detailed self-shadowing)
2.2 光线追踪方案的局限性

讲座探讨了使用光线追踪(Ray Tracing)来渲染高精度阴影的可能性,但指出了当前面临的几个主要障碍。

  • 性能瓶颈:

    • 光线数量 (Ray Count): 阴影光线的数量通常多于主视图的光线。因为场景中平均每个像素会接收到多于一个光源的照射,需要追踪更多的阴影光线。
    • 速度要求 (Speed Requirement): 阴影渲染方案的速度至少需要和 Nanite 渲染主视图的速度一样快,才能满足实时渲染的需求。
  • API 灵活性 (API Flexibility):

    • 当前的硬件光线追踪 API 不够灵活,无法满足 Nanite 这种高度定制化的渲染管线和数据结构的需求。

附注:讲座中提到 sample_grad 指令被用于虚拟纹理(Virtual Texture)的采样,以处理因纹理图块(tile)切换而产生的接缝/不连续问题。


Nanite 的阴影解决方案:虚拟阴影贴图 (Virtual Shadow Maps)

一、 技术选型:为何选择光栅化而非光线追踪?

尽管硬件光线追踪速度很快,但在讲座发生时,它对于 Nanite 的阴影渲染仍存在一些核心的局限性,导致团队最终选择了一套基于光栅化的方案。

  • API 灵活性不足:当时的硬件光追 API 不够灵活,难以在光追流程中高效地评估 Nanite 复杂的材质逻辑(shading logic)。
  • 内存占用过高:为了适配光追特定的三角形数据格式,会极大地增加内存占用,这与 Nanite 的设计哲学相悖。
  • BVH 更新成本高昂:缺乏部分更新 BVH(包围盒层次结构) 的能力。这意味着任何场景动态变化都可能导致对包含数百万元素的 BVH 进行昂贵的从零开始的重建。

核心观点:基于以上限制,团队决定打造一个基于光栅化的阴影解决方案,以便能更好地利用和整合 Nanite 已有的高效渲染管线和数据结构。


二、 核心挑战与解决思路

  • 核心挑战像素级多光源(Many Lights Per Pixel) 带来的性能问题。在现代渲染中,一个像素可能被数十甚至上百个光源照亮,如果每个光源的阴影计算成本都很高,性能会急剧下降。必须控制 Nanite 的渲染成本不因阴影计算而失控

  • 解决思路与关键洞察缓存(Caching)。在绝大多数场景中,大部分光源和产生阴影的几何体都是静态的。这意味着它们的阴影关系是固定的,可以被预计算或缓存,从而避免重复工作。

  • 最终方案:基于 Nanite 的底层架构,团队实现了一种全新的、极其高效的阴影技术——虚拟阴影贴图(Virtual Shadow Maps, VSM)


三、 虚拟阴影贴图 (Virtual Shadow Maps - VSM) 详解

VSM 是一种革命性的 Shadow Map 技术,旨在以极高的分辨率和极低的性能开销为海量几何体提供高质量的动态阴影。

核心思想
  • 超高分辨率:为场景中的所有光源(或按需分组)统一使用 16K x 16K 的超高分辨率虚拟阴影贴图。这在传统方法中是不可想象的。
  • 按需渲染与分配:VSM 的核心是将阴影贴图(Shadow Map)的渲染分辨率与屏幕空间像素所需的实际精度相匹配。只渲染和分配那些在屏幕上可见且有贡献的阴影区域。
关键特性与优势
  1. 屏幕空间精度匹配 (Screen-space Precision Matching)

    • 系统会分析阴影投射到的表面在屏幕上的覆盖范围。
    • 如果该表面离摄像机很近,在屏幕上占很大面积,系统就会为其分配高分辨率的 Shadow Map 页面(Page)。
    • 反之,如果表面很远,只占几个像素,系统就只使用低分辨率的 Mipmap 等级。
    • 优势:从根本上解决了传统 Shadow Map 的欠采样(undersampling,导致锯齿)和过采样(oversampling,浪费性能)问题。
  2. 高效的剔除 (Efficient Culling)

    • 工作剔除:如果 Shadow Map 的某个区域没有投射到屏幕上任何可见的物体,那么该区域对应的几何体光栅化工作就会被完全跳过
    • 内存剔除:更进一步,系统甚至不会为这些屏幕上不可见的阴影区域分配贴图内存。这是与传统优化(如视锥剔除)最本质的区别。
  3. 虚拟化与稀疏化 (Virtualized and Sparse)

    • 这张巨大的 16K 贴图并非一整块连续的物理内存,而是虚拟且稀疏的。它由许多小的页面(Page) 组成。
    • 页面大小128x128 像素。
    • Mip 0 结构:在最高分辨率(Mip 0)下,这张 16K 贴图由 128 x 128 个页面网格组成。
    • 只有被标记为“需要”的页面才会被真正分配物理内存并进行渲染。
工作流程:页面分配 (Page Allocation)

系统每一帧都会动态决定哪些页面是“需要”的,其流程大致如下:

  1. 遍历屏幕上的每一个像素
  2. 对于每个像素,找出所有影响它的光源
  3. 将该像素的世界坐标投影到每个光源的阴影贴图空间中
  4. 根据投影后像素在阴影空间中的“足迹”(footprint)大小,确定所需的 Mip-map 等级
  5. 标记该 Mip 等级下对应的页面为“需要渲染和分配”

这个过程确保了只有对最终画面有贡献的 Shadow Map 区域才会被处理,从而实现了在巨大虚拟分辨率下的高性能渲染。


与 Nanite 的高效集成

本节深入探讨了虚拟阴影贴图(Virtual Shadow Maps)如何与 Nanite 系统高效结合,以实现大规模场景下的高性能、高精度阴影。


一、VSM 页面标记与缓存机制回顾

为了理解后续的集成优化,首先需要回顾 VSM 的核心工作流程。VSM 是一个按需渲染(demand-driven) 的系统。

  • 核心观点: VSM 的渲染决策完全由屏幕像素的需求驱动。系统首先会确定哪些阴影数据是当前帧视野内实际需要的,然后才去渲染它们。

  • 页面标记(Page Marking)流程:

    1. 遍历所有影响当前画面的光源。
    2. 对于屏幕上的每一个像素,将其位置投影到光源的阴影贴图空间
    3. 根据投影大小,选择一个合适的 Mip Level,其选择标准是一个阴影贴图的纹素(texel)大小约等于一个屏幕像素的大小。这确保了阴影分辨率与视图匹配,避免了资源浪费和摩尔纹。
    4. 在该 Mip Level 中,将对应的页面(Page)标记为 “需要渲染”(needed)
  • 缓存机制(Caching):

    • 关键术语:缓存(Caching)
    • VSM 支持跨帧缓存。如果一个页面在前一帧已经被渲染,并且其内容没有发生变化,那么这一帧就可以直接复用,无需重新绘制。
    • 这意味着,每一帧主要更新的区域是:
      • 移动物体所在的区域。
      • 因摄像机移动而产生的视锥体边缘区域。
    • 对于静态场景和静止的摄像机,阴影更新的开销会降到极低。

二、Nanite 集成的挑战与解决方案

虽然 VSM 支持所有类型的网格,但它与 Nanite 的结合才能真正发挥其威力。然而,直接将 Nanite 用于 VSM 会遇到严重的性能瓶颈。

  • 性能瓶颈:Nanite 的调用开销

    • 核心观点: Nanite 的渲染管线非常深,虽然单次运行效率极高,但其启动和同步开销(synchronization overhead) 相对较大。
    • 如果采用朴素的实现方式,例如为每个光源的每个 Mip Level 都单独调用一次 Nanite 来渲染所需的页面,这种“小批量、高频次”的调用方式会带来灾难性的性能表现,因为大量的开销都耗费在了管线启动上,而不是实际的渲染工作。
  • 解决方案:多视图渲染(Multi-View Rendering)

    • 关键术语:多视图渲染(Multi-View)
    • 为了解决上述问题,Epic 对 Nanite 进行了改造,为其增加了多视图渲染的支持。
    • 现在,Nanite 渲染管线不再是接收单个视图(View),而是可以接收一个视图数组(array of views)
    • 最终效果: Nanite 可以在一个单一的、依赖性的间接调度链(a single chain of dependent dispatch indirects) 中,一次性渲染出场景中所有光源所有需要的虚拟化 Mipmap 的阴影数据。
    • 这种极致的批处理(Batching)将原本成千上万次的独立绘制调用合并为一次超级调用,在极端情况下带来了高达 100 倍的性能提升。

三、在 Nanite 中实现高效剔除

为了让多视图渲染发挥最大效能,还需要确保 Nanite 不会做任何无效的工作,即不会向那些未被标记为“需要”的页面中绘制任何几何体。

  • 核心观点: 将 VSM 的页面需求信息集成到 Nanite 原本的剔除(Culling)流程中。

  • 实现机制:

    1. Nanite 自身拥有一套非常高效的、基于层级 Z 缓冲(HCB/HZB)的剔除系统。
    2. 在原有的剔除测试(如边界框与 HZB 的测试)旁边,增加了一个额外的测试
    3. 这个新测试会检查一个 Nanite 实例(instance)簇(cluster) 是否与之前生成的 “需要渲染的页面”掩码(needed pages mask) 有重叠。
    4. 如果一个几何体(实例或簇)没有与任何一个“需要渲染的页面”相交,它就会被提前剔除,完全不会进入后续的渲染阶段。

通过这种方式,Nanite 的细粒度剔除能力与 VSM 的按需渲染哲学完美结合,确保了 GPU 资源只用于绘制屏幕上真正可见的、高精度的阴影。


VSM中的剔除与LOD管理:充分利用Nanite的优势

1. 基于“需求页面掩码”的剔除 (Culling based on "Needed Pages Mask")

  • 核心观点: 在将物体渲染到虚拟阴影贴图(VSM)之前,会进行一次高效的粗粒度剔除。
  • 关键术语: needed pages mask (需求页面掩码)
  • 工作流程:
    • 系统首先确定当前帧哪些虚拟阴影贴图(VSM)的页面(Pages)是需要被渲染的,并生成一个 needed pages mask
    • 然后,针对每一个准备渲染的实例(Instance)或簇(Cluster),检测其包围体是否与这个 mask 所表示的“需要”页面有重叠。
    • 如果一个实例或簇完全没有与任何“需要”页面重叠,那么它将被直接剔除(culled),无需进入后续的渲染流程。

2. 处理跨页簇(Cluster)的渲染

  • 核心挑战: VSM的物理纹理在显存中并不是连续的,而簇在虚拟地址空间中可能是连续的。当一个簇跨越了多个虚拟页面的边界时,其像素地址不能直接映射到物理地址,需要特殊处理。

  • 软件光栅化器方案 (Software Rasterizer):

    • 核心思想: 保持内循环(inner loop)的极致简洁。性能测试表明,即使在内循环中增加一个额外的位移操作,都会对性能产生可测量的影响。
    • 实现方式:
      1. 复制簇: 对于一个跨越了N个页面的簇,系统会向光栅化器提交 N个可见簇的绘制指令
      2. 预先转换与裁剪: 每个提交的绘制指令都只针对一个页面。页面的虚拟到物理地址转换只在簇的层级执行一次,然后使用裁剪(Scissor) 将绘制范围限制在该页面对应的像素区域内。
    • 合理性: 软件光栅化器处理的簇通常很小,绝大多数只覆盖一个页面,因此这种方式导致的额外开销很小。
  • 硬件光栅化器方案 (Hardware Rasterizer):

    • 核心思想: 硬件光栅化器处理的簇更大,复制顶点和三角形数据的开销过高,不可取。
    • 实现方式: 在像素着色器(Pixel Shader)中进行逐像素(per-pixel)的页表转换
    • 技术支持: 硬件渲染路径使用了原子UAV写入(atomic UAV writes)。这意味着像素着色器可以自由地将结果“散射(scatter)”写入到UAV(Unordered Access View)中的任意位置,而无需关心物理内存的连续性。这使得逐像素的地址转换变得高效可行。

3. Nanite LOD与VSM的协同工作

  • 核心观点: Nanite的细节层次(LOD)选择机制是VSM能够实现高效渲染的关键之一,其目标是让阴影渲染开销与屏幕分辨率大致成正比,而非场景几何复杂度。
  • LOD选择标准:
    • 与在主视图中渲染一样,Nanite为阴影渲染选择LOD的标准依然是维持一个像素的误差(one pixel error)
    • 关键区别: 此处的“一个像素”指的是其正在渲染的VSM Mipmap层级上的一个像素(texel)。
    • 结合VSM本身的机制(选择合适的Mip层级以保证一个texel约等于一个屏幕像素),这一标准确保了渲染到阴影贴图中的三角形密度总是与最终显示在屏幕上的像素密度相匹配。

4. 解决主视图与阴影视图的LOD不匹配问题

  • 核心问题: 由于主视图(摄像机视角)和阴影视图(光源视角)的投影方式不同,Nanite为同一个物体选择的LOD可能是不同的。这会导致渲染到主视图的三角形和渲染到阴影贴图的三角形不完全一致。
  • 产生的瑕疵: 这种不匹配会导致错误的自阴影(incorrect self-shadowing)
  • 解决方案: 使用一次短距离的屏幕空间追踪(screen space trace)。这个追踪过程可以有效地弥合和修正因LOD不匹配而可能产生差异的区域,修正自阴影的错误。

几何的虚拟内存方案

本节探讨了 Nanite 实现几何流式加载的核心思想——几何的虚拟内存(Virtual Memory for Geometry)。这个概念与我们熟知的虚拟纹理(Virtual Texturing) 非常相似,但由于几何数据的独特性,其具体实现细节和挑战有所不同。

一、 核心思想:几何的虚拟内存

几何虚拟化借鉴了虚拟纹理的理念,建立了一套按需加载和卸载几何数据的系统。

  • 基本流程:
    1. GPU 发起请求: 当 GPU 在渲染过程中发现当前持有的几何数据精度不足以满足屏幕空间的显示要求时,它会发起数据请求。
    2. CPU 异步加载: CPU 接收到请求后,会异步地 (Asynchronously) 从磁盘加载更高精度的几何数据。
    3. 数据交付: 加载完成后,新的、更精细的数据被传输给 GPU 使用,从而提升渲染质量。

二、 与虚拟纹理的异同

尽管核心机制相似,但几何虚拟化与虚拟纹理在实现上存在几个关键差异。

1. 相似之处:按需加载机制

两者都采用 按需请求 (On-demand Requesting)异步加载 (Asynchronous Loading) 的模式。GPU 扮演消费者的角色,当发现当前资源(纹理或几何体)质量不足时,便会请求更高质量的版本。

2. 关键差异
  • 数据一致性与裂缝问题

    • 核心挑战: 在加载和卸载几何数据时,必须保证任何时刻渲染的都是一个有效的、完整的几何表示
    • 关键术语: 有效切分 (Valid Cut)。系统在 DAG(有向无环图)中的任何一次“切分”都必须是完整的,以避免在不同 LOD 层级的几何体拼接处产生裂缝 (Cracks)
  • 流送的最小单元

    • 虚拟纹理: 最小单元通常是固定大小的纹理块(Tile)。
    • Nanite 几何: 最小流送单元是 簇组 (Cluster Group),而非单个簇 (Cluster)。
    • 原因: Nanite 的简化操作是在簇组层面上进行的。一个父节点必须被一整组 (a full group) 子节点完整替换,反之亦然。如果只加载一个组内的部分簇,会导致“不完整替换”,从而破坏几何的连续性并导致渲染错误。
  • 数据大小的可变性

    • 虚拟纹理: Tile 的数据大小是固定的。
    • Nanite 几何: 每个簇组包含的顶点数量、属性数据等都是可变的 (Variable Size)
    • 解决方案: 为了避免内存碎片 (Memory Fragmentation),系统仍然采用固定大小的页 (Fixed-size Pages) 来管理内存。这意味着需要将一个或多个可变大小的簇组打包进一个固定大小的页中。

三、 页面打包与数据组织

为了高效地将可变大小的簇组装入固定大小的页,系统采用了一种优化的打包策略。

  • 核心策略: 将多个簇组打包进一个固定大小的页
  • 打包依据:
    1. 空间局部性 (Spatial Locality): 在物理上彼此靠近的簇组,很可能在同一帧被同时需要,因此应优先放在同一个页中。
    2. DAG 层级 (Level in the DAG): 在 DAG 结构中层级相近的簇组也可能被一起请求,这也是打包时的一个重要考量因素。
  • 最终目标: 通过智能打包,最小化运行时可能需要加载的页面数量

四、 根节点的特殊处理

  • 根页面 (Root Page): 包含 DAG 最顶层节点的第一个页面,被设计为始终常驻内存 (Always Resident)
  • 内容: 这个页面会尽可能多地装入 DAG 的顶层数据(最粗糙的 LOD)。
  • 作用: 确保无论何时,系统至少有一个可以立即用于渲染的基础几何表示,作为整个流式加载过程的起点。

GPU 常驻数据管理与流式请求

本节内容深入探讨了 Nanite 系统中两个关键的底层机制:如何在 GPU 上高效地组织和管理已经加载的几何体数据,以及如何智能地决定下一步需要从磁盘加载哪些数据。

一、GPU 显存中的数据组织与优化

为了在 GPU 上高效地存储和访问几何数据,Nanite 采用了一种基于页(Page)的虚拟化管理方案。

1. 基本结构:常驻页与根页面
  • 核心观点: GPU 上维护一个由常驻页(Resident Pages) 组成的巨大缓冲区(Byte Address Buffer),用于存放所有当前可用的几何数据。
  • 关键设计: 这个缓冲区中始终包含 DAG (有向无环图) 的顶部数据,即最粗糙的 LOD 层级,我们称之为根页面(Root Page)
  • 设计目的: 确保无论摄像机在何处、流式加载状态如何,场景中总有最基础的几何体可以被渲染,避免了模型“消失”的问题,提供了一个渲染的底线。
2. 内存浪费问题:组(Group)的粒度过大
  • 背景: Nanite 的数据以组(Group) 为单位进行组织,一个组包含多个(8到32个)簇(Cluster)。每个簇的大小可达 2KB。这意味着一个组的大小可能相当可观且变化范围很大。
  • 问题: 如果以组(Group) 为单位填充内存页,会产生严重的内存空闲(Slack) 问题。因为组的大小与页的大小往往不匹配,会导致页内有大量空间被浪费。
  • 核心矛盾: 数据组织的逻辑单元(Group)与物理内存管理的单元(Page)之间存在粒度失配。
3. 解决方案:以簇(Cluster)为粒度进行拆分
  • 核心观点: 将大的组(Group)簇(Cluster) 的粒度上进行拆分,以更小的、更规整的数据单元来填充内存页。
  • 关键规则:
    • 一个被拆分的组,只有当其所有的组成部分(即所有簇)都被成功加载到显存后,这个分裂组(Split Group) 才被认为是“激活”和可用的。
    • 这是因为一个簇在渲染时,需要其兄弟簇(Siblings)的信息,所以必须保证同一个组内的所有簇都同时在位。
  • 成果: 通过这种精细化的管理,内存页的浪费率被控制在 1% 左右,极大地提升了显存利用效率。

二、流式请求决策:如何确定要加载什么

在解决了数据如何“存放”之后,下一个关键问题是如何决定“加载”什么。

1. 挑战:与虚拟纹理(Virtual Texturing)的根本不同
  • 虚拟纹理(VT): 决策过程相对简单。可以直接通过UV 坐标Mipmap 等级(或 UV 梯度)来精确计算出需要加载哪些纹理页。这是一个直接映射的过程。
  • Nanite: 决策过程更为复杂。无法通过查询位置和误差值直接计算出需要的数据。必须通过层级遍历(Hierarchy Traversal) 来找到需要加载的节点。
2. 解决方案:超越当前流式边界的遍历
  • 核心观点: 为了决定是否需要加载更深层次的、尚未加载的节点,渲染时需要对层次结构进行遍历,并且这个遍历必须能“看到”当前流式切割边界(Streaming Cut) 之外的节点信息。
  • 实现方式: 这要求描述整个 Nanite 对象层级结构的元数据(Metadata) 必须比实际加载的几何数据(Cluster Group)更加完整。
3. 关键决策:保持整个层级结构常驻
  • 核心观点: 为了简化设计并提高效率,Nanite 将整个对象的层级结构(Hierarchy)完全保留在显存中
  • 可行性: 这个层级结构本身只包含关于簇组(Cluster Group)的元数据(Metadata),如其在 DAG 中的位置、父子关系、误差值等,数据量非常小,因此完全常驻显存的成本很低。
4. 常驻层级结构的优势
  • 解耦遍历与流式状态: 数据请求的遍历过程完全独立于当前的流式加载状态。无论当前加载到哪个 LOD 级别,都可以完整地遍历整个层级树,来决定最理想的数据请求。
  • 快速响应与减少延迟: 当一个新物体进入视野时,由于其完整的层级结构已经在显存中,系统可以立即遍历并一次性请求出达到目标渲染质量所需的所有层级的数据,而不需要逐级加载、逐级发现,从而显著减少了“Pop-in”(模型由低精度突然变高精度)现象和加载延迟。

Nanite 的流式请求与数据管理 (Streaming Request & Data Management)

本节深入探讨 Nanite 如何高效地请求和管理几何数据,以避免传统流式加载中常见的延迟和卡顿问题。

核心观点

Nanite 的流式系统设计旨在最小化 I/O 延迟对画面的影响。它通过一次性请求所有必需层级的数据,并采用异步 CPU/GPU 协作流程,来避免逐级加载数据所带来的延迟累积效应。

1.1 请求的生成与特点 (Request Generation & Characteristics)

  • 跨层级直接请求 (Direct Multi-Level Request):

    • 与传统的流式系统需要逐级请求(例如,先请求LOD1,加载后再请求LOD2)不同,Nanite 一次性请求目标渲染质量所需的所有层级的数据
    • 这种设计避免了 I/O 延迟随层级深度增加而倍增的问题,从而显著减少了可见的模型“跳变”(Pop-in) 现象。
  • 请求的生成时机:

    • 流式请求 (Streaming Requests) 是在 GPU 端进行层次化集群剔除 (Hierarchical Cluster Culling) 遍历过程中生成的。
  • 请求的内容与优先级:

    • 一个请求包含了一系列页面 (Pages) 的范围。
    • 每个请求都带有一个优先级 (Priority),该优先级是基于LOD 误差 (LOD error) 计算得出的。误差越大(意味着在屏幕上越重要),优先级越高。
  • 多视图支持:

    • 系统不仅为主视锥(Primary View)生成请求,也为所有激活的阴影视图 (Shadow Views) 生成请求,但阴影视图的请求通常被赋予较低的优先级。
  • 常驻页面优先级更新:

    • 一个关键细节是,即使某个数据页面已经加载到内存中(即常驻页面, Resident Pages),系统依然会为其生成请求。
    • 这样做的目的是为了实时更新其优先级,这对于后续的内存管理和页面淘汰策略至关重要。

1.2 异步处理流程 (Asynchronous Processing Workflow)

数据的加载和管理是一个跨越 GPU 和 CPU 的异步协作流程:

  1. GPU 端生成请求:

    • 在剔除过程中,GPU 生成包含页面范围和优先级的请求列表。
  2. CPU 端异步回读与处理:

    • CPU 异步 (Asynchronously) 地从 GPU 回读这些请求。
    • CPU 负责解析请求,补全任何缺失的有向无环图 (DAG) 依赖关系,确保数据加载的完整性。
    • 根据优先级,CPU 为最高优先级的页面发出 IO 请求 (IO Requests),从磁盘读取数据。
    • 同时,为了给新数据腾出空间,CPU 会淘汰 (Evicts) 内存中优先级较低的页面。
  3. GPU 端数据安装与更新:

    • 当 IO 请求完成,数据从磁盘加载到系统内存后,CPU 会将页面数据安装(Install)到 GPU 显存中。
    • 数据上传后,必须更新 GPU 端的多个数据结构,这个过程称为 “指针修复” (Pointer Fix-up) ,具体包括:
      • 更新指向已加载或已卸载页面的指针。
      • 更新因数据加载完成或中断而变化的分裂组 (Split Groups) 的状态指针。
      • 将新加载的、无需再细分的集群标记为叶子节点 (Leaves),或将因父节点卸载而需要重新细分的集群取消叶子节点标记。

Nanite 的几何压缩策略 (Geometry Compression Strategy)

为了在性能和内存占用之间取得平衡,Nanite 采用了两种不同的几何压缩格式。

核心观点

Nanite 采用双格式设计理念,针对不同的使用场景(运行时渲染 vs. 磁盘存储)优化压缩策略,实现了在保证渲染性能的同时,最大化地节省显存和磁盘空间。

2.1 内存表示格式 (In-Memory Representation)

这是专为 GPU 运行时设计的格式,直接被光栅化和延迟材质通道等渲染代码使用。

  • 核心目标:

    • 尽可能降低流式池(Streaming Pool)所需的 GPU 显存占用
  • 关键要求:

    • 近乎瞬时解码 (Near-instant to decode): 解码速度必须极快,因为数据在渲染时被直接访问,不能有明显的性能开销。
    • 支持随机访问 (Random Access): 可见性缓冲区 (Visibility Buffer) 的查找结果可能会请求任意一个三角形,因此必须能够高效地随机访问到任意三角形数据,而不能是线性解压。

数据压缩与内存优化:为虚拟化几何设计的双重数据表示

本次讲座的核心在于如何设计高效的数据结构,以支持大规模虚拟化几何体的实时流式加载与渲染。其关键思想是为数据在不同阶段(磁盘存储 vs. 内存渲染)设计完全不同的表示方法,以最大化各自阶段的效率。

核心目标:优化内存池与缓存效率

在讨论具体技术之前,首先要明确我们的根本目标:

  • 降低内存占用: 严格控制 流式加载池(streaming pool) 所需的显存/内存,使其尽可能小。
  • 提升缓存效率: 即使拥有充足的显存,一个更紧凑的数据表示也意味着能将更多的数据塞进GPU的各级缓存中。
  • 最终效果: 更高的缓存命中率直接转化为 更少的IO请求更低的“物件弹出”(pop-in)风险,从而提升最终渲染的流畅度和视觉稳定性。

双重数据表示方案:磁盘与内存的权衡

为了同时满足磁盘存储和运行时渲染的需求,系统采用了一套双重数据表示方案。数据在从磁盘流式读入内存的过程中,会经历一次 转码(Transcode) 操作。

  1. 磁盘表示 (Disk Representation)
  2. 内存表示 (Memory Representation)

1. 磁盘表示 (Disk Representation)

此格式专为最小化存储空间而设计。

  • 核心观点: 优化的目标不是原始数据大小,而是经过通用压缩算法(如LZ系列)处理后的最终文件大小
  • 特性:
    • 无需随机访问 (No Random Access Required): 数据在流式加载时是顺序读取的,因此不需要支持高效的随机寻址。
    • 允许复杂处理 (Allows Complex Processing): 由于流式加载的频率远低于渲染频率(每秒几十次 vs. 每秒数千次),我们可以在数据打包时采用计算成本更高、但压缩率也更高的先进技术。

2. 内存表示 (Memory Representation)

此格式专为GPU高效访问和渲染而设计,同时追求极致的紧凑性。

  • 核心观点: 放弃全局统一的顶点格式,为每个几何簇(Cluster)量身定制独一无二的、最紧凑的顶点数据布局,以消除每一个冗余比特。
顶点数据的极致压缩
  1. 全局量化 (Global Quantization)

    • 模型的所有顶点属性(位置、法线、UV等)首先会经过一个全局的量化过程,将浮点数转换为整数。这个过程结合了 美术师的显式控制启发式算法,以在精度和数据大小之间取得平衡。
  2. 局部空间与最小位数存储 (Local Space & Minimum Bit Storage)

    • 在每个Cluster内部,量化后的顶点属性值会被转换到相对于该Cluster包围盒(min/max range)的 局部坐标系 中。
    • 系统会精确计算出表达这个局部范围内的所有值所需的 最小位数(minimum number of bits),并仅使用这个位数来存储数据。
    • 例如,如果一个Cluster内所有顶点的局部X坐标值的范围是[0, 15],那么存储X坐标就只需要4个bit,而不是常规的8、16或32 bit。
  3. 专用顶点格式 (Specialized Vertex Format)

    • 这一系列优化的直接结果是,系统中不存在一个固定的、全局的顶点格式。
    • 每个Cluster都拥有一个根据其自身数据特征生成的专用顶点格式。这使得数据存储效率远超传统的固定顶点布局。
  4. 紧凑位流 (Compact Bitstream)

    • 每个顶点的数据被打包成一个 固定长度的比特串(fixed length string of bits)
    • 数据存储是比特对齐的,无需按字节(byte)甚至任何边界对齐,从而消除了所有内存空洞和填充浪费。
    • 尽管是紧凑的位流,但因为在单个Cluster内部,每个顶点的比特长度是固定的,所以它依然支持高效的 随机访问(例如,通过 vertexID * bits_per_vertex 计算偏移),解码过程也相对简单。
  5. 解码依赖 (Decoding Dependency)

    • 为了让GPU能够正确解析这个高度定制化的位流,在渲染时必须首先获取描述该Cluster数据布局的元数据,即 紧凑顶点声明(compact vertex declaration)
索引数据的优化

三角形索引数据也采用了类似的压缩策略。

  1. 索引旋转 (Index Rotation)
    • 三角形的三个顶点索引会被重新排序(旋转),以确保 索引值最小的那个始终排在第一个。这个预处理步骤有助于后续的压缩算法(如差分编码)取得更好的效果。
  2. 最小位数存储 (Minimum Bit Storage)
    • 与顶点数据一样,索引值也只使用表达该Cluster内顶点索引范围所必需的 最小位数 来存储。

顶点属性与材质

本节聚焦于 Nanite Cluster 内部更具体的数据压缩策略,特别是针对顶点索引、顶点属性(UV、法线、切线)以及材质ID的存储方式。这些策略旨在最大化数据密度,减少GPU显存占用和带宽。

一、 顶点索引编码 (Vertex Index Encoding)

为了高效地存储构成三角形的三个顶点索引,Nanite 避免了直接存储三个完整的索引值,而是采用了一种巧妙的增量编码方案。

  • 核心观点: 使用增量编码(Delta Encoding) 来压缩三角形的三个顶点索引,利用了 Nanite 构建过程中对顶点索引范围的强力约束。
  • 编码方法:
    1. 基准索引 (Base Index): 存储三角形三个顶点中索引值最小的那个。它使用集群(Cluster)内索引范围所需的最小位数来存储(原文提到通常是 7 位)。
    2. 增量索引 (Delta Indices): 另外两个顶点的索引,被存储为相对于基准索引的 5 位正向偏移量 (positive 5-bit deltas)
  • 关键设计约束:
    • 这种编码方式得以实现,是因为 Nanite 的构建过程保证了单个三角形的顶点索引跨度不会超过 32 (2^5 = 32)。这个约束是压缩效率的关键前提。

二、 顶点属性编码 (Vertex Attribute Encoding)

对于顶点位置之外的其他属性,Nanite 也采用了各自量身定制的压缩方案。

UV 坐标
  • 核心观点: 针对 UV 数据中常见的接缝(seams) 问题,采用了特殊的编码方案以提高压缩率。
  • 关键技术: 该编码会排除掉 UV 数据范围中最大的间隙(gap),从而将一个带有大间隙的范围视作两个连续的子范围 (two ranges per component) 进行编码。这种方法能更高效地处理UV不连续的情况,避免了为存储一个很大的空隙而浪费位数。
法线 (Normals)
  • 核心观点: 使用八面体坐标(Octahedral Coordinates) 进行编码。
  • 关键技术: 这是一种将三维单位向量(如法线)高效压缩到二维空间的常用技术,能够在保持较高精度的同时显著减少存储空间。
切线 (Tangents)
  • 核心观点: 完全不存储切线数据,以极致地节省空间。
  • 关键技术:
    • 切线信息是在像素着色器中(per-pixel) 通过三角形的 UV 梯度(UV gradients) 实时、隐式地计算出来的。
    • 适用场景: 这种方法对于 Nanite 常用的高度细分(highly tessellated) 的几何体效果尤其好,因为局部 UV 梯度足以提供准确的切线方向。
    • 未来展望: 团队计划在必要时(例如,某些依赖精确切线的各向异性材质)支持显式存储的切线(explicit tangents)

三、 材质 ID 编码 (Material ID Encoding)

一个 Mesh Cluster 可能包含来自不同材质的三角形,如何高效地存储这些材质信息至关重要。

  • 核心观点: 为了在支持多材质的同时保持数据紧凑,Nanite 按材质对三角形进行排序,并存储每个材质对应的三角形索引范围(range),而不是逐三角形存储材质ID。
  • 存储结构:
    • 常规路径 (≤ 3 种材质): 这是最常见的情况。材质范围表被高度压缩并存储在一个 32 位的字段中。
    • 备用路径 (> 3 种材质): 对于拥有更多材质的复杂 Cluster,会使用一个间接指针(indirection),指向一个可变大小的表,该表最多可支持每个 Cluster 64 种材质。
  • 查找方式:
    • 在渲染时,GPU 会获取当前三角形的索引,然后在这个范围表中进行搜索,以确定该索引落在哪一个材质的范围内,从而找到对应的材质。
  • 设计权衡:
    • 这种设计的灵活性远高于“强制每个Cluster只使用一种材质”的方案。后者虽然简单,但会严重限制网格简化的效果,并引入复杂的裂缝(crack)处理问题。

磁盘压缩策略与硬件加速


核心压缩技术:拥抱硬件 LZ 解压缩

讲座的这一部分深入探讨了 Nanite 用于磁盘存储(Disk Representation)的数据压缩策略。核心思想是放弃自研复杂的压缩算法,转而最大化利用现代硬件提供的原生解压缩能力

  • 技术选型:硬件 LZ 解压缩 (Hardware LZ Decompression)
    • 是什么:一种利用专用硬件实现的 LZ 系列算法(专注于熵编码字符串去重)的解压缩方案。
    • 为什么选
      1. 无与伦比的速度:硬件实现的速度是纯软件方案无法比拟的。
      2. 平台趋势:该技术已在现代游戏主机(如 PS5)上普及,并将通过 DirectStorage 等技术登陆 PC 平台,是未来的标准。
    • 战略考量:鉴于硬件解压缩将成为常态,重新发明一套自定义的熵编码方案被认为是浪费时间。Nanite 的设计理念是顺应并利用这一趋势。

压缩哲学:为通用压缩器“定制”数据

既然底层压缩算法(LZ)已经由硬件固定,优化的焦点就从“发明新算法”转移到了“如何让数据更适合现有算法”。

  • 核心策略:领域特定变换 (Domain-Specific Transforms)
    • 目标:不替换压缩器,而是通过预处理来“特化”数据(Specialize the data to the compressor),使其结构更符合 LZ 算法的工作原理。
    • 具体做法
      1. 分析冗余:专注于找出并消除那些 LZ 算法本身不擅长处理的数据冗余
      2. 重塑数据:通过变换操作(所谓的 "massaging the data"),将原始数据重排、编码成一种对 LZ 压缩器更“友好”的格式。

实现方式与优势:GPU 转码 (GPU Transcoding)

将数据变换的任务交给 GPU,可以充分发挥其并行计算的优势,并带来一系列好处。

  • 分工明确

    • 硬件 LZ:负责处理计算密集且本质上串行的熵编码和字符串匹配工作。
    • GPU:负责执行数据变换任务,这些任务通常具有高度并行的特性,非常适合 GPU 架构。
  • GPU 转码的优势

    • 极高的性能:即使是未优化的代码,在 PS5 上的转码速度也达到了惊人的 ~50 GB/s
    • 潜力巨大:未来可以部署在异步计算 (Async Compute) 队列上,进一步优化性能,不阻塞渲染主线程。
    • Nanite 特定优势:GPU 转码器可以直接引用已经加载到显存中的父节点页面数据,进行增量解码或处理。这避免了在 CPU 中保留一份额外的数据副本,显著降低了内存开销。

待解决的问题:LZ 压缩的局限性

尽管硬件 LZ 方案强大,但它并非完美适配所有数据类型。这里引出了下一个需要解决的技术挑战。

  • 核心矛盾LZ 压缩是基于字节的 (byte-based)
  • 问题所在:图形数据(如顶点位置、法线等)通常是紧凑的、非字节对齐的(例如,一个法线可能用 10-bit + 10-bit + 10-bit 存储)。这种数据结构与 LZ 的字节流处理方式存在根本性的不匹配 (poor fit),直接应用会导致压缩效率低下。

接下来的内容将会探讨 Nanite 是如何通过数据变换来解决这一不匹配问题的。


高级数据压缩技术:通用预处理与特定域转换

本节笔记主要探讨了两种为提升压缩率而设计的数据优化策略:第一种是针对通用压缩算法(如LZ系列)的数据预处理技巧;第二种是针对图形学中特定问题(如网格LOD)的领域特定数据转换方法。

一、 通用压缩算法的数据预处理 (Data Pre-processing for General Compression)

核心观点在于,通用压缩算法(如LZ77/LZSS)并非对所有数据格式都表现最优。通过在压缩前对数据进行“预处理”或“规整”(massaging),可以显著提升其压缩效果。

1. 字节对齐与填充 (Byte Alignment & Padding)
  • 核心问题: LZ压缩基于字节(Byte-based) 的,它在匹配和替换数据时以字节为单位进行操作。这对于非对齐的比特流数据(unaligned bitstream) 来说是一个天然的缺陷。如果两个完全相同的数据序列因为比特位没有对齐,LZ算法将无法识别它们为重复项。

  • 解决方案: 将数据填充(Padding) 至字节对齐。

    • 对LZ压缩的增益: 尽管这会增加原始未压缩数据的大小,但它使得LZ算法能够找到更多、更长的匹配字符串,最终的压缩后体积反而可能显著减小
    • 对熵编码的增益: 非对齐的数据会扰乱字节统计的规律性(garbling the byte statistics)。通过对齐,字节的统计分布会更加清晰和稳定,从而提升熵编码(Entropy Coding)(如霍夫曼编码、算术编码)的效率。
2. 数据排序与数值选择 (Data Ordering & Value Selection)
  • 核心观点: 数据的组织方式和数值范围直接影响压缩率。

  • 关键技术:

    • 数据局部性 (Data Locality): 将相同类型的数据尽可能排列在一起。这样做可以最小化匹配偏移量(match offsets),因为LZ算法在寻找匹配项时,相似的数据总是在附近,可以用更短的编码来表示偏移地址。
    • 偏斜统计分布 (Skewing Statistics): 在设计数据格式时,应优先使用较小的字节值(例如,多用0-10,少用200-255)。这会使得数据的字节统计分布尽可能地偏斜(skewed)。
    • 核心收益: 统计分布越偏斜(即某些值的出现概率远高于其他值),熵编码的效果就越好,因为它能用更短的码表示高频出现的值。

二、 面向几何数据的特定域转换 (Domain-Specific Transforms for Geometry)

核心观点在于,在处理复杂的几何数据结构(如基于簇 Cluster 的LOD层级)时,会产生一些通用压缩算法无法有效处理的特殊冗余。我们需要利用领域知识来消除这些冗余。

1. 冗余数据的来源

在网格聚类(Clustering)和简化(Simplification)的过程中,会产生大量冗余,主要有两种类型:

  • 簇边界的重复顶点 (Duplicated Vertices on Edges): 将一个连续的网格拆分成独立的簇(Clusters) 时,位于共享边界上的顶点会在相邻的簇中被复制。
  • 简化过程中未改变的顶点 (Unaffected Vertices in Simplification): 在生成更粗糙的LOD层级时,某些顶点可能完全不受简化过程影响,其数据与它在子节点(更高精度的LOD)中的源顶点完全相同
2. 通用压缩算法的局限性

LZ这类算法通常无法识别上述的冗余,原因在于:

  • 编码不一致: 即使顶点数据在逻辑上是相同的,但在不同簇中的具体编码形式可能不同,导致LZ无法匹配。
  • 数据不可见 (Parent Page Reference): 更关键的是,当一个子簇的顶点与父LOD簇(Parent Page)中的顶点相同时,LZ压缩器在处理子簇数据流时,根本无法“看到” 父LOD的数据。因为父LOD的数据可能存储在GPU显存的页池(Page Pool) 中,而不是当前正在被处理的连续内存块里。
3. 解决方案:使用引用代替冗余存储
  • 核心策略: 不再重复存储这些顶点数据,而是存储一个引用(Reference) 指向原始的、唯一的顶点数据。

  • 关键实现细节与约束:

    • 流式加载的挑战: 由于系统是流式加载数据的,我们不能随意引用内存中的任何数据,因为无法保证它一定已被加载。
    • 可靠的引用目标: 我们可以安全地引用父页面(Parent Pages) 中的数据。这是因为渲染系统为了保证正确性,必须维持一个有向无环图(DAG)的有效切片(a valid cut of the DAG)。这意味着,当一个子节点(精细LOD)被加载和渲染时,它的所有父节点(粗糙LOD)必须已经加载在内存中
    • 结论: 这个渲染机制上的依赖关系,为我们提供了一个健壮的、无须额外检查的引用机制,从而高效地消除了跨LOD层级的顶点冗余。

高级几何体数据流(续):磁盘数据编码与压缩优化

本节聚焦于 Nanite 系统中用于磁盘存储(On-Disk Encoding) 的数据压缩技术。其核心目标是在保证数据可被高效流式加载和解码的前提下,最大程度地减小几何数据的体积。这些技术建立在数据已经被组织成 DAG(有向无环图)Cluster 的结构之上。


一、 利用父节点数据进行顶点压缩

核心观点: 由于子节点的 Cluster 是父节点 Cluster 的精细化版本,两者之间存在大量重复的顶点数据。因此,我们可以通过引用父节点数据来代替存储重复的顶点,从而实现压缩。

  • 数据有效性保证 (Valid Cut of DAG): 渲染系统要求加载的数据必须是 DAG 的一个有效切片 (valid cut)。这意味着当一个 Cluster Page 被加载时,其所有父节点的 Page 也必定已经加载在内存中。这为引用父节点数据提供了先决条件。
  • 引用机制 (Referencing):
    • 一个 Cluster 中的顶点可以引用当前 Page 内的其他 Cluster任何父节点 Page 中的顶点数据。
    • 根据实践,一个 Cluster 中大约 30% 的顶点可以通过这种引用方式进行编码,显著减少了需要存储的顶点数据量。
  • 未来展望:从精确匹配到预测 (Prediction):
    • 目前的技术依赖于精确匹配 (exact matching),即子节点顶点与父节点顶点完全相同。
    • 未来的一个重要优化方向是引入预测 (prediction) 机制。即使顶点不完全相同,也可以存储一个基于父节点数据的差值(delta),有望获得更大的压缩收益。

二、 拓扑数据压缩:广义三角带 (Generalized Triangle Strips)

核心观点: 传统的每个三角形存储三个顶点索引的方式效率低下。通过将三角形组织成三角带 (Triangle Strips),可以大幅减少索引的存储量。

  • 传统三角带 (Triangle Strips):
    • 原理:第一个三角形需要 3 个索引,之后每个相邻的三角形只需要 1 个新的索引即可定义。
    • 公式:一个包含 N 个三角形的 strip 只需要 N+2 个索引,而不是 3N 个。
  • 广义三角带 (Generalized Triangle Strips):
    • 动机: 传统三角带在生成时需要在左右两侧交替添加顶点,这限制了所能形成的长带的数量和长度。
    • 改进: 广义三角带允许在每一步显式选择是在左侧还是右侧添加新顶点。
    • 成本与收益:
      • 成本: 需要额外存储每个三角形 1 比特 (bit) 来表示方向(左/右)。
      • 收益: 能够形成更长的三角带,从而在宏观上节省更多的索引存储空间。即便有额外开销,总体上仍然是净收益 (net win)

三、 顶点索引的重排序与压缩

核心观点: 通过巧妙地重排顶点索引的顺序,可以隐式地存储一部分索引,并用极小的位数来存储另一部分索引。

  • 首次使用重排序 (Reorder by First Use):
    • 隐式索引 (Implicit Index): 顶点被重新排序,使得任何一个顶点在索引流中首次被引用时,其索引值恰好是“到目前为止已经出现的独立顶点总数”。这意味着这个索引值是可推断的,无需显式存储
    • 显式索引 (Explicit Index): 当引用一个已经出现过的顶点时(即“回指”),则需要显式存储其索引。
  • 回指索引的压缩 (Back-reference Compression):
    • 构建器保证: 数据构建过程(Builder)保证,任何一个显式存储的回指索引,其值与“当前已见过的最大索引值”之间的差值非常小。
    • 编码: 这个差值可以用一个 5 比特的偏移量 (5-bit offset) 来表示,极大地压缩了这部分索引的存储空间。

四、 最终编码结构与效率

核心观点: 上述所有技术被整合为一套高效的位流编码方案,最终的磁盘数据由一系列的位掩码 (Bit Masks) 构成。

  • 位掩码的作用:
    • 标记何时需要重置/开始一个新的三角带
    • 标记广义三角带的方向(左/右)
    • 标记一个顶点引用是隐式的(新顶点)还是显式的(回指)
  • 最终效率:
    • 通过这套组合拳,拓扑数据的编码成本被压缩到了惊人的 每个三角形 5 比特 (five bits per triangle) 的水平。这是实现大规模、高细节几何体高效流送的关键所在。

Nanite 数据压缩与存储

核心观点:内存与磁盘格式的对比

为了实现高效的流式加载和最小化存储占用,Nanite 采用了两种不同的数据表示方法:一种用于运行时内存,另一种则为磁盘存储特别优化。

  • 运行时内存格式 (Runtime Memory Format):

    • 这种格式为 GPU 解码和渲染进行了优化,数据布局更便于快速访问。
    • 每个三角形的编码成本约为 17 bits
  • 磁盘格式 (Disk Format):

    • 这种格式以极致的压缩率为目标,其编码更为紧凑。
    • 每个三角形的编码成本仅为 5 bits
    • 这种显著的压缩是通过更复杂的编码策略实现的,它在加载时会被解压成对 GPU 更友好的内存格式。

数据分析:Lumen in the Land of Nanite

通过分析 Epic 的官方演示项目,我们可以直观地看到 Nanite 压缩技术的强大效果。

  • 基础数据:

    • 源三角形数量 (Source Triangles): 约 4.33 亿。
    • 生成的 Nanite 三角形数量: 约是源数量的两倍,因为包含了完整的簇层次结构(Cluster Hierarchy)。
  • 数据体积对比:

    • 原始未压缩数据 (Raw Uncompressed Data):
      • 假设顶点位置、法线、UV 使用 32位浮点数存储,切线隐式计算,总体积约为 26 GB
      • 即便使用半精度浮点数(half precision),体积仍在 13 GB 左右。
    • Nanite 运行时内存表示 (Nanite Memory Representation):
      • 经过 Nanite 的内存格式编码后,数据体积大幅缩减至 7.7 GB
      • 直接对此内存格式进行通用压缩(如 Lempel–Ziv),只能再减少约 10% 的体积,说明其数据已有较高的熵。
    • Nanite 磁盘格式 (Nanite Disk Format):
      • 采用专为磁盘存储优化的格式后,最终体积仅为 4.6 GB
  • 平均数据足迹 (Average Data Footprint):

    • ~5.6 字节/Nanite 三角形: 这个数值包含了所有必要的簇和层级结构元数据(cluster and hierarchy metadata)。
    • ~11.4 字节/源三角形: 这个指标对于评估项目最终打包大小更具参考价值。

注意: 以上数据来自最新的内部代码,相比 UE5 早期预览版已有进一步优化。

现状与未来展望

  • 当前状态:

    • Nanite 已在 UE5 早期预览版 (Early Access) 中提供,并附带完整源代码。
    • 它已非常接近最初 虚拟化几何体 (Virtualized Geometry) 的宏大构想,但仍有部分特性尚未完全实现。
  • 未来方向:

    • 持续优化压缩: 团队认为当前的压缩算法仍有很大的优化空间("a lot left to squeeze"),未来版本的数据体积有望进一步减小。
    • 逐步扩大支持范围: 第一个版本的 Nanite 有意识地将支持范围限定在 静态刚体几何体 (Rigid Geometry) 上,因为这类资产在绝大多数场景中占据了主导地位。未来的工作将逐步扩展到其他类型的几何体。

Nanite 总结与展望

本部分是系列讲座的总结,重点阐述了 Nanite 当前的技术局限,并对未来的发展方向和宏伟愿景进行了展望。

一、 当前局限与未来挑战

Nanite 在初版设计中,优先专注于场景中占比最大的部分——刚性几何体 (Rigid Geometry),因为这是最直接且高效的切入点。然而,这套系统目前仍存在一些明确的局限性。

1. 支持范围的局限

Nanite 的虚拟化几何渲染管线目前不兼容以下类型的材质和几何体:

  • 半透明 (Translucent) 或蒙版 (Masked) 材质: 这类材质需要复杂的混合或 Alpha 测试,与 Nanite 基于不透明深度优先的渲染管线不兼容。
  • 非刚性形变 (Non-rigid Deformation): 任何形式的顶点动画,无论是骨骼动画 (Skeletal Animation) 还是其他静态或动态的顶点形变 (Vertex Deformation),都无法在当前的 Nanite 系统中直接使用。这意味着角色、布料等动态变化的物体仍需走传统渲染路径。

2. 性能表现的局限

对于特定类型的几何体,Nanite 的核心优势——“渲染成本与屏幕像素分辨率成正比”——会受到挑战。

  • 核心问题: 在处理由大量细碎、不连续表面构成的聚合体 (Aggregates) 时,例如草地 (Grass)树叶 (Leaves),Nanite 的性能表现并不理想。
  • 原因分析: 这类几何体通常具有极高的几何复杂度和 overdraw(过度绘制),难以形成大的、连续的三角面片簇(Cluster)。这使得 Nanite 的 LOD 简化和剔除效率降低,其性能优势不再显著。

二、 未来愿景:Nanite Everywhere

尽管存在局限,但团队的终极目标是让 Nanite 成为虚幻引擎中渲染几何体的唯一方式,实现一个无缝、统一的渲染管线。

1. 终极目标:统一的几何渲染管线

  • 核心愿景: “Nanite Everything”。未来的引擎中将不再有“Nanite 网格”和“非 Nanite 网格”的概念。Nanite 将成为渲染所有几何体的默认且唯一的方式。

2. 未来技术探索方向

团队认为 Nanite 的核心思想(虚拟化几何)可以应用于许多目前看来不切实际的领域,从而解锁全新的图形技术能力。

  • 核外光线追踪 (Out-of-core Ray Tracing): 对超出显存容量的超大规模场景进行光线追踪。
  • 微多边形曲面细分 (Micro-poly Tessellation): 在渲染时动态生成电影级的细微几何细节,类似 RenderMan 的 REYES 架构。
  • 像素级置换贴图 (Pixel-scale Displacement Mapping): 实现真正的、与像素精度匹配的几何置换,而不仅仅是视差效果。
  • 分形实例化 (Fractal Instancing): 通过程序化方式创建无限细节的实例化几何体。

3. 待征服的关键领域

为了实现 "Nanite Everything" 的愿景,团队将把从刚性几何体上学到的经验和思想,应用到当前尚不支持的关键领域:

  • 植被与叶片 (Foliage)
  • 动画 (Animation)
  • 地形 (Terrain)

三、 结语与致谢

讲者最后感谢了 Nanite 和虚拟阴影贴图(Virtual Shadow Maps)的合作者、UE5 渲染团队、艺术家以及 Epic Games 提供的机会。

  • 附加资源: 本次讲座的幻灯片(Slide Deck)将会发布,其中包含讲座中未涉及的额外细节 (Bonus Slides) 和对相关前期工作 (Prior Work) 的讨论,为希望深入研究的开发者提供了宝贵的参考资料。