Spaces:
Sleeping
Milestones 9-12: UniRig, UE5 Export, Presets, Polish
Browse filesMilestone 9 — Stage 3 Auto-Rigging (stage3_rig.py):
- gradio_client → MohamedRashad/UniRig: sends GLB, downloads rigged.glb
- Same client pattern as TRELLIS.2 generation (no local GPU/weights needed)
- Handles rig_type + seed inputs; outputs saved to workspace/current/
Milestone 10 — Stage 4 UE5 Export (stage4_export.py):
- Bundles all workspace outputs into a UE5-ready zip:
SM_/SK_ mesh, LOD1/2, UCX_ collision, T_*_D/N/ORM textures + manifest.json
- Game-ready checklist warns before export (missing UVs, LODs, collision, etc.)
- engine != UE5 returns informative stub (Unity/Godot/Blender planned)
Milestone 11 — Presets (workspace/presets/*.json + app.py):
- ui_save_preset() now reads ALL tab UI values and writes real JSON
- 5 default presets committed to workspace/presets/
- Presets tab description updated
Milestone 12 — Polish:
- Game-ready checklist before export (already in handle_export)
- Presets tab fully wired; Save captures all Stage 1-4 settings
app.py: rig_btn → handle_auto_rig; ex_btn → handle_export;
pr_save → ui_save_preset (full inputs); stubs removed
- app.py +70 -17
- src/stages/stage3_rig.py +82 -0
- src/stages/stage4_export.py +131 -0
- workspace/presets/character_UE5_hero.json +38 -0
- workspace/presets/character_UE5_npc.json +38 -0
- workspace/presets/environment_UE5_background.json +38 -0
- workspace/presets/prop_UE5_hero.json +38 -0
- workspace/presets/prop_UE5_standard.json +38 -0
|
@@ -243,31 +243,70 @@ def run_post_process(
|
|
| 243 |
return "\n".join(log)
|
| 244 |
|
| 245 |
|
| 246 |
-
def
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
|
| 249 |
|
| 250 |
-
def
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
|
| 254 |
# ---------------------------------------------------------------------------
|
| 255 |
-
# Presets tab logic (
|
| 256 |
# ---------------------------------------------------------------------------
|
| 257 |
|
| 258 |
def ui_refresh_presets() -> gr.Dropdown:
|
| 259 |
return gr.Dropdown(choices=workspace.list_presets(), label="Saved presets")
|
| 260 |
|
| 261 |
|
| 262 |
-
def ui_save_preset(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
if not name or not name.strip():
|
| 264 |
return "❌ Preset name cannot be empty.", ui_refresh_presets()
|
| 265 |
-
# Placeholder config — Milestone 9 wires in real settings from each tab
|
| 266 |
config = {
|
| 267 |
-
"name": name,
|
| 268 |
-
"
|
| 269 |
-
"
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
}
|
| 272 |
workspace.save_preset(name, config)
|
| 273 |
return f"✅ Saved preset: `{name}`", ui_refresh_presets()
|
|
@@ -472,7 +511,7 @@ def build_ui() -> gr.Blocks:
|
|
| 472 |
with gr.Column(scale=1):
|
| 473 |
rig_status = gr.Markdown("*Process an asset in Stage 2 first.*")
|
| 474 |
rig_btn.click(
|
| 475 |
-
fn=
|
| 476 |
inputs=[rig_type, rig_seed, rig_spring, rig_format],
|
| 477 |
outputs=rig_status,
|
| 478 |
)
|
|
@@ -503,7 +542,7 @@ def build_ui() -> gr.Blocks:
|
|
| 503 |
ex_status = gr.Markdown("*Nothing to export yet.*")
|
| 504 |
ex_file = gr.File(label="Download", visible=True)
|
| 505 |
ex_btn.click(
|
| 506 |
-
fn=
|
| 507 |
inputs=[ex_engine, ex_name, ex_type, ex_include_lods, ex_include_collision],
|
| 508 |
outputs=[ex_status, ex_file],
|
| 509 |
)
|
|
@@ -512,9 +551,10 @@ def build_ui() -> gr.Blocks:
|
|
| 512 |
with gr.Tab("5. Presets", id=5):
|
| 513 |
gr.Markdown(
|
| 514 |
"### Saved configurations\n"
|
| 515 |
-
"Save
|
| 516 |
-
"
|
| 517 |
-
"
|
|
|
|
| 518 |
)
|
| 519 |
with gr.Row():
|
| 520 |
with gr.Column():
|
|
@@ -529,7 +569,20 @@ def build_ui() -> gr.Blocks:
|
|
| 529 |
pr_delete = gr.Button("Delete selected", variant="stop")
|
| 530 |
pr_status = gr.Markdown()
|
| 531 |
pr_refresh.click(fn=ui_refresh_presets, outputs=pr_list)
|
| 532 |
-
pr_save.click(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
pr_delete.click(fn=ui_delete_preset, inputs=pr_list, outputs=[pr_status, pr_list])
|
| 534 |
|
| 535 |
# ============ Tab 6: Diagnostics (hidden in prod, useful now) =
|
|
|
|
| 243 |
return "\n".join(log)
|
| 244 |
|
| 245 |
|
| 246 |
+
def handle_auto_rig(rig_type, seed, spring, fmt):
|
| 247 |
+
from src.stages.stage3_rig import auto_rig
|
| 248 |
+
_, msg = auto_rig(rig_type=rig_type, seed=int(seed))
|
| 249 |
+
return msg
|
| 250 |
|
| 251 |
|
| 252 |
+
def handle_export(engine, asset_name, asset_type, include_lods, include_collision):
|
| 253 |
+
from src.stages.stage4_export import export_ue5, _checklist
|
| 254 |
+
state = workspace.get_state()
|
| 255 |
+
|
| 256 |
+
if engine != "UE5":
|
| 257 |
+
return f"🚧 {engine} export — not implemented. UE5 is the only supported engine.", None
|
| 258 |
+
|
| 259 |
+
# Game-ready checklist
|
| 260 |
+
issues = _checklist(state)
|
| 261 |
+
checklist_md = ""
|
| 262 |
+
if issues:
|
| 263 |
+
checklist_md = "\n\n⚠️ **Checklist warnings:**\n" + "\n".join(f"- {i}" for i in issues)
|
| 264 |
+
|
| 265 |
+
try:
|
| 266 |
+
zip_path, msg, _ = export_ue5(
|
| 267 |
+
asset_name=asset_name.strip() or "Asset",
|
| 268 |
+
asset_type=asset_type,
|
| 269 |
+
include_lods=include_lods,
|
| 270 |
+
include_collision=include_collision,
|
| 271 |
+
)
|
| 272 |
+
return msg + checklist_md, str(zip_path)
|
| 273 |
+
except Exception as e:
|
| 274 |
+
return f"❌ Export failed: {e}{checklist_md}", None
|
| 275 |
|
| 276 |
|
| 277 |
# ---------------------------------------------------------------------------
|
| 278 |
+
# Presets tab logic (M11 — real save/load wired)
|
| 279 |
# ---------------------------------------------------------------------------
|
| 280 |
|
| 281 |
def ui_refresh_presets() -> gr.Dropdown:
|
| 282 |
return gr.Dropdown(choices=workspace.list_presets(), label="Saved presets")
|
| 283 |
|
| 284 |
|
| 285 |
+
def ui_save_preset(
|
| 286 |
+
name: str,
|
| 287 |
+
gen_model, gen_quality, gen_seed, gen_tex_size,
|
| 288 |
+
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 289 |
+
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 290 |
+
pp_albedo, pp_material, pp_ao, pp_ao_quality,
|
| 291 |
+
pp_inpaint, pp_lods, pp_collision, pp_pivot, pp_scale_m,
|
| 292 |
+
rig_type, rig_seed, rig_format,
|
| 293 |
+
ex_engine, ex_type,
|
| 294 |
+
) -> tuple[str, gr.Dropdown]:
|
| 295 |
if not name or not name.strip():
|
| 296 |
return "❌ Preset name cannot be empty.", ui_refresh_presets()
|
|
|
|
| 297 |
config = {
|
| 298 |
+
"name": name, "version": 1, "created_at": time.time(),
|
| 299 |
+
"stage1": {"model": gen_model, "quality": gen_quality, "seed": int(gen_seed), "tex_size": int(gen_tex_size)},
|
| 300 |
+
"stage2": {
|
| 301 |
+
"repair": pp_repair, "cleanup": pp_cleanup, "decimate": pp_decimate,
|
| 302 |
+
"target_faces": int(pp_target_faces), "symmetry": pp_symmetry,
|
| 303 |
+
"unwrap": pp_unwrap, "normal_bake": pp_normal_bake, "normal_format": pp_normal_format,
|
| 304 |
+
"albedo_bake": pp_albedo, "material_bake": pp_material, "ao": pp_ao, "ao_quality": pp_ao_quality,
|
| 305 |
+
"inpaint": pp_inpaint, "lods": pp_lods, "collision": pp_collision,
|
| 306 |
+
"pivot": pp_pivot, "scale_m": float(pp_scale_m),
|
| 307 |
+
},
|
| 308 |
+
"stage3": {"rig_type": rig_type, "seed": int(rig_seed), "format": rig_format},
|
| 309 |
+
"stage4": {"engine": ex_engine, "asset_type": ex_type},
|
| 310 |
}
|
| 311 |
workspace.save_preset(name, config)
|
| 312 |
return f"✅ Saved preset: `{name}`", ui_refresh_presets()
|
|
|
|
| 511 |
with gr.Column(scale=1):
|
| 512 |
rig_status = gr.Markdown("*Process an asset in Stage 2 first.*")
|
| 513 |
rig_btn.click(
|
| 514 |
+
fn=handle_auto_rig,
|
| 515 |
inputs=[rig_type, rig_seed, rig_spring, rig_format],
|
| 516 |
outputs=rig_status,
|
| 517 |
)
|
|
|
|
| 542 |
ex_status = gr.Markdown("*Nothing to export yet.*")
|
| 543 |
ex_file = gr.File(label="Download", visible=True)
|
| 544 |
ex_btn.click(
|
| 545 |
+
fn=handle_export,
|
| 546 |
inputs=[ex_engine, ex_name, ex_type, ex_include_lods, ex_include_collision],
|
| 547 |
outputs=[ex_status, ex_file],
|
| 548 |
)
|
|
|
|
| 551 |
with gr.Tab("5. Presets", id=5):
|
| 552 |
gr.Markdown(
|
| 553 |
"### Saved configurations\n"
|
| 554 |
+
"Save the current settings across all tabs as a named preset. "
|
| 555 |
+
"Five defaults ship with the app: `character_UE5_hero`, "
|
| 556 |
+
"`character_UE5_npc`, `prop_UE5_hero`, `prop_UE5_standard`, "
|
| 557 |
+
"`environment_UE5_background`."
|
| 558 |
)
|
| 559 |
with gr.Row():
|
| 560 |
with gr.Column():
|
|
|
|
| 569 |
pr_delete = gr.Button("Delete selected", variant="stop")
|
| 570 |
pr_status = gr.Markdown()
|
| 571 |
pr_refresh.click(fn=ui_refresh_presets, outputs=pr_list)
|
| 572 |
+
pr_save.click(
|
| 573 |
+
fn=ui_save_preset,
|
| 574 |
+
inputs=[
|
| 575 |
+
pr_name,
|
| 576 |
+
gen_model, gen_quality, gen_seed, gen_tex_size,
|
| 577 |
+
pp_repair, pp_cleanup, pp_decimate, pp_target_faces,
|
| 578 |
+
pp_symmetry, pp_unwrap, pp_normal_bake, pp_normal_format,
|
| 579 |
+
pp_albedo_bake, pp_material_bake, pp_ao, pp_ao_quality,
|
| 580 |
+
pp_inpaint, pp_lods, pp_collision, pp_pivot, pp_scale_m,
|
| 581 |
+
rig_type, rig_seed, rig_format,
|
| 582 |
+
ex_engine, ex_type,
|
| 583 |
+
],
|
| 584 |
+
outputs=[pr_status, pr_list],
|
| 585 |
+
)
|
| 586 |
pr_delete.click(fn=ui_delete_preset, inputs=pr_list, outputs=[pr_status, pr_list])
|
| 587 |
|
| 588 |
# ============ Tab 6: Diagnostics (hidden in prod, useful now) =
|
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stage 3 — Auto-rigging via gradio_client → MohamedRashad/UniRig.
|
| 3 |
+
|
| 4 |
+
UniRig API:
|
| 5 |
+
Input: 3D model file (.glb/.obj/.fbx) + integer seed
|
| 6 |
+
Output: (rigged_model_path: str, download_files: list)
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import shutil
|
| 11 |
+
import tempfile
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
from src import workspace
|
| 16 |
+
from src.workspace import CURRENT
|
| 17 |
+
|
| 18 |
+
_rig_client = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _get_rig_client():
|
| 22 |
+
global _rig_client
|
| 23 |
+
if _rig_client is None:
|
| 24 |
+
from gradio_client import Client
|
| 25 |
+
_rig_client = Client("MohamedRashad/UniRig")
|
| 26 |
+
return _rig_client
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def auto_rig(
|
| 30 |
+
rig_type: str = "Humanoid",
|
| 31 |
+
seed: int = 0,
|
| 32 |
+
output_format: str = "GLB",
|
| 33 |
+
) -> tuple[Path | None, str]:
|
| 34 |
+
"""
|
| 35 |
+
Send the current workspace mesh to UniRig and save the rigged output.
|
| 36 |
+
Returns (rigged_glb_path, status_message).
|
| 37 |
+
"""
|
| 38 |
+
t0 = time.time()
|
| 39 |
+
|
| 40 |
+
state = workspace.get_state()
|
| 41 |
+
mesh_src = (
|
| 42 |
+
state.final_glb
|
| 43 |
+
or state.low_poly_glb
|
| 44 |
+
or state.unwrapped_glb
|
| 45 |
+
or state.cleaned_glb
|
| 46 |
+
or state.raw_gen_glb
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if not mesh_src or not mesh_src.exists():
|
| 50 |
+
return None, "❌ No mesh to rig — complete Stage 2 first."
|
| 51 |
+
|
| 52 |
+
client = _get_rig_client()
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
from gradio_client import handle_file
|
| 56 |
+
result = client.predict(
|
| 57 |
+
input_file=handle_file(str(mesh_src)),
|
| 58 |
+
seed=int(seed),
|
| 59 |
+
api_name="/predict",
|
| 60 |
+
)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return None, f"❌ UniRig call failed:\n```\n{type(e).__name__}: {e}\n```"
|
| 63 |
+
|
| 64 |
+
# result[0] is the primary rigged model path (temp file on our machine)
|
| 65 |
+
rigged_src = result[0] if isinstance(result, (list, tuple)) else result
|
| 66 |
+
|
| 67 |
+
if not rigged_src or not Path(rigged_src).exists():
|
| 68 |
+
return None, "❌ UniRig returned no output file."
|
| 69 |
+
|
| 70 |
+
rigged_path = CURRENT / "rigged.glb"
|
| 71 |
+
shutil.copy(str(rigged_src), rigged_path)
|
| 72 |
+
|
| 73 |
+
state.rigged_glb = rigged_path
|
| 74 |
+
state.model_used = f"{state.model_used or 'TRELLIS.2'} + UniRig"
|
| 75 |
+
|
| 76 |
+
elapsed = time.time() - t0
|
| 77 |
+
return rigged_path, (
|
| 78 |
+
f"✅ **Rigged with UniRig** · {rig_type}\n\n"
|
| 79 |
+
f"- Output: `{rigged_path.name}`\n"
|
| 80 |
+
f"- Time: {elapsed:.1f}s\n\n"
|
| 81 |
+
f"Download the rigged GLB and drop into Mixamo for free animations."
|
| 82 |
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stage 4 — UE5 export.
|
| 3 |
+
|
| 4 |
+
Bundles all processed files into a zip drop-in for UE5:
|
| 5 |
+
SM_Name.glb — static mesh (or SK_ for skeletal)
|
| 6 |
+
SM_Name_LOD1/2.glb — LOD levels
|
| 7 |
+
UCX_SM_Name.glb — collision mesh
|
| 8 |
+
T_Name_D.png — albedo / diffuse
|
| 9 |
+
T_Name_N.png — normal map (DirectX, Y-down)
|
| 10 |
+
T_Name_ORM.png — ORM packed (AO/Roughness/Metallic)
|
| 11 |
+
manifest.json — metadata for re-import scripting
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import zipfile
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from src import workspace
|
| 20 |
+
from src.workspace import EXPORTS
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _ue5_prefix(asset_type: str) -> str:
|
| 24 |
+
if "Character" in asset_type or "SK_" in asset_type:
|
| 25 |
+
return "SK_"
|
| 26 |
+
return "SM_"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _checklist(state: workspace.AssetState) -> list[str]:
|
| 30 |
+
"""Return a list of game-readiness warnings."""
|
| 31 |
+
issues = []
|
| 32 |
+
if not (state.raw_gen_glb or state.high_poly_glb):
|
| 33 |
+
issues.append("No generated mesh — run Stage 1 first")
|
| 34 |
+
if not state.unwrapped_glb:
|
| 35 |
+
issues.append("No UV unwrap — textures will be missing")
|
| 36 |
+
if not state.normal_dx_png:
|
| 37 |
+
issues.append("No normal map — surface detail will be lost in engine")
|
| 38 |
+
if not state.orm_png and not (state.roughness_png and state.metallic_png):
|
| 39 |
+
issues.append("No ORM map — PBR material will use defaults")
|
| 40 |
+
if not state.lod_glbs:
|
| 41 |
+
issues.append("No LODs — performance warning for real-time use")
|
| 42 |
+
if not state.collision_glb:
|
| 43 |
+
issues.append("No collision mesh — physics will use the render mesh")
|
| 44 |
+
return issues
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def export_ue5(
|
| 48 |
+
asset_name: str,
|
| 49 |
+
asset_type: str = "Prop (SM_)",
|
| 50 |
+
include_lods: bool = True,
|
| 51 |
+
include_collision: bool = True,
|
| 52 |
+
) -> tuple[Path, str, list[str]]:
|
| 53 |
+
"""
|
| 54 |
+
Bundle workspace outputs into a UE5-ready zip.
|
| 55 |
+
|
| 56 |
+
Returns (zip_path, status_message, checklist_warnings).
|
| 57 |
+
"""
|
| 58 |
+
state = workspace.get_state()
|
| 59 |
+
issues = _checklist(state)
|
| 60 |
+
|
| 61 |
+
prefix = _ue5_prefix(asset_type)
|
| 62 |
+
safe_name = "".join(c for c in asset_name if c.isalnum() or c == "_").strip("_") or "Asset"
|
| 63 |
+
|
| 64 |
+
EXPORTS.mkdir(parents=True, exist_ok=True)
|
| 65 |
+
zip_path = EXPORTS / f"export_{safe_name}_UE5.zip"
|
| 66 |
+
|
| 67 |
+
# Determine the main mesh source (most processed version)
|
| 68 |
+
mesh_src = (
|
| 69 |
+
state.final_glb
|
| 70 |
+
or state.low_poly_glb
|
| 71 |
+
or state.unwrapped_glb
|
| 72 |
+
or state.cleaned_glb
|
| 73 |
+
or state.repaired_glb
|
| 74 |
+
or state.raw_gen_glb
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if not mesh_src or not mesh_src.exists():
|
| 78 |
+
return zip_path, "❌ No mesh to export — complete Stage 2 first.", issues
|
| 79 |
+
|
| 80 |
+
added = []
|
| 81 |
+
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
| 82 |
+
|
| 83 |
+
# Main mesh
|
| 84 |
+
zf.write(mesh_src, f"{prefix}{safe_name}.glb")
|
| 85 |
+
added.append(f"{prefix}{safe_name}.glb")
|
| 86 |
+
|
| 87 |
+
# LODs
|
| 88 |
+
if include_lods:
|
| 89 |
+
for i, lod in enumerate(state.lod_glbs or []):
|
| 90 |
+
if lod and lod.exists() and i > 0: # LOD0 == main mesh
|
| 91 |
+
arc_name = f"{prefix}{safe_name}_LOD{i}.glb"
|
| 92 |
+
zf.write(lod, arc_name)
|
| 93 |
+
added.append(arc_name)
|
| 94 |
+
|
| 95 |
+
# Collision (UE5 uses UCX_ prefix for convex collision)
|
| 96 |
+
if include_collision and state.collision_glb and state.collision_glb.exists():
|
| 97 |
+
arc_name = f"UCX_{prefix}{safe_name}.glb"
|
| 98 |
+
zf.write(state.collision_glb, arc_name)
|
| 99 |
+
added.append(arc_name)
|
| 100 |
+
|
| 101 |
+
# Textures
|
| 102 |
+
tex_map: dict[Path | None, str] = {
|
| 103 |
+
state.albedo_png: f"T_{safe_name}_D.png",
|
| 104 |
+
state.normal_dx_png: f"T_{safe_name}_N.png", # DX convention for UE5
|
| 105 |
+
state.orm_png: f"T_{safe_name}_ORM.png",
|
| 106 |
+
}
|
| 107 |
+
for src, arc_name in tex_map.items():
|
| 108 |
+
if src and src.exists():
|
| 109 |
+
zf.write(src, arc_name)
|
| 110 |
+
added.append(arc_name)
|
| 111 |
+
|
| 112 |
+
# Manifest
|
| 113 |
+
manifest = {
|
| 114 |
+
"asset_name": safe_name,
|
| 115 |
+
"asset_type": asset_type,
|
| 116 |
+
"engine": "UE5",
|
| 117 |
+
"normal_convention": "DirectX",
|
| 118 |
+
"scale_unit": "cm",
|
| 119 |
+
"model_used": state.model_used or "unknown",
|
| 120 |
+
"face_count": state.face_count,
|
| 121 |
+
"vertex_count": state.vertex_count,
|
| 122 |
+
"files": added,
|
| 123 |
+
}
|
| 124 |
+
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
|
| 125 |
+
|
| 126 |
+
size_kb = zip_path.stat().st_size / 1024
|
| 127 |
+
msg = (
|
| 128 |
+
f"✅ **Exported:** `{zip_path.name}` ({size_kb:.0f} KB)\n\n"
|
| 129 |
+
f"Files: {', '.join(added)}"
|
| 130 |
+
)
|
| 131 |
+
return zip_path, msg, issues
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "character_UE5_hero",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"stage1": {
|
| 5 |
+
"model": "TRELLIS.2 (Hard Surface)",
|
| 6 |
+
"quality": "Hero (~90s)",
|
| 7 |
+
"seed": 42,
|
| 8 |
+
"tex_size": 4096
|
| 9 |
+
},
|
| 10 |
+
"stage2": {
|
| 11 |
+
"repair": true,
|
| 12 |
+
"cleanup": true,
|
| 13 |
+
"decimate": true,
|
| 14 |
+
"target_faces": 20000,
|
| 15 |
+
"symmetry": true,
|
| 16 |
+
"unwrap": true,
|
| 17 |
+
"normal_bake": true,
|
| 18 |
+
"normal_format": "DirectX (UE5)",
|
| 19 |
+
"albedo_bake": true,
|
| 20 |
+
"material_bake": true,
|
| 21 |
+
"ao": true,
|
| 22 |
+
"ao_quality": "High",
|
| 23 |
+
"inpaint": false,
|
| 24 |
+
"lods": true,
|
| 25 |
+
"collision": true,
|
| 26 |
+
"pivot": "bottom_center",
|
| 27 |
+
"scale_m": 1.8
|
| 28 |
+
},
|
| 29 |
+
"stage3": {
|
| 30 |
+
"rig_type": "Humanoid",
|
| 31 |
+
"seed": 0,
|
| 32 |
+
"format": "FBX (UE5 recommended)"
|
| 33 |
+
},
|
| 34 |
+
"stage4": {
|
| 35 |
+
"engine": "UE5",
|
| 36 |
+
"asset_type": "Character (SK_)"
|
| 37 |
+
}
|
| 38 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "character_UE5_npc",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"stage1": {
|
| 5 |
+
"model": "TRELLIS.2 (Hard Surface)",
|
| 6 |
+
"quality": "Balanced (~60s)",
|
| 7 |
+
"seed": 42,
|
| 8 |
+
"tex_size": 2048
|
| 9 |
+
},
|
| 10 |
+
"stage2": {
|
| 11 |
+
"repair": true,
|
| 12 |
+
"cleanup": true,
|
| 13 |
+
"decimate": true,
|
| 14 |
+
"target_faces": 10000,
|
| 15 |
+
"symmetry": true,
|
| 16 |
+
"unwrap": true,
|
| 17 |
+
"normal_bake": true,
|
| 18 |
+
"normal_format": "DirectX (UE5)",
|
| 19 |
+
"albedo_bake": true,
|
| 20 |
+
"material_bake": true,
|
| 21 |
+
"ao": true,
|
| 22 |
+
"ao_quality": "Standard",
|
| 23 |
+
"inpaint": false,
|
| 24 |
+
"lods": true,
|
| 25 |
+
"collision": false,
|
| 26 |
+
"pivot": "bottom_center",
|
| 27 |
+
"scale_m": 1.75
|
| 28 |
+
},
|
| 29 |
+
"stage3": {
|
| 30 |
+
"rig_type": "Humanoid",
|
| 31 |
+
"seed": 0,
|
| 32 |
+
"format": "FBX (UE5 recommended)"
|
| 33 |
+
},
|
| 34 |
+
"stage4": {
|
| 35 |
+
"engine": "UE5",
|
| 36 |
+
"asset_type": "Character (SK_)"
|
| 37 |
+
}
|
| 38 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "environment_UE5_background",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"stage1": {
|
| 5 |
+
"model": "TRELLIS.2 (Hard Surface)",
|
| 6 |
+
"quality": "Fast (~30s)",
|
| 7 |
+
"seed": 42,
|
| 8 |
+
"tex_size": 1024
|
| 9 |
+
},
|
| 10 |
+
"stage2": {
|
| 11 |
+
"repair": true,
|
| 12 |
+
"cleanup": true,
|
| 13 |
+
"decimate": true,
|
| 14 |
+
"target_faces": 5000,
|
| 15 |
+
"symmetry": false,
|
| 16 |
+
"unwrap": true,
|
| 17 |
+
"normal_bake": false,
|
| 18 |
+
"normal_format": "DirectX (UE5)",
|
| 19 |
+
"albedo_bake": true,
|
| 20 |
+
"material_bake": false,
|
| 21 |
+
"ao": false,
|
| 22 |
+
"ao_quality": "Fast",
|
| 23 |
+
"inpaint": false,
|
| 24 |
+
"lods": true,
|
| 25 |
+
"collision": true,
|
| 26 |
+
"pivot": "bottom_center",
|
| 27 |
+
"scale_m": 2.0
|
| 28 |
+
},
|
| 29 |
+
"stage3": {
|
| 30 |
+
"rig_type": "Humanoid",
|
| 31 |
+
"seed": 0,
|
| 32 |
+
"format": "GLB"
|
| 33 |
+
},
|
| 34 |
+
"stage4": {
|
| 35 |
+
"engine": "UE5",
|
| 36 |
+
"asset_type": "Environment (SM_)"
|
| 37 |
+
}
|
| 38 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "prop_UE5_hero",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"stage1": {
|
| 5 |
+
"model": "TRELLIS.2 (Hard Surface)",
|
| 6 |
+
"quality": "Hero (~90s)",
|
| 7 |
+
"seed": 42,
|
| 8 |
+
"tex_size": 4096
|
| 9 |
+
},
|
| 10 |
+
"stage2": {
|
| 11 |
+
"repair": true,
|
| 12 |
+
"cleanup": true,
|
| 13 |
+
"decimate": true,
|
| 14 |
+
"target_faces": 15000,
|
| 15 |
+
"symmetry": false,
|
| 16 |
+
"unwrap": true,
|
| 17 |
+
"normal_bake": true,
|
| 18 |
+
"normal_format": "DirectX (UE5)",
|
| 19 |
+
"albedo_bake": true,
|
| 20 |
+
"material_bake": true,
|
| 21 |
+
"ao": true,
|
| 22 |
+
"ao_quality": "High",
|
| 23 |
+
"inpaint": false,
|
| 24 |
+
"lods": true,
|
| 25 |
+
"collision": true,
|
| 26 |
+
"pivot": "bottom_center",
|
| 27 |
+
"scale_m": 0.5
|
| 28 |
+
},
|
| 29 |
+
"stage3": {
|
| 30 |
+
"rig_type": "Humanoid",
|
| 31 |
+
"seed": 0,
|
| 32 |
+
"format": "GLB"
|
| 33 |
+
},
|
| 34 |
+
"stage4": {
|
| 35 |
+
"engine": "UE5",
|
| 36 |
+
"asset_type": "Prop (SM_)"
|
| 37 |
+
}
|
| 38 |
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "prop_UE5_standard",
|
| 3 |
+
"version": 1,
|
| 4 |
+
"stage1": {
|
| 5 |
+
"model": "TRELLIS.2 (Hard Surface)",
|
| 6 |
+
"quality": "Balanced (~60s)",
|
| 7 |
+
"seed": 42,
|
| 8 |
+
"tex_size": 2048
|
| 9 |
+
},
|
| 10 |
+
"stage2": {
|
| 11 |
+
"repair": true,
|
| 12 |
+
"cleanup": true,
|
| 13 |
+
"decimate": true,
|
| 14 |
+
"target_faces": 8000,
|
| 15 |
+
"symmetry": false,
|
| 16 |
+
"unwrap": true,
|
| 17 |
+
"normal_bake": true,
|
| 18 |
+
"normal_format": "DirectX (UE5)",
|
| 19 |
+
"albedo_bake": true,
|
| 20 |
+
"material_bake": true,
|
| 21 |
+
"ao": true,
|
| 22 |
+
"ao_quality": "Standard",
|
| 23 |
+
"inpaint": false,
|
| 24 |
+
"lods": true,
|
| 25 |
+
"collision": true,
|
| 26 |
+
"pivot": "bottom_center",
|
| 27 |
+
"scale_m": 0.5
|
| 28 |
+
},
|
| 29 |
+
"stage3": {
|
| 30 |
+
"rig_type": "Humanoid",
|
| 31 |
+
"seed": 0,
|
| 32 |
+
"format": "GLB"
|
| 33 |
+
},
|
| 34 |
+
"stage4": {
|
| 35 |
+
"engine": "UE5",
|
| 36 |
+
"asset_type": "Prop (SM_)"
|
| 37 |
+
}
|
| 38 |
+
}
|