第 30 章:属性数组按需加载与网络请求观察
学习目标
本章把“数据集列出一个数组”与“浏览器已经下载数组的数值”分开。你会用 vtkHttpDataSetReader 先读清单,再按用户选择下载 Density、Contrast,将已下载的数组设为 active scalars,并用等值面检查它们确实参与计算。可运行页面是按需加载示例,实现见 src/arrays.js。
30.1 为什么要拆开清单与数值
本例数据集有 index.json 和两份独立二进制数组,每个 Float32 数组含 32³=32768 个值,占 131072 字节。index.json 只有约 1.6 KB,描述了 vtkImageData 的空间信息、数组名、数据类型、长度、数值范围和数组文件引用。进入页面时只请求清单;若用户只想知道有哪些字段,没有理由立即把全部字节下载到内存。
启动:index.json → 数据集结构 + 可选数组的登记
点击 Density:data/Density → Point Data 增加 Density
点击 Contrast:data/Contrast → Point Data 增加 Contrast
切换下拉框:选择已下载数组 → 无新数组请求
这是网络层的按需加载。它不会让一份已下载的大数组在内存中自动消失,也不等于沿视口区域做空间分块。对更大的数据集,你仍需要考虑完整体数据的内存与 GPU 成本。教学数据由固定公式生成,并非真实测量;Density 与 Contrast 两个名称只用于演示字段选择。
30.2 Reader 的四个时点
创建 Reader 时禁用自动数组选择:
const reader = vtkHttpDataSetReader.newInstance({ enableArray: false });
await reader.setUrl('/sample/volume/index.json', { loadData: false });
const image = reader.getOutputData();
const entries = reader.getArrays();
此时 image 已具备维度与空间信息,reader.getArrays() 可列出 pointData / Density 和 pointData / Contrast,但 image.getPointData().getArrayByName('Density') 仍是空值。Reader 登记了尚未请求的数组引用,并没有把二进制数值放进 Point Data。页面初始只显示清单请求,是对此最直接的验证。
用户点击加载按钮时再执行:
reader.enableArray('pointData', 'Density', true);
await reader.loadData();
const density = image.getPointData().getArrayByName('Density');
if (!density) throw new Error('下载后仍未得到 Density');
image.getPointData().setActiveScalars('Density');
enableArray() 只变更选择状态;loadData() 才取启用且尚有外部引用的数组。若没有 await,紧接着检查 Point Data 可能早于下载完成。页面把按钮置为 disabled,防止重复点击;失败时恢复按钮并显示错误。加载第二个数组沿用同一个 Reader 与 ImageData,不应重新建一个空数据集来替换已有数组。
getArrays() 反映清单登记与 enable 状态;“已下载”则通过 image.getPointData().getArrayByName(name) 判断。这两个观察值要并列看。清单里有 ranges 元数据,不等于你拥有每个样本的 Float32 值;只有下载后才能对该数组做等值面计算。
30.3 从数组选择到等值面
页面用 vtkImageMarchingCubes 生成阈值为 75 的等值面:
const iso = vtkImageMarchingCubes.newInstance({
contourValue: 75, computeNormals: true, mergePoints: true,
});
iso.setInputData(image);
mapper.setInputConnection(iso.getOutputPort());
mapper.setScalarVisibility(false);
actor.setMapper(mapper);
这里 vtkImageData → vtkImageMarchingCubes → vtkPolyData → vtkMapper → vtkActor 是完整的数据与显示链。与第 29 章直接体渲染不同,等值面是从体场重新计算的三角形表面;它不是把每个体素当成立方体绘制。setScalarVisibility(false) 使 Actor 用固定颜色,避免刚从字段选择问题转到输出 PolyData 的标量颜色问题。
选择下拉框中的另一个已下载数组时,调用 image.getPointData().setActiveScalars(name)。本版本的 vtkImageMarchingCubes 读取 getScalars().getData();active scalars 一变,输入数据对象的修改状态会促使下一次取输出时重新计算。示例通过 iso.getOutputData() 立即得到当前结果并显示三角形数,再渲染。若只改变下拉框文字而不调用 setActiveScalars,过滤器仍会按旧数组计算。阈值滑块只在 change 事件,即释放时更新 setContourValue(),减少拖动中的连续重算。
页面默认不显示 Actor,直到至少加载一个数组;否则过滤器没有可计算的 active scalars。Density 与 Contrast 值域虽都可达约 220,但空间分布不同;相同阈值并不表示相同形状。对真实数据,比较两字段时还需考虑单位和语义,不能只比较三角形数量。
30.4 用 Network 证明加载顺序
打开浏览器开发者工具的 Network 面板,勾选保留日志,刷新页面,然后按顺序点击:
- 页面初始:应看到
/sample/volume/index.json,不应有data/Density或data/Contrast请求。 - 点击“加载 Density”:新增
/sample/volume/data/Density;页面显示 32768 个数值、原始范围和等值面单元数。 - 点击“加载 Contrast”:新增
/sample/volume/data/Contrast;两个数组都显示已下载。 - 在已加载数组下拉框中来回切换:不应新增数组文件请求,变化发生在内存中的 active scalars 与等值面结果。
页面底部的 URL 与字节数来自浏览器 performance.getEntriesByType('resource')。它是辅助观察:记录可能受资源计时缓冲区、跨域限制和浏览器缓存影响;decodedBodySize、transferSize 也不能当成数据对象真实内存大小。要判断 HTTP 状态、缓存来源、实际请求时机及传输量,应以 Network 面板为准。浏览器开发模式还可能出现 Vite 模块与热更新请求,它们不属于数据集数组请求。

30.5 失败和性能如何处理
如果清单加载失败,先检查 URL、状态码、是否从 Vite 本地服务器访问,以及 index.json 的 JSON 结构;不要直接把 .vti、.vtp 的 URL 交给 vtkHttpDataSetReader。这类 VTK XML 文件使用相应 XML Reader,第 28 章是 VTP 的例子。若清单成功但数组请求失败,查看 ref.basepath、ref.id、实际请求路径和二进制大小是否匹配 size 与 dataType。若数组已下载但画面空,查 active scalars、阈值是否落在数组有效范围、Filter 的输入和相机裁剪。
加载流程最好把“正在请求”“已下载”“加载失败”分别呈现在 UI 上,同时禁用重复操作。示例只是小数据教学:实际大体数据可能需要压缩、降采样预览、分块、多分辨率或服务端筛选。按需下载只解决“不用的字段暂时不请求”;一旦同时载入多个大数组,网络、主内存、过滤器输出和 GPU 内存仍会累加。要分析慢在哪里,可沿第 26 章的网络、解析、过滤与绘制四段计时。
30.6 阶段收束
三个综合项目分别展示了三种常见工作的完整边界:VTP 表面查看器侧重几何与标量着色;体数据查看器对同一个 vtkImageData 使用切片和射线合成;本章把元数据、数组字节和过滤结果的生命周期分开。它们共同的调试路径是:先确认数据对象和数组,再确认 Mapper/Filter 连接,最后确认显示参数和浏览器请求。完成第 9 阶段练习时,可以用这条路径解释每次画面变化由哪一层引起。