#!/usr/bin/env python3 """将地块贴图 .meta 设为 sprite-frame,对齐 Unity pivot,按透明边裁剪以贴满格子。""" from __future__ import annotations import argparse import json import re import struct import uuid from pathlib import Path TILE_GLOBS = [ "assets/resources/textures/*/Baseblock.png", "assets/resources/textures/*/JumpBlock.png", "assets/resources/textures/*/WallBlock.png", "assets/resources/textures/*/Decor23.png", "assets/resources/textures/*/kuai11.png", "assets/resources/textures/*/素材切图-23.png", "assets/resources/textures/*/素材切图2-23.png", ] COCOS_TO_UNITY = { "default": "Level", "silu": "silu", "snow": "snow", "sanxing": "sanxing", "chinese": "Chinese", "numMan": "numMan", "redarmy": "redArmy", "redArmy": "redArmy", } def png_size(path: Path) -> tuple[int, int]: with path.open("rb") as f: f.read(16) return struct.unpack(">II", f.read(8)) def alpha_bbox(png_path: Path) -> tuple[int, int, int, int] | None: try: from PIL import Image except ImportError: return None try: img = Image.open(png_path).convert("RGBA") box = img.getbbox() if not box: return None return box except Exception: return None def read_unity_pivot(unity_tex: Path, theme_folder: str, tile_name: str) -> tuple[float, float]: theme_dir = unity_tex if theme_folder == "Level" else unity_tex / theme_folder meta_path = theme_dir / f"{tile_name}.png.meta" if not meta_path.is_file() and tile_name == "Decor23": meta_path = theme_dir / "素材切图-23.png.meta" if not meta_path.is_file(): return 0.5, 0.92 text = meta_path.read_text(encoding="utf-8") m = re.search(r"spritePivot:\s*\{x:\s*([^,]+),\s*y:\s*([^}]+)\}", text) if not m: return 0.5, 0.92 return float(m.group(1)), float(m.group(2)) def resolve_trim( png_path: Path, full_w: int, full_h: int, pivot_x: float, pivot_y: float, ) -> dict: """按透明边裁剪,并换算 Unity pivot 到裁剪后 sprite。""" bbox = alpha_bbox(png_path) if not bbox: return { "trimType": "none", "trimX": 0, "trimY": 0, "width": full_w, "height": full_h, "rawWidth": full_w, "rawHeight": full_h, "offsetX": 0, "offsetY": 0, "pivotX": pivot_x, "pivotY": pivot_y, } left, top, right, bottom = bbox trim_w = right - left trim_h = bottom - top if trim_w <= 0 or trim_h <= 0 or (trim_w >= full_w and trim_h >= full_h): return { "trimType": "none", "trimX": 0, "trimY": 0, "width": full_w, "height": full_h, "rawWidth": full_w, "rawHeight": full_h, "offsetX": 0, "offsetY": 0, "pivotX": pivot_x, "pivotY": pivot_y, } pivot_bottom = pivot_y * full_h trim_bottom = full_h - bottom pivot_bottom_trim = pivot_bottom - trim_bottom new_pivot_y = max(0.0, min(1.0, pivot_bottom_trim / trim_h)) new_pivot_x = max(0.0, min(1.0, (pivot_x * full_w - left) / trim_w)) return { "trimType": "custom", "trimX": left, "trimY": top, "width": trim_w, "height": trim_h, "rawWidth": full_w, "rawHeight": full_h, "offsetX": 0, "offsetY": 0, "pivotX": round(new_pivot_x, 4), "pivotY": round(new_pivot_y, 4), } def ensure_image_meta(meta_path: Path, display_name: str) -> None: if meta_path.is_file(): return base_uuid = str(uuid.uuid4()) meta_path.write_text( json.dumps( { "ver": "1.0.27", "importer": "image", "imported": False, "uuid": base_uuid, "files": [".json", ".png"], "subMetas": { "6c48a": { "importer": "texture", "uuid": f"{base_uuid}@6c48a", "displayName": display_name, "id": "6c48a", "name": "texture", "userData": { "imageUuidOrDatabaseUri": base_uuid, "isUuid": True, "visible": False, }, "ver": "1.0.22", "imported": False, "files": [".json"], "subMetas": {}, } }, "userData": {"type": "texture", "redirect": f"{base_uuid}@6c48a"}, }, indent=2, ), encoding="utf-8", ) def patch_meta(meta_path: Path, png_path: Path, trim: dict) -> None: ensure_image_meta(meta_path, meta_path.stem.replace(".png", "")) meta = json.loads(meta_path.read_text(encoding="utf-8")) base_uuid = meta["uuid"] tex_uuid = f"{base_uuid}@6c48a" sf_uuid = f"{base_uuid}@f9941" meta["userData"] = { "type": "sprite-frame", "redirect": tex_uuid, "hasAlpha": True, "fixAlphaTransparencyArtifacts": False, } meta["subMetas"]["f9941"] = { "ver": "1.0.12", "importer": "sprite-frame", "uuid": sf_uuid, "imported": True, "files": [".json"], "subMetas": {}, "userData": { "wrapModeS": "clamp-to-edge", "wrapModeT": "clamp-to-edge", "minfilter": "linear", "magfilter": "linear", "premultiplyAlpha": False, "generateMipmap": False, "anisotropy": 1, "trimType": trim["trimType"], "trimThreshold": 1, "rotated": False, "offsetX": trim["offsetX"], "offsetY": trim["offsetY"], "trimX": trim["trimX"], "trimY": trim["trimY"], "width": trim["width"], "height": trim["height"], "rawWidth": trim["rawWidth"], "rawHeight": trim["rawHeight"], "borderTop": 0, "borderBottom": 0, "borderLeft": 0, "borderRight": 0, "isUuid": True, "imageUuidOrDatabaseUri": tex_uuid, "atlasUuid": "", "mipfilter": "none", "packable": True, "pixelsToUnit": 100, "pivotX": trim["pivotX"], "pivotY": trim["pivotY"], "meshType": 0, }, "displayName": meta_path.stem, "id": "f9941", "name": "spriteFrame", } meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8") rel = meta_path.relative_to(meta_path.parents[4]) print( f" patched {rel} -> draw {trim['width']}x{trim['height']} " f"(raw {trim['rawWidth']}x{trim['rawHeight']}) pivot=({trim['pivotX']},{trim['pivotY']})" ) def collect_tile_pngs(root: Path, themes: set[str] | None) -> list[Path]: found: set[Path] = set() for pattern in TILE_GLOBS: for p in root.glob(pattern): if not p.is_file(): continue if themes: theme = p.parent.name if theme not in themes: continue found.add(p) return sorted(found) def main(): ap = argparse.ArgumentParser() ap.add_argument("--unity-root", help="Unity 项目根目录,用于读取 spritePivot") ap.add_argument("--themes", help="仅处理指定 Cocos 主题目录,逗号分隔,如 snow") args = ap.parse_args() root = Path(__file__).resolve().parent.parent theme_filter = {t.strip() for t in args.themes.split(",") if t.strip()} if args.themes else None unity_tex = Path(args.unity_root) / "Assets" / "Texture" if args.unity_root else None pngs = collect_tile_pngs(root, theme_filter) for png in pngs: meta = png.with_suffix(".png.meta") if not png.is_file(): print(f" skip missing {png.relative_to(root)}") continue w, h = png_size(png) tile_name = png.stem cocos_theme = png.parent.name if unity_tex and unity_tex.is_dir(): unity_folder = COCOS_TO_UNITY.get(cocos_theme, cocos_theme) pivot_x, pivot_y = read_unity_pivot(unity_tex, unity_folder, tile_name) else: pivot_x, pivot_y = 0.5, 0.92 trim = resolve_trim(png, w, h, pivot_x, pivot_y) patch_meta(meta, png, trim) print(f"Patched {len(pngs)} tile metas. 请在 Cocos Creator 中刷新 assets/resources/textures。") if __name__ == "__main__": main()