问题描述
这篇帖子是 CF 素材提取的第二期,主要介绍如何将穿越火线中的 DTX 文件转换为 PNG 文件。
完整代码的链接:Python代码
DTX 文件是穿越火线中的纹理文件格式,为了方便预览和在其他地方使用,需要转换到 PNG 格式。网络上能找到用于转换的 Adobe Photoshop CS4 插件,但是用起来很不方便,只能一个个打开转换,因此本篇帖子介绍使用 Python 脚本批量转换 DTX 文件到 PNG。
注意,这个程序只能做 DTX 到 PNG 的转换,不做反向的转换。如果有这方面的需要,请自行使用 Photoshop 插件来完成。
预处理
CF 从 REZ 当中提取出来的 DTX 文件,都经过 LZMA 算法的压缩,因此使用 Python 的 lzma 库,来做文件读取的操作。
为了兼容性考虑,这里先用文件的头部 4 字节来判断文件是否被压缩,如果是压缩过的文件,则使用 lzma 库来读取文件,否则直接读取文件。
if open(path, "rb").read(4) == b"\x5D\x00\x00\x00":
self.fStream = lzma.open(path, "rb")
else:
self.fStream = open(path, "rb")
读取文件头
DTX 文件的头部是 164 字节的文件头,具体格式请看代码定义:
class BPPIdent(Enum):
BPP_8P = 0 # 8 bit palettized
BPP_8 = 1 # 8 bit RGB
BPP_16 = 2
BPP_32 = 3
BPP_S3TC_DXT1 = 4
BPP_S3TC_DXT3 = 5
BPP_S3TC_DXT5 = 6
BPP_32P = 7 # this was added for true color palette support
BPP_24 = 8
class DtxHeader:
# Accessors.
def __init__(self, hdr: bytes):
# Define the structure format
# Note: 'I' for uint32, 'i' for int32, 'H' for uint16, 's' for string
# The union is not directly represented in struct format, so we handle it separately
struct_format = 'IiHHHHii12s128s'
# Unpack the data
unpacked_data = struct.unpack(struct_format, hdr)
# Extract the values
self.m_ResType, \
self.m_Version, \
self.m_BaseWidth, \
self.m_BaseHeight, \
self.m_nMipmaps, \
self.m_nSections, \
self.m_IFlags, \
self.m_UserFlags, \
self.m_Extra, \
self.m_CommandString = unpacked_data
# Convert the extra data to a list of uint8
self.m_Extra = list(self.m_Extra)
def get_bpp_ident(self):
return self.m_Extra[2] if self.m_Extra[2] != 0 else BPPIdent.BPP_32
这当中会用到的是:
m_BaseWidth
:纹理的宽度m_BaseHeight
:纹理的高度get_bpp_ident
:这是最重要的,用来判断纹理的格式m_nMipmaps
:纹理的 mipmap 数量(大于一的话,会有 1/2 1/4 … 的多级缩略图, 我们只提取第一个最高分辨率的)
根据类型分别解析像素数据
BPP_32
BPP_32 是最简单的格式,直接读取像素数据,然后写入到 PNG 文件当中即可。注意,DTX 文件中的 BPP_32 格式,是 BGRA 格式,需要转换成 RGBA 格式。
def _read_bpp_32(self):
for x in range(self.height):
for y in range(self.width):
b, g, r, a = struct.unpack('<BBBB', self.fStream.read(4))
self._pixels[x, y] = [r, g, b, a]
BPP_S3TC_DXT1
DXTn 的这三种格式,请参考S3_Texture_Compression。
BPP_S3TC_DXT1 是一种压缩格式,又称 BC1 (Block Compressed 1),每个 block 代表 4x4 个像素。
读取颜色值
首先我们需要处理颜色的压缩,DXT1 使用 RGB565 格式来存储颜色,我们需要将其转换为 RGBA 格式,透明度为 255(不透明)。
def rgb565_to_rgba(rgb565):
r = ((rgb565 >> 11) & 0x1F) << 3 # Extract and scale red component (5 bits to 8)
g = ((rgb565 >> 5) & 0x3F) << 2 # Extract and scale green component (6 bits to 8)
b = (rgb565 & 0x1F) << 3 # Extract and scale blue component (5 bits to 8)
return np.array((r, g, b, 255)) # Alpha is always 255 for DXT1
读取数据块
接下来根据 DXT1 的 BC1 格式,读取每个 block 的数据,然后解压出每个像素的颜色值。
每个数据块 8 字节(64 位),前 4 字节是颜色 0 和颜色 1 各 16 位,后 4 字节是 4x4 像素的索引值,每个像素 2 位。 根据颜色 0 和颜色 1 的值,有两种差值方法,可以计算出中间的 2 个颜色值,共 4 个颜色,然后根据每个像素的索引值,解压出每个像素的颜色值。
def decompress_bc1_block(block:bytes, alpha:bool=True):
# First 2+2 bytes: color0 and color1 (16-bit RGB565)
c0, c1 = struct.unpack('<HH', block[0:4])
# Decode colors into RGBA format
colors = [rgb565_to_rgba(c0), rgb565_to_rgba(c1)]
# Intermediate colors
if c0 > c1 or not alpha:
colors.append((colors[0] * 2 + colors[1]) / 3)
colors.append((colors[0] + colors[1] * 2) / 3)
else:
colors.append((colors[0] + colors[1]) / 2)
colors.append(np.array([0, 0, 0, 0]))
# Next 4 bytes: pixel indices (2 bits per pixel for 4x4 block)
indices = struct.unpack('<L', block[4:8])[0]
pixel_data = []
for bit_offset in range(0, 32, 2):
pixel_index = (indices >> bit_offset) & 0x03
pixel_data.append(colors[pixel_index])
return np.array(pixel_data, dtype=np.uint8).reshape((4, 4, 4))
拼装完整的图像
按块复制粘贴到完整的图像中即可。
def _read_bpp_s3tc_dxt1(self):
block_size = 8 # Each BC1 block is 8 bytes
block_dim = 4 # Each BC1 block is 4x4 pixels
for block_y in range(0, self.height, block_dim):
for block_x in range(0, self.width, block_dim):
# Calculate the offset of the current block in the texture data
block = self.fStream.read(block_size)
# Decompress the block
self._pixels[
block_y: block_y + block_dim,
block_x: block_x + block_dim
] = decompress_bc1_block(block)
BPP_S3TC_DXT3
BPP_S3TC_DXT3 是一种压缩格式,又称 BC2 (Block Compressed 2),每个 block 代表 4x4 个像素。每个 block 包含 2 部分,首先是一个 4x4 的 alpha 透明度值,每个像素对应 4 位,然后是 8 字节的 BC1 数据块。
读取数据块
接下来根据 DXT3 的 BC2 格式,读取每个 block 的数据,然后解压出每个像素的颜色值。
前 8 个字节,每 4 位代表一个像素的透明度值。
接下来是 8 字节的 BC1 数据块,解压出每个像素的颜色值。然后合并透明度和颜色值,得到每个像素的 RGBA 值。
def decompress_bc2_block(block:bytes):
# BC2 block contains the same color data as BC1, but with an additional 8-byte alpha block
alpha_block, color_block = block[0:8], block[8:16]
# 8 bytes (4 bits per pixel for 4x4 block)
alpha_bits = struct.unpack("<Q", alpha_block)[0]
alpha_data = [((alpha_bits >> i) & 0x0F) << 4 for i in range(0, 64, 4)]
# Apply alpha data to color block
pixel_data = decompress_bc1_block(color_block, alpha=False)
pixel_data[:, :, 3] = np.array(alpha_data, dtype=np.uint8).reshape((4, 4))
return pixel_data
拼装完整的图像
按块复制粘贴到完整的图像中即可。
def _read_bpp_s3tc_dxt3(self):
block_size = 16 # Each BC2 block is 8 bytes
block_dim = 4 # Each BC2 block is 4x4 pixels
for block_y in range(0, self.height, block_dim):
for block_x in range(0, self.width, block_dim):
# Calculate the offset of the current block in the texture data
block = self.fStream.read(block_size)
# Decompress the block
self._pixels[
block_y: block_y + block_dim,
block_x: block_x + block_dim
] = decompress_bc2_block(block)
BPP_S3TC_DXT5
BPP_S3TC_DXT5 是一种压缩格式,又称 BC3 (Block Compressed 3),每个 block 代表 4x4 个像素。每个 block 包含 2 部分,首先是一个 4x4 的透明度数据块,然后是 8 字节的 BC1 数据块。
读取数据块
接下来根据 DXT5 的 BC3 格式,读取每个 block 的数据,然后解压出每个像素的颜色值。
前 8 个字节使用类似 BC1 的插值索引方案,透明度数据块 8 字节(64 位),前 2 字节是透明度 0 和透明度 1,各 8 位,后 6 字节是 4x4 像素的索引值,每个像素 3 位。 根据透明度 0 和透明度 1 的值,有两种差值方法,可以计算出中间的 6 个透明度值,共 8 个透明度,然后根据每个像素的索引值,解压出每个像素的透明度值。
接下来是 8 字节的 BC1 数据块,解压出每个像素的颜色值。然后合并透明度和颜色值,得到每个像素的 RGBA 值。
def decompress_bc3_block(block:bytes):
# BC3 block contains the same color data as BC1, but with an additional 8-byte alpha block
alpha_block, color_block = block[0:8], block[8:16]
# First 1+1 bytes: alpha1 and alpha2 (8-bit)
a0, a1 = struct.unpack('<BB', alpha_block[0:2])
alphas = [a0, a1]
# Intermediate alphas
if a0 > a1:
for i in range(1, 7):
alphas.append((a0 * (7 - i) + a1 * i) // 7)
else:
for i in range(1, 5):
alphas.append((a0 * (5 - i) + a1 * i) // 5)
alphas.extend([0, 255])
# Next 6 bytes: pixel indices (3 bits per pixel for 4x4 block)
## Pad with 2 zero bytes to make it 8 bytes
indices = struct.unpack('<Q', alpha_block[2:8] + b'\x00\x00')[0]
alpha_data = []
for bit_offset in range(0, 48, 3):
pixel_index = (indices >> bit_offset) & 0x07
alpha_data.append(alphas[pixel_index])
# Apply alpha data to color block
pixel_data = decompress_bc1_block(color_block, alpha=False)
pixel_data[:, :, 3] = np.array(alpha_data, dtype=np.uint8).reshape((4, 4))
return pixel_data
拼装完整的图像
按块复制粘贴到完整的图像中即可。
def _read_bpp_s3tc_dxt5(self):
block_size = 16 # Each BC3 block is 8 bytes
block_dim = 4 # Each BC3 block is 4x4 pixels
for block_y in range(0, self.height, block_dim):
for block_x in range(0, self.width, block_dim):
# Calculate the offset of the current block in the texture data
block = self.fStream.read(block_size)
# Decompress the block
self._pixels[
block_y: block_y + block_dim,
block_x: block_x + block_dim
] = decompress_bc3_block(block)
其他格式
CF 目前没用到,我也就不写了。
类封装
接下来就是写亿点点代码,把这些东西串起来,封装成一个类。详细代码可下载,我这里只写调用方法:
dtx = DtxImage(filename)
dtx.image.save(filename.rsplit(".", maxsplit=1)[0] + ".png")