第 18 章:vtkPiecewiseFunction——标量如何变成不透明度

学习目标

第 17 章让标量变成颜色。本章用 vtkPiecewiseFunction 让标量变成不透明度。读完后,你应该能建立和查询一条透明度曲线,解释阈值、斜坡、钳制及采样间距的影响,并能在“体数据有颜色却看不见”时定位原因。

18.1 为什么还需要另一条函数

颜色函数 fcolor(s) 的输出是 RGB;不透明度函数 fopacity(s) 的输出通常是 0–1 的 α。它们可以共享输入标量,却不能互相替代:

标量 s ──> vtkColorTransferFunction ──> 颜色 c
      └─> vtkPiecewiseFunction ─────> 不透明度 α

α=0 表示该位置对本次合成不贡献颜色,也不遮挡后方;α=1 表示本次采样会吃掉此前尚未被挡住的透过率。一个亮黄色样本若 α=0,最终仍不可见。体渲染中,“哪些值可见”往往比“可见值涂成什么颜色”更决定结果。

18.2 用控制点定义曲线

建立一条简单的“低值透明、高值逐渐可见”曲线:

const opacityFunction = vtkPiecewiseFunction.newInstance();
opacityFunction.addPoint(0, 0);
opacityFunction.addPoint(50, 0);
opacityFunction.addPoint(70, 0.12);
opacityFunction.addPoint(200, 0.75);

每个 addPoint(x, y) 的 x 是原始标量,y 是该标量处的函数值。控制点之间默认平滑过渡;上例在 50 以下保持透明,从 50 到 70 开始出现,再向高值逐渐增加不透明度。查询:

opacityFunction.getValue(60); // 位于 50 与 70 之间的值
opacityFunction.getRange();   // 控制点的 x 范围:[0, 200]

getRange() 返回的是控制点的输入范围,不是 y 的最小最大值,也不是图像数组的 getRange()。三个范围名字相似:数组值范围、函数定义域、显示时选择的采样范围,排错时要明确是哪一个。

建议体渲染的不透明度输出维持在 [0,1]。函数本身是一般的分段数值函数,vtk.js 不会替你判断“这个 y 是否符合当前物理解释”;示例在合成前把查询值限制到 0–1。

18.3 阈值与斜坡怎样改变可见结构

第 5 阶段示例提供“透明阈值”滑块。它每次先清空旧节点,再按新阈值重建曲线:

opacityFunction.removeAllPoints();
opacityFunction.addPoint(0, 0);
opacityFunction.addPoint(threshold, 0);
opacityFunction.addPoint(threshold + 20, 0.12 * gain);
opacityFunction.addPoint(200, 0.75 * gain);

完整示例对输出做了 1 的上限限制。阈值升高时,更多低标量区域变透明,视线能到达更深的位置;阈值降低时,原本透明的外围可能先挡住高值核心。gain 增大时,同一标量的局部不透明度上升,累计 A 更快接近 1,后方贡献被压低。

这解释了一个常见误区:只把 α 整体调高,未必能让深处目标更清楚。若前方组织也同时更不透明,深处反而更难看见。设计曲线时要结合数值分布,给需要透过的区间保留低 α,给目标区间合理的较高 α。

18.4 定义域外与形状控制

vtkPiecewiseFunction 默认开启 clamping。请求值低于最小控制点时取左端 y,高于最大控制点时取右端 y。若关闭钳制:

opacityFunction.setClamping(false);

在定义域外查询会得到 0。这一差别很大:以上例为例,若实际图像有值 230,默认会沿用 200 处的较高不透明度;关闭钳制后 230 却变成完全透明。真实数据范围应先与控制点范围对照。

普通 addPoint 使用默认的 midpoint 和 sharpness。addPointLong(x, y, midpoint, sharpness) 可以改变相邻节点间的过渡,做更陡的边界或特殊曲线。先用几个简单控制点画出曲线,再调整形状参数;若标量噪声较多,过陡的阈值可能让画面闪烁或出现硬边。

对标签数据,区间可能需要明确的阶梯函数,而非连续斜坡。此时不仅要设计分段曲线,还要考虑采样插值:若对离散标签做线性插值,会出现原本不存在的中间编号。函数设计和数据插值必须一起考虑。

18.5 函数值并非整条视线的最终 A

opacityFunction.getValue(s) 给的是单次采样的参考不透明度,它不是该像素最终的不透明度。视线上的每个样本还要按第 16 章的公式合成,最终 A 才反映整段路径的遮挡。

如果改变步长,参考不透明度需要根据走过的距离换算。设参考距离 d0=1、函数返回 αref=0.36,半距离应使用:

αhalf = 1 − (1 − 0.36)^(0.5/1) = 0.2

走两次半距离的总遮挡为 1−(1−0.2)^2=0.36。第 5 阶段示例的“按采样间距修正不透明度”开关可直接比较修正前后。真实体渲染可通过 vtkVolumeProperty.setScalarOpacityUnitDistance(0, distance) 指定标量不透明度的参考物理距离,再结合 Mapper 的采样设置;第 20 章会把这些对象连起来。

18.6 把函数交给 VolumeProperty

在正式体渲染管线中,通常把颜色和不透明度函数同时交给 vtkVolumeProperty:

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

0 表示标量数组的第 0 个分量。此处只认识配置位置;vtkVolume、vtkVolumeMapper 的创建与渲染留到第 19–20 章。第 5 阶段示例直接调用 getValue(),让曲线对每个标量给出的 α 可以逐项检查。

18.7 排错的最短路径

现象 检查
完全看不到体 数组值是否落在非零 α 区间;函数是否被交给正确的分量
只有外壳,内部看不到 前方的 α 是否过高,累计 A 是否很快接近 1
改颜色函数没效果 对应数值的 α 是否为 0;前方是否已遮挡
缩小采样间距后体变得异常实 是否正确考虑不透明度的参考距离
高值突然消失 是否关闭了 clamping,或最高控制点低于实际数据值

本章小结

vtkPiecewiseFunction 用标量控制点定义不透明度曲线;与颜色函数共同决定体数据在每条视线上的贡献。函数值是局部采样的输入,最终像素仍取决于视线顺序、采样间距和累积。至此你已有理解 vtkVolumeMapper、vtkVolume 和 vtkVolumeProperty 的原理基础。