最近在折腾魔兽世界的贴图提取,BLP 是暴雪自研的 2D 纹理容器,WOW 里角色皮肤、UI 图标、地形贴图全是这个格式。本文从二进制层面拆解 BLP2 的结构,最后给一个纯 Python 的 blp2png 转换 demo。
BLP2 是什么
BLP(Blizzard Layered Picture)是暴雪为旗下游戏设计的纹理容器格式,WOW 使用的版本为 BLP2(magic = "BLP2")。它本质上是一个带 mipmap 链的图片封装,支持三种编码方式:
| encoding 值 | 名称 | 说明 |
|---|---|---|
| 1 | Palettized | 256 色调色板 + 索引,类似 GIF |
| 2 | Compressed | S3TC/DXT 块压缩(DXT1/3/5) |
| 3 | BGRA | 原始 32 位像素,无压缩 |
配合 alphaBitDepth 和 alphaEncoding 两个字段,可以细分出 9 种实际子格式。
文件整体布局
1 | ┌─────────────────────────────────────────────┐ Offset 0 |
Palette 区域始终存在且固定 1024 字节,即使 DXT/BGRA 编码不使用它(此时全零填充)。
Header 结构(148 字节)
所有字段小端序(little-endian)。以下是 BLPConverter.h 中的原始定义:
1 | const char BLPID[4] = {'B', 'L', 'P', '2'}; |
字段偏移表
| Offset | Size | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | id |
Magic:'B','L','P','2' |
| 4 | 4 | version |
必须为 1 |
| 8 | 1 | encoding |
1=Palettized, 2=DXT, 3=BGRA |
| 9 | 1 | alphaBitDepth |
0 / 1 / 4 / 8 / 72 |
| 10 | 1 | alphaEncoding |
DXT 子类型标识 |
| 11 | 1 | hasMips |
0=无 mip 链, 1=有 |
| 12 | 4 | xResolution |
像素宽度 |
| 16 | 4 | yResolution |
像素高度 |
| 20 | 64 | mipOffsets[16] |
每级 mip 相对文件头的偏移 |
| 84 | 64 | mipSizes[16] |
每级 mip 的数据字节数 |
各字段详解
id(4 bytes)
文件标识。合法值为 "BLP2"(0x42 0x4C 0x50 0x32)。若读到 "PTCH" 说明这是补丁文件而非纹理,应直接跳过。其他任何值均视为非法文件。
version(uint32)
格式版本号,WOW 使用的 BLP2 固定为 1。不存在其他合法版本。
encoding(uint8)
决定像素数据的编码方式,是解析分支的第一判据:
| 值 | 枚举名 | 含义 |
|---|---|---|
| 0 | BLP_ENCODING_INVALID |
非法,不应出现 |
| 1 | BLP_ENCODING_PALETTIZED |
256 色调色板索引 |
| 2 | BLP_ENCODING_COMPRESSED |
S3TC/DXT 块压缩 |
| 3 | BLP_ENCODING_BGRA |
原始 32-bit BGRA 像素 |
alphaBitDepth(uint8)
Alpha 通道位深,含义随 encoding 变化:
- Palettized(encoding=1):0=无 alpha,1=1-bit,4=4-bit,8=完整 8-bit
- DXT(encoding=2):0=DXT1 不透明,1=DXT1 带 alpha,4/8 配合 alphaEncoding 区分 DXT3/DXT5
- 特殊值
72:见于部分 doodad 贴图,行为等同 DXT5(暴雪工具链 bug 遗留)
alphaEncoding(uint8)
仅在 DXT 模式下有意义,用于区分 DXT3 与 DXT5:
1→ DXT3(显式 4-bit alpha)7→ DXT5(插值 alpha)- Palettized 模式下写入经验值:A1 格式写
1,其余写8(读取时忽略)
hasMips(uint8)
是否包含 mipmap 链。0 表示只有 mip0(全尺寸),1 表示后续还有逐级减半的 mip 数据。即使为 0,mipOffsets[0] / mipSizes[0] 仍然有效。
xResolution / yResolution(uint32 × 2)
图像宽高(像素)。必须为非零值,通常为 2 的幂(WOW 贴图规范),但格式本身不强制。
mipOffsets[16](uint32 × 16)
每级 mip 数据相对文件起始的字节偏移。mip0 通常为 1172(= 148 header + 1024 palette)。未使用的槽位为 0。
mipSizes[16](uint32 × 16)
每级 mip 数据的字节数。与 mipOffsets 配对使用,data[offset : offset+size] 即为该级原始像素数据。未使用的槽位为 0。
Python struct 解包
1 | import struct |
格式串含义:< 小端,4s magic,I version,4B 四个 uint8 标志位,II 宽高,16I16I 偏移与大小数组。
格式分类矩阵
三个字段组合决定实际编码:
flowchart TB
A[encoding] -->|1| B[Palettized]
A -->|2| C[DXT Compressed]
A -->|3| D[Raw BGRA]
B --> B0[alphaBitDepth=0 → PAL_A0]
B --> B1[alphaBitDepth=1 → PAL_A1]
B --> B4[alphaBitDepth=4 → PAL_A4]
B --> B8[alphaBitDepth=8 → PAL_A8]
C --> C1[abd=0 → DXT1 不透明]
C --> C2[abd=1 → DXT1 1-bit alpha]
C --> C3[abd=4, ae=1 → DXT3]
C --> C4[abd=8, ae=1 → DXT3]
C --> C5[abd=8, ae=7 → DXT5]
C --> C6[abd=72 → DXT5 特殊情况]
完整对照表:
| encoding | alphaBitDepth | alphaEncoding | 格式 ID | 说明 |
|---|---|---|---|---|
| 1 | 0 | - | BLP_PAL_A0 | 调色板,无 alpha |
| 1 | 1 | - | BLP_PAL_A1 | 调色板,1-bit alpha |
| 1 | 4 | - | BLP_PAL_A4 | 调色板,4-bit alpha |
| 1 | 8 | - | BLP_PAL_A8 | 调色板,8-bit alpha |
| 2 | 0 | any | BLP_DXT1_A0 | DXT1 不透明 |
| 2 | 1 | any | BLP_DXT1_A1 | DXT1 带 1-bit alpha |
| 2 | 4 | 1 | BLP_DXT3 | DXT3 |
| 2 | 8 | 1 | BLP_DXT3 | DXT3 |
| 2 | 8 | 7 | BLP_DXT5 | DXT5 |
| 2 | 72 | any | BLP_DXT5 | DXT5(doodad 常见) |
| 3 | 8 | - | BLP_BGRA | 原始 BGRA |
Palette 格式
固定 256 条目,每条 4 字节,存储顺序为 B, G, R, x(x 为填充字节,无意义)。
读取时需要交换通道:
1 | raw = np.frombuffer(data, dtype=np.uint8, count=1024, offset=148) |
各编码的 Mip 数据布局
Palettized(encoding=1)
Mip0 数据由两部分拼接:
1 | ┌──────────────────────────────┬──────────────────────────┐ |
| alphaBitDepth | Alpha 数据 | 大小 |
|---|---|---|
| 0 | 无 | 0 |
| 1 | 位打包,每像素 1 bit | ⌈W×H / 8⌉ bytes |
| 4 | 半字节打包,每像素 4 bit(0-15 映射到 0-255) | ⌈W×H / 2⌉ bytes |
| 8 | 完整 alpha 平面 | W×H bytes |
DXT Compressed(encoding=2)
DXT 以 4×4 像素块为单位压缩:
| DXT 类型 | 块大小 | 特点 |
|---|---|---|
| DXT1 | 8 bytes | 2 个 RGB565 参考色 + 2-bit 索引表 |
| DXT3 | 16 bytes | 8B 显式 4-bit alpha + 8B 颜色块 |
| DXT5 | 16 bytes | 8B 插值 alpha + 8B 颜色块 |
Mip 数据大小公式:
1 | blocks_x = (width + 3) / 4 |
Raw BGRA(encoding=3)
最简单的情况,每像素 4 字节 B, G, R, A 顺序排列:
1 | size = width × height × 4 |
读取后交换 R/B 通道即得 RGBA。
DXT 解码原理
DXT1 颜色块(8 字节)
1 | Bytes 0-1: color0 (RGB565, little-endian) |
RGB565 → RGB888 转换:
1 | def c565_to_rgb(c: int) -> tuple[int, int, int]: |
颜色表生成规则:
- 若
color0 > color1:4 色不透明模式,插值出 2 个中间色 - 若
color0 <= color1:3 色 + 透明黑(alpha=0)
DXT3 Alpha(前 8 字节)
每像素 4 bit 显式 alpha,乘以 17 映射到 0-255:
1 | alpha[i] = ((block[i // 2] >> ((i % 2) * 4)) & 0xF) * 17 |
DXT5 Alpha(前 8 字节)
两个参考值 a0, a1 + 48-bit 索引表(每像素 3 bit):
a0 > a1:8 级线性插值a0 <= a1:6 级插值 + 强制 0 和 255 端点
解析流程
flowchart TB
A[读取文件字节] --> B{文件 >= 148B?}
B -->|No| ERR1[NotBlpError]
B -->|Yes| C[解析 Header]
C --> D{magic == BLP2?}
D -->|PTCH| ERR2[PatchFileError]
D -->|其他| ERR1
D -->|Yes| E{version == 1?}
E -->|No| ERR3[VersionError]
E -->|Yes| F[classify_format]
F --> G{encoding?}
G -->|1| H[读取 Palette + 索引 + Alpha]
G -->|2| I[DXT 解压缩]
G -->|3| J[BGRA 通道交换]
H --> K[ImageBuffer]
I --> K
J --> K
K --> L[输出 PNG]
Python blp2png Demo
以下是一个精简但完整的 BLP2 → PNG 转换脚本,覆盖 Palette / DXT1 / DXT3 / DXT5 / BGRA 全部格式。依赖 numpy 和 Pillow,DXT 解码为纯 Python 实现(无需 imagecodecs)。
1 | #!/usr/bin/env python3 |
使用方式
1 | pip install numpy Pillow |
运行效果
1 | $ python blp2png.py CharacterSkin00.blp |
小结
- BLP2 = 148B Header + 1024B Palette + N 级 Mip 数据,结构紧凑
- 编码由
encoding+alphaBitDepth+alphaEncoding三字段联合决定 - Palettized 本质是索引图 + 独立 alpha 平面(位/半字节/字节三种打包)
- DXT 以 4×4 块为单位,DXT1 8B/块、DXT3/5 16B/块
- Python 解析核心就是 struct 拆 header → numpy 操作像素 → Pillow 写 PNG
- 完整实现见
analysis/blpconv.py,可直接作为库使用:from blpconv import load_blp, blp_to_png