这类图混淆的不是像素值本身,而是像素块的空间排列关系。01_Atlas0.png 只是“砖块仓库”,真正决定原图长什么样的是旁边的元信息和运行时代码,不是 PNG 本身。
它和常见“图片加密”的区别是:
- 不是改颜色、改通道、改压缩算法
- 不是简单行列置乱后就结束
- 它把原图切成小块,加入边缘 padding,再打包进 atlas
- 最终显示时,靠一张索引表把这些块重新拼成完整 CG
这套格式的代码定义在 DicingTextures.cs 和 DicingTextureData.cs。
组成部分
一套完整资源通常由 4 部分组成:
- atlas PNG
比如 01_Atlas0.png。它存放被切碎后的图块。 - 元信息 .asset
比如 01.asset。这里面有块大小、padding、输出尺寸、块索引表。 - 运行时重组代码
游戏运行时按 C# 逻辑把 atlas 里的块重新拼成图。 - thumbnail
比如缩略图往往是正常图,用来预览,也能拿来做验证,但不是恢复所必需的。
关键字段
在 01.asset 里,你能看到这几个核心字段:
- cellSize
atlas 里每个物理格子的边长。这里是 64。 - padding
每个物理格子四周保留的边距。这里是 3。 - atlasTextures
这个资源会用到哪些 atlas 图。 - textureDataList
真正的每张 CG/差分定义列表。 - name
某个差分名,比如 01x10、01x11。 - atlasName
当前差分使用哪个 atlas。 - width / height
最终输出图尺寸。 - cellIndexList
最重要的字段。记录“输出第 N 块应该去 atlas 的哪一个块取图”。 - transparentIndex
某些块可直接视为透明并跳过。当前 01x10 是 -1,表示没用透明块索引。
它到底做了什么
这套格式的核心动作是:
- 把原图切成逻辑块
- 每个逻辑块不是直接裸存,而是放进 atlas 的物理格子里
- 物理格子比逻辑块大,因为四周带 padding
- 最终靠 cellIndexList 指挥每个逻辑块去哪个物理格子取内容
以你这份资源为例:
- cellSize = 64
- padding = 3
- 所以真正有效图像内容块大小是 64 - 3*2 = 58
- 01x10 的目标尺寸是 1920 x 1080
- 所以逻辑网格是:
ceil(1920 / 58) = 34 列
ceil(1080 / 58) = 19 行 - 总逻辑块数是 34 * 19 = 646
- 这正好和脚本列出来的 cells=646 一致
也就是说,一张 01x10 不是一整张图直接存下来,而是被拆成 646 个逻辑块,再按照索引表重组。
为什么要有 padding
padding 不是多余垃圾,而是图形工程里的常见做法。作用主要有:
- 防止双线性采样时串色
- 防止缩放或 mipmap 时读到隔壁块
- 保证拼起来时没有边缘接缝
所以恢复时必须:
- 不能整块复制 64x64
- 必须跳过四周各 3 像素
- 真正取的是中间的 58x58
- 边缘块还要按实际剩余宽高裁剪
如果你把 padding 也一起拷进去,图通常会有细边、接缝、重复边缘像素。
差分是怎么回事
你说“多个差分”,这类资源里的差分不是传统意义上的“底图 + 补丁层”。
它更像这样:
- 01.asset 里有很多条 textureDataList
- 比如 01x10、01x11、01x12……
- 每一条都有自己完整的 cellIndexList
- 每一条都能独立恢复成一张完整 CG
- 它们共享同一个 atlas,所以看起来像差分存储
好处是:
- 相同区域可以复用同一组物理块
- 只改动表情、眼睛、嘴、手等局部时,不必重复存整张图
- 存储体积会更小
所以这更接近“块级别复用”,而不是“图层叠加”。
坐标系为什么容易错
这是这类恢复里最容易踩坑的点。
代码在 DicingTextureData.cs 里直接把 atlas 坐标转成 Unity UV,没有做 1 - y 翻转。这说明它按 Unity 纹理坐标系解释,也就是:
- 原点在左下
- x 向右增大
- y 向上增大
所以逻辑块的遍历顺序是:
- 每行从左到右
- 行从下往上
但 PNG 文件本身是按左上角坐标读写的,所以恢复程序里要做一次坐标换算。这个如果做错,会出现:
-
图块上下倒序
-
人物位置怪异
-
和缩略图明显对不上
恢复公式
核心恢复逻辑可以概括成这几步:
effective = cellSize - 2 * padding
cols = ceil(width / effective)
rows = ceil(height / effective)
atlas_cols = ceil(atlas_width / cellSize)
对每个逻辑块 k:
col = k % cols
row = k // cols
atlas_index = cellIndexList[k]
src_cell_x = (atlas_index % atlas_cols) * cellSize + padding
src_cell_y = (atlas_index // atlas_cols) * cellSize + padding
copy_w = min(effective, width - col * effective)
copy_h = min(effective, height - row * effective)
从 atlas 取出 (src_cell_x, src_cell_y) 起始的有效区域
粘贴到输出图对应的逻辑块位置