探索连续细节层次(Continuous LOD):深入解析 NVIDIA 的 nv_cluster_lod_builder

在实时渲染中,平衡海量几何细节与性能一直是一个核心挑战。传统的解决方案是离散 LOD(Discrete LOD),即预先制作几个不同精度的完整模型,随着距离变化整体切换。这种方法简单,但容易导致明显的视觉突变(Popping),并且难以精细控制细节。

今天,我们将探讨一种更先进的技术------连续细节层次(Continuous LOD, CLOD) ,特别是基于"簇(Cluster)"的实现方案。我们将通过分析 NVIDIA 的一个研究项目 nv_cluster_lod_builder 来了解其工作原理。https://github.com/nvpro-samples/nv_cluster_lod_builder

重要提示nv_cluster_lod_builder 仓库已被 NVIDIA 归档并不再维护。它主要作为一个研发项目,用于提供基于簇的光线追踪连续 LOD 的快速入门。对于类似的活跃维护代码,请参考 meshoptimizer/demo/clusterlod.h at master · zeux/meshoptimizer

什么是基于簇的连续 LOD?

nv_cluster_lod_builder 是一个连续 LOD 网格库。与传统 LOD 不同,它允许对网格内的几何细节进行更细粒度的控制。

其核心思想是将一个包含数百万三角形的巨大网格分割成许多小的三角形"簇"。这些簇经过精心预计算和简化,使得它们可以在不同的 LOD 级别之间无缝结合。

图 1:不同细节级别的簇无缝拼接在一起演示。

在渲染时,系统会根据相机的位置,自适应地选择一部分簇来提供所需的细节量。这种方法的主要优势包括:

  • 更快的渲染:将三角形分配到最需要它们的地方(高细节区域)。

  • 减少内存使用:特别适用于光线追踪应用,可以通过几何流式传输按需加载数据。

工作原理:构建 LOD 数据

实现连续 LOD 的关键在于一种能够允许跨网格进行规则、无缝 LOD 过渡的减面(Decimation)策略。

为了保证过渡时边界匹配,减面时必须保持三角形边缘的边界固定。但如果边界一直固定,这些边缘就永远无法被简化。因此,该库采用了一种迭代策略:在连续的迭代中选择不同的边界,并固定新的边缘,从而允许旧的边缘进行减面。

构建过程是一个重复循环,直到只剩下一个代表整个网格的簇。流程如下图所示:

图 2:LOD 生成过程的迭代步骤。

这三个步骤是:

  1. 生成簇(Make Clusters):将三角形划分成固定大小的簇。

  2. 簇分组(Group Clusters):这是关键一步。将簇组合成"组",并鼓励跨越旧组的边界进行分组。这样可以将旧的边界包含在新的组内部,使其在下一步可以被简化。

  3. 组内减面(Decimate within groups, keep border) :计算组与组之间共享的顶点并锁定它们,然后使用简化算法(如 meshoptimizer)对每个簇组进行减面,目标是减少一半的三角形数量。减面后的结果将作为下一次迭代的输入。

这个过程产生的数据结构不是一棵简单的树,而是一个有向无环图(DAG)。LOD 的过渡只能发生在组的边界上。

图 3:簇、组及其生成关系形成的有向无环图(DAG)。

运行时:选择正确的簇

构建完成后,我们得到了一堆簇和它们之间的关系。在运行时,我们需要决定渲染哪些簇。目标通常是让几何误差小于一个像素的大小。

该库使用**二次误差(Quadric Error)**来衡量简化后的网格与原始高分辨率网格之间的距离。

为了确定是否需要渲染某个簇组,我们会计算从相机角度看的最大可能角度误差(Angular Error)。对于一个组来说,最坏的情况是其包围球上距离相机最近的点,其误差等于该组的二次误差半径。如果这个角度误差大于我们设定的阈值,说明细节不够,我们需要使用更精细的层级。

图 4:基于包围球和二次误差计算最大角度误差的示意图。

为了保证渲染出一个无孔洞、无重叠的单一连续表面,该库采用了一个巧妙的方法:人为地增大父节点(生成组)的包围球,使其包含子节点(被生成的几何体)。这保证了在进行误差阈值判断时,不会同时选择父节点和子节点。

选择渲染簇的逻辑可以总结为:渲染那些"其自身的误差小到足够好,但其父级的误差还不够好"的簇。

空间层次结构与流式传输

为了加速运行时的查找,避免检查每一个簇,该库构建了一个空间层次结构(包围球树)。

图 5:利用空间层次结构剔除距离过远或细节过高的区域。

通过遍历这个树,我们可以快速剔除那些对于当前相机距离来说细节过高或过低的簇组。

图 6:在层次结构中选择满足误差阈值的节点(蓝色十字)。

这个层次结构也为流式传输(Streaming)提供了基础。在遍历层次结构时,如果遇到尚未加载的叶子节点,可以触发加载请求,从而实现按需加载几何数据以节省内存。

代码示例

以下是如何使用 nv_cluster_lod_builder C++ API 构建 LOD 数据,以及在运行时进行选择的核心逻辑的简要示例。

