Featured image of post 对着杯子按一下扳机:把生成式 3D 接进 XR 运行时

对着杯子按一下扳机:把生成式 3D 接进 XR 运行时

一张照片变成头显里可以转动的 3D 模型。难的不是生成,是让一条不可控的生成管线接上一个必须可控的运行时。

举着 Quest,对着桌上的杯子按下扳机。

几十秒后,一个可以拿在手里转的杯子出现在你面前。

这就是这个项目要做的事:把生成式 3D 接进 XR 运行时。

也正因为"接进"两个字,它比听起来难得多。


为什么难

把 2D 生成接进网页,你只要拿回一张图,塞进一个 image 标签就行。

但把 3D 生成接进 XR,你要面对三个运行时约束:

① 材质不能在运行时可靠重建

生成式管线经常把材质烘进 shader graph。而在 XR 渲染管线里,动态加载的 shader graph 无法可靠还原——它依赖编辑器期的编译结果。

② 生成模型的坐标是不可信的

自动生成的模型,pivot 位置随机、坐标原点不规则、内部结构千奇百怪。直接拿它的 pivot 去转,你会看到模型一边转一边漂。

③ 尺度完全不可控

有的模型生成出来像指甲盖,有的大得像房子。

这三个问题都不是"模型生成得不好"——是生成结果和 XR 运行时之间的接口没有对齐。


系统的三个部分

系统工作流

整个系统分三块:

组件跑在哪职责
XR 运行时Meta Quest 一体机(Unity)透视取景、交互控制、运行时装配与渲染
服务端编排轻量控制器工作流协调、状态管理、资产路由
生成管线ComfyUI + Hunyuan3D所有计算密集的生成任务

通信走 REST:异步上传、状态轮询、资产拉取。

整个系统完全在运行时工作——不需要任何离线预处理或编辑器期资产准备。这是刻意的:真实部署的 XR 应用就是这么跑的。


决策一:在生成端就把几何和纹理拆开

这是整个系统最核心的一条。

常规做法是生成一个带材质的整体 GLB,丢进引擎直接用。

我们没这么做。

ComfyUI 工作流最后输出两个独立的东西:

  • 一个只含几何的 GLB 网格
  • 一张纹理图片

然后在 XR 运行时里,显式创建一个原生渲染材质,把纹理绑到对应的输入通道上。

为什么

因为这样绕过了隐式的 shader 编译机制。

一旦材质是烘在资产里的,你就把控制权交给了资产生成方。而生成方给的材质,在 XR 运行时里未必能加载、未必能编译、也未必要得。

拆开之后,材质绑定变成一个确定性的、你可以完全控制的步骤。

一句话:牺牲了"资产自包含"的便利,换来了运行时行为的确定性。


决策二:运行时显式装配

生成结果不会直接扔进场景。

系统走一条显式装配流程:

  1. 收到两个独立资产(GLB + 纹理)
  2. 实例化原生材质、绑定纹理
  3. 挂到转盘锚上(下一节)
  4. 自动归一化:用包围盒估算空间尺寸,算出一个统一缩放系数,让模型落进一个预设的"感知尺寸区间"
  5. 挂到更高层的空间根节点,成为可交互的 XR 对象

第 4 步解决的就是"有的像指甲盖有的像房子"。

而且这个归一化是在运行时算的——不需要人工调、不需要离线预处理。


决策三:转盘锚(Turntable Anchor)

这是解决"pivot 不可信"的答案。

思路很干脆:不去修正模型的 pivot,而是把它挂在一个锚下面。

  • 模型自身的局部变换完全不动
  • 所有关于"呈现"的变换——旋转、漂浮、缩放——全部作用在锚上

数学上就是:

1
最终世界变换  T = T_a ∘ T_m

其中 T_m 是模型的局部变换(不动),T_a 是锚的变换(动态更新)。

为什么这比"修 pivot"好

