Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
8.7 KiB
Python
281 lines
8.7 KiB
Python
#!/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()
|