第 8 章:vtkDataArray——值、分量与元组

学习目标

本章先不关心图像怎样上色,而是弄清 vtk.js 如何保存一串科学数值。读完后,你应该能根据底层数组长度和分量数计算元组数,正确读取标量与向量,并知道直接修改 TypedArray 后为什么还需要通知 vtk.js。

8.1 为什么需要 vtkDataArray

JavaScript 的 <code>Float32Array</code> 能高效存储数值,却不知道这些数字叫什么、多少个数字组成一个向量,也不知道它们属于哪些点或单元。<code>vtkDataArray</code> 在底层数值数组之外提供名称、分量数、元组访问、数值范围和对象修改状态。后面它会被放入 Point Data 或 Cell Data,获得明确的空间归属。

先看一个标量数组:

const temperature = vtkDataArray.newInstance({
  name: "Temperature",
  numberOfComponents: 1,
  values: new Float32Array([0, 0, 1, 1]),
});

底层有 4 个数字,每个元组有 1 个分量,所以有 4 个元组。若这个数组被挂到 4 个点的 Point Data 上,便可以理解为每个点一个温度值。单独创建数组时,它还没有“属于点”的含义。

8.2 分量与元组

分量是一个元组内的数字位置。标量通常有 1 个分量;三维速度向量通常有 3 个。元组是一组应共同解释的数值。例如:

const velocity = vtkDataArray.newInstance({
  name: "Velocity",
  numberOfComponents: 3,
  values: new Float32Array([
    1, 0, 0,  // tuple 0
    1, 1, 0,  // tuple 1
    0, 1, 0,  // tuple 2
    0, 0, 0,  // tuple 3
  ]),
});

这里底层数组有 12 个数,却只有 4 个元组。计算规则是:

元组数 = 底层数值个数 ÷ 每个元组的分量数

同一个元组的三个分量在 TypedArray 中相邻存储。第 2 个元组从下标 <code>1 × 3 = 3</code> 开始,对应 <code>[1, 1, 0]</code>。不要把“12 个数”误读成“12 个空间点”。

常用检查方法:

velocity.getNumberOfComponents(); // 3
velocity.getNumberOfTuples();     // 4
velocity.getTuple(1);             // [1, 1, 0]
velocity.getComponent(1, 0);      // 第 1 个元组的第 0 个分量:1
velocity.getData();               // 底层 TypedArray

<code>getRange(0)</code> 可取得第 0 个分量的数值范围。对于多分量数组,本教程版本的 <code>getRange()</code> 默认讨论向量幅值;需要某个轴的范围时,显式传入分量下标更清楚。单分量标量数组直接调用 <code>getRange()</code> 即可。

8.3 选择底层数值类型

<code>values</code> 通常使用 TypedArray。类型应与数据含义相符:

数据例子 常见选择 注意事项
连续测量值、坐标或计算结果 <code>Float32Array</code> 支持小数,但有浮点精度限制
非负整数标签 <code>Uint8Array</code>、<code>Uint16Array</code> 等 要确认最大标签值不会溢出
需要更高精度的计算值 <code>Float64Array</code> 内存占用更高;后续渲染仍受 GPU 路径限制

本教程的小示例使用 <code>Float32Array</code> 是为了让类型直观、与常见图形数据习惯相近,并不意味着所有医学影像或仿真数据都应转换成 Float32。真实数据读取时应先检查原始值范围和类型,再决定是否转换。

8.4 修改数组:API 与直接写内存的区别

使用 <code>setComponent</code> 或 <code>setTuple</code>,vtk.js 能知道数据发生变化:

velocity.setComponent(0, 1, 2);
velocity.setTuple(1, [2, 3, 0]);

<code>getData()</code> 返回底层数组。如果为了批量处理直接写它:

const raw = velocity.getData();
raw[0] = 9;
velocity.dataChange();

最后的 <code>dataChange()</code> 很重要。直接修改 TypedArray 不会自动通知 vtk.js;在本教程版本中,<code>dataChange()</code> 会清除已缓存的数值范围并标记对象已修改。只调用 <code>modified()</code> 虽能更新修改时间,却可能留下旧的范围缓存。此问题会在第 25 章结合管线更新机制再次讨论。

修改后还应确保数据形状没有失配。若一个三分量数组有 4 个元组,底层有效数值个数必须对应 12 个。改变分量数或整体数据时,使用对象 API 重设数据比随意改变数组长度更安全。

8.5 名称、归属与活动数组

名称帮助你在同一数据集里区分温度、压力、速度等数组。例如第 3 阶段示例中的 <code>Velocity</code> 是三分量数组,<code>PointTemperature</code> 是单分量数组。二者都能放在同一份 <code>vtkPolyData</code> 的 Point Data 中。

需要特别区分三个层次:

  1. <code>vtkDataArray</code> 保存数值及其结构。
  2. Point Data 或 Cell Data 决定这些元组对应哪类实体。
  3. Mapper 决定当前画面使用哪个数组、怎样把值映射成颜色。

数据集里存在一个数组,并不意味着它会自动显示。当前显示数组的选择留到第 10 和第 14 章。

8.6 用阶段示例检查

打开第 3 阶段示例,查看页面中的数据摘要。<code>Velocity</code> 的底层有 12 个数,页面显示“分量数 3,第 1 个元组 [1, 0, 0]”。同时,4 个点标量的范围是 <code>[0, 1]</code>。

若你把 <code>Velocity</code> 的 <code>numberOfComponents</code> 错设为 1,同一串数字会被解释成 12 个标量元组。内存没有变,数据意义却完全变了。这正是学习 vtk.js 数据结构时必须先掌握元组概念的原因。

本章小结

<code>vtkDataArray</code> 不是一个神秘的多维矩阵;它的核心是一段带类型的扁平数值,以及“每几个数算一个元组”的规则。下一章会把空间点和单元连接关系加上,构造一份真正的 <code>vtkPolyData</code>。