IMX6U-Game/tools/pack_sprite_atlas.py

124 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
将多个独立 sprite PNG 按 JSON 配置打包为 sprite atlas生成 C++ header。
用法:
python tools/pack_sprite_atlas.py config.json output.h
JSON 格式:
{
"atlas_width": 512,
"atlas_height": 512,
"sprites": [
{ "name": "player_idle_0", "file": "player_idle_0.png" },
{ "name": "player_idle_1", "file": "player_idle_1.png" },
...
]
}
- atlas_width / atlas_height: 输出 atlas 尺寸
- sprites: 按顺序排列的 sprite 列表
- 每个 sprite 保持原始尺寸,从左到右、从上到下排列
- 生成的 header 包含每个 sprite 的子矩形信息
"""
import json
import os
import sys
from PIL import Image
def pack(config_path: str, output_path: str):
config_dir = os.path.dirname(os.path.abspath(config_path))
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
atlas_w = cfg["atlas_width"]
atlas_h = cfg["atlas_height"]
sprites = cfg["sprites"]
atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
regions = []
cursor_x = 0
cursor_y = 0
row_height = 0
for s in sprites:
png_path = os.path.join(config_dir, s["file"])
img = Image.open(png_path).convert("RGBA")
w, h = img.size
if cursor_x + w > atlas_w:
cursor_x = 0
cursor_y += row_height
row_height = 0
if cursor_y + h > atlas_h:
print(f"错误: atlas 空间不足sprite '{s['name']}' 放不下")
sys.exit(1)
atlas.paste(img, (cursor_x, cursor_y))
regions.append({
"name": s["name"],
"x": cursor_x,
"y": cursor_y,
"w": w,
"h": h,
})
cursor_x += w
row_height = max(row_height, h)
var_name = os.path.splitext(os.path.basename(output_path))[0]
pixels = list(atlas.getdata())
packed = []
for r, g, b, a in pixels:
packed.append(
((r >> 3) << 11)
| ((g >> 3) << 6)
| ((b >> 3) << 1)
| (1 if a else 0)
)
with open(output_path, "w", encoding="utf-8") as f:
f.write("// Auto-generated by tools/pack_sprite_atlas.py\n")
f.write(f"#pragma once\n#include <cstdint>\n#include \"Image.h\"\n#include \"Sprite.h\"\n\n")
f.write(f"namespace {var_name} {{\n\n")
f.write(f"static const int32_t atlas_width = {atlas_w};\n")
f.write(f"static const int32_t atlas_height = {atlas_h};\n\n")
f.write(f"static const uint16_t atlas_pixels[] = {{\n")
for i in range(0, len(packed), 16):
chunk = packed[i : i + 16]
line = ", ".join(f"0x{p:04X}" for p in chunk)
f.write(f" {line},\n")
f.write("};\n\n")
f.write(f"static const RenderData::Image atlas_image(atlas_pixels, atlas_width, atlas_height);\n\n")
for r in regions:
f.write(f"static const RenderData::Sprite spr_{r['name']}("
f"&atlas_image, {r['x']}, {r['y']}, {r['w']}, {r['h']});\n")
f.write(f"\n}} // namespace {var_name}\n")
print(f"已生成: {output_path}")
print(f" atlas: {atlas_w}x{atlas_h}, {len(regions)} sprites")
for r in regions:
print(f" {r['name']}: ({r['x']},{r['y']}) {r['w']}x{r['h']}")
def main():
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} config.json output.h")
sys.exit(1)
pack(sys.argv[1], sys.argv[2])
if __name__ == "__main__":
main()