Files
AIGame/Tools/campguide_mixamo_kimodo.py

165 lines
6.7 KiB
Python

"""CampGuide after Studio: game FBX, Mixamo TokenRig (40-50 bones), Kimodo in-place idle and walk."""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STUDIO = Path(r"D:\UD\AI\XModel\LLM3DGraphics")
BLENDER = Path(r"D:\Tools\Blender\blender-5.0.0-windows-x64\blender.exe")
MESH = ROOT / "Tools/output/campguide_studio"
NAME = "CampGuideStudio"
UNITY = ROOT / "AIClient/Assets/Bundles/Characters/Human/CampGuideStudio"
RIG = MESH / "rig"
MOTIONS = [
("Idle", "a young boy stands in place and breathes gently, idle, feet planted, no walking"),
("Walk", "a young boy walks in place with alternating steps, no forward travel, gentle arm swing"),
]
CONVERT = r'''
import sys
from pathlib import Path
import bpy
from mathutils import Matrix
from math import pi
src, dst = [Path(p) for p in sys.argv[sys.argv.index("--") + 1:]]
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.fbx(filepath=str(src))
if any(o.type == "ARMATURE" for o in bpy.context.scene.objects):
raise SystemExit("expected a static mesh")
for obj in bpy.context.scene.objects:
if obj.parent is None:
obj.matrix_world = Matrix.Rotation(pi / 2, 4, "X") @ obj.matrix_world
dst.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.export_scene.gltf(filepath=str(dst), export_format="GLB", export_yup=True)
print("OK", dst)
'''
def joints(glb: Path) -> list[str]:
data = glb.read_bytes()
if data[:4] != b"glTF":
raise RuntimeError(f"not a glb: {glb}")
import struct
json_size = struct.unpack_from("<I", data, 12)[0]
document = json.loads(data[20:20 + json_size])
names = []
for skin in document.get("skins") or []:
for index in skin.get("joints") or []:
names.append(document["nodes"][index].get("name") or "")
return names
def run(cmd, log: Path, timeout: int) -> None:
log.parent.mkdir(parents=True, exist_ok=True)
with log.open("w", encoding="utf-8") as handle:
completed = subprocess.run(cmd, stdout=handle, stderr=subprocess.STDOUT, timeout=timeout)
if completed.returncode != 0:
tail = log.read_text(encoding="utf-8", errors="replace")[-2000:]
raise RuntimeError(f"command failed ({completed.returncode}): {tail}")
def export_static() -> Path:
if not (MESH / "model_ex.glb").is_file():
raise SystemExit("Studio mesh is not ready")
fbx = MESH / "static" / f"{NAME}.fbx"
if not fbx.is_file():
run(
[str(BLENDER), "-b", "-t", "1", "--factory-startup", "--python",
str(ROOT / "Tools/model_tools/bl_prepare_studio_trial.py"), "--", str(MESH), NAME],
MESH / "static_export.log", 180,
)
sys.path.insert(0, str(ROOT / "Tools/model_tools"))
from mt_run import run_blender_script
from png_compress import compress_dir_pngs
xpbr = MESH / "xpbr"
if not (xpbr / f"{NAME}_base.png").is_file():
run_blender_script(str(BLENDER), "bl_convert.py", [str(fbx), str(xpbr), "1024"], blender_args=("-t", "1"))
compress_dir_pngs(str(xpbr), suffixes=("_base.png", "_mix.png"))
return fbx
def rig_mixamo(fbx: Path) -> Path:
sys.path.insert(0, str(STUDIO))
from backend.motion_branches import apply_mixamo_profile
from backend.tokenrig_runner import run_tokenrig
RIG.mkdir(parents=True, exist_ok=True)
source = RIG / "source.glb"
if not source.is_file():
script = RIG / "_convert.py"
script.write_text(CONVERT, encoding="utf-8")
run([str(BLENDER), "-b", "--factory-startup", "--python", str(script), "--", str(fbx), str(source)],
RIG / "convert.log", 180)
ready = RIG / "mixamo_ready.glb"
# The default vroid sample stopped at a 23-bone body. rignet is the densest
# complete Mixamo sample this mitten-handed mesh produced.
chosen = RIG / "rignet.glb"
fbx = RIG / "mixamo_ready.fbx"
if chosen.is_file() and not (RIG / "skeleton.json").is_file():
if fbx.is_file():
fbx.unlink()
apply_mixamo_profile(chosen, RIG, RIG / "tokenrig.log")
elif not ready.is_file():
rigged = Path(run_tokenrig(source, RIG, RIG / "tokenrig.log", skeleton_template="mixamo"))
apply_mixamo_profile(rigged, RIG, RIG / "tokenrig.log")
names = joints(ready)
mixamo = [name for name in names if name.startswith("mixamorig:")]
report = {"joints": len(names), "mixamo": len(mixamo), "names": names}
(RIG / "skeleton.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
required = {"mixamorig:Hips", "mixamorig:Head", "mixamorig:LeftHand", "mixamorig:RightHand",
"mixamorig:LeftFoot", "mixamorig:RightFoot"}
if not required.issubset(names) or not 30 <= len(mixamo) <= 65:
raise SystemExit(f"Mixamo skeleton rejected: {len(mixamo)} mixamorig joints. See {RIG / 'skeleton.json'}")
if not (RIG / "mixamo_ready.fbx").is_file():
raise SystemExit("mixamo_ready.fbx was not exported")
return ready
def animate(ready: Path) -> None:
sys.path.insert(0, str(STUDIO))
from backend.kimodo_runner import submit_kimodo_job
for label, prompt in MOTIONS:
dest = RIG / label
if (dest / "animated.fbx").is_file():
continue
work = RIG / f"_work_{label}"
if work.exists():
shutil.rmtree(work)
work.mkdir()
submit_kimodo_job(rig_path=ready, prompt=prompt, directory=work, log_path=work / "kimodo.log", job_id=label)
dest.mkdir(parents=True, exist_ok=True)
for name in ("animated.fbx", "animated.glb", "kimodo_motion.bvh"):
shutil.copy2(work / name, dest / name)
def install() -> None:
for folder in (UNITY, UNITY / "Rigged", UNITY / "Anims"):
folder.mkdir(parents=True, exist_ok=True)
shutil.copy2(MESH / "xpbr" / f"{NAME}_base.png", UNITY / f"{NAME}_base.png")
shutil.copy2(MESH / "xpbr" / f"{NAME}_mix.png", UNITY / f"{NAME}_mix.png")
import zipfile
package = RIG / "mixamo_ready.fbx"
with zipfile.ZipFile(package) as archive:
archive.extract("model.fbx", UNITY / "Rigged")
(UNITY / "Rigged" / "model.fbx").replace(UNITY / "Rigged" / f"{NAME}_Rigged.fbx")
shutil.copy2(RIG / "Idle" / "animated.fbx", UNITY / "Anims" / f"{NAME}_Idle.fbx")
shutil.copy2(RIG / "Walk" / "animated.fbx", UNITY / "Anims" / f"{NAME}_Walk.fbx")
def main() -> int:
fbx = export_static()
ready = rig_mixamo(fbx)
animate(ready)
install()
skeleton = json.loads((RIG / "skeleton.json").read_text(encoding="utf-8"))
print(json.dumps({"ok": True, "mixamoJoints": skeleton["mixamo"], "unity": str(UNITY)}, ensure_ascii=False), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())