第一件设备取进来的时候,没人想到会卡在“尺度”上。
控制室里只剩一块屏幕亮着。要取的第一件是控制环基座——整条产线的原点,所有坐标都从它开始算。生成过程本身不复杂:描述、等待、下载。麻烦的是下载回来之后。
模型很漂亮。曲面干净,金属反光到位,缩略图看上去像一张工业摄影。但把它放进参照系里,它站错了地方——直径比真实基座小了将近三分之一。
这件事的严重性在当时并不明显。一个模型尺度不对,改一改就行;但如果四十六件资产各自生成、各自落地,每一件都带着自己的尺度理解,那么拼装那一天就会发现:它们不属于同一个世界。
于是第一件资产没有立刻入库,而是先被钉在一张对照表上:直径、高度、安装面朝向、与相邻设备的间距。全部按真实设备实测值填。填完之后再回看模型,发现生成端给的是一个“视觉上正确”的尺寸,而不是一个“可以被装配”的尺寸。
最终的决定是:宁可让单件看起来朴素,也要让它在尺度上可信。贴图上限压到 2048,几何做减面,全量 Draco 压缩,把原始一百四十一兆的资产压到一百零一兆。体积让位于可信度,这是刻意的取舍,不是技术妥协。
第一件资产入库那天,它拿到了自己的编号:CF-001。编号不是文件名,是身份——此后它在任何页面、任何仓库、任何一次对外展示里,都只能叫这个名字。
四十六件资产摊开在一张平面上,看上去像一堆零件。真正的问题不是它们长什么样,而是它们该站在哪里。
第一次布局是错的。涂装工位被放到了焊装的下游偏侧,从俯视图看很匀称,四个象限差不多满。但把白车身的行进路线画上去,问题立刻暴露:车身要绕一个不必要的折角才能进涂装,节拍断了。
布局不是排版。排版追求视觉平衡,布局必须让物料流动成立。这条线一旦划错,后面所有交互、所有视角、所有巡检路线都建在一个不成立的空间上。
重排之后,位置被钉死:冲压焊装在前,涂装居中,总装与电池 PACK 在后,检测收尾。整条线铺开在 x 轴正负十二米之间,传送带约二十米,八台机械臂按工位分工,谁也不越界。
装配阶段还遇到一个更现实的问题:四十六件资产加起来一百多兆,一次性全部加载,首屏要等很久。解决办法是分层——十一件核心组阻塞加载,约十七兆,完成即进入场景,保证打开就能看见产线骨架;其余三十四件进入后由四个并发任务在后台静默加载,逐件放置,不打断交互。另设十秒硬超时与错误捕获,任何一件失败都不至于让整页黑掉。
这套分层后来被写进了页面源码,成为两个写死的数字:核心组十一,扩展组三十四。它们加起来是四十五——比四十六少一件。少的那一件是拆分副件,不单独入场。这个差异在后来的每一次对外口径核对里都被重新算过一遍,每次都要解释一次。
layout 数组与写死的 coreTotal / totalUnique
四层关系:47 文件 / 46 编号 / 45 加载 / 11 核心 + 34 扩展,报告书同段可逐项对撞
产线立起来之后,第一次夜巡是由一台四足机器人完成的。
它的关节链来自一份 URDF,四个自由度,走路时会晃。晃动不是动画师调出来的,是物理引擎算出来的——重心偏一点,机身就歪一点。这一点很重要:如果它走得过于稳,就说明它没有被物理约束住,那它就只是一段循环播放的影像。
夜巡的路线穿过焊装段。走到第三工位附近时,状态面板弹出了一条报警:温度异常,已触发停机。
这条报警不是装饰。它来自状态机的一个分支:温度越过阈值,停机指令下发,机械臂停止动作,传送带停转。整条线在一秒之内从运行态切到停机态。如果这个分支不成立,那么“数字孪生”四个字就只剩下前两个字。
但第一次上线时,这条报警做得太用力了。红色遮罩直接盖在画面正中,字号大、对比强,用户刚进入场景还没来得及看清产线,先被一块警告挡住半个屏幕。手机竖屏下更糟——遮罩几乎吃掉整个视野。
于是报警被改成顶部一条细带,宽度不超过视口六成,字号压到十像素。信息还在,压迫感撤掉了。同时能力清单在移动端缩到六成宽、最高三十百分比高度,把画面还给场景本身。
这些调整看起来是界面细节,实际上回答的是同一个问题:这个世界是给谁看的。如果它是给评审看的一次性演示,报警越醒目越好;如果它是给用户日常巡检用的工具,报警就该让位给视野。
第四件事发生在第一座工厂之外:有人要一条不一样的线。
不是汽车总装,是电池 PACK 专线。工位更少,节拍更慢,安全约束更密。按传统做法,这是一个新项目,要重新建模、重新布局、重新调交互。
但在这个世界里,它只是两组数据的替换:一份资产谱,一张布局表。资产谱决定这条线上有哪些居民——模组、电池包、测试柜、对位台;布局表决定它们站在哪里。换掉三十四个坐标,产线就换了。
可锻造性有三个前提,缺一不可。资产必须是可复用的原子件,不能是烘死在场景里的整体;布局必须与资产解耦,改坐标不改模型;场景必须能被外部消费,尤其是被机器人策略拿去训练。三条都成立,才谈得上“按需生产”。
这也是为什么前面三章里那些看起来琐碎的取舍——尺度优先于好看、工序优先于匀称、视野优先于醒目——最终都指向同一个结论。它们不是审美偏好,是这套能力成立的必要条件。
一座工厂被锻造一次,是作品。同一套能力把不同工厂锻造出来,是生产能力。前者靠灵感,后者靠纪律。
第五章不是设计出来的。它是被人发来一张手机截图之后,才补上的。
截图里,页面在手机上打开,右下角写着九十九帧,画面上只有一个橙色方块,几条细线穿过空白,正中间压着一块红色遮罩,上面写着焊接工位三号温度异常、已触发停机。整幅画面像是有人在黑板上画了一个点,然后用最醒目的颜色盖上警告。
第一眼看上去它运行得很流畅。但同一屏右下角还有另一个数字:Drawcall 一。一帧只画一个对象,帧率自然高。这两个数字放在一起才说得清事实——循环在跑,世界是空的。
查下去是四个原因叠加。最要紧的一个是视野:相机用的是四十五度垂直视场角,桌面上没问题,可手机竖屏的宽高比只有零点六五,水平方向剩不到三十度,十四米之外只能覆盖两米五左右——而设备铺到正负十二米。相机参数本身没错,错的是没有人在竖屏上验证过一次。
第二个更隐蔽。并发加载队列的失败分支里只写了一行警告日志,界面上没有任何计数、没有任何标记。失败了不响,用户看到的是空场景,而不是一句错误。沉默的失败比响亮的报错更伤人,因为它让人误判成“这东西本来就这样”。
另外两个是界面与检测:能力清单占掉半屏,报警在进入二十秒后自动弹出、正好盖住中央;WebGL 检测只要构造渲染器抛一次错,就直接判定环境不支持,连降级尝试都没有。
修复不是逐个补丁,而是按依赖顺序推进:先把外部依赖搬回站内,三次握手从将近三十秒压到两秒以内,这一条同时解决了手机端加载失败与 3DGS 黑屏;再把 WebGL 检测改成三级降级;然后做竖屏相机补偿,按宽高比调整视场角与机位;再把加载过程可见化,已加载与失败数量直接写在界面上;最后收起移动端的能力清单,把报警压成顶部一条窄带。
返修之后,页面在这个没有图形加速的环境里仍然停在最后一层降级提示上。它没有变成一张好看的图,但它的失败方式变成了可解释的失败——而不是一片沉默的黑。
最后一章不写这个世界,写它如何被交出去。
递交的对象有三类,要求各不相同。评委要看的是能力是否成立、证据是否可查;体验用户要看的是打开之后能不能立刻看见东西、点下去有没有反应;生态合作方要看的是这套方法能不能复制到他们自己的产线上。同一份作品要同时满足这三种读法,靠的不是多写几段话,而是把每一层都做成可核对的形式。
于是对外拆成八个页面:主场景、高斯泼溅场景、架构说明、世界观设定、这本编年史、项目报告书、成长轨迹、元进化看板。每个页面只回答一个问题,互相之间用链接咬合,不留孤岛。
源码也一并交出。压缩包九百三十万字节,含八个页面、四个本地依赖与巡检脚本,解压置于任意静态目录即可运行。依赖随包走,意味着它不依赖某一家内容分发网络的可用性。
更重要的是两条巡检线。中枢那条每小时第七分钟跑一次,核对六个页面、五个依赖、四十七件资产与报告书里的四个口径数字;编年史这条每小时第十七分钟跑一次,核对章节完整、正文厚度、视觉实现与溯源标识。两条各自写自己的日志、各自生自己的看板,互不混入。
交付的本质不是把成品递出去,而是把核算方式一起递出去。看得见的东西会过期,能被别人重新算一遍的东西不会。
这本书成文时,世界里有 46 个编号、47 个文件。此后电池 PACK 专线做了第二代复现,补了五件(CF-047–051),又加了一件安全围栏(CF-052)。
这五件与 CF-029–033 同语义 —— 第一次生成时我没有先查清单,它们现在在场外待命。编号增至 52,文件增至 53,入场仍是 45,七件为储备。
正文里的 46 没有改。它是那一刻的真实,改掉就不是编年史了。
世界不是因为被看见才成立,
是因为可以被重新锻造才成立。