Files
XCEngine/docs/plan/毕设/初稿/第七章.md

20 KiB
Raw Blame History

第七章 基于 NanoVDB 的体积渲染模块设计与实现

在前文完成渲染引擎主体、编辑器与工作流分析之后,本章进一步聚焦当前课题中最重要的高级渲染扩展,即基于 NanoVDB 的体积渲染模块。与前几章偏向引擎主体不同本章的重点不再是通用运行时框架而是围绕体积数据加载、GPU 访问、光线步进、跳空优化和体积阴影等关键流程,说明当前项目中这部分功能是如何落地的。需要说明的是,按照当前工程进展,体积渲染部分已经完成了独立原型验证,并在主引擎中接入了体数据资源与组件接口;但正式渲染通道并入主线仍处于收尾推进阶段。因此,本章的写法将同时覆盖“已经完成的原型实现”和“已经进入主引擎的数据接口部分”。

7.1 模块设计目标

7.1.1 面向现有渲染引擎扩展体积渲染能力

体积渲染模块的设计目标并不是脱离现有渲染引擎单独实现一个演示程序,而是在已有图形接口抽象、资源系统、场景系统和编辑器验证能力的基础上,补充云、雾、烟等参与介质的实时渲染能力。从课题定位看,这一模块属于当前渲染引擎中的高级渲染扩展,它需要复用已有 DirectX 12 资源管理能力,也需要为后续进入场景资源流、组件系统和编辑器工作流预留接口。

结合当前项目实际实现,体积渲染部分采用了“两步推进”的工程路径。第一步是在 mvs/VolumeRenderer 中完成 NanoVDB 数据访问与实时体光线步进的独立原型验证,先把关键算法和数据通路跑通;第二步是在主引擎中补齐 VolumeFieldVolumeRendererComponent、资源导入与场景提取等基础接口,为后续正式并入渲染主链做准备。这种组织方式既保证了算法验证效率,也减少了在主线尚未稳定时大规模改动引擎主体的风险。

7.1.2 模块实现需要解决的核心问题

从工程实现角度看,当前体积渲染模块主要需要解决四个问题。第一,如何把 .nvdb 文件中的稀疏体数据读取为适合 GPU 使用的连续数据,并保持 NanoVDB 原有的层级结构信息。第二,如何在 shader 中以较低代价访问这些稀疏体数据,并利用其层级特性跳过空区域。第三,如何在实时条件下完成体积颜色和透射率累积,使渲染结果在质量与性能之间保持可接受平衡。第四,如何把这部分能力逐步接到主引擎的资源系统和场景系统中,而不是长期停留在独立实验程序层面。

本章后续几个小节正是围绕这四个问题展开。其基本技术路线可以概括为:由 CPU 侧完成 NanoVDB 数据加载与 GPU 上传,在像素着色器中结合 PNanoVDB 访问接口完成体数据遍历,并在光线步进过程中叠加单次散射近似和体积阴影,最后通过主引擎中的资源与组件接口为正式主线并入提供接入点。

7.1.3 当前实现路线的取舍

需要特别说明的是,开题阶段曾将 Compute Shader 和 DXR 体积阴影作为重要扩展方向提出但从当前仓库中的真实实现看已经完成的核心原型首先落在基于图形管线的像素着色器体积渲染方案上。具体来说当前原型通过全屏四边形驱动像素着色器在像素着色器中完成相机光线构造、NanoVDB 访问和体积积分过程。这种方案的优点是接入 DirectX 12 图形管线较直接,便于先验证 NanoVDB 读取、稀疏遍历和单次散射近似是否成立;其局限则是后续若要进一步提升结构清晰度和扩展 DXR 体积遮挡,仍需要继续向更正式的渲染通道推进。

因此,当前体积渲染模块的设计目标可以概括为:先完成 NanoVDB 稀疏体数据实时渲染的关键原型,再把资源与场景侧接口逐步接入主引擎,最终为正式体积渲染 pass 的并入打下基础。

