TL;DR: 我们构建了一条管线,能将一行文字简介转化为八个朝向的成品、可受光照的64像素像素画——由PixelLab生成,成品和光照完全由我们自己的代码完成,并通过自动化测试进行逐字节验证。难点不在于生成,而在于你无法从生成的图像中恢复部件标识——测试帧中56%的纹素所呈现的颜色被两种或更多材质共用。解决办法是让调色板本身携带标识。 下方是每个阶段的真实图像,以及通往最终方案的三个被证伪的假设和六次失败的尝试。

最终成果,来自自动化测试截图(角色上没有任何手工修改的像素):

https://preview.redd.it/1d3ky52dmxnh1.png?width=960&format=png&auto=webp&s=a2144fe933267f07ee8190bc849097688ab61b88

背景:为什么这很困难

我们的引擎使用固定64色调色板,并且从第一天起就将其设为硬性约束:任何地方都不得出现调色板外的颜色。在其之上是一套命名色阶库——一系列相邻调色板条目的短梯级,描述一种材质从阴影到高光的变化(皮肤、布料、皮革、头发)。色阶之间的不重叠性在构建时由机器检查。

这带来一个其他所有功能依赖的特性:合规精灵的每个纹素都有一个地址。一个像素不仅仅是RGB——它是(色阶行,步数),比如"布料,从暗部起第三阶"。渲染器不采样固有色颜色,而是采样固有色以获取地址,然后遍历色阶。这使得光照效果确定且天然符合调色板规定。

但这同时也意味着生成的图像不能只是看起来正确——它必须是可寻址的。而这就是所有有趣问题发生的地方。

各个阶段

以下所有图像均由管线自己的离线工具从我们实际铸造的一个角色的已提交原始贴图生成。没有模拟图。

第一步——生成(PixelLab,无约束)

PixelLab的create-character-v3端点根据文字简述返回八个旋转方向图像。无调色板锁定,无共享放置,粗略的alpha通道。你得到的就是这样,看起来还一点也不像游戏:

管线以前使用的是PixelLab的旧版端点,因为那是唯一接受force_colors(一个将生成锁定到你的颜色字母表的调色板约束)的端点。这个锁定在我们旧设计中是承重结构(更多原因见下文),并且让我们付出了代价:我们故意在供应商最弱的端点上进行铸造。调色板从来不是问题——我们移除force_colors重新生成,输出几乎没变,19种不同颜色对比17-21种。问题出在端点上。

直接从PixelLab V3输出

第二步——适配

每个面片被裁剪到其alpha边界,水平居中,并底部对齐到一条共享的脚线——原始旋转图像都没有这些。轮廓线从alpha边缘自身推导,因此提供者颜色选择不能违反轮廓契约。此处的颜色仍然是未约束的:

脚总是接触同一点

第三步——细化+量化

两次处理:

  1. 粉末处理——对整个图像(每个面片,保留二进制alpha)进行反射式2×2形态学开运算/闭运算,加上孤立点清理。这是我们对一个社区像素画工具流行外观的重新实现:小特征刻意简化,噪点消失,从未握过铅笔的模型质感被去除。我们的第一个版本是一个遮罩变体,拒绝跨越部件边界。它原则上是正确的,经过测试,但看起来不对——外观恰恰依赖于跨越边界(帽子融入帽带,缝合细节被擦除)。验收图像在测试之前就告诉了我们这一点。
  2. 量化——每个不透明纹素被捕捉到最近的调色板条目(sRGB中的平方欧几里得距离),其他所有纹素置零。此步骤后,每个纹素都是具有已知地址的确切调色板条目。角色至此变成我们的。

复制来自 https:\/\/jenissimo.itch.io\/unfaker 的外观

第四步——编写法线

代码编写的、确定性的切线空间法线贴图:每种材质的每个连通区域根据角色获得高度剖面——冠部隆起、布料褶皱、其他平坦——编码到切线空间,带有最大倾斜上限(~30°)。这让方向光能照亮帽檐下侧。作为图像看起来奇怪;在光照房间中看起来正确:

