生成出来的网格,离一个 WebXR 场景还隔着什么

生成这一步变快了,它两头的步骤没有。时间正是花在这两者之间的落差里。

约 10 分钟

Demo 和上线是两个问题

文生 3D 模型能凭一句话给你一个带贴图的网格。在生成它的那个工具里看,中性背景、转台上转着,它看起来是完成品。把同一个文件丢进一体机上跑的 WebXR 场景,结果通常是这么几样的组合:巨大、朝向不对、陷进地板、明暗不对、以及吃掉的帧时间比场景里其余所有东西加起来还多。

这些都不说明生成得不好。它说明的是:输出是一个看图用的资产,而需求是一个运行时资产,而这两者之间的距离,并不会因为第一步变快了就跟着缩短。有个说法比较贴切:生成替代掉的是建模,而建模从来就不是「把一个 3D 物件送进交互应用」里最贵的那部分。

模型不知道一米有多长

这是第一堵墙,大约要花二十分钟,而且人人都会撞一次。WebXR 是个公制系统:参考空间锚在地板上、一只手大致就在手该在的位置、一把高 0.9 单位的椅子就是 0.9 米高。而一个生成出来的网格没有单位。它有一个包围盒,用的是模型自己随便定下的某个尺度,这个尺度和米之间的关系是未定义的。

于是你导入一把椅子,它十一米高,或者四厘米高。你凭眼睛缩放一下,这招能用到下一个资产到来 —— 那个又是另一个随便的尺度,现在场景里没有任何两样东西是彼此协调的。正确做法是在导入时归一化,而不是逐个资产手调:挑一个你知道的真实尺寸,用包围盒算出缩放系数,然后施加上去。十行代码,而且是这条流水线上性价比最高的一次自动化。

原点是同一个问题的安静版本。生成的网格通常以包围盒为中心,这意味着把它放到命中测试的落点上,会有一半埋进地板。要摆在表面上的物件,原点该在底部;要挂起来的物件,原点该在挂载点。没有任何生成器知道你的是哪一种,因为那是关于你的应用的事实,不是关于这个形状的事实。

导入时归一化尺度与原点

在加载时对每一个资产跑一遍,生成的和非生成的都跑。「无条件执行」正是重点:一条「有些资产归一化了、有些是可信的」的流水线,最终会让你为那个「本不该被信任却被信任了」的资产搭进去一个下午。

import { Box3, Vector3 } from 'three';

// targetHeight 的单位是米,它是关于你场景的一个决定,
// 不是文件本身的属性。一把餐椅大约 0.9。
function normalise(object, targetHeight, anchor = 'base') {
  const box = new Box3().setFromObject(object);
  const size = box.getSize(new Vector3());

  const scale = targetHeight / size.y;
  object.scale.multiplyScalar(scale);

  // 缩放之后要重算:旧的包围盒用的是旧单位。
  const scaled = new Box3().setFromObject(object);
  const centre = scaled.getCenter(new Vector3());

  object.position.x -= centre.x;
  object.position.z -= centre.z;
  object.position.y -= anchor === 'base' ? scaled.min.y : centre.y;

  return object;
}

缩放之后重算包围盒,这一点比看上去重要。缩放一个对象不会更新你之前抓到的那个 Box3,而用缩放前的中心去减缩放后的对象,得到的是一个随资产大小变化的偏移误差 —— 于是它在每个模型上看起来都像是另一个 bug。

三角面数是个诚实的数字,而它通常是错的

生成出来的网格往往很密。重建那一步没有理由节俭,单个道具几十万面是常态。在桌面上渲染起来没问题。在一台以 90Hz 画两个视图的一体机上,这样一个道具就能吃掉帧里可观的一块。

减面工具能把数字降下来,对于背景物件,这往往就是全部答案。但对任何用户会凑近看的东西,自动减面的劣化方式很具体:它保轮廓、毁细节。于是物件隔着房间看还认得出来,拿到手里就糊成一团。而这恰好和 XR 场景需要的方向相反 —— 在 XR 里,用户随时可以走过去。