7.2 NanoVDB 数据加载与 GPU 上传

7.2.1 .nvdb 文件读取与 CPU 侧数据准备

当前原型工程中的体积数据加载由 NanoVDBLoader 完成。加载时,系统首先调用 nanovdb::io::readGrid 读取 .nvdb 文件,并通过 nanovdb::GridHandle<nanovdb::HostBuffer> 获取 NanoVDB 的连续内存缓冲区。与传统基于指针的树结构不同NanoVDB 的优势之一就在于其树结构已经被线性化为适合 GPU 访问的连续内存布局,因此在读取完成后,系统不需要在 CPU 侧重新构造复杂指针关系,而是直接按字节复制原始缓冲区内容即可。

在当前实现中,加载器会根据 gridHandle.buffer().bufferSize() 计算字节总量,并进一步换算为以 uint32 为单位的元素数量,随后在 CPU 侧申请一段连续内存,将 NanoVDB 原始缓冲区完整拷贝到本地缓存中。这样处理后,体数据在 CPU 侧已经具备了与 GPU 侧 StructuredBuffer<uint> 对应的线性布局,为后续上传和 shader 访问提供了基础。

7.2.2 元数据提取与体数据描述

除了主体 payload 外,体积渲染还需要体数据的边界范围、体素尺度等元数据,用于调试显示、体积定位和后续资源化处理。当前原型在 CPU 侧直接从 NanoVDB 线性缓冲区中提取 worldBBoxvoxelSize,并把这些信息写入 NanoVDBData 结构体。该结构体中除 cpuDatabyteSizeelementCount 外,还保存了 worldBBox[6],从而能够在后续渲染阶段得到体数据在世界空间下的包围信息。

在主引擎中,体数据被进一步抽象为 VolumeField 资源。VolumeField 内部记录 storageKindboundsvoxelSize 和原始 payload并由 VolumeFieldLoader 支持 .nvdb.xcvol 两种加载路径。与此同时,ArtifactFormats.h 中还定义了 VolumeFieldArtifactHeader,为 storageKindboundsMinboundsMaxvoxelSizepayloadSize 预留了专门的持久化字段。这说明当前主引擎已经在资源结构上为体数据正式并入做了准备。

7.2.3 GPU 上传流程与资源绑定方式

在获得 CPU 侧连续缓冲区之后,当前原型采用 DirectX 12 的默认堆加上传堆组合完成 GPU 上传。系统首先在默认堆中创建一块 BUFFER 类型资源,初始状态为 COPY_DEST;随后在上传堆中创建等大小的上传缓冲,将 CPU 侧 NanoVDB 数据拷贝到上传堆映射内存中,再通过 CopyBufferRegion 将数据复制到默认堆资源。复制完成后,系统再插入资源状态屏障,把体数据缓冲从 COPY_DEST 切换到 GENERIC_READ,使其可以被 shader 以只读方式访问。

在绑定方式上,当前原型为体积渲染单独建立了一套根签名。该根签名包含两个根参数:一个是绑定在 b1 的常量缓冲视图,用于传递逆视图投影矩阵、相机位置、步长、最大步数、光照方向等参数;另一个是绑定在 t1 的着色器资源视图,用于传递 NanoVDB 线性缓冲区。shader 端则把该缓冲声明为 StructuredBuffer<uint>,再借助 PNanoVDB.hlsl 中提供的读取函数和访问器完成数据解释。

此处建议插入图 7-1“NanoVDB 数据加载与 GPU 上传流程图”,展示 .nvdb 文件读取、CPU 连续缓冲准备、上传堆复制、默认堆持久化和 StructuredBuffer<uint> 绑定之间的关系。

7.3 体积渲染核心流程实现

7.3.1 基于全屏四边形的相机光线构造

