动森二维码生成器:服装类(前篇)
在开发动森二维码生成器的时候,对于服装类设计的生成,一开始我并没有很好的想法。游戏中对贴图的是分块绘制的,并且有一个动态模型可以展示衣服绘制的效果。对比同类工具 Animal Crossing Pattern Tool(下称 ACPT),他们也提供了 3D 模型用于展示贴图效果。但要如何在 Aseprite 中实现展示 3D 模型,我并不是很有把握,毕竟这是一款用于绘制 2D 像素图的工具。另外如何给贴图分块,也需要深入分析。本文先记录一下贴图的划分,之后再写一篇分析 3D 模型的渲染。
通过分析 ACPT 的源码,我了解到服装类的贴图其实是由 6 个部分组成的:
- 32*32 的衬衣正面图(长袍上部正面)
- 32*32 的衬衣背面图(长袍上部背面)
- 32*16 的左袖图
- 32*16 的右袖图
- 32*16 的长袍正面下摆图
- 32*16 的长袍背面下摆图
这 6 个小图正好可以拼成一个 64*64 的大图。至于在绘制的时候如何摆放这 6 个部分,并没有限定,只要生成二维码的时候,图像二进制数据按上述组成部分顺序排列即可。不过我觉得 ACPT 的摆放比较不合理,它把长袍的上下部分开了,容易造成绘制上的困难(不容易对齐)。所以在我的版本里,我将长袍的上下部分放在了一起,这也是官方 APP 扫描图案后生成预览的布局。
由于数据量较大,需要生成四张版本 18 的二维码才能容纳。不过我使用的 LuaQRCode 开源库不支持这种扩展二维码。所以我通过阅读 ZXing 的实现方式,自己稍微魔改了一下。简单讲就是在每个二维码的头部放置 4+4+4+8 共 20 位元信息:
- 4 bits: 字面量 “3” 表示这是一组二维码中的一张;
- 4 bits: 当前二维码编号,由 0 开始;
- 4 bits: 该组二维码的最大编号,n 则表示该组二维码有 n+1 张;
- 8 bits: 二维码组编号,相同的编号表示同一组二维码,避免拼接到其它二维码。
在测试服装设计的时候,我使用了一张长宽 64*64 的模版图,由于暂时不支持 3D 模型预览,我还在 README 里用长篇大段介绍了如何在一张 64*64 像素的图上摆放衣服的各个部分:
跑个题。正是这个测试图让我发现了一个奇怪的 Bug:当调色盘中编号为 0 颜色被大块使用的时候(例如右上角),生成的二维码扫描后在色块上会出现规律的杂点。通过大量调试,最终确定是 LuaQRCode 的 Bug,在编码处理长串的 0 的时候,LuaQRCode 计算出了错误的纠错码。
于是在 github 上给 LuaQRCode 的作者提了 Issue。可惜作者表示这 N 前写的代码看起来有点陌生,不保证能修好。
等了两周没有音讯,于是我按耐不住自己研究起了二维码编码。还翻了一下之前提到的回形针的二维码的秘密。不过这个只是科普文,对具体的算法实现帮助不大。经过一番搜索,我在网上找到了一个非常棒的二维码生成算法的系列文章:QR Code Tutorial。文章非常细致了描述了二维码里的编码生成的具体步骤和使用到的技术:里德所罗门编码、加罗瓦域、二项式短除法等等。
通过 Review LuaQRCode 的源码,我发现它在处理加罗瓦域的编码以及二项式短除法(当被除数为 0 的时候)出现了边界问题。所幸不是什么大问题,很简单就修复了。增加了测试用例,并提交了 Pull Request,作者反馈很及时,当场就 Merge 了请求。
回到正题。贴图和二维码的生成都解决了,对于这个二维码生成器插件本身,功能已经很完整了。不过如果用户在绘制服装的时候,不能直观的看到自己绘制出来的效果,对体验来说是不及格的。我觉得要做至少也要做到及格线。而剩下的问题就是:如何在绘制的时候给用户提供一个直观的预览呢?
首先要有 3D 模型。我在 ACPT 的源码里找到了一些 gltf 格式的模型文件,不过看起来比较粗糙。不知道是自己创建的模型,还是从 Animal Crossing: New Leaf 游戏中导出来的。另外我还在 Models-Resource 找到了从 Animal Crossing: New Horizons 游戏中导出的高精度模型,可惜没有主角人物的模型,参考了一下最终我选了比较接近的猫咪村民的模型。
这个模型精度很高,不过它的贴图并不符合我上面设计的布局,没法直接用。幸好年初无聊的时候,稍微学习了一下 blender 3d 建模。于是把模型导入到 blender 中,删除不需要的部分,只留下衣服。然后对衣服和 UV Mapping 进行的重新布局,如下图。
虽然只有六种服装类型,却折腾了我一个晚上,真是体力活。有了模型、贴图坐标,只要把想个方法把它「画」出来,并配上设计好的 64*64 贴图,就可以用于展示绘制效果的吧。但要如何实现呢?我将另外写一篇博客来分享我的心路历程。