第 19 章:vtkVolumeMapper——让三维采样场进入视线渲染

学习目标

第 16 章把一条视线的采样与合成拆开计算。本章认识实际负责整张画面的 vtkVolumeMapper。读完后,你应该能把 vtkImageData 接到 Mapper,理解沿视线采样间距与屏幕采样间距的区别,切换合成和 MIP 模式,并知道质量、速度与采样上限怎样相互影响。

19.1 它在管线中的位置

表面渲染使用 vtkPolyData → vtkMapper → vtkActor。三维体渲染使用:

vtkImageData → vtkVolumeMapper → vtkVolume → vtkRenderer
                     ↑                 ↑
             采样和混合方式       vtkVolumeProperty 管颜色、透明度

vtkVolumeMapper 接收三维规则采样场,决定怎样沿视线读取它。它不会凭空创造数据,也不单独定义每个标量的颜色和不透明度;这些函数放在 Volume 的 vtkVolumeProperty 中。一个 vtkImageData 可以同时供切片 Mapper 和体 Mapper 使用,正如第 6 阶段示例所做。

最小连接:

const volumeMapper = vtkVolumeMapper.newInstance();
volumeMapper.setInputData(imageData);

若输入来自 Reader 或 Filter 的输出端口,使用 setInputConnection(upstream.getOutputPort()) 保留管线连接。这里与第 12 章的 vtkMapper 规则相同。手工创建的数据可以直接 setInputData。vtkVolumeMapper 主要面向 vtkImageData 这样的规则体数据,不能把 vtkPolyData 三角网格直接当作规则体去采样。

19.2 渲染实现与 WebGL 三角形

底层 WebGL 仍绘制图元,但体渲染的关键计算在体的投影视线内部:对屏幕像素确定视线与体范围的交段,沿该段多次采样三维纹理并混合结果。画面不是把每个体素的六个面都绘成网格。vtkVolumeMapper 管理这类渲染过程和 GPU 资源;你交给它的是规则网格及标量,而不是自己手写每条视线的 JavaScript 循环。

体数据尺寸会影响上传与采样成本。第 6 阶段示例为 40×40×40 个单分量 Float32 采样值,原始数组约 40³×4=256000 字节。真实 512³ Float32 体的原始标量就约 512 MiB,还可能有 CPU 与 GPU 副本。第 26 章将专门讨论大数据性能。

19.3 沿视线的 sampleDistance

volumeMapper.setSampleDistance(1);
volumeMapper.setAutoAdjustSampleDistances(false);

sampleDistance 控制沿一条视线走多远取一次样,按体所在空间的距离理解。体厚约 40 个单位时,理想化地说,间距 1 会取约 40 次;间距 0.5 约翻倍,间距 2 约减半。实际次数还受视线方向、体相交长度、早停、优化策略与采样上限影响。

间距过大可能跨过细小结构;间距过小会增加运算量,并可能让细节改善已经不明显。调这个参数前要检查 spacing:如果各轴的采样点距离不同,“间距 1”不能简单当成“每次走一个 k 层”。本例 spacing 恰好是 [1,1,1],才便于建立直觉。

autoAdjustSampleDistances 默认开启,Renderer 可能根据交互或运行条件调整有效采样。在示例中关闭它,是为了让滑块改变 sampleDistance 时观察关系更直接。真实应用可保留自动调整以平衡交互流畅度。手动设置采样间距后仍应实测目标设备,而不是只凭数值猜测帧率。

19.4 不要混淆三个“采样”设置

设置 控制什么 典型影响
setSampleDistance(d) 沿一条三维视线的采样间隔 小结构、体内插值精度和每条视线的计算量
setImageSampleDistance(d) 屏幕平面上的射线密度 每隔多少屏幕像素发一条视线,影响画面分辨率与总射线数
setMaximumSamplesPerRay(n) 一条视线允许的采样上限 防止极端长视线或过小步长带来无限制循环

imageSampleDistance=1 通常约为每屏幕像素一条视线;增大它可减少射线数量,画面也可能变粗。它不是 z 方向层间距。maximumSamplesPerRay 默认有限,若体很厚、步长过小,可能触及上限而无法按预期走完整段视线。排查切掉的深处结构时,除传递函数外也检查采样配置。

第 5 阶段的透明度参考距离仍有用。改变 Mapper 的采样间距时,VolumeProperty 的 setScalarOpacityUnitDistance 帮助保持对物理路径长度的合理解释。即使做了距离修正,采样点位置不同仍可能改变边界细节,不能期待每个像素完全一样。

19.5 合成与 MIP 是两种混合方式

默认的合成方式沿视线累积颜色和不透明度:

volumeMapper.setBlendModeToComposite();

最大强度投影(MIP)则沿视线寻找最大的标量:

volumeMapper.setBlendModeToMaximumIntensity();

前者保留前后遮挡的线索,但强烈依赖不透明度函数;后者突出高值,却把不同深度的最大值压成同一投影平面。MIP 的颜色显示仍可能经过颜色或不透明度映射,本例为 MIP 配了一条单独的显示用不透明度函数,让最大值明亮呈现;这不会改变“沿视线取最大标量”的选择规则。

同一个 Mapper 也有最小强度、平均强度等混合模式。先掌握 Composite 与 MIP 的差别,再根据任务选择其他模式。不要把 MIP 叫作“更亮的体渲染”:它的统计规则不同,无法像前向后合成那样表达体内遮挡。

19.6 输入与画面不一致时怎么查

  1. 用 imageData.getDimensions()、Point Data 的 getScalars() 和 getRange() 确认真有三维标量。
  2. 用 volumeMapper.getInputData() 确认 Mapper 拿到预期对象。
  3. 检查 volume.getMapper()、Renderer 中的 Volume,以及相机是否看到体范围。
  4. 检查 VolumeProperty 的不透明度函数:若全映成 0,正确的 Mapper 也只能画出空白。
  5. 再看采样间距、混合模式、GPU/WebGL 报错。

改变 Mapper 设置后调用 renderWindow.render()。直接修改 getData() 的 TypedArray 时,还要按第 8 章调用 dataChange(),否则只重绘不一定足以更新范围或纹理。

本章小结

vtkVolumeMapper 让规则体数据参与逐像素视线采样。sampleDistance 管视线深度方向,imageSampleDistance 管屏幕射线密度,混合模式决定是透明度合成还是最大值投影。下一章把 Mapper 包装成 vtkVolume,并用 vtkVolumeProperty 明确颜色、透明度、插值与光照。