第 17 章:vtkColorTransferFunction——标量如何变成颜色

学习目标

上一章在每个采样位置需要一个 colorFunction(s)。本章用 vtk.js 的 vtkColorTransferFunction 实现它。读完后,你应该能建立控制点、查询任意标量的 RGB、理解控制点之间的插值与端点行为,并知道颜色函数与不透明度函数各管哪一部分。

17.1 输入是数值,输出是 RGB

颜色传递函数可看作一条从标量到颜色的函数:

fcolor: scalar → [R, G, B]

标量可能是密度、温度、速度幅值或图像强度。RGB 分量在 vtk.js 这个 API 中使用 0–1 范围。函数本身不表示空间位置,也不读取三维数组;它只回答“给定这个数,取什么颜色”。同一颜色函数可供许多位置反复查询。

第 5 阶段示例建立四个控制点:

const colorFunction = vtkColorTransferFunction.newInstance();
colorFunction.addRGBPoint(0,   0.06, 0.14, 0.35);
colorFunction.addRGBPoint(60,  0.12, 0.75, 0.78);
colorFunction.addRGBPoint(120, 0.98, 0.62, 0.16);
colorFunction.addRGBPoint(200, 0.98, 0.94, 0.88);

第一个参数是原始标量值,后面三个参数是该值对应的 RGB。它不是“第几个控制点”,也不是数组下标。若数据实际值在 [1000,2000],却把控制点全部放在 [0,200],大部分采样就会落到函数定义域以外,颜色失去层次。

17.2 控制点之间怎样得到颜色

只有四个控制点并不意味着只能显示四种颜色。函数会在相邻点之间插值。默认颜色空间是 RGB、常用线性刻度;例如标量 90 位于 60 与 120 中间,会得到这两处颜色之间的一个过渡色。第 5 阶段示例在色带中显示了 0–200 的连续结果,样本表里的颜色小块则是对当前插值标量逐次调用传递函数得到的。

查询颜色时,需要提供输出数组:

const rgb = [0, 0, 0];
colorFunction.getColor(90, rgb);
// rgb 现在装着三个 0–1 分量;getColor 不把数组作为返回值。

这点容易与返回新数组的普通 JavaScript 函数混淆。若要画网页 CSS 色块,需要把 0–1 分量转换成 0–255 的 CSS RGB 值。计算体渲染合成时继续使用 0–1 值更直接。

控制点通常按标量顺序阅读;vtk.js 会维护节点顺序。getSize() 可以核对节点数量,getRange() 可核对节点的数值范围。默认开启端点钳制:低于最小控制点通常取最左颜色,高于最大控制点取最右颜色。端点钳制不是扩展了真实数据的范围,更不会修改数据中的标量。

17.3 颜色空间会改变中间色

同样的两个端点,在 RGB、HSV 或 LAB 等颜色空间中插值,路径可能不同:

colorFunction.setColorSpace(
  vtkColorTransferFunction.ColorSpace.RGB
);

RGB 插值直接在红、绿、蓝分量之间过渡;HSV 更侧重色相、饱和度和明度的路径;LAB 试图让感知变化更均匀。颜色空间改变的是中间标量的显示颜色,控制点标量及其物理意义仍不变。做数值比较时,应保持颜色空间、控制点和范围一致,否则两幅图即使标量相同,也可能出现不同颜色。

默认平滑过渡适合连续量;分类标签通常不适合在不同类别颜色之间创造过渡。addRGBPointLong 可进一步控制相邻节点间的 midpoint 和 sharpness,但初学时先用普通控制点理解区间与颜色,避免在尚未看清数组范围时调整曲线形状。

17.4 它与第 14 章 vtkLookupTable 的关系

两者都能实现“数值 → 颜色”。vtkLookupTable 常用于表面 Mapper 的常规色表,能生成一定数量的颜色,也适合按类别配置;vtkColorTransferFunction 直接用数值控制点定义连续颜色函数,特别适合体渲染中精细指定不同强度的颜色。不要以为 vtkColorTransferFunction 只属于体数据:它也可以用于其他需要按标量取色的场景。

在体渲染对象管线中,颜色函数通常交给 vtkVolumeProperty 的第 0 个分量:

volume.getProperty().setRGBTransferFunction(0, colorFunction);

这里的 0 是数组分量编号。完整的 Volume、VolumeMapper、VolumeProperty 连接将在第 20 章实现。本章示例先直接调用 getColor(),便于核对每一处颜色,随后才会把函数交给 GPU 渲染管线。

17.5 改颜色为什么可能毫无效果

若某段标量的不透明度全是 0,颜色函数即便把它们设为鲜红,也不会在体渲染结果里出现。上一章合成公式中的贡献是 (1−A)×α×c:α=0 时颜色贡献必为 0。反过来,前方区域若早已让 A≈1,深处的颜色也难以透出。因此“颜色没变”时同时检查标量值是否落在控制点区间、不透明度函数是否让它可见,以及前方是否遮挡。

颜色传递函数不是照明模型。真实 VolumeProperty 还可能启用阴影或梯度相关的明暗效果,最终屏幕像素不一定等于 getColor(s) 原样输出。为学习函数本身,第 5 阶段示例只计算无光照的单条视线合成。

17.6 修改控制点的安全方法

使用 removePoint(x) 删除指定标量处的节点,或先 removeAllPoints() 再添加整套新节点。不要直接改 getDataPointer() 返回的内部节点对象:直接写入会绕过排序、范围更新和修改通知。重建函数后,依赖它的显示对象才会收到正确的变化状态。

做比较实验时,先记录原始数组值的范围,再记录控制点与颜色空间。单看一张彩色图无法反推出物理量大小。

本章小结

vtkColorTransferFunction 用标量控制点定义连续 RGB 映射。它不决定透明度,也不改变原始数据。下一章给同一批标量定义不透明度曲线,解释为什么它往往比颜色更直接地决定体内哪些结构能被看见。