第 28 章:带标量着色的表面查看器
学习目标
本章把前面分散的 Reader、vtkPolyData、Mapper、Actor、颜色表和相机组成一个能使用的查看器。完成后,你应能解释:文件中的坐标和标量如何进入渲染管线;为什么选择数组与设置颜色范围是两件事;导入陌生文件时应该先核对哪些数据。完整代码在第 9 阶段表面示例,入口脚本是 src/surface.js。
28.1 需求先落到对象上
查看器要支持读取教学文件与本地 VTP、显示多边形、切换单分量 Point Data、设置映射范围、切换线框与重置相机。相应的对象链是:
surface.vtp / 本地 File
↓ vtkXMLPolyDataReader
vtkPolyData(Points + Polys + Point Data)
↓ vtkMapper + vtkLookupTable
vtkActor → vtkRenderer → vtkRenderWindow
Three.js 中你可能会把位置、索引和颜色塞进 BufferGeometry,再交给 Mesh 与材质。这里的对应关系并非一一同名:VTP 的 Points 是顶点坐标,Polys 是单元连接,PointData 是随点存储的数组;vtkMapper 决定读取哪个数组并如何转成颜色,vtkActor 保存表示方式和场景位置。底层仍然生成三角形供 WebGL 绘制,但应用中的数据结构保留了“数据在点上还是单元上”的语义。
本例数据由固定公式生成:31×31 个点组成 1800 个三角形,两个 Point Data 数组名为 Height 与 Heat,均无物理单位。它们不是地形测量或医学数据。你可以运行示例目录中的 npm run generate:data 重建文件;生成脚本也同时产生第 29、30 章的体数据。
28.2 先读取,再检查输出
vtkXMLPolyDataReader 针对 VTK XML PolyData,即 .vtp。网页中的默认读取顺序是:
const reader = vtkXMLPolyDataReader.newInstance();
await reader.setUrl('/sample/surface.vtp');
const polydata = reader.getOutputData(0);
if (!polydata?.isA('vtkPolyData')) throw new Error('文件没有输出 vtkPolyData');
setUrl() 完成异步下载与解析后,再取输出。getOutputData(0) 指第一个输出;本查看器面向单 Piece VTP。一个 VTP 有多个 Piece 时,不能只取第一个就声称显示了全部文件。检查 getNumberOfPoints()、getPolys().getNumberOfCells(),比看到一个空画布时盲目调相机有效得多。本例预期为 961 点与 1800 个多边形单元。
本地文件入口不需要上传到服务器。浏览器的 File.arrayBuffer() 读取字节后,交给同一个 Reader:
const buffer = await file.arrayBuffer();
const localReader = vtkXMLPolyDataReader.newInstance();
const parsed = localReader.parseAsArrayBuffer(buffer);
if (!parsed) throw new Error('VTP 解析未成功');
const polydata = localReader.getOutputData(0);
Reader 对象单独保存,保证其输出与当前数据对应。示例用递增的 loadSequence 排除竞态:若用户在旧请求未结束时选择新文件,旧请求完成后不能覆盖新结果。失败时把错误显示在状态区,成功时才启用颜色与线框控件。
关于浏览器构建
固定版本 @kitware/vtk.js 37.4.2 的 XML Reader 引入 xmlbuilder2;该依赖的默认入口包含 Node 的 events,直接让 Vite 打包会在浏览器启动时报 events.EventEmitter 不存在。本示例在 vite.config.js 把这一依赖指向 xmlbuilder-browser.js,用浏览器 DOMParser 提供 XML Reader 实际需要的 create(xml).root() 和节点遍历接口。这个适配器只覆盖本示例使用的读取路径;若加入 XML Writer 或其他 xmlbuilder2 功能,应另行核对。遇到“构建成功但页面没有运行”的情况,应看浏览器 Console,不能只看 npm run build。
28.3 数组属于哪里,决定怎样选择
文件载入后,示例从 polydata.getPointData().getArrays() 取候选数组,仅保留单分量、元组数等于点数的数组:
const arrays = polydata.getPointData().getArrays().filter((array) =>
array.getNumberOfComponents() === 1 &&
array.getNumberOfTuples() === polydata.getNumberOfPoints()
);
这是有意收窄的教学查看器:它不把 Cell Data 当 Point Data,也不擅自把三分量向量解释成单一标量。若自己的文件只有 Cell Data,页面会单色显示,并提示没有可用的单分量 Point Data。要支持 Cell Data,需要改 Mapper 的 scalar mode 和数组列表来源;要显示向量长度,还需要明确分量或模长的定义。
选中数组后,关键配置是:
mapper.setInputData(polydata);
mapper.setScalarModeToUsePointFieldData();
mapper.setColorByArrayName(array.getName());
mapper.setScalarVisibility(true);
setInputData 连接几何,setScalarModeToUsePointFieldData 指定数组关联方式,setColorByArrayName 指定哪个字段用于着色,setScalarVisibility 决定是否应用标量颜色。取消标量着色后,Actor 的单色 vtkProperty.setColor() 才容易直接观察。线框按钮只更改 actor.getProperty().setRepresentationToWireframe();它不重新生成数据。
28.4 原始范围与显示范围
array.getRange() 给出当前数组的数值范围;颜色映射范围决定哪些数值落在色带两端。二者可以不同。例如 Heat 的原始范围约为 [20.00, 95.61],把映射最小值改为 10 会改变颜色分配,却不会把数组中的最小值改成 10。
const [dataMin, dataMax] = array.getRange();
mapper.setScalarRange(displayMin, displayMax);
lookupTable.setMappingRange(displayMin, displayMax);
lookupTable.build();
“自动使用当前数组范围”让两个显示端点跟随 getRange();关闭自动后可手填。输入必须是有限数,而且最小值小于最大值。切换 Height 与 Heat 时,示例重新读取新数组范围并更新字段说明。不要用旧字段的范围去比较新字段,尤其当它们的单位或含义不同。
色带也应与实际颜色表一致。本例调用 Mapper 所用 vtkLookupTable.mapScalars() 对等距样本着色,再把所得 RGBA 值放入 HTML 渐变。这样色带并非凭印象写出的装饰;但它只是颜色说明,不会把深度、光照或透明度效果编码进去。若修改颜色表的 hue、颜色节点或离散方式,应同步重新采样色带。
28.5 相机、交互与一次完整操作
vtkGenericRenderWindow 提供 Renderer、RenderWindow 与交互器;Viewer 的固定重置视角设置焦点、位置、up 向量、平行投影及裁剪范围。先有非空数据再让 Actor 可见,避免初始布局触发渲染时 Mapper 报没有输入。通常数据导入后可以调用 resetCamera();本例为了让教学文件的各次比较拥有同一视角,采用固定参数。
建议按下面顺序操作,而非只确认“画出来了”:
- 打开表面页,核对 961 点、1800 单元及两个数组。
- 选
Heat,观察原始范围与图例;关闭自动范围,将映射最小值改成10,确认原始范围仍从约20开始。 - 开启线框,确认显示的是三角形拓扑;取消标量着色,观察 Actor 单色。
- 选择本地教学
surface.vtp再读取一次;若换成自己的文件,先确认 VTP 格式、Piece 数、Point Data 关联方式及数值单位。

28.6 常见故障的定位顺序
空画面先查浏览器 Console、网络请求与状态提示,再查输出是否为 vtkPolyData、点和 Polys 是否非空、Actor 是否可见,最后看相机和裁剪范围。单色画面先看数组是否真在 Point Data、元组数是否匹配、scalar visibility 是否开启、数组名是否拼对。颜色与期待不符时打印 getRange() 和显示范围,不要从图例反推数据值。XML 解析失败时确认文件是 vtk.js 当前 Reader 支持的 VTP 编码与数据类型;文件扩展名正确不等于内容可解析。
本章构成可复用的表面查看器骨架。下一章沿用“Reader 产生数据、Mapper 解释数据、Prop 加入 Renderer”的方式,但输入换成三维采样场,显示分成切片与体渲染两条管线。