因为旋转轴只在世界空间里定义,与模型内部几何无关。

于是:

  • 模型只会原地震转,不会一边转一边漂
  • 不需要依赖任何几何启发式(比如"用包围盒中心当 pivot"——对不对称模型经常算错)
  • 缩放、漂浮、透视/虚拟场景切换,全部在锚这一层统一处理

与其去猜一个结构不可信的对象"中心在哪",不如给它一个外部坐标系。


一个很具体的设计问题:什么时候该阻塞用户输入

这个问题看着小,但处理不好会直接打崩服务端。

场景:用户按下扳机触发取景。

问题:什么时候禁止再次触发?

如果等到服务端响应才阻塞——那么在这段时间窗口里,用户可以连续触发多次取景。每一次都会向 ComfyUI 发一个新请求,任务队列直接被压垮。

更麻烦的是输入抖动:一次物理按键,可能产生多个输入事件。这会让系统平白提交好几个重复的生成任务,烧掉大量算力。

解法

在第一个取景命令触发的瞬间就阻塞输入。

然后在上传完成并拿到服务端响应时释放——包括失败和超时的情况。

正常流程下这个阻塞窗口很短,用户基本感知不到。而如果生成成功,阻塞会一直保持到整个工作流结束。

另外还加了帧缓冲与丢弃机制,避免网络延迟导致的错误决策。

这个例子的价值在于:它不是一个算法问题,是一个时序问题。 而时序问题只能靠想清楚状态机来解决。


生成管线

ComfyUI 生成管线

工作流搭在 ComfyUI 里,核心生成模型是 Hunyuan3D-dit-v2.0。

几个关键阶段:

阶段做什么
输入预处理把透视 ROI 图缩放到固定方形分辨率
去背景剥离环境,得到干净的目标
几何生成(GenerateMesh)图像 → 中间隐表示 → 解码出三维表面
网格后处理修整产物
去光照(Delighting)把原图的光照信息剥掉,避免"烤"进纹理
多视角渲染用于纹理采样
多视角纹理采样 + UV 烘焙把各视角的颜色烘到统一 UV 上

去光照这一步很容易被忽略,但很关键。 如果不去,原图里的阴影和高光会被当成材质的一部分烘进去,模型换个光照就露馅。


实测结果

生成结果:正面与背面

系统在 Meta Quest 一体机上完整跑通。

指标实测
端到端延迟(取景 → 可视化)数十秒,取决于管线复杂度和服务端负载
连续多次生成稳定,无持久性失败或不可恢复状态
失败/超时正确释放输入阻塞、回到 idle,无需重启应用
旋转与漂浮动画在不同模型间保持稳定,无轨道运动或漂移
XR 交互生成期间取景/上传被门控,但视角操作仍然响应

一个诚实的说明

这个延迟不满足严格的实时约束。

“近实时"在这里指的是:通过异步执行和视觉反馈,让用户在等待期间保持参与感,而不是"几十秒内出结果”。

这个边界我写进了论文,也写在这里。


这个项目真正学到的东西

回头看,最值钱的不是"跑通了 Hunyuan3D"。

而是这三个判断:

  1. 接口比模型重要。 生成质量再好,如果和运行时的接口对不齐,也用不了。
  2. 不可信的东西,给它一个外部参照。 pivot 不可信就给锚,尺度不可控就归一化。
  3. 先想时序,再写代码。 输入阻塞那个问题,想清楚了只有十行代码;没想清楚会让服务端崩溃。

技术栈

Unity · Meta Quest · OpenXR · C# · ComfyUI · Hunyuan3D-dit-v2.0 · Python / Flask · REST 异步编排

论文:A System for Near Real-Time Generation and Runtime Visualization of Generative 3D Models in XR Environments(IEEE 会议格式)


最后一句是我写在论文里的判断:

这套系统的价值不在于生成得多好,而在于它把一条不可控的生成管线,接上了一个必须可控的运行时。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计