Chapter 125: Volume Rendering
体渲染处理的是一块有内部结构的数据。传统三角形渲染把可见表面交给光栅化管线,体渲染把相机射线推进到三维数据内部,在射线经过的每个采样点读取密度、温度、强度或其它标量,再把这些样本累积成屏幕像素。读完本章后,读者应能追踪一帧体渲染从体数据、transfer function、采样步长、GPU 资源到最终颜色的完整路径,并能判断画面噪声、层状条纹、交互卡顿或组织边界丢失来自哪一段。
本章使用一个贯穿材料:一份 的 CT 标量体数据。数据被上传为 3D texture,每个体素存储一个标量值;屏幕上的每个像素生成一条相机射线,射线与体数据包围盒相交后,在包围盒内部做 ray marching。每次采样得到标量、梯度和 transfer function 输出的颜色透明度,最后通过前向 alpha compositing 得到像素颜色。医学 CT 只是贯穿材料,地质体、云场、烟雾密度和仿真标量场也可以套用同一条资源路径。
体渲染的主判断顺序可以压缩为五步:先确认标量场的空间单位和取值范围,再设计 transfer function,把数据值映射为颜色和透明度;然后确定采样步长和积分规则;接着把体数据、transfer function、相机和包围盒组织成 GPU pass;最后用交互延迟、空洞、条纹、过曝和边界清晰度反推数据、采样和 GPU 资源的瓶颈。
125.1 Volume Scalar Field Transfer Function and Ray Marching
体渲染的输入是 volume scalar field,即定义在三维空间中的标量场。对贯穿材料来说,标量场可以写成 ,其中 是体数据局部空间中的位置, 返回该位置的 CT 强度。离散数据里, 存在于体素网格上;shader 在任意位置采样时,3D texture 采样器会按滤波模式在邻近体素之间插值。这个定义决定了体渲染的第一个边界:它直接渲染体数据内部的数值分布,表面只是在某些取值或透明度设置下显现出来的视觉结果。
Transfer function 是从标量值到视觉属性的映射。最常见形式是把一个标量值映射成 RGBA,其中 RGB 表示颜色,A 表示该采样点对射线的遮挡贡献。对 CT 数据来说,低强度区可以映射为透明或低透明度,软组织可以映射为半透明颜色,骨骼可以映射为较高透明度和更亮的颜色。这个映射直接承担数据分类和可见性控制:哪些结构进入积分,哪些结构在累积过程中被压低权重,都由它的颜色和透明度曲线决定。
Ray marching 是体渲染的核心执行路径。每个屏幕像素先生成一条相机射线,射线与体数据包围盒求交得到进入点和离开点。shader 从进入点开始沿射线推进,每隔一个 step size 采样一次体数据。采样点的标量经过 transfer function 变成颜色和透明度,再按前向顺序累积。射线走出包围盒或透明度接近饱和后,该像素的体渲染结束。
下面的图表示一条像素射线在 3D texture 内部的最小路径。图的范围限定在直接体渲染的单次 pass,重点是主射线积分、采样和累积。
这条路径里,屏幕像素只负责提供射线起点和方向;包围盒负责限制积分区间;3D texture 提供标量场采样;transfer function 把数值转成可累积的视觉贡献;opacity accumulation 把多个半透明样本合成一个像素。任何一段配置错误都会表现为具体画面症状:包围盒求交错误会出现体数据被切掉或完全消失,transfer function 范围错误会使结构全透明或全白,累积公式错误会让透明度随采样次数异常增长。
前向 alpha compositing 的常用写法是用已累积颜色 和透明度 接收新样本颜色 和透明度 。一次更新可以写成 ,。这个公式的工程含义很直接:射线前方已经累积的透明度越高,后方样本贡献越小。它把体数据的遮挡关系转成每个像素上的顺序积分,所以采样顺序和 alpha 语义需要保持一致。
梯度为体渲染提供局部方向信息。标量场的梯度 指向数值上升最快的方向,常用中心差分从邻近体素估计。梯度长度可以作为边界强度,梯度方向可以参与局部光照。对 CT 数据来说,组织边界附近的标量变化更明显,梯度幅度通常更高;同一 transfer function 如果结合梯度维度,就能把内部均匀区域压低,把边界区域突出。这里的关键判断是:标量决定“这里是什么值”,梯度帮助判断“这里是否位于变化边界”。
一个最小 GPU 片段着色器可以表达这条数据路径。代码是简化 GLSL 风格,只展示 ray marching、transfer function 和前向累积关系;真实工程还需要处理坐标约定、边界采样、格式归一化和平台资源绑定。
vec4 marchVolume(Ray ray, vec2 hitRange) {
vec3 position = ray.origin + ray.direction * hitRange.x;
float t = hitRange.x;
vec4 accum = vec4(0.0);
for (int stepIndex = 0; stepIndex < MAX_STEPS && t <= hitRange.y; ++stepIndex) {
float scalar = texture(volumeTex, position).r;
vec4 sampleColor = texture(transferTex, scalar);
float remain = 1.0 - accum.a;
accum.rgb += remain * sampleColor.a * sampleColor.rgb;
accum.a += remain * sampleColor.a;
if (accum.a > 0.98) {
break;
}
position += ray.direction * stepSize;
t += stepSize;
}
return accum;
}
这段代码对应的资源关系是:volumeTex 是体数据 3D texture,transferTex 是一维或二维 transfer function,hitRange 是射线进入和离开包围盒的距离,stepSize 控制采样间隔。调试时可以分别固定这些输入:把 transfer function 改成灰度 ramp 可观察标量范围,把 stepSize 减半可观察条纹是否下降,把 hitRange 可视化为颜色可检查包围盒求交。
125.2 采样与积分策略
采样策略决定每条射线读取多少个点,积分策略决定这些点如何变成像素颜色。对 的 CT 体数据,如果每个像素沿射线采样 256 次,1080p 画面会产生数亿次 3D texture 读取。体渲染的交互性能因此直接受 step size、提前结束、空区跳过和缓存访问局部性约束。
Step size 是沿射线的采样间隔。它通常以体数据局部空间长度或体素尺寸为单位。步长过大时,细薄结构会被跳过,画面出现层状条纹或边界断裂;步长过小时,采样次数上升,fragment 或 compute pass 的纹理读取和循环成本增加。稳定做法是先根据体素 spacing 设定基准步长,再按画面质量和帧预算调整。对非等距体素,射线推进需要考虑 x、y、z 三个方向的物理 spacing,否则某个方向的结构会被拉伸或积分过密。
透明度需要和步长绑定。Transfer function 输出的 alpha 常常被理解为“这个值有多不透明”,但 ray marching 中的单次采样透明度还取决于这一步覆盖的长度。一个可迁移写法是把密度 rho 转成按路径长度衰减的透明度:,其中 DeltaS 是 step size。这样调整步长时,透明度随路径长度重新归一,画面亮度和遮挡关系会更稳定。
Early termination 利用前向累积的透明度饱和特性减少后续采样。当累积 alpha 接近 1 时,后方样本对像素的可见贡献已经很低,shader 可以结束循环。这个策略对骨骼、金属、浓烟等高透明度设置收益明显;对整体低透明度的云雾或医学半透明观察,收益会下降。判断 early termination 是否有效,要看透明度分布和相机射线长度,而非只看体数据尺寸。
Empty-space skipping 处理的是体数据内部大块无贡献区域。贯穿材料中,如果 transfer function 把低强度空气区设为透明,射线在这些区域持续采样会消耗纹理带宽和循环次数。工程上常把体数据分成 brick,为每个 brick 预计算 min-max、占用标记或最大 alpha;ray marching 到透明 brick 时直接跳到下一个有贡献区域。这里的前提是 transfer function 和 acceleration data 之间保持一致:当 transfer function 改变后,brick 是否有贡献也可能改变,需要重新计算或使用能覆盖新映射的保守判断。
Pre-integration 用于降低采样间隔带来的颜色跳变。普通采样只在离散点读取标量并查 transfer function;当 transfer function 有尖锐峰值时,两个采样点之间可能跨过高透明区,普通采样会漏掉贡献。Pre-integration 把相邻两个标量和步长作为输入,预先估计这段路径上的积分结果。它适合医学数据中窄窗口观察结构边界,也适合 transfer function 变化频繁但采样预算受限的交互场景。
Jitter 用随机或低差异偏移改变每条射线的起始采样位置。它把固定步长造成的规则条纹转成更细的噪声,后续可以通过时间累积或低通滤波压低噪声。Jitter 的收益来自打散采样误差;成本是单帧稳定性下降,交互旋转或 transfer function 编辑时可能产生闪烁。判断是否使用 jitter,要同时观察静态截图质量和动态交互稳定性。
采样与积分的排查顺序可以固定为:先用灰度 transfer function 确认体数据范围,再把 step size 减半比较结构是否新增;然后打开 early termination 观察帧耗时变化;接着启用 empty-space skipping 并检查跳过区域是否仍然透明;最后加入 jitter 或 pre-integration 处理残余条纹。这个顺序把视觉错误和性能瓶颈分开定位,能减少把 transfer function 问题误判成 GPU 算力问题的概率。
125.3 GPU 加速体渲染实现方式
GPU 体渲染的常见实现把体数据放入 3D texture,把 transfer function 放入 1D 或 2D texture,把包围盒、相机矩阵、步长、体数据 spacing 和窗口参数放入 uniform 或 constant buffer。渲染时可以绘制全屏三角形,让 fragment shader 为每个像素生成射线;也可以绘制体数据包围盒,把可见片元作为进入体数据的像素集合。两种方式的核心都相同:把每个像素映射为射线区间,再在 shader 内循环采样。
3D texture 的格式决定带宽和精度。单通道 8-bit 格式适合低动态范围密度或已归一化数据;16-bit 或 float 格式适合医学、仿真和地质数据中的高动态范围标量。Transfer function 通常使用 RGBA 纹理,编辑 UI 输出一张查找表,shader 用标量归一化值索引它。若使用二维 transfer function,可以把标量和梯度幅度一起作为查找坐标,让边界和内部区域得到不同透明度。
Ray box intersection 是 GPU pass 中最容易被低估的部分。对全屏 pass 来说,shader 需要把屏幕坐标反投影成世界空间或体数据局部空间射线,再与体数据的 axis-aligned box 求交。对包围盒绘制法来说,片元位置可以提供进入点,但相机在包围盒内部时还要处理近裁剪面和射线起点。调试这一步时,可以输出进入距离、离开距离或本地坐标颜色;如果颜色在包围盒边界不连续,后续 marching 的所有结果都会被污染。
Fragment shader 实现简单,接近传统透明体渲染 pass。它适合单个或少量体数据对象,也容易接入现有后处理链。Compute shader 实现更灵活,可以按 tile 组织线程,使用 group shared memory 存储局部参数,也可以把结果写入中间 texture 供后续 pass 使用。选择 fragment 或 compute 的核心维度是工作组织方式:fragment 跟随屏幕像素和固定图形管线,compute 更适合自定义调度、空区跳过、分辨率缩放和异步计算。
资源状态和同步在显式 API 中需要明确管理。Vulkan、Direct3D 12 和 Metal 都要求开发者描述 texture 的用途转换、buffer 更新时机和 pass 间依赖。一个典型帧里,CPU 更新 transfer function texture 或参数 buffer,GPU 在 volume pass 中读取它们,后处理 pass 再读取体渲染结果。若 transfer function 编辑和体渲染同帧发生,资源上传、layout transition、encoder boundary 或 barrier 的位置会直接影响正确性和延迟。
体渲染通常同时受 shader ALU、纹理带宽和循环分支影响。标量采样、梯度估计和 transfer function 查表消耗纹理读取;光照、pre-integration 和多通道分类增加 ALU;early termination、empty-space skipping 和最大步数控制引入分支。分析 GPU capture 时,应把一次 volume pass 拆成 draw 或 dispatch、3D texture 读取次数、循环迭代分布、render target 写入和同步等待。只看总帧耗时无法判断瓶颈落在采样密度、纹理格式还是 pass 依赖上。
跨 API 的实现差异需要落到可检查对象。OpenGL 隐式状态较多,问题常出在 texture unit、sampler state 和 blend state;Vulkan 和 Direct3D 12 问题常出在 descriptor、resource state 和 pipeline layout;Metal 问题常出在 texture usage、argument binding 和 encoder 边界;WebGPU 需要关注 bind group、texture format、采样器限制和 WGSL 坐标约定。把差异写成资源绑定和状态转换清单,比记忆某个 API 的函数名更可靠。
125.4 数据压缩与层次细节策略
体数据的工程瓶颈常常来自容量。一个 的 16-bit 单通道体数据约占 256 MB;如果再存梯度、砖块 min-max、mipmap 或多时间步,显存压力和上传成本会快速增加。数据压缩与层次细节策略的目标是让渲染 pass 读取与当前视角、transfer function 和屏幕精度相匹配的数据,而把远处、透明区或低贡献区域的资源开销压低。
最直接的压缩是格式选择。医学和仿真数据如果原始精度高,可以在离线预处理阶段做归一化、窗口裁剪、量化或 delta 编码,把渲染所需范围打包到 8-bit、12-bit 或 16-bit 纹理。量化会改变 transfer function 的输入粒度,窄窗口观察时更容易出现台阶,所以量化策略需要和目标交互任务一起设计。若章节贯穿材料的 CT 数据主要观察骨骼和软组织边界,保留边界附近的数值分辨率比保留全范围均匀精度更有价值。
Brick layout 把体数据分成小块,例如 、 或 的块。每个 brick 可以单独压缩、单独上传、单独标记 min-max 和占用状态。Ray marching 时先在粗层级判断当前射线段经过哪些 brick,再进入有贡献的 brick 内部细采样。Brick 尺寸需要在跳过效率和元数据开销之间取平衡:brick 太大时透明区域跳过粒度粗,brick 太小时索引、边界处理和调度开销上升。
层次细节可以通过 mipmap、octree、sparse texture 或虚拟纹理实现。Mipmap 适合远处或屏幕占比小的体数据,射线步长变大时读取较粗层级能降低高频闪烁。Octree 或 VDB 类稀疏结构适合大范围空域和局部细节并存的数据,例如云场、地质体和流体模拟。Sparse texture 与虚拟纹理把体数据页化,运行时只把当前视角和当前 transfer function 可能贡献的页驻留在 GPU 上。
压缩数据进入 shader 后会带来解码成本。若压缩格式能由硬件纹理单元直接采样,瓶颈多半转向带宽和缓存;若需要 shader 手动解码,ALU 和分支成本会上升。对交互体渲染来说,好的压缩方案需要同时看显存节省、上传延迟、采样质量和 shader 解码成本。减少文件大小需要转化为 GPU pass 中的可观察收益,例如更少纹理读取、更高缓存命中或更短射线段。
多分辨率策略还要处理 transfer function 编辑。用户改变窗口、透明度峰值或颜色映射后,原本透明的区域可能变成可见,原本可跳过的 brick 可能需要加载细节。稳定工程做法是把数据层级、brick min-max 和 transfer function 查询关联起来:先用 min-max 判断某个 brick 的数值范围是否与当前 transfer function 的非透明区间相交,再决定是否采样或加载。这样交互编辑会改变渲染工作集,系统可以解释卡顿来自数据页切换还是 shader 采样。
数据压缩的检查顺序可以按资源生命周期展开:离线阶段检查标量范围、spacing 和量化误差;加载阶段检查 brick 索引、页表和 min-max;渲染阶段检查每帧驻留页数、3D texture 或 sparse texture 命中情况;画面阶段检查边界台阶、空洞和 LOD 切换痕迹。这个顺序把“数据变小”转成“渲染路径可控”的工程判断。
125.5 医学或地理数据可视化实例
把贯穿材料放入一个医学 CT 观察场景,可以得到完整的调试闭环。输入是一份头部或胸部 CT 体数据,体素 spacing 写入元数据,标量值经过窗口归一化后上传为 16-bit 3D texture。用户通过 transfer function 调整软组织、血管和骨骼的颜色透明度;渲染 pass 对每个像素沿视线 marching;UI 同时显示切片视图、三维体视图和当前帧时间。这个场景要求视觉结果准确、交互延迟可控、结构边界可读。
医学数据的第一类问题是 transfer function 分类。若骨骼和造影血管取值区间接近,单维标量 transfer function 可能把二者同时增强。加入梯度幅度后,可以把边界强、内部均匀的区域与大块实心区域区分开。答案取决于任务所需的分类维度:观察骨骼轮廓时强调高强度和边界,观察软组织连续性时降低高透明遮挡,观察血管结构时需要窄窗口、连通性和交互旋转配合。
第二类问题是采样质量。CT 体数据由切片堆叠而来,z 方向 spacing 可能和 x、y 方向不同。若 shader 直接把体 texture 当成单位立方体并使用统一步长,某些方向会出现层状条纹或厚度误判。正确做法是把物理 spacing 纳入本地坐标到 texture 坐标的变换,并按射线在物理空间中的长度控制步长。这样同一个器官在不同观察方向下的边界厚度才会保持一致。
第三类问题是交互性能。用户拖动 transfer function 时,透明区域和高贡献区域会不断变化,early termination 和 empty-space skipping 的效果也会变化。若 UI 卡顿只在 transfer function 编辑时出现,瓶颈可能来自 transfer function 上传、brick 占用重算或 sparse page 调度;若旋转视角时卡顿更明显,瓶颈可能来自射线长度变化、缓存局部性下降或透明区域减少。性能分析需要把交互动作和 GPU pass 证据对应起来。
地理或地质体数据的关注点略有不同。地质体常常覆盖大范围空间,数据分辨率分层明显,局部区域才有高精度钻孔或仿真结果。体渲染 pass 需要把坐标系、单位、地形表面、剖切面和透明体一起组织。Transfer function 可以把密度、孔隙率、温度或概率映射成颜色透明度;剖切面提供局部内部结构;多分辨率 brick 支撑远近不同的观察精度。这里的核心判断是把体渲染当成数据查询界面:视觉结果要回答“哪里有结构、结构强度如何、边界是否可信、当前分辨率是否足够”。
气象体数据也能使用相同框架。云水含量、湿度、温度或雷达反射率可以构成标量场,transfer function 把不同范围映射成云体、降水核心或温度层。气象数据通常包含时间维度,交互系统还要处理时间步缓存和渐进加载。若动画中云团边界闪烁,应检查 jitter、mipmap 层级、时间插值和 transfer function 稳定性;若剖切面与体渲染不一致,应检查数据归一化和坐标变换。
医学、地质和气象实例共享同一条判断链:数据值先成为 3D texture 中的可采样标量,transfer function 决定它对画面的贡献,ray marching 决定它被读取的密度,GPU pass 决定交互延迟,工具证据把画面现象映射回资源和状态。体渲染的工程能力体现在可追溯判断上:把一个像素的颜色追溯到射线、样本、透明度和数据层级,并据此修改数据、采样或 GPU pass。
最小自检任务
给定一份 CT 体数据,已经上传为 16-bit 3D texture。当前体渲染使用单维 transfer function,步长为一个体素宽度,前向 alpha compositing。用户反馈两个现象:骨骼边缘有明显层状条纹,拖动 transfer function 时帧时间从 12 ms 上升到 35 ms。请按本章的判断顺序说明你会检查哪些对象,并给出可能的调整方向。
答案要点
先检查体数据 spacing 和射线步长,确认 x、y、z 方向物理间距是否已经进入坐标变换;若 z 方向切片间距较大,统一体素步长会造成方向相关条纹,应按物理空间长度设置步长,并对步长变化后的 alpha 做路径长度归一。接着检查 transfer function 是否有窄峰值;若窄峰值导致采样漏过骨骼边界,可降低步长、使用 pre-integration 或加入稳定 jitter,把规则条纹转为更细误差。然后检查 early termination 的命中率和 empty-space skipping 的 brick 判断;拖动 transfer function 后帧时间上升,说明非透明区间扩大、brick 占用状态变化或页加载增加。最后在 GPU capture 中查看 volume pass 的 3D texture 读取次数、循环迭代分布、transfer function 上传和资源同步位置,用证据区分采样成本、数据页切换和同步等待。
本章知识点总结
- 标量场:体渲染输入是三维空间中的可采样数值分布,表面来自数值和透明度映射后的视觉结果。
- 体素采样:3D texture 把离散体素变成 shader 可读取的连续采样入口,滤波和格式会影响边界质量。
- Transfer Function:Transfer function 把标量或标量加梯度映射为颜色透明度,直接决定哪些内部结构进入积分。
- Ray Marching:每个像素沿相机射线在体数据包围盒内推进,采样、查表和累积共同生成最终颜色。
- 透明累积:前向 alpha compositing 让前方样本削弱后方贡献,采样顺序和透明度语义需要保持一致。
- 梯度作用:梯度描述标量变化方向和强度,可用于边界增强、局部光照和二维 transfer function 分类。
- 步长绑定:Step size 同时影响质量和成本,透明度应按路径长度归一以保持积分稳定。
- 提前结束:Early termination 在累积透明度接近饱和时停止采样,收益取决于 transfer function 和射线长度。
- 空区跳过:Empty-space skipping 通过 brick min-max 或占用标记跳过透明区域,前提是它和当前 transfer function 保持一致。
- 预积分:Pre-integration 用相邻样本估计一段路径贡献,适合 transfer function 有窄峰值的交互观察。
- GPU资源:体数据、transfer function、相机参数、包围盒和输出纹理构成 volume pass 的核心资源链。
- 压缩层级:量化、brick、mipmap、sparse texture 和稀疏层级结构需要同时评估容量、上传、采样和解码成本。
- 实例判断:医学、地质和气象体数据共享数据值、映射、采样、GPU pass 和工具证据这条分析链。