第 5 章:vtkGenericRenderWindow 与 vtkRenderWindow
学习目标
第 1 阶段用 <code>vtkFullScreenRenderWindow</code> 快速显示了一个球,但没有解释画布从哪里来、窗口尺寸为何影响图像,也没有展示怎样把 vtk.js 嵌入网页中的指定区域。本章拆开这一层。读完后,你应该能创建一个嵌入式视图、处理容器尺寸变化,并区分 <code>vtkGenericRenderWindow</code> 与 <code>vtkRenderWindow</code>。
5.1 “渲染窗口”并不只是一个 canvas
在 Three.js 中,你可能熟悉把 <code>WebGLRenderer.domElement</code> 放到某个 DOM 容器中。vtk.js 将相关职责拆开:
网页 DOM 容器
└─ 具体绘制视图:浏览器中的 canvas 与 WebGL 后端
vtkRenderWindow
├─ vtkRenderer:场景对象、相机与背景
└─ 绘制视图:把渲染结果呈现到容器
vtkRenderWindowInteractor
└─ 从容器接收鼠标、键盘等输入
<code>vtkRenderWindow</code> 是组织渲染的核心对象。它可以添加 Renderer 和绘制视图,调用 <code>render()</code> 发起绘制。它本身不等同于 DOM 元素。浏览器里的实际 canvas 由具体绘制视图管理。
<code>vtkGenericRenderWindow</code> 是一个方便在应用中使用的组合对象。创建它时,它会准备 RenderWindow、Renderer、具体绘制视图和 Interactor,并提供相应的 getter。第 1 阶段的 <code>vtkFullScreenRenderWindow</code> 也是辅助对象,但更偏向占满页面的快速示例。本阶段改用 Generic 版本,把视图放在自己指定的容器中。
5.2 把视图放进指定 DOM 容器
第 2 阶段示例的核心初始化如下:
import vtkGenericRenderWindow
from "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow";
const viewport = document.querySelector("#viewport");
const genericRenderWindow = vtkGenericRenderWindow.newInstance({
background: [0.10, 0.12, 0.17],
listenWindowResize: false,
});
genericRenderWindow.setContainer(viewport);
genericRenderWindow.resize();
const renderer = genericRenderWindow.getRenderer();
const renderWindow = genericRenderWindow.getRenderWindow();
const interactor = genericRenderWindow.getInteractor();
<code>setContainer</code> 告诉绘制视图放到哪里,也让 Interactor 从这个区域接收事件。随后立即调用 <code>resize()</code>:在本教程版本中,设置容器并不自动保证画布像素尺寸已经根据容器更新。
容器还必须有非零尺寸。只创建一个空的 <code>div</code>,却没有通过布局或 CSS 给它高度,常会得到“代码没有报错,但什么都看不到”的结果。示例的 <code>.viewport</code> 通过 CSS 指定了高度和 <code>position: relative</code>。
5.3 谁负责尺寸变化
<code>vtkGenericRenderWindow.resize()</code> 会读取容器的边界尺寸,结合设备像素比设置绘制视图的像素大小,然后请求渲染。这里有两个尺寸概念:
- CSS 尺寸:容器在页面布局中占多宽、多高。
- 绘制缓冲区尺寸:canvas 实际使用的像素数量,还会受设备像素比影响。
二者不一致时,画面可能模糊、拉伸,或鼠标位置与图像位置不协调。你不需要自己计算每台设备的像素比,通常调用 Generic 对象的 <code>resize()</code> 即可。
Generic 对象默认可以监听浏览器窗口的尺寸变化。但在实际应用中,侧栏开合、标签切换、面板拖拽也可能改变容器尺寸,窗口本身却没有 resize。本示例选择关闭内置窗口监听,使用 <code>ResizeObserver</code> 观察真正的视图容器:
const resizeObserver = new ResizeObserver(() => {
genericRenderWindow.resize();
});
resizeObserver.observe(viewport);
运行第 2 阶段示例后,点击“缩窄画布”:容器宽度会变,视图也会跟着调整。若去掉上面的 Observer,再只改变容器的 CSS 宽度,你就能观察到为什么单靠窗口尺寸事件并不够。
5.4 什么时候直接使用 vtkRenderWindow
本阶段主要通过 Generic 对象取得 RenderWindow,然后调用:
renderWindow.render();
你也可以手动创建 <code>vtkRenderWindow</code>,再自行添加 Renderer、创建具体绘制视图、将其接到 DOM、设置像素尺寸,并建立 Interactor。这样的做法适合需要多个 Renderer、特殊视图布局或自己管理后端的应用。它也意味着你必须负责原本由 Generic 对象完成的初始化和清理。
在普通单视图页面中,先用 Generic 对象并不妨碍访问底层对象:
const renderWindow = genericRenderWindow.getRenderWindow();
const renderer = genericRenderWindow.getRenderer();
const interactor = genericRenderWindow.getInteractor();
这三个对象仍然是可以单独调用 API 的 vtk.js 对象,不是只能通过 Generic 对象操作的“隐藏状态”。
5.5 渲染与生命周期
<code>renderWindow.render()</code> 请求绘制当前场景;<code>genericRenderWindow.resize()</code> 则先同步尺寸并触发绘制。因此,只改变相机或 Actor 时调用 <code>render()</code>,容器尺寸变化时调用 <code>resize()</code>。
在单页应用中,组件卸载时还应停止自己的 DOM 观察与事件订阅,并删除 Generic 对象。例如:
resizeObserver.disconnect();
genericRenderWindow.delete();
不要在每次界面重新渲染时都创建一个新的 Generic 对象,否则旧 canvas、事件监听和图形资源可能被留在页面中。应让它与承载视图的组件拥有相同的生命周期。
5.6 常见问题
| 现象 | 检查点 |
|---|---|
| 页面有容器但没有图像 | 容器是否有非零宽高;是否调用 <code>setContainer</code> 与 <code>resize</code> |
| 拖动侧栏后图像尺寸不对 | 是否在容器尺寸变化时调用 <code>resize()</code> |
| 页面里叠了多个 canvas | 是否重复创建 Generic 对象而没有清理旧实例 |
| 调用了 <code>render()</code> 仍看不到 Actor | 继续检查 Renderer 中的对象和相机,见下一章 |
本章小结
<code>vtkRenderWindow</code> 组织渲染;<code>vtkGenericRenderWindow</code> 帮你把 RenderWindow、Renderer、具体绘制视图和 Interactor 接到网页容器。尺寸是这个连接的一部分,不能只插入 DOM 后就不再管理。下一章进入 Renderer 和 Camera,解释视图中究竟看到了什么。