为 LightGame 提供素材转换工具和转换后的头文件
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
将多个独立 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()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
将多个独立 tile PNG 按 JSON 配置打包为 tile atlas,生成 C++ header。
|
||||
|
||||
用法:
|
||||
python tools/pack_tile_atlas.py config.json output.h
|
||||
|
||||
JSON 格式:
|
||||
{
|
||||
"tile_size": 32,
|
||||
"columns": 4,
|
||||
"tiles": [
|
||||
{ "id": 0, "file": "empty.png" },
|
||||
{ "id": 1, "file": "ground_top.png" },
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
- tile_size: 每个 tile 的像素尺寸(正方形)
|
||||
- columns: atlas 每行放几个 tile
|
||||
- tiles: 按 tile_id 排序的列表,id 从 0 开始连续
|
||||
- file: 相对于 config.json 所在目录的 PNG 路径
|
||||
- 未指定的 tile_id 自动填充透明
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
|
||||
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)
|
||||
|
||||
tile_size = cfg["tile_size"]
|
||||
columns = cfg["columns"]
|
||||
tiles = cfg["tiles"]
|
||||
|
||||
max_id = max(t["id"] for t in tiles)
|
||||
tile_count = max_id + 1
|
||||
rows = (tile_count + columns - 1) // columns
|
||||
|
||||
atlas_w = columns * tile_size
|
||||
atlas_h = rows * tile_size
|
||||
|
||||
atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
|
||||
|
||||
tile_map = {t["id"]: t for t in tiles}
|
||||
|
||||
for tid in range(tile_count):
|
||||
col = tid % columns
|
||||
row = tid // columns
|
||||
x = col * tile_size
|
||||
y = row * tile_size
|
||||
|
||||
if tid in tile_map:
|
||||
png_path = os.path.join(config_dir, tile_map[tid]["file"])
|
||||
img = Image.open(png_path).convert("RGBA")
|
||||
if img.size != (tile_size, tile_size):
|
||||
img = img.resize((tile_size, tile_size), Image.NEAREST)
|
||||
atlas.paste(img, (x, y))
|
||||
|
||||
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_tile_atlas.py\n")
|
||||
f.write(f"#pragma once\n#include <cstdint>\n#include \"Image.h\"\n\n")
|
||||
f.write(f"static const int32_t {var_name}_width = {atlas_w};\n")
|
||||
f.write(f"static const int32_t {var_name}_height = {atlas_h};\n")
|
||||
f.write(f"static const int32_t {var_name}_tile_size = {tile_size};\n")
|
||||
f.write(f"static const int32_t {var_name}_columns = {columns};\n\n")
|
||||
f.write(f"static const uint16_t {var_name}_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 {var_name}_image({var_name}_pixels, {var_name}_width, {var_name}_height);\n")
|
||||
|
||||
print(f"已生成: {output_path}")
|
||||
print(f" atlas: {atlas_w}x{atlas_h}, {tile_count} tiles ({columns}x{rows})")
|
||||
print(f" var: {var_name}")
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user