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>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user