""" 将多个独立 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 \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()