第 20 章:vtkVolume 与 vtkVolumeProperty——组织可见体及其外观

学习目标

本章完成第一条真正的 vtk.js 体渲染管线。读完后,你应该能把 vtkImageData、vtkVolumeMapper、vtkVolume、vtkRenderer 连起来,并正确配置颜色函数、不透明度函数、参考距离、体内插值和梯度光照。你也应该知道 Volume 与表面 Actor 哪些地方相似、哪些设置不能混用。

20.1 vtkVolume 是场景中的体对象

Mapper 有输入仍不会自动出现在画面上。创建 Volume 并加入 Renderer:

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

const volume = vtkVolume.newInstance();
volume.setMapper(mapper);
renderer.addVolume(volume);
renderer.resetCamera();
renderWindow.render();

它与 vtkActor 一样属于可见场景对象,可以设置位置、可见性,也需要相机和 Renderer。但 vtkActor 通常搭配 vtkMapper 与表面 vtkProperty;vtkVolume 搭配 vtkVolumeMapper 与 vtkVolumeProperty。不要试图给 Volume 调 actor.getProperty().setRepresentationToWireframe():体显示没有“把每个体素变成线框”这一表面表示方式。

第 6 阶段示例始终让同一份 vtkImageData 存在,仅在四种模式间切换可见对象和 Mapper 设置。volume.setVisibility(false) 只隐藏体,不删除原始图像。切回时不必重新生成所有 64,000 个标量。

20.2 vtkVolumeProperty 装着两条传递函数

从 Volume 取得属性对象,接入第 17–18 章的函数:

const property = volume.getProperty();
property.setRGBTransferFunction(0, colorFunction);
property.setScalarOpacity(0, opacityFunction);
property.setScalarOpacityUnitDistance(0, 1);

第一个参数 0 是标量数组的第 0 个分量,不是第 0 个体素。我们的输入为单分量 Point Data,因此使用分量 0。若真实数据有多个分量,必须明确每个分量代表什么,并检查 independentComponents 等设置;不能把 RGB 像素、向量场和单分量密度都套入同一套假设。

setRGBTransferFunction 定义 s→RGB,setScalarOpacity 定义 s→α。两者都没有改变 vtkDataArray 中的标量。若低值透明,高值可见,画面出现明亮内部结构;若把整个范围的 α 设为 0,体会消失,即使颜色函数和 Mapper 都正确。

20.3 单位距离决定 α 的物理含义

setScalarOpacityUnitDistance(0, 1) 告诉 VolumeProperty:不透明度函数中的 α 以 1 个世界距离单位作为参考。Mapper 的实际采样间距可以不同。概念上,若参考距离是 d0、当前采样区段长度是 d,可以用第 16 章的距离修正理解局部不透明度:

αstep ≈ 1 − (1 − αref)^(d / d0)

例如 αref=0.36 且 d0=1,两个各长 0.5 的区段应分别约为 0.2,累计遮挡仍为 0.36。真实渲染还受数据插值和具体实现影响,但这个关系能防止把“采样更多次”误解成“物体本身更厚”。若 spacing 使用毫米等实际单位,参考距离也应在同一世界坐标单位下解释。

第 6 阶段示例的 spacing 为 [1,1,1],参考距离也设为 1,便于观察。换成各向异性医学影像时,不要沿用这些数字而不检查物理尺度。

20.4 体内插值与光照是两件事

property.setInterpolationTypeToLinear();
property.setShade(false);

线性插值决定视线在网格点之间取得什么标量;最近邻插值则保留离散采样边界。它对应第 16 章的三线性插值概念。setShade 控制是否利用标量梯度和光照让结构呈现明暗。二者互不替代:开启光照不能弥补采样间距过粗,改线性插值也不会自动产生阴影。

开启梯度光照:

property.setShade(true);
property.setAmbient(0.2);
property.setDiffuse(0.7);
property.setSpecular(0.25);
property.setSpecularPower(16);
renderWindow.render();

这些名字与 Three.js 的环境光、漫反射、高光概念接近;不同之处在于体渲染的“法向”通常来自三维标量场的局部梯度,而不是预先存在的三角网格法线。它可增强边界立体感,也会让相同标量在不同朝向下显示出不同亮度。若要按颜色判断数值,需知道是否启用了光照。

20.5 一段完整、可运行的连接

以下代码省略了前几章已解释的窗口和 Renderer 创建,只展示体对象必需的连接。imageData 已含有 Point Data 标量:

import "@kitware/vtk.js/Rendering/Profiles/Volume";
import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper";
import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume";
import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";

const colorFunction = vtkColorTransferFunction.newInstance();
colorFunction.addRGBPoint(0, 0.1, 0.2, 0.5);
colorFunction.addRGBPoint(200, 1, 0.8, 0.4);

const opacityFunction = vtkPiecewiseFunction.newInstance();
opacityFunction.addPoint(0, 0);
opacityFunction.addPoint(60, 0);
opacityFunction.addPoint(200, 0.5);

const mapper = vtkVolumeMapper.newInstance();
mapper.setInputData(imageData);
mapper.setSampleDistance(1);

const volume = vtkVolume.newInstance();
volume.setMapper(mapper);
const property = volume.getProperty();
property.setRGBTransferFunction(0, colorFunction);
property.setScalarOpacity(0, opacityFunction);
property.setScalarOpacityUnitDistance(0, 1);
property.setInterpolationTypeToLinear();

renderer.addVolume(volume);
renderer.resetCamera();
renderWindow.render();

完整项目在第 6 阶段示例。其中使用 Rendering/Profiles/All,因为同一页面还需要图像切片和等值面;只做体显示时,Profiles/Volume 即可注册相应渲染后端。

20.6 改属性后怎样排错

颜色函数控制点、透明度函数控制点、插值或光照改变后,调用 renderWindow.render()。若画面不变,先检查当前显示方式是否真的是 Composite:示例在 MIP 模式下改用单独的显示不透明度函数,并关闭梯度光照;切片与等值面根本不是由 VolumeProperty 显示。

若场景空白,按“数据有无值 → Mapper 输入 → Volume 是否加入 Renderer → 相机 → 透明度函数”的顺序检查。imageData.getPointData().getScalars().getRange() 可以帮助判断函数控制点有没有覆盖实际数值区间。别先怀疑颜色空间或高光参数。

本章小结

vtkVolume 是可见体对象,vtkVolumeProperty 给它颜色、不透明度、参考距离、插值和光照。VolumeMapper 决定沿视线如何读取和混合数据,Renderer 与相机决定在哪里观察它。下一章将这条管线与切片、MIP、等值面放在同一份数据上比较。