当前原型的体积渲染 pass 采用全屏四边形驱动的方式执行。主程序先构建一个覆盖标准化设备坐标空间的四边形网格,并使用专门的 volume.hlsl 顶点着色器与像素着色器完成体积渲染。顶点着色器本身较为简单,它一方面把输入顶点直接输出到屏幕空间,另一方面利用逆视图投影矩阵把屏幕空间位置还原为世界空间位置,从而为像素着色器提供构造视线方向所需的信息。

在像素着色器中,系统通过 normalize(input.worldPos - _CameraPos_Density.xyz) 计算当前像素对应的相机射线方向。这样一来,虽然渲染入口是传统图形管线中的全屏四边形绘制,但真正的体积积分过程仍然以“每像素一条视线”的方式展开,本质上与常见的 ray marching 体渲染流程一致。

7.3.2 体积内部采样与颜色累积

在构造出相机光线之后shader 会先初始化 NanoVDB 访问结构。当前实现中定义了 NanoVolume 结构,并在 initVolume 中依次建立 grid handletree handleroot handle 以及 read accessor。完成初始化后shader 调用 get_hdda_hit 沿当前相机光线执行第一次 NanoVDB 层级遍历,以确定光线是否进入有效体积区域以及进入位置对应的参数值 tmin。如果光线未命中体数据,则该像素直接输出透明结果。

当光线命中体积后主循环开始按照最大步数限制进行步进。对每一个步进位置shader 都会计算当前采样点坐标,并利用 get_value_coord 读取该处的体素值,再乘以 _DensityScale 获得当前密度。随后程序以该密度作为散射项 sigmaS 的基础,并通过指数衰减形式更新透射率 transmittance。散射贡献部分则采用单次散射近似,将当前点的散射强度、光照衰减和相位函数结果组合为 Sint,再累积到颜色结果中。

从流程上看,这一实现与第三章中的体积积分离散化过程是一致的:沿相机光线前向步进,对每个采样点估计局部散射与透射率,再将结果累计为最终颜色。为了避免无意义的长尾积分,当前实现还设置了两类提前终止条件:其一是当累积密度 acc_density 超过阈值时停止步进,其二是当透射率衰减到足够低时直接终止。这些处理在保证运行效率方面起到了重要作用。

7.3.3 渲染参数组织与结果输出

当前体积渲染 pass 所需的运行参数统一存放在一块常量缓冲中。该常量缓冲依次包含逆视图投影矩阵、相机位置与密度缩放、体积边界最小值与步长、体积边界最大值与最大步数、绕 Y 轴旋转参数以及光照方向与阴影采样参数等内容。主程序会在每帧更新这些参数,并将其绑定到体积渲染根签名对应的 CBV 槽位。

从图形状态设置上看当前体积渲染管线采用了颜色混合开启、深度测试开启但深度写入关闭的方式。这种设置使体积结果能够作为半透明层叠加到已有场景结果上同时避免破坏前面已生成的深度缓冲。最终输出阶段shader 还会对结果执行一次 gamma 校正,并把累积密度作为 alpha 输出。这样既能得到较自然的颜色表现,也便于后续混合。

此处建议插入图 7-2“体积渲染结果示意图”可选用 cloud.nvdbbunny.nvdb 的当前渲染结果截图。

7.4 稀疏体数据访问与跳空优化

7.4.1 StructuredBuffer<uint>PNanoVDB 访问方式

NanoVDB 的核心优势在于其树结构已经被线性化,因此 GPU 侧不需要复杂指针跳转即可访问体数据。当前 shader 端把体数据统一表示为 StructuredBuffer<uint> buf,随后通过 PNanoVDB.hlsl 提供的缓冲读取函数、网格句柄、树句柄、根节点句柄以及访问器接口解释这段线性缓冲区。具体来说shader 会先读取 grid type再通过 pnanovdb_grid_get_treepnanovdb_tree_get_rootpnanovdb_readaccessor_init 建立访问器,之后即可通过统一接口查询给定位置的体素值和层级维度信息。

