AI 3D建模工具: Tripo 3D | Meshy AI
在 3D 场景里放一块"真"的 UI,过去只有两条路:把 HTML 用绝对定位盖在 canvas 上面,或者把 DOM 截成图片贴进贴图。前者对不齐透视、挡不住遮挡;后者只剩一张死图,点不了也读不出。
Chrome 157(2026-11-04 进稳定版)开始默认带上第三种做法:HTML-in-Canvas。我们下的 Chrome for Testing 157 没加任何 flag,这两个 API 就在。DOM 直接住在 <canvas> 里,用 ctx.drawElementImage() 画进去,但输入框还是输入框——能点、能打字、光标在闪、读屏软件里仍然是一个 textbox。
我们在一台 Windows 机器上把它跑通了,量了开销,也踩到了文档没写的那几个点。下面这些数字和报错都是本机实跑的结果。

为什么这件事值得看
对做 3D 页面的人来说,痛点很具体:
- BIM / 构件标注。标注要能改尺寸、选材料。用 overlay 就得自己算世界坐标到屏幕坐标的投影,还得跟着镜头每帧重排;一旦镜头转到模型背面,HTML 还傻乎乎浮在上面。
- 配置器的参数面板。面板要"贴在"模型某个部件上,随模型一起缩放、被前遮挡。overlay 做不到遮挡,只能靠 z-index 假装。
- 导出。用户截一张图,overlay 的 UI 不在渲染结果里;要一致就得再拼一次。
现在的做法把这三件事合到一条渲染管线上:DOM 是画进 canvas 的内容,所以它跟着 canvas 一起合成、一起截图。
157 里实际长什么样
10 月 23 日 Chrome 官方那篇 origin trial 公告写的是 <canvas layoutsubtree>;在 157 上照抄,会直接报错。三处差异,都是本机实测:
| 官方博客(2026-10-23) | Chrome 157 实际 | 报错原文 |
|---|---|---|
<canvas layoutsubtree> |
<canvas content="drawable"> |
getElementTransform requires the canvas to have the content=drawable attribute. |
| 子元素不用加属性 | 每个要画的子元素加 drawable |
Descendants passed to drawElementImage must have the 'drawable' attribute. |
| — | 子孙必须是这个 canvas 的后代 | The element can only be drawn into its nearest ancestor <canvas>. |
第三条最容易踩:把面板写成 canvas 的兄弟节点(外面套个容器),updateElementGeometry 会直接抛错。
另外两点值得知道:
- 156 上什么都没有。同一台机器上的稳定版 Chrome 156.0.8078.80,
drawElementImage和onpaint都不存在(我们没试 156 的 flag,只测了默认状态)。要跑,用 157 及以上。 - 157 里多出来一组几何 API:
updateElementGeometry、getElementTransform、getElementImageDefaultSize、captureElementImage、clearElementGeometry、requestPaint、onpaint、onelementgeometryupdate。drawElementImage的返回值是一个 DOMMatrix,官方博客的写法是把它写回元素的style.transform,让 DOM 位置跟在画面上。
最小示例
这是能跑起来的最小一套。HTML:
<canvas id="cv" content="drawable" width="960" height="600">
<div class="panel" drawable>
<label for="nm">尺寸</label>
<input id="nm" value="2400 mm">
</div>
</canvas>
JS:
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const panel = document.querySelector('.panel');
cv.onpaint = () => {
ctx.reset();
ctx.fillStyle = '#0d1013';
ctx.fillRect(0, 0, cv.width, cv.height);
ctx.drawElementImage(panel, 150, 150); // 画在哪,就点在哪
};
cv.requestPaint(); // 触发第一帧
几个当时不明白、试出来的细节:
ctx.reset()要写在最前面。2D 上下文不会自动清屏,不重置的话上一帧的内容会和这一帧叠在一起(官方示例也是先 reset 再画)。- 坐标不用自己再对一遍。我们画在
(150,150),点屏幕上输入框的位置,焦点就落进去了;命中测试跟着绘制位置走,不需要手写一套 offset。这一点比 overlay 方案省事得多——overlay 里你必须把 CSS 的位置和真实坐标对齐。 updateElementGeometry()不是首次绘制的前置条件。我们单独建了一块只加drawable、不登记几何的面板,照样画出来了;它的用处在位置/尺寸变化后重新同步。- 想手动改位置的,别去动
style.transform碰运气——官方给的路径是拿drawElementImage返回的矩阵回写。
实测一:它到底还是不是"真" UI
四条,都是在 canvas 内部点操作:
| 动作 | 结果 |
|---|---|
| 点 canvas 里的输入框 | document.activeElement.id === 'nm',光标可见、有焦点环 |
| 输入文字 | 值变成 2400 mm × 2,canvas 里同步显示,不用重绘代码 |
| 点 canvas 里的按钮 | 注册的 onclick 正常触发 |
| 用 CDP 读无障碍树 | 两个测试页里都读到了真节点:textbox(名字"尺寸")、button(名字"保存"),标签文字在 StaticText 里 |
第 4 条是这个 API 和"截屏贴图"的分水岭:读屏、键盘 Tab、输入法这些依赖无障碍树的链路仍然成立,因为 DOM 真的还在。顺带说明,它们在 DOM 树里就是普通元素:getElementById、事件、样式、querySelector 全部照旧。
实测二:开销
测试方法:一块 1280×800 的 canvas,每块面板是一个 200×110 的 div(标题 + 输入框),全部在 onpaint 里用 drawElementImage 逐个画。跑 90 帧,去掉前 10 帧后取中位数与 p95。环境:Chrome for Testing 157.0.8103.38、Windows、headless、DPR 1、关闭帧率上限。
| 面板数 | paint 回调 中位 / p95 | 整帧(rAF 间隔)中位 / p95 |
|---|---|---|
| 1 | 0.0 / 0.1 ms | 0.6 / 1.6 ms |
| 10 | 0.1 / 0.2 ms | 1.6 / 2.1 ms |
| 50 | 0.3 / 0.5 ms | 2.0 / 3.2 ms |
| 200 | 0.9 / 1.3 ms | 4.6 / 7.5 ms |
同一个页面里换成"一张 200×110 的贴图用 drawImage 叠 N 次"作为基线:200 次是 0.4 / 0.7 ms。也就是说真 DOM 大约是纯贴图路径的 2 倍多,但两者都远在 16.7 ms 之下。
读这张表要注意边界,我们只测了这些:
- 时间是主线程提交 + 合成往返的观察值,不是完整的 GPU 光栅化拆解;真实项目里还要叠加你自己的 3D 渲染。
- headless、DPR 1、没有 WebGL 场景、没有滚动和动画曲线。DPR 2 的机器(MacBook、高分屏)应当另测。
- 面板里的内容很轻(一个标题 + 一个输入框)。真实标注里塞表格、图片、长文本,成本会明显不同。
现在别用它的三种情况
- 要跨浏览器。Firefox 和 Safari 目前都不支持,Chromium 之外没有退路。对外产品必须在设计阶段就把 overlay 作为降级方案,而不是上线后补。
- 只是"看起来在场景里"。如果 UI 不需要输入、也不需要读屏,一张贴图快照更便宜(我们的基线数字就是它),而且不担兼容风险。
- 需要导出一致性的老管线。如果导出走的是另一条路径(服务端渲染、离线出图),canvas 里的 DOM 不会自动出现在那条路径里,得自己补截图。
我们的整理
三条判据,用来判断一个 UI 该走哪条路:
| 你的 UI 需要 | 选 |
|---|---|
| 眼熟的位置、不需要输入、不需要读屏 | 贴图快照(最省) |
| 位置随镜头变、要能输入、要过无障碍 | HTML-in-Canvas(157+,设计好降级) |
| 必须全浏览器、且允许 UI"浮"在场景上 | overlay + 投影对齐(老办法,仍然最稳) |
两个可以在半天内做完的验证,用来判断它值不值得进你的项目:
- 把你最复杂的那块面板原样丢进去,不要用玩具表单。我们测的 200 块面板证明数量不是瓶颈,内容复杂度才是。
- 在真机上开 DPR 2 再跑一遍。
canvas.width必须按设备像素设,否则画进去的字会糊——官方示例给的是ResizeObserver+devicePixelContentBoxSize的写法,别用window.devicePixelRatio硬乘。
想直接试 3D 模型里塞面板的效果,用 3D 模型在线查看器 拖进一个模型,再照上面的写法往 canvas 里加一块 drawable 面板就能看到。
Sources
- Chrome for Developers,《Introducing the HTML-in-Canvas API origin trial》,2026-10-23:https://developer.chrome.com/blog/html-in-canvas-origin-trial
- WICG 提案(explainer):https://github.com/WICG/html-in-canvas
- 社区文档与 demo(含 3D 房间 demo):https://html-in-canvas.dev/
- 官方 demo 集:https://github.com/GoogleChromeLabs/css-web-ui-demos/tree/main/html-in-canvas
- Chrome Status:feature 5172548013916160(desktop / android / webview 均为 157)
同主题阅读
本文属于「Web3D / 游戏引擎」专题。文中性能与交互数据均为本机实测(Chrome for Testing 157.0.8103.38 / Windows / headless / DPR 1),测法与边界见正文;API 名称以 Chrome 157 的实际行为为准,官方博客发布于 157 之前的 origin trial 阶段。BimAnt 编译整理,转载请注明出处。