模型形态审计:方法沉淀

August 14, 2026 · View on GitHub

2026-08 对全部 60 个程序化模型做了两轮肉眼审计,修掉 10 只「明显不像」的虫和 一个全局几何缺陷。审计本身也踩了不少坑 —— 图太小漏判、指标选错白跑一轮、 诊断脚本自己有 bug 差点推翻正确假设。这份笔记沉淀的是方法, 下次再审(或有人给自己的程序化模型做验收)直接照着走,别把坑重踩一遍。

第一条,也是唯一的前提:测试全绿 ≠ 长得像

审计启动时全仓 4000 多个断言全绿,其中形态断言几百条 —— 而肉眼一轮扫下来, 60 只里查出 10 只明显不像。没有任何一条断言能回答「它像不像那只虫」。

原因在 README 的测试一节展开过,这里只留一句:形态断言量的是数字, 人看的是长相,两者可以毫无关系。 一块侧立的平板和一片花瓣, 「宽度 ≥ 厚度的 3.5 倍」这个数字上一模一样。断言能守住的是「修好之后别再坏回去」, 守不住「第一次就做对」。

所以:验收必须看图。 出图看图是审计的主体,测试只是底线。

出图规格:小图会说谎

两个都是实际漏判过的教训:

  • 每格 ≥340px。 300px 的缩略图只够看出「该有的部件在不在」, 看不出「部件的比例对不对」——甘薯腊龟甲的裙边比拱顶宽 3.5 倍这种 离谱比例,在 300px 小图上完全无感。第一轮就是这么漏掉它的; 换 ≥340px 重扫,才在 60 只里定位到剩余问题。
  • 每只至少顶 / 侧 / 前 / 斜四个机位。 只出默认 3/4 机位等于只审了一半: 龟甲尾端被看穿成黑洞的缺陷,在默认视角下几乎不可见,换到偏后方视角才暴露。 同理,白蚁兵蚁的两颚在世界坐标里分得很开,默认机位的视线方向恰好把 分离压扁,屏幕上糊成一根独角 —— 换个机位就现形。

剪影差集法:一次扫出所有「被剔除的面」

背景:loft() 生成的三角面绕向曾与外法线不一致,side: FrontSide 下 两壳交叠处或开口处的面会被背面剔除吃掉,露出背景色 —— 症状是 「壳上有一块颜色不对的楔形/白缝」,看起来完全像材质问题。 瓢虫这一处此前查了四轮材质方向(长宽比、斑点、清漆、环境反射……)全部否定, 因为它根本不是材质参数问题。

把这类缺陷一次性扫出来的办法:

  1. 把全部材质压成自发光纯白(emissive 白、关光照),得到视角无关的剪影;
  2. 同一机位渲两遍:一遍单面(FrontSide)、一遍双面(DoubleSide);
  3. 两张剪影做像素差集 —— 差出来的就是「本该有面、却被剔除」的洞。

按差集占比排序即得受灾名单(当时修前:红萤 15.2%、日本埋葬虫 6.9%、 榆蓝叶甲 5.6%、海滨蠼螋 5.4%、七星瓢虫 4.5%、日本弓背蚁 4.2%,中位数 0.7%)。

⚠️ 第一版指标是「单面 vs 双面的整图像素差」,没用。 中位数高达 36%—— 透明件的绘制顺序变化加上抗锯齿噪声,把真实信号完全淹没。必须先压成剪影 再做差集,噪声源(光照、透明混合、AA)才会全体退场。

最终修法记录在 kit.tsfinalize():不透明材质统一 DoubleSide (半透明件不动,双面会打乱它们的绘制顺序)。

反复出现的形态病(下次照单先查)

四种病各中招过不止一只,看到相似症状先对号:

症状病根修法
头/壳有「带倒角的圆桶感」横截面宽高比全程恒定 —— 恒定就一定像管子让截面宽高比随体轴位置变化
腹部读成一颗松果尾端收成尖锥 + 节间沟太深中段最粗、尾端圆钝、节间只留浅褶
两段衔接处一道裂缝两段共用同一条包络但 loft 采样密度不同,折线弦高不一致、互相穿插后一段整体收细,塞进前一段之下
壳面过曝成白铬中间调壳色套上 elytra() 的默认高光/清漆压 clearcoat 与高光强度(瓢虫、龟甲都是这么修的)

诊断脚本自身要带证伪手段

修模型免不了写一次性诊断脚本(改个参数渲一张对比图)。这轮吃过的亏: 脚本自己的 bug 会生产一堆假的「无变化」,拿去把正确假设推翻掉。 实际撞过三种:

  • roughness 传了 undefined,材质纹丝不动,看图以为「粗糙度与此无关」;
  • URL 选虫参数没生效,一整轮实验渲的都是另一只虫;
  • 两个 mesh 共享同一枚材质实例,「把 A 改红、把 B 改蓝」其实是对同一个对象 连写两次,结论自然是「A 没影响」。

所以每轮实验必须自带三样证伪输出,缺一不可:

  1. 打印实际写入的参数快照(写进去的是不是你以为的值);
  2. 打印命中的 mesh 数(选择器有没有选空/选串);
  3. 用像素差计数确认改动真的进了渲染(0 差异 = 实验没生效,不是「无影响」)。

定位「这块像素属于谁」:ID 图

看图发现一块不对的像素后,第一步不是猜,是问出它属于哪个 mesh: 给每个 mesh 发一种纯色、关光照渲一张 ID 图,颜色即身份,一次定位。 src/preview.tsx 留了 window.__preview 调试出口({ scene, camera, gl, model }), 脚本从这里拿场景遍历替换材质即可,不用改产品代码。

审计循环(汇总)

  1. 全量出图:≥340px/格,每只顶/侧/前/斜四机位;
  2. 肉眼过一遍记问题清单 —— 此时只记「哪里看着不对」,不猜原因;
  3. 疑似「缺面/看穿」的先跑剪影差集,别去调材质;
  4. 定位像素归属用 ID 图;
  5. 修完把「这次为什么不像」写成一条量到用户所见的断言 (投影到成像平面去量,别量世界坐标里的裸数字 —— 例子见 README 测试一节);
  6. 验收看图,不看测试。