这种实现方式的重要意义在于,体数据访问不再依赖 CPU 端构造额外的数据映射表,也不需要在 GPU 端额外展开稠密体素网格。体渲染 pass 可以直接以 NanoVDB 原始线性布局为输入,从而较完整地保留其稀疏结构优势。

7.4.2 基于 HDDA 的层级遍历与空域跳过

为了避免对大量空区域执行逐体素采样,当前实现引入了 PNanoVDB 中的 HDDA 遍历能力。首先shader 通过 get_hdda_hit 调用 pnanovdb_hdda_tree_marcher,在层级结构上快速找到光线进入有效体积区域的位置。进入主循环后,又通过 get_dim_coord 查询当前位置所在节点的维度信息。若该维度值大于 1说明当前命中的是较粗层级的 tile 区域,此时程序并不执行逐体素积分,而是直接进行更大步长的跳跃,从而跳过大片空域。

除显式的层级跳过外,当前 shader 还根据密度阈值设置了第二层启发式优化。当当前采样位置的密度低于设定阈值时,系统会采用较大的步长直接向前推进;一旦重新接近有效区域,又会通过回退部分跳跃距离的方式减少错过细节的风险。虽然这部分处理带有一定工程经验性质,但从当前原型看,它与 NanoVDB 的层级跳过机制共同起到了减少无效采样的作用。

7.4.3 稀疏访问优化的作用与边界

对于体积渲染而言,真正耗时的部分往往不是单次采样本身,而是“在大片空区域中做了大量没有意义的采样”。当前实现通过 NanoVDB 层级结构与 HDDA 机制,把“先找到有效区域,再进入局部积分”的思路落实到了 shader 中,这也是原型能够在实时条件下运行的关键原因之一。与稠密三维纹理逐点扫描相比,这种方式更符合云、雾等稀疏参与介质的空间分布特征。

当然,当前实现中的具体跳跃步长和回退系数仍然带有明显的实验性质,它们尚未被完全整理为统一的引擎参数系统。后续若正式并入主引擎主线,还需要把这些策略进一步规范化,并与材质参数、质量档位和性能测试结果结合起来。

7.5 光照与体积阴影实现

7.5.1 单次散射近似

当前体积渲染模块在光照模型上采用的是单次散射近似,而不是完整的多次散射求解。其基本思路是:在相机光线上每个采样点处,只考虑该点受主光照方向直接照射后产生的一次散射贡献,再结合透射率把这部分贡献累积到观察结果中。这样做虽然牺牲了部分物理真实性,但显著降低了实时渲染开销,也符合当前项目作为工程型毕设的实现阶段。

在 shader 代码中,当前相位函数被简化为常量函数 phase_function() = 1.0,也就是把散射过程近似为各向同性响应。这意味着当前实现并未进一步展开如 Henyey-Greenstein 相位函数这样的方向性散射模型,而是先以结构正确、代价较低的形式完成体积光照闭环。对于现阶段的模块验证而言,这样的取舍是合理的。

7.5.2 沿光照方向的体积阴影估计

为了让体积结果不至于表现为完全均匀发亮的云团,当前实现还引入了沿光照方向的体积阴影估计。其核心函数为 volumetric_shadow。在该函数中,程序从当前采样点沿光照方向继续步进,对后续路径上的密度进行估计,并通过指数衰减累积出一条简化的光照透射率。随后,该透射率作为当前采样点的阴影系数参与颜色积分。

从实现细节看,当前阴影步进采用了指数增长的步长策略:每次采样后将 step_size 扩大一倍,以较少的采样次数快速覆盖更远距离的阴影路径。这样做能够在有限采样预算内得到一条近似可用的阴影衰减曲线,并降低阴影求解带来的额外成本。与此同时,主程序还通过常量缓冲向 shader 传递 LightDir 和阴影相关参数,使体积光照方向能够随运行状态变化。

7.5.3 当前光照模型的局限