确定性法线

第五步——光照

我们的光照契约:编写的图像是不变基线。 在固定的参考辐照度下,渲染的精灵必须逐字节等于编写的文件。在非中性光照下,光照只做一件事——将纹素移动到其自身色阶上的相邻步骤。从不重新着色,从不跳跃两步,从不接触轮廓。超过色阶暗端后,纹素取轮廓条目,因此黑暗房间中的精灵会闭合为剪影。

同一个人物正面,一半参考/中性/一半再亮,通过着色器遵循的相同CPU路径解析:

法线在明暗层次间选择;每个纹素处的辐照度(通过法线贴图)选择步骤。它看起来像手工放置的经典像素画阴影,因为它本质上就是调色板原生阴影,每帧精心选择。

低光 / 中光 / 高光

PC视图

手机视图

标识问题,以及为什么调色板拥有它

这部分耗费了三周时间。色阶通过颜色寻址纹素,因此角色的色阶必须不重叠——两种材质不能声称同一个调色板条目,否则烘焙会失败。在旧版端点下,force_colors使这变得非常简单:受约束的颜色就是标签。

v3不提供调色板锁定。那么能否从成品美术中推导出每个角色的色阶?我们在尝试之前测量了它:在一个测试帧中,在材质确定无疑的方块内——

颜色 纹素数 声称该颜色的材质
#7f708a 135 帽带、衬衫、短裤、运动鞋
#484a77 102 帽带、衬衫、短裤、运动鞋
#625565 99 衬衫、运动鞋
#92a984 92 帽带、衬衫

763个非轮廓纹素中的428个——56%——带有两种或更多材质使用的颜色。 当衬衫和短裤被涂成相同颜色时,没有颜色划分能给它们分配不同的行。色调桶、K均值、高斯混合模型、谱聚类、空间邻接图——全部同样失败,因为这是算术问题,不是度量缺陷。信息不在图像中。(第六次从成品美术中恢复标识的尝试失败。其他五次因更软性的原因失败。)

解决办法反转了问题:调色板自身的布局已经划分为十三个连续数值段——由调色板设计者在项目开始前数年编写。这些段成为第二个色阶库。量化后的美术通过其调色板条目所属的段来寻址;共享的灰色在所有地方一起变暗,这对于共享中性色是正确的。标识从人类编写的某物中读取,而不是从美术中恢复,也不向供应商索取。

回报:我们的编解码器、烘焙、法线编写、细化处理和着色器未作任何修改就处理了首批v3角色。在中性参考辐照度下,零纹素移动,通过CPU解析器验证。

六周前我们会告诉自己什么

  1. 自动化门控是必要的,但不能预测质量。 面片数量、单元大小、二进制alpha、调色板覆盖率、倾斜上限——我们测量所有这些,但它只能捕获无效美术。在一次铸造中,三个候选角色中有两个通过了所有检查但看起来有问题;一个在后向旋转中丢失了外套并变成了不同的人。审查顺序很重要:先看最差面片,再看次优,最后看主角镜头。
  2. 提示词是请求,不是约束。 我们要求浅金色头发和金色镶边;结果得到了银色头发和八个面片上总共十个纹素的镶边,而29%的角色位于"靴子"色阶上。任何你需要真正强制执行的东西,都必须通过代码在输出上强制实现,而不是在提示词中要求。
  3. 不要推断你能提供的东西。 六次从成品美术中恢复部件标识的尝试失败。成功的那一次从编写的来源——调色板自身的段——提供了标识。
  4. 无固定截图即无证据。管线中每个视觉声明都以固定光栅化器中的逐字节比较截图结束,而不是争论。我们的黄金测试套件有25个场景;接受的美术更改只移动恰好一个角色大小的方块(约2%帧像素),其余保持字节一致。

仍然存在的问题:复合角色(分层/可交换部件)——目前管线在每个角色处停止为一个网格,以上所有内容都不是关于它们的声明。

关于任何阶段的问题,欢迎提问。