而且真正的开销很少来自某一个主角物件。它来自三十个生成的道具,每一个单独看都可以接受,每一个都带着自己的材质,没有一个被实例化或合批,最后堆出一个减多少面都救不回来的绘制调用数。盯着三角面数看、而绘制调用数在涨,是一种非常常见的「优化了一小时、什么都没变」。

生成器给你的,和一个运行时资产需要的

按「最后真正卡住上线的是哪一项」的出现频率排序。

拓扑
生成的网格通常是重建出来的表面而不是建模出来的:密、不规则,而且在需要形变的地方没有边环。做静态道具没问题,做任何要弯折的东西就不能用,而重拓扑至今仍是手工活。
UV 布局
就算有 UV,也往往是自动展开且浪费的 —— 这体现为贴图显存占用,而不是一眼可见的缺陷。它变得可见的那一刻,是你想烘焙光照或者贴一个贴花的时候,因为这两件事都需要一套有人认真想过的布局。
材质通道
多数输出是一张烘好的颜色贴图,有时连光照都已经烘进去了。一个有实时光源的 WebXR 场景需要把粗糙度和金属度分离出来;一张把高光画死在里面的 diffuse 贴图,除了生成它的那个角度之外,从任何角度看都是错的。
碰撞体
没有。在一个密集的生成物件上直接拿渲染网格做物理,是最贵的做法;而生成一个凸包代理这一步,除非流水线忘了做,否则没人会手动去做。
LOD
也没有。一个生成了二十个道具、不管远近全都按完整精度画的场景,把大部分预算花在了几米开外的像素上。
尺度与原点
上面讲过了,这里再列一次,因为它正是那种「本该在导入时修一次、结果被逐个资产手工修」的问题。

时间确实省下来的地方

把上面那张清单读成一句判决是不对的。有两件事确实变得便宜多了,而且都很要紧。

「一样东西该长什么样」的迭代,以前必须先有人把它建出来。现在你可以在决定之前先看九个变体 —— 这改变的是什么被纳入了考虑,而不只是做得多快。对任何设计还没定下来的场合都是实打实的变化,而 XR 里的设计通常没定下来,因为一样东西在一臂距离上读起来是什么感觉,很难提前预测。

贴图生成比几何生成落地得更干净,原因也讲得通:贴图就是图像,图像模型成熟,而且一张略微奇怪的贴图就只是一张略微奇怪的贴图,不是一个坏掉的资产。给一个本来就做得好的网格重新贴图,是这条流水线上少数几个「自动结果可以直接进场景」的地方。

两边共同的模式是:生成在「输出由眼睛评判」的地方最强,在「输出必须满足一个模型从没见过的约束」的地方最弱。帧预算、碰撞正确性、以及一米就是一米,全都属于后一类约束。

一条默认网格不可信的流水线

务实的安排是:把生成资产当成一个整备步骤的原料,而不是当成成品文件;并且对所有资产都跑这一步,这样就不存在一条会被遗忘的可信路径。

这一步做的是那些机械的部分:按一个已知尺寸归一化缩放、把原点挪到底部或挂载点、生成凸包碰撞代理、产出两到三级 LOD、以及拿三角面数和材质数去对一个会大声失败的预算。这些都不新鲜,任何资产流水线一直都在做同样的整备。变的是量 —— 生成让「为一个场景做四十个道具」变得轻而易举,而一个在六个道具时还能忍的手工整备步骤,到四十个就是瓶颈。

仍然手工的那些部分,正是一直以来最贵的那些:任何会形变的东西的拓扑、任何要烘焙或贴花的东西的 UV 布局、以及「哪些物件值得花这个预算」的判断。文生 3D 挪走的是这份工作里便宜的那一段,而一条建立在「它把整件事都挪走了」这个假设上的流水线,会把省下来的时间花在返工上。

相关页面