1. 构建 LOD 数据

cpp 复制代码
#include <nvclusterlod/nvclusterlod_hierarchy.h>
#include <nvclusterlod/nvclusterlod_mesh.h>
// ... 其他必要的头文件 ...

// 假设已填充输入网格数据 indices 和 positions
std::vector<vec3u> indices   = ...;
std::vector<vec3f> positions = ...;

// 创建上下文
nvcluster_Context context;
nvcluster_ContextCreateInfo contextCreateInfo{};
nvclusterCreateContext(&contextCreateInfo, &context);

nvclusterlod_Context lodContext;
nvclusterlod_ContextCreateInfo lodContextCreateInfo{.clusterContext = context};
nvclusterlodCreateContext(&lodContextCreateInfo, &lodContext);

// 1. 创建减面的簇网格 (LOD Mesh)
const nvclusterlod_MeshInput meshInput{
    .indices      = reinterpret_cast<const nvclusterlod_Vec3u*>(indices.data()),
    .indexCount   = static_cast<uint32_t>(indices.size()),
    .vertices     = reinterpret_cast<const nvcluster_Vec3f*>(positions.data()),
    .vertexCount  = static_cast<uint32_t>(positions.size()),
    .vertexStride = sizeof(nvcluster_Vec3f),
    .decimationFactor = 0.5, // 每次迭代减少一半三角形
};

nvclusterlod::LocalizedLodMesh mesh;
// 核心生成函数
nvclusterlod::generateLocalizedLodMesh(lodContext, meshInput, mesh);

// 2. 构建用于加速选择的空间层次结构 (Hierarchy)
const nvclusterlod_HierarchyInput hierarchyInput {
    .clusterGeneratingGroups = mesh.lodMesh.clusterGeneratingGroups.data(),
    .groupQuadricErrors      = mesh.lodMesh.groupQuadricErrors.data(),
    // ... 传入 mesh 中的其他数据 ...
    .lodLevelCount           = static_cast<uint32_t>(mesh.lodMesh.lodLevelGroupRanges.size())
};

nvclusterlod::LodHierarchy hierarchy;
// 核心生成函数
nvclusterlod::generateLodHierarchy(lodContext, hierarchyInput, hierarchy);

// ... 清理上下文,上传数据到 GPU ...

2. 运行时选择逻辑(伪代码)

在运行时遍历层次结构或检查簇时,使用以下条件来决定是否选择渲染某个簇。核心是比较当前组和其生成组(父级)的误差与阈值。

cpp 复制代码
// 计算角度误差的伪函数
function errorOverDistance(transform, boundingSphere, quadricError) { ... }

// 选择簇的条件:
bool shouldRender =
    // 条件1:当前组的减面几何体的误差大于等于阈值(细节刚好够用或不够用)
    errorOverDistance(
        objectToEyeTransform,
        hierarchy.groupCumulativeBoundingSpheres[clusterGroup],
        hierarchy.groupCumulativeQuadricError[clusterGroup]
    ) >= threshold
    &&
    // 条件2:其生成组(更粗糙的级别)的误差小于阈值(细节太差了)
    errorOverDistance(
        objectToEyeTransform,
        hierarchy.groupCumulativeBoundingSpheres[clusterGeneratingGroup],
        hierarchy.groupCumulativeQuadricError[clusterGeneratingGroup]
    ) < threshold;

if (shouldRender) {
    // 渲染该簇
}

总结

nv_cluster_lod_builder 虽然已归档,但它清晰地展示了构建基于簇的连续 LOD 系统的核心概念:通过巧妙的迭代分组和减面策略生成 DAG 结构,并利用基于误差的度量和空间层次结构在运行时高效地选择合适的几何细节。

对于希望深入研究大规模几何渲染和流式传输技术的开发者来说,理解这些概念至关重要。如果你计划在实际项目中应用类似技术,建议参考更活跃的开源实现,如 meshoptimizer 中的演示。

相关推荐
watson_pillow15 分钟前
c++ 协程的初步理解
开发语言·c++
故事和你9132 分钟前
洛谷-算法1-2-排序2
开发语言·数据结构·c++·算法·动态规划·图论
Tanecious.3 小时前
蓝桥杯备赛:Day6-B-小紫的劣势博弈 (牛客周赛 Round 85)
c++·蓝桥杯
流云鹤3 小时前
Codeforces Round 1090 (Div. 4)
c++·算法
小菜鸡桃蛋狗3 小时前
C++——string(上)
开发语言·c++
wljy13 小时前
第十三届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(个人见解,已完结)
c语言·c++·算法·蓝桥杯·stl
清空mega4 小时前
C++中关于数学的一些语法回忆(2)
开发语言·c++·算法
想唱rap4 小时前
线程池以及读写问题
服务器·数据库·c++·mysql·ubuntu
望眼欲穿的程序猿4 小时前
Vscode Clangd 无法索引 C++17 或者以上标准
java·c++·vscode
6Hzlia4 小时前
【Hot 100 刷题计划】 LeetCode 42. 接雨水 | C++ 动态规划与双指针题解
c++·算法·leetcode