Files
AIGame/Tools/output/run_townsquare_pipeline.py
T
udxandCursor 9793d8581d Add TownSquare scene assets, resource production docs, and model pipeline updates.
Bring in plaza props/prefabs, ZhaoYun source GLB, design originals, and Hunyuan/Blender tooling used to produce them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 14:28:46 +08:00

194 lines
6.4 KiB
Python

"""TownSquare 分件:图生3D → 减面烘焙 → 安装到 Bundles/Scenes/Town/TownSquare/{name}/"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT / "Tools") not in sys.path:
sys.path.insert(0, str(ROOT / "Tools"))
PARTS = [
("ground", 800),
("shop", 1800),
("tree", 1500),
("lamp", 500),
("board", 800),
("fountain", 1800),
("bench", 700),
("bush", 500),
]
SCENE_TYPE = "Town"
SCENE_ID = "TownSquare"
TEX_SIZE = 1024
def _decode(raw: bytes | None) -> str:
if not raw:
return ""
return raw.decode("utf-8", errors="replace")
def run(cmd: list[str]) -> dict:
print(f"RUN {' '.join(cmd)}", flush=True)
env = dict(os.environ)
env["PYTHONIOENCODING"] = "utf-8"
proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, env=env)
out = _decode(proc.stdout).strip()
err = _decode(proc.stderr).strip()
if proc.returncode != 0:
raise RuntimeError(
f"exit {proc.returncode}\nstdout:\n{out[-4000:]}\nstderr:\n{err[-4000:]}"
)
last = None
for line in out.splitlines():
line = line.strip()
if line.startswith("{") and line.endswith("}"):
try:
last = json.loads(line)
except json.JSONDecodeError:
continue
if last is None and out.startswith("{"):
try:
last = json.loads(out)
except json.JSONDecodeError:
last = None
return last or {"ok": True, "raw": out[-2000:]}
def is_valid_glb(path: Path) -> bool:
from hunyuan_3d.glb_check import glb_textures_ok
ok, _ = glb_textures_ok(path)
return ok
def pick(files: list[Path], pred) -> Path | None:
for path in files:
if path.is_file() and pred(path.name.lower()):
return path
return None
def install_part(name: str, xpbr_dir: Path, concept: Path) -> Path:
dest = ROOT / "AIClient/Assets/Bundles/Scenes" / SCENE_TYPE / SCENE_ID / name
dest.mkdir(parents=True, exist_ok=True)
files = list(xpbr_dir.iterdir())
fbx = pick(files, lambda n: n.endswith(".fbx"))
base = pick(files, lambda n: n.endswith("_base.png") or n.endswith("_basemap.png"))
mix = pick(files, lambda n: n.endswith("_mix.png") or n.endswith("_mixmap.png"))
mat = pick(files, lambda n: n.endswith(".mat"))
if not fbx or not base:
raise FileNotFoundError(f"XPbr 不完整: {xpbr_dir} files={[p.name for p in files]}")
mapping = {
fbx: dest / f"{name}.fbx",
base: dest / f"{name}_base.png",
mix: dest / f"{name}_mix.png" if mix else None,
mat: dest / f"{name}.mat" if mat else None,
}
copied = []
for src, target in mapping.items():
if src is None or target is None:
continue
shutil.copy2(src, target)
meta = Path(str(src) + ".meta")
if meta.is_file():
shutil.copy2(meta, Path(str(target) + ".meta"))
copied.append(target.name)
print(json.dumps({"installed": str(dest), "files": copied, "design": str(concept)}, ensure_ascii=False), flush=True)
return dest
def main() -> int:
summary = []
for name, tris in PARTS:
img = ROOT / "Doc/Design/Scene/TownSquare" / f"{name}.png"
glb = ROOT / "Tools/output/townsquare_3d" / name / f"{name}.glb"
bake_out = ROOT / "Tools/output/townsquare_game" / name
glb.parent.mkdir(parents=True, exist_ok=True)
print(json.dumps({"event": "part_start", "name": name, "tris": tris}, ensure_ascii=False), flush=True)
if not is_valid_glb(glb):
if glb.is_file():
print(
json.dumps({"event": "hunyuan_regen", "name": name, "reason": "invalid_glb"}, ensure_ascii=False),
flush=True,
)
glb.unlink()
hunyuan = run(
[
sys.executable,
str(ROOT / "Tools/hunyuan_3d_generate.py"),
"-i",
str(img),
"-o",
str(glb),
"--faces",
"50k",
"--result-only",
"--quiet",
]
)
if hunyuan.get("ok") is False or hunyuan.get("status") not in (None, "succeeded"):
raise RuntimeError(f"hunyuan failed {name}: {hunyuan}")
if not is_valid_glb(glb):
raise RuntimeError(f"hunyuan produced invalid glb: {glb}")
else:
print(
json.dumps({"event": "hunyuan_skip", "name": name}, ensure_ascii=False),
flush=True,
)
xpbr_dir = bake_out / f"{name}_xpbr"
bake: dict = {}
has_xpbr = xpbr_dir.is_dir() and any(xpbr_dir.glob("*.fbx"))
if has_xpbr:
print(
json.dumps({"event": "bake_skip", "name": name}, ensure_ascii=False),
flush=True,
)
else:
bake = run(
[
sys.executable,
str(ROOT / "Tools/model_tools/model_lowpoly_bake.py"),
str(glb),
"-o",
str(bake_out),
"--tris",
str(tris),
"--size",
str(TEX_SIZE),
"--result-only",
"--quiet",
]
)
if bake.get("xpbr_dir"):
xpbr_dir = Path(bake["xpbr_dir"])
dest = install_part(name, xpbr_dir, img)
rec = {
"name": name,
"tris_target": tris,
"tris_after": bake.get("tris_after"),
"glb": str(glb.relative_to(ROOT)).replace("\\", "/"),
"dest": str(dest.relative_to(ROOT)).replace("\\", "/"),
}
summary.append(rec)
print(json.dumps({"event": "part_done", **rec}, ensure_ascii=False), flush=True)
out = ROOT / "Tools/output/townsquare_pipeline_summary.json"
out.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"ok": True, "summary": summary}, ensure_ascii=False), flush=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False), flush=True)
raise