
1. 项目缘起为什么盯着“油画物理引擎”而不是图像生成第一次看到 stillwet 这个项目我盯了很久的不是它的输出画作而是它的实现路径不用图像模型让大模型写代码去驱动一个 Rust 写的油画物理引擎作画。这个思路在当下“万物皆可扩散模型”的氛围里显得有点反骨但恰恰是这种反骨把一个被图像生成掩盖掉的问题重新摆到了桌面上——大模型到底懂不懂“画一笔”这件事的物理后果我们平时说“让 AI 画一张油画”绝大多数人的第一反应是调用文生图模型给一段提示词等一张图。这条路的问题在于你拿到的是像素结果中间过程是黑盒。模型不知道颜料是湿的、笔刷是有压强的、亚麻布是有纹理的、两种颜色相邻会互相渗染。它只是把训练集中“油画风格”的统计特征重组了一遍。而 stillwet 想验证的是另一件事如果把“颜料未干”“笔触叠加”“画布吸油”这些物理约束写成可执行的代码交给大模型去写这段代码模型会不会在代码层面暴露出它对绘画过程的先验理解——也就是标题里说的“审美先验”。这个项目的核心价值不在于产出 75 幅画而在于它设计了一个可观测的实验框架同一个物理引擎同一套底层能力12 个大模型分别写代码来调用它最后横向对比 75 幅作品。这相当于把“模型的绘画能力”从不可解释的像素空间拉回到了可阅读的代码空间。你不需要猜模型为什么画成这样你直接看它写了什么参数、按什么顺序落笔、怎么处理图层混合。适合谁来读这篇拆解一是对程序化绘画 / 生成艺术感兴趣想搞明白物理仿真和渲染管线怎么搭的人二是做大模型评测、想知道除了跑分还能怎么测模型能力边界的从业者三是写 Rust、想看看一个偏图形学的 Rust 项目在工程上会踩哪些坑的开发者。基础要求不算高但需要你对“笔刷是像素蒙版”“颜料是带黏度的颜色层”这类概念有一点直觉后面的内容我会尽量用生活化的类比把它们讲透。先把这个项目的整体骨架说清楚避免后面读散。它大致分四层最底层是颜料与画布的物理模型负责模拟湿度、扩散、附着中间层是笔刷与笔触系统把一次落笔拆成路径、压强、载墨量、抬笔上层是渲染与合成处理图层、干燥、扫描式输出最外层是给大模型的接口层也就是模型真正要写代码去调的那套 API。12 个模型拿到的任务是用这套 API 写一段程序画出一幅“有意图”的油画。75 幅画是这个任务的产物也是分析素材。理解了这四层后面所有关于“模型暴露了什么先验”的讨论才有落脚点。2. 物理引擎的四层设计把“湿画法”拆成可计算的状态2.1 颜料状态机湿度、黏度、载墨量三要素油画最迷人的地方是“湿”,最麻烦的地方也是“湿”。stillwet 这个名字本身就是题眼——still wet还没干。真实油画里湿颜料会流动、会互溶、会被下一笔带起来这就是所谓湿画法wet-on-wet。要用代码模拟这件事第一步是把“一坨颜料”抽象成一个带状态的对象。我的理解是这个引擎里每一笔落下去本质上是在画布的一个局部区域写入一个颜料层单元这个单元至少带三个参数湿度wetness、黏度viscosity、载墨量pigment load。湿度决定它还能不能和邻居混合、还能不能被后续笔刷带走黏度决定它扩散的半径和边缘的锐利程度载墨量决定这一笔的颜色浓度和不透明度。为什么是这三个而不是别的因为这三个量刚好对应真实绘画里画家的三个手感判断这笔还湿不湿能不能叠色、这笔稠不稠会不会糊开、这笔颜料蘸得多不多颜色实不实。大模型在写代码时如果只设颜色不设湿度那它画出来的就是“贴图”不是“油画”。这恰恰是后面分析模型先验时一个非常关键的观察点——会不会主动设置湿度参数几乎可以当作模型是否理解油画媒介的试金石。这里有个工程上的取舍值得说。完整的流体仿真比如基于 Navier-Stokes 的颜料流动精度高但算不动一幅画可能要跑几个小时。实际可行的方案是元胞网格 局部规则把画布切成细网格每个格子存一个颜料状态混合只发生在相邻格子之间用一套简化的扩散规则近似流动。这是一种典型的“够用就好”的工程妥协牺牲了物理精确性换来了可交互的渲染速度。我个人的经验是做生成艺术类项目仿真的可信度比仿真的精度更重要观众要的是“看起来像油画”不是“算得对”。2.2 画布模型吸收、纹理与干燥曲线画布不是白纸它会吸油。stillwet 里的画布模型至少要处理两件事吸收率和纹理。吸收率决定颜料渗进纤维的速度直接影响到边缘的毛糙感纹理决定笔刷扫过时颜料是均匀铺开还是被布纹“刮”出断续的条纹。干燥是另一个必须建模的维度因为“still wet”意味着存在一个时间轴。真实油画表层干燥要几小时到几天但引擎里显然不能真等通常用一条干燥曲线湿度随模拟步数指数衰减。这条曲线的形状很关键——衰减太快湿画法就没意义了第二笔下去第一笔已经干了叠不出融合效果衰减太慢画面会一直糊着永远到不了“完成”状态。注意干燥曲线是整个引擎里最容易调崩的参数。我见过类似项目里把衰减系数设得太激进结果所有笔触都是硬边画面像矢量插画设得太保守最后一团颜色互相污染什么都看不出来。经验值是从“一笔落下后大约还能承受 3 到 5 笔叠加才开始明显变干”这个手感倒推。纹理这块还有一个隐藏细节笔刷方向和布纹方向的夹角会影响笔触的视觉质感。顺着布纹扫颜料铺得开、边缘柔和逆着布纹扫颜料被纤维挡住会形成断续的颗粒。这个细节在大模型写代码时几乎不会被主动考虑但它恰恰是区分“随手涂”和“有绘画意识”的一个分水岭。2.3 笔刷抽象把一次落笔拆成路径与压力笔刷是人和颜料之间的中介。在 stillwet 里笔刷不应该被实现成“一个圆形蒙版贴图”而应该是一组沿路径采样的落笔点每个点携带压强、速度、载墨量。压强影响这一点的铺开面积速度影响颜料的拖拽长度快扫会拉出飞白载墨量随落笔过程递减画一笔笔上的颜料会越用越少。这个拆法为什么重要因为它决定了 API 的形态。如果笔刷只是一个贴图那模型写代码时能做的只是“选一个笔刷、选一个位置、盖上去”表达力极其有限。而把落笔拆成路径 每点参数模型就有了真正的创作空间它可以画一条曲线、可以让压强从大到小、可以在中途抬笔再落笔制造断续。表达力越强模型写出来的代码越能反映它的意图这正是这个项目作为评测框架的价值所在。载墨量递减这个设计还有个副作用很有意思它天然制造“一笔画到后面越来越淡”的效果非常接近真实油画里颜料耗尽的观感。模型如果意识到这一点会主动在长笔画中途“蘸墨”重置载墨量这个行为在代码里是可观测的又能作为一个分析维度。2.4 渲染与合成图层、透明度与最终输出渲染层要处理的是“很多层半透明的东西叠在一起最后长什么样”。这里有两条路线一是直接混合每一笔直接改画布像素二是分层合成每笔落在独立图层最后按顺序合成。前者快、省内存后者灵活、可回溯。stillwet 这种要对比 75 幅画的实验大概率需要某种程度的可回溯性——你得能重现某一幅画是怎么一步步画出来的否则分析模型行为时只能看结果、看不到过程等于丢掉了一半信息。所以更合理的做法是记录落笔事件流而不是存一大堆图层位图。事件流的好处是体积小、可重放、可 diff两个模型画同一主题你可以直接对比它们的事件序列看谁先铺底、谁先抠细节、谁在中途改主意。合成时还有一个绕不开的问题透明度累积。油画是不透明的正常情况下后一笔覆盖前一笔。但如果引擎允许半透明叠加模型可能会滥用透明度画出一种“水彩感”或者“数码感”而不是油画感。这又是一个能暴露模型先验的点它对“油画应该是覆盖式的”这件事有没有概念。3. 给大模型的接口层为什么“写代码”比“写提示词”更能测出东西3.1 接口设计的三条原则这是整个项目里我最想细聊的部分因为它决定了实验的有效性。给大模型一套绘画 API设计得好不好直接决定你测到的是模型的绘画理解还是模型对你 API 文档的阅读理解。我总结下来有三条原则。第一条语义要贴近绘画动作而不是贴近图形 API。如果你给的接口是drawCircle(x, y, r, color)模型只会画圆。如果你给的是brushStroke(points, pressureCurve, load)模型就得思考“我要画一条什么样的线、压力怎么变、颜料蘸多少”。接口的语义层级决定了模型的思考层级。第二条参数要有物理含义并且可观测。湿度、黏度、载墨量这些参数必须有默认值但不能隐藏。隐藏了模型就不会去动它暴露了模型动不动它本身就是数据。默认值的选择也有讲究默认值太“好”模型懒得调默认值太“差”模型光顾着修参数画不出东西。第三条必须允许组合与循环。一幅画不是一次调用能画完的模型需要写循环来铺底色、写函数来复用笔触。允许组合模型才会暴露出“它有没有构图规划”——是先大面积铺底再抠细节还是一上来就画重点。这个行为差异在各个模型之间往往比画作本身更有意思。3.2 从提示词到代码观测维度的变化用提示词让模型画图你只能观测到最终的像素中间全是黑盒。用代码让模型画图你能观测到的东西一下子多了好几个维度。观测维度提示词方式代码方式落笔顺序不可见完全可见可对比参数选择不可见湿度、压强、载墨量全部可读构图策略只能从结果反推从代码结构直接读出纠错行为不可见是否重画、是否叠加修正可见复用与抽象不可见是否定义函数、是否用循环可见这张表基本就是这个项目的方法论核心。代码是一面镜子模型对绘画的理解有多深代码里藏不住。一个只理解“颜色和位置”的模型写出来的代码就是一堆散点一个理解“层次和过程”的模型会先铺底、再中间调、最后高光代码结构上就能看出这三段。3.3 12 个模型同台变量控制怎么做才干净12 个模型写代码画同一个主题要得出可信的结论变量控制必须干净。这里最容易犯的错是提示词不统一。如果给 A 模型的指令是“画一幅风景油画”给 B 模型的是“画一幅有山有水的油画注意光影”那画出来的差异里就混进了提示词差异没法归因到模型本身。干净的做法是系统提示词完全一致任务描述完全一致API 文档完全一致唯一变量是模型本身。同时要记录每个模型的采样参数温度等因为高温下的输出随机性大同一个模型跑两次可能差异巨大这时候你对比的其实是随机种子而不是模型。稳妥的做法是每个模型跑多次取代表性样本或者固定低温减少抖动。还有一个隐藏变量是上下文长度和代码长度上限。有的模型能写几百行有的写几十行就截断了。截断的画和完整的画放一起比不公平。这一点在做横向评测时特别容易被忽略但对结论可靠性影响极大。4. 75 幅画背后的审美先验我观察到的几类典型行为4.1 铺底策略谁在“打底”谁在“直接画”看这批画最有意思的地方不是看谁画得好看——这玩意儿主观性太强——而是看代码里的铺底行为。有经验的人类画家画油画第一步几乎都是铺一层底色甚至可以是很脏的颜色把画面的明暗大关系定下来再往上叠。这一步在代码里表现为先来一次大范围、低精度、高载墨量的扫刷把整块画布盖住。我观察到模型大致分三派。一派会主动铺底代码里能看到明显的“全画布大笔刷”调用之后再逐步缩小笔刷尺寸做细节这个行为和人类画家的流程高度一致。一派直接画主体背景留白或者最后才补画出来往往主体突出但画面“飘”缺少整体色调。还有一派更极端上来就是一堆小笔触等于把油画当像素画在点工作量巨大但缺少层次。这个差异背后其实是对“过程”有没有建模。铺底本质上是一种“先粗后细”的过程规划模型能不能想到这一步反映了它在训练数据里有没有吸收到“绘画是一个分阶段过程”这个模式而不只是“图像是一堆像素”。4.2 湿画法的直觉有几个模型真的在用“未干”这件事stillwet 这个名字的核心考点在这里。湿画法的精髓是利用未干的颜料做融合在湿的底色上点一笔边缘会自然晕开两种湿颜色相邻会互相渗染出中间色。这在代码里的表现是模型会不会在上一笔还没“干”的时候落下一笔以及会不会故意让两笔重叠。实测下来基于这类实验的常见观察真正会利用湿度的模型是少数。多数模型的落笔是“离散事件”一笔画完画下一笔笔与笔之间在时间上隔得很开湿度早就衰减了等于在干画布上画融合效果全无。少数模型会在代码里显式地控制落笔间隔或者在同一个位置短时间内多次落笔这才吃到了湿画法的红利。提示如果你的引擎里湿度衰减是按“步数”而不是“真实时间”算的那模型对“间隔”的感知会失真。更合理的做法是把衰减绑到模拟步进上让模型通过控制步进来控制干湿这样它的意图才能被准确执行。4.3 笔触的“意图性”随机涂抹 vs 有方向的扫刷还有一个特别能拉开差距的维度是笔触方向。人画画时笔触方向是有意义的画天空横着扫画树干竖着扫画圆形物体顺着结构线扫。笔触方向承载了形体信息。模型写代码时如果只是随机生成一堆点和角度画出来就是一团毛如果它让笔触角度和它想表达的形状相关联比如用弧形路径画圆形物体的体积感那画出来就有“塑造感”。我在类似实验里见过最惊艳的一类输出是模型用一组渐变的、方向一致的短笔触去暗示一个曲面这已经是印象派思路了——用笔触的方向和密度暗示形体而不是用轮廓线勾勒。能走到这一步的模型说明它对绘画风格的理解不止于“颜色”而是到了“笔法”层面。4.4 纠错与覆盖画错了怎么办油画的一个特性是可覆盖。画错了等干了盖一层新的上去或者趁湿直接刮掉重来。这个特性在代码里对应两种行为一是覆盖式纠错后一笔完全盖住前一笔二是叠加式修正用半透明或局部笔触调整。观察模型怎么处理“画错”特别能看出它对媒介的理解。有的模型画完主体后会额外补几笔去“压暗”或者“提亮”某些区域这其实是在做关系调整是很成熟的绘画行为。有的模型则是一路往前画从不回头改画成什么样就是什么样。前者说明它在生成代码时有一个“整体效果”的心理模型会做后处理后者说明它是“局部生成、逐笔拼接”缺少全局观。这个维度也提醒我们一件事代码生成模型的能力很大程度上取决于它有没有“全局规划”。绘画是这样写软件也是这样。一个会先规划再动手的模型和一个走一步看一步的模型产出的东西在结构上完全不同。5. 工程实操把这样一个实验跑起来的关键环节5.1 环境与依赖Rust 图形项目的典型坑Rust 做图形 / 仿真类项目工程上的坑和别的语言不太一样。首先是依赖选择图形相关的 crate 生态更新快版本之间的 API 变动也大锁版本这件事必须做否则今天能跑的代码下周就编不过。建议把依赖版本写死在配置文件里并且在文档里记录每个关键依赖的用途避免后来者一脸懵。其次是数值计算的确定性。这个实验的核心是横向对比如果同一个模型同一段代码跑两次结果不一样那对比就失去意义。浮点运算在不同平台、不同优化级别下可能有细微差异颜料扩散这种迭代计算会把差异放大。稳妥的做法是固定随机种子、固定迭代顺序、避免依赖并行归约的不确定顺序。第三是性能与可调试性的平衡。物理仿真迭代慢75 幅画如果每幅都要跑几分钟整个实验周期会拉得很长。但过度优化比如上 GPU会牺牲可读性和可调试性。我的经验是先用最直白的实现跑通全流程确认输出合理再针对瓶颈优化。瓶颈往往不在你以为的地方——可能是渲染合成而不是物理步进。5.2 任务下发与结果回收把 12 个模型的输出管起来12 个模型、每个可能多轮、每轮一段代码这个量级手工管会乱。必须有脚本化的流程统一的任务模板、统一的 API 文档拼接、统一的代码落盘命名、统一的执行与渲染。这里有个特别实际的坑模型生成的代码不一定能跑。语法错误、调用了不存在的 API、参数类型不对、死循环全都会遇到。所以执行环节必须有沙箱和超时保护一段代码跑超过 N 秒直接掐掉记录为失败样本。失败样本本身也是数据——一个连 API 都用不对的模型和画得不好是两回事要分开统计。回收结果时我建议同时保存三样东西原始代码、执行日志、渲染图。只看图会丢失过程信息只看代码又难以直观判断效果三者对齐才能做完整分析。文件命名上把模型标识、轮次、状态都编进去后面做统计和对图会省大量时间。5.3 参数标定怎么确认引擎本身是“讲道理”的在拿它去测模型之前必须先确认引擎自身的行为符合预期。这一步最容易被跳过但跳过之后所有结论都不可信——你测到的可能是引擎的 bug而不是模型的先验。标定的方法是人工写几段基准代码一段只铺底色一段画单一方向的平行笔触一段测试湿画法融合一段测试载墨量递减。看输出是否符合直觉平行笔触应该有方向感湿画法应该有晕染载墨量递减应该越画越淡。如果基准都对不上先修引擎别急着上模型。注意基准代码同时也是给模型的“示例”。示例的质量直接影响模型输出的质量。一份能跑、简洁、有注释的示例比十页文字文档有用。但示例也不能太完整太完整模型会照抄反而测不出它自己的东西。这个度要拿捏给骨架不给成品。5.4 从代码到画面的可复现链路一个完整的实验链路应该是任务描述 API 文档 → 模型生成代码 → 沙箱执行 → 事件流记录 → 渲染输出 → 归档。这条链路上每一环都要可复现同样的输入、同样的种子、同样的引擎版本必须得到同样的画。可复现性不只是为了严谨更是为了回溯分析。当你发现某幅画特别有意思想搞清楚它是怎么画出来的你需要能重放它的事件流。事件流里应该记录每一步的完整参数笔刷路径、每点压强、载墨量、落笔时刻的湿度。有了这些你可以把一幅画拆成时间轴看模型的“作画节奏”。我个人体会是归档结构的设计比代码本身更影响长期效率。一开始图省事把东西都堆在一个目录里等到要对比 12 个模型的某一类行为时就得写一堆临时脚本去捞。不如一开始就按“模型 / 主题 / 轮次”分好目录元数据用统一的 JSON 记录后面无论做统计还是做展示都顺手。6. 常见问题与排查实录6.1 画面糊成一团先查湿度衰减再查载墨量这是这类项目最高频的问题。画出来一片混沌看不出笔触也看不出结构。排查顺序应该是先看湿度衰减是不是太慢导致所有笔触一直互相污染再看载墨量是不是普遍太高每一笔都覆盖大面积最后看笔刷尺寸是不是单调缺少大小对比。一个快速验证方法是画三条平行笔触间隔逐渐拉大看它们之间的边界能否保住。保不住就是湿度问题边界太硬就是衰减太快。这个“三笔测试”几乎能定位大部分糊图问题。6.2 笔触像贴图检查落笔是否被拆成了路径如果画出来的笔触边缘完美、颜色均匀、没有任何拖拽感和飞白那八成是笔刷被实现成了硬边蒙版落笔没有拆成沿路径的采样点。油画笔触的魅力在于不均匀——压强变化、颜料耗尽、布纹干扰这些都会让笔触带上手工感。全都没了就是贴图。修复方向是把落笔改成路径采样并且让压强和载墨量沿路径变化。哪怕只是加一个简单的线性递减视觉效果都会立刻“活”过来。6.3 模型代码跑不通分类统计比逐个 debug 有效12 个模型里总有那么几个生成的代码跑不起来。逐个 debug 效率极低正确做法是按错误类型分类统计语法错误、API 误用、类型不匹配、运行时异常、超时。统计完你会发现规律——某类模型特别容易 API 误用可能是它对文档里参数顺序的理解有偏差某类模型特别容易超时可能是它倾向于写大循环铺满画布。这个统计本身就是有价值的评测结果。一个模型“画得一般但代码全跑通”和一个模型“偶尔画出神作但一半代码跑不通”在工程可用性上是完全不同的两个档次。问题现象最可能的原因排查动作画面整体发灰发浑湿度衰减过慢笔触持续互溶缩短衰减步数做三笔测试笔触边缘过于锐利湿度衰减过快或未启用混合放缓衰减检查混合半径颜色越叠越脏未做颜料减色混合用了加法混合改用减色 / 透射式混合大面积空白无内容模型只画局部未铺底检查代码是否有全画布级调用输出每次都不一样随机种子未固定或温度过高固定种子降低采样温度执行超时模型写了遍历全网格的嵌套循环加超时保护限制迭代上限6.4 颜色越叠越脏减色混合这件事必须较真油画颜料是减色的蓝加黄出绿这是颜料吸收光谱的结果。如果引擎用 RGB 加法混合去模拟颜料叠加结果会越来越亮、越来越脏完全不像油画。正确做法是在颜料层用减色模型比如基于吸收系数的混合或者至少在混合时做一次色彩空间转换。这个问题在模型写代码时也会被放大如果模型习惯用高透明度叠加多层加法混合下画面会迅速“曝掉”。修好混合模型后很多“模型画得脏”的结论可能要推翻——脏的可能是引擎不是模型。6.5 对比不公平温度、长度、重试次数都要对齐最后这个坑最隐蔽也最影响结论。两个模型对比一个跑低温一次成稿一个跑高温重试五次挑最好的那比出来的不是模型能力是筛选力度。横向评测的铁律是采样参数一致、重试策略一致、代码长度上限一致。做不到这三点结论就只能当参考不能当定论。我自己的做法是给每个模型固定同一个温度和同一个最大长度失败不重试直接记失败这样虽然会低估某些模型的真实水平但保证了对比的公平性。真要展示模型的“上限”另开一组实验明确标注是“多轮择优”和公平对比分开呈现。7. 这套框架还能怎么用几条我实际试过的延展方向玩法上这套东西其实很通用不局限于油画。换一套底层引擎同样的“模型写代码驱动”框架可以直接迁移到水墨把湿度换成宣纸吸水性、把笔刷换成毛笔的提按、水彩更强的扩散和留白、甚至版画刻刀的走向和排线。核心不变把媒介的物理约束写成 API让模型在约束里做选择然后从代码里读它的选择。另一个我觉得很有价值的方向是把事件流做成可视化。一幅画是结果事件流是过程。如果把落笔顺序做成时间轴回放你能直观看到模型是“从整体到局部”还是“从局部到整体”是“一层层叠”还是“一笔笔画”。这种可视化对分析模型行为比静态图有用得多也更适合分享。还有就是把对比维度做细。现在比的是“画得好不好”这个太粗。可以拆成构图完整度、笔触意图性、湿度利用率、纠错行为、代码结构清晰度。每个维度单独打分最后形成一个多维画像。这样做出来的分析比一句“某模型画得最好”有价值得多也更接近这个项目想说的“审美先验”——先验不是一个总分是一组偏好。最后分享一个我在做类似对比时的教训别急着下“谁审美更好”的结论。审美本来就主观模型的输出又受提示词、采样、引擎状态多重影响。更有意义的是描述差异而不是排名——A 模型倾向铺底重层次B 模型倾向直接画主体C 模型爱用细碎笔触堆细节这些都是客观的、可复现的观察比排名稳得多也更有讨论空间。真正值钱的不是那 75 幅画里哪幅最好看而是从这 75 幅画里能读出 12 种不同的“作画习惯”以及这些习惯背后模型对“画一幅画”这件事到底理解到了哪一层。