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 机器上把它跑通了,量了开销,也踩到了文档没写的那几个点。下面这些数字和报错都是本机实跑的结果。

在 canvas 里排版的两块真 DOM:左边是带输入框的构件标注,右边是只读的材质卡;背景的透视网格和梁是用 2D 上下文画的,两层在同一个 canvas 里

为什么这件事值得看

对做 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、高分屏)应当另测。
  • 面板里的内容很轻(一个标题 + 一个输入框)。真实标注里塞表格、图片、长文本,成本会明显不同。

现在别用它的三种情况

  1. 要跨浏览器。Firefox 和 Safari 目前都不支持,Chromium 之外没有退路。对外产品必须在设计阶段就把 overlay 作为降级方案,而不是上线后补。
  2. 只是"看起来在场景里"。如果 UI 不需要输入、也不需要读屏,一张贴图快照更便宜(我们的基线数字就是它),而且不担兼容风险。
  3. 需要导出一致性的老管线。如果导出走的是另一条路径(服务端渲染、离线出图),canvas 里的 DOM 不会自动出现在那条路径里,得自己补截图。

我们的整理

三条判据,用来判断一个 UI 该走哪条路:

你的 UI 需要 选
眼熟的位置、不需要输入、不需要读屏 贴图快照(最省)
位置随镜头变、要能输入、要过无障碍 HTML-in-Canvas(157+,设计好降级)
必须全浏览器、且允许 UI"浮"在场景上 overlay + 投影对齐(老办法,仍然最稳)

两个可以在半天内做完的验证,用来判断它值不值得进你的项目:

  1. 把你最复杂的那块面板原样丢进去,不要用玩具表单。我们测的 200 块面板证明数量不是瓶颈,内容复杂度才是。
  2. 在真机上开 DPR 2 再跑一遍。canvas.width 必须按设备像素设,否则画进去的字会糊——官方示例给的是 ResizeObserver + devicePixelContentBoxSize 的写法,别用 window.devicePixelRatio 硬乘。

想直接试 3D 模型里塞面板的效果,用 3D 模型在线查看器 拖进一个模型,再照上面的写法往 canvas 里加一块 drawable 面板就能看到。


Sources

同主题阅读

本文属于「Web3D / 游戏引擎」专题。文中性能与交互数据均为本机实测(Chrome for Testing 157.0.8103.38 / Windows / headless / DPR 1),测法与边界见正文;API 名称以 Chrome 157 的实际行为为准,官方博客发布于 157 之前的 origin trial 阶段。BimAnt 编译整理,转载请注明出处。