虽然当前实现已经具备体积光照与阴影的基本效果,但其近似性质也十分明显。第一,当前只实现了单次散射,没有处理多次散射带来的能量回填和颜色传播,因此云体内部的柔和感仍然有限。第二,相位函数采用常量近似,没有体现前向散射或后向散射差异。第三,阴影积分仍采用简化步进,没有与真正的场景几何遮挡、级联阴影或 DXR 光线查询结合起来。也就是说,当前模块已经完成了“体积渲染能成立”的关键闭环,但距离更完整、更物理化的体积光照模型还有后续扩展空间。

此处建议插入图 7-3“体积阴影开启与关闭对比图”用于展示单次散射近似和阴影积分对体积层次感的改善作用。

7.6 当前实现状态分析

7.6.1 已完成的核心原型部分

从当前仓库中的真实实现看,体积渲染最核心的原型已经在 mvs/VolumeRenderer 中完成。该原型已经具备 NanoVDB 文件读取、CPU 连续缓冲准备、DirectX 12 默认堆上传、StructuredBuffer<uint> 绑定、PNanoVDB 层级访问、HDDA 进入测试、主光线步进积分以及简化体积阴影估计等关键能力。主程序能够直接加载 cloud.nvdb 等测试数据,并在独立窗口中实时输出体积渲染结果。就“关键算法与数据通路是否打通”这一问题而言,当前答案是肯定的。

7.6.2 已进入主引擎的资源与场景接口部分

除独立原型外,主引擎中与体积渲染相关的资源和场景接口也已经基本建立完成。资源层面,VolumeFieldVolumeFieldLoader 已经支持 .nvdb.xcvol 两类体数据资源,AssetDatabase 也已经加入了 VolumeField 的 artifact 生成流程;组件层面,VolumeRendererComponent 已经能够持有体数据资源和材质资源,并支持同步、异步与基于 AssetRef 的加载方式;场景提取层面,RenderSceneExtractorRenderSceneUtility 已经能够把场景中的体积对象提取为 VisibleVolumeItem,并将其纳入 RenderSceneData

这说明体积渲染并不是完全游离于主引擎之外的孤立实验,而是已经在资源系统、组件系统和场景提取结构中获得了明确的位置。换言之,正式并入主渲染通道所需的很多前置数据接口已经准备好。

7.6.3 正在收尾与尚未正式并入主线的部分

当前仍在推进的部分主要集中在“把已有原型真正并入主引擎渲染主链”这一阶段。虽然 RenderSceneData 中已经预留了 visibleVolumes,但当前主渲染管线中尚未正式消费这一数据并生成对应的体积 passVolumeRendererComponent 虽已进入组件系统,但编辑器端尚未形成对应的组件检查与参数编辑面板;主引擎资源管线虽然已经具备 VolumeFieldArtifactHeader 结构,但对 .nvdb 源数据中的边界和体素尺寸信息仍需继续工程化整理。除此之外,开题阶段提出的 DXR 体积阴影扩展,目前也尚未以正式代码路径并入现有主线。

因此,更准确的表述应当是:当前体积渲染已经完成了关键原型验证,并已在主引擎中接入资源和场景接口;而正式主线渲染接入、编辑器参数化支持以及更高质量光照扩展,仍处于最后的工程收尾阶段。

7.7 本章小结

本章围绕当前项目中的 NanoVDB 体积渲染模块实现进行了分析。可以看到,该模块已经在独立原型层面完成了从 .nvdb 文件读取、GPU 上传、StructuredBuffer<uint> 访问,到基于 PNanoVDB 的层级遍历、光线步进积分和体积阴影近似的完整闭环;与此同时,主引擎中也已经建立起 VolumeField 资源、VolumeRendererComponent 组件以及 VisibleVolumeItem 场景提取结构,为正式并入渲染主线提供了数据基础。

从当前工程状态看,体积渲染部分已经不再停留在理论分析层面,而是形成了可运行、可验证、可继续并入主引擎的实际模块。下一章将在此基础上进一步结合测试场景和实验结果,对当前渲染引擎主体与体积渲染扩展的功能表现和实现效果进行分析。