Add the external-mesh adapt intake command for the MakeHuman pipeline
This commit is contained in:
1 parent
e984129597
commit
28fcd96b59
7 files changed
+130
-20
No files matched your search
@@ -2119,6 +2119,74 @@ def inspect_glb(path, category=None) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def adapt_glb(path, out_path, target_tris=8000):
|
||||
"""Import an external GLB and adapt it toward the library contract.
|
||||
|
||||
Applies node transforms, grounds the feet (lowest vertex at y = 0), welds
|
||||
duplicated seam vertices and decimates to `target_tris`, then exports the
|
||||
result to `out_path` as a GLB. The five tint slots and the 13-bone contract
|
||||
armature are deliberately NOT auto-mapped — that is a manual retopology/rig
|
||||
step. Returns before/after measurements for the intake report.
|
||||
"""
|
||||
if bpy is None:
|
||||
return {"ok": False, "error": "adapt must run inside Blender"}
|
||||
file_path = Path(path).expanduser()
|
||||
if not file_path.is_file():
|
||||
return {"ok": False, "error": f"missing file {file_path}"}
|
||||
reset_scene()
|
||||
import_log = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stdout(import_log), contextlib.redirect_stderr(import_log):
|
||||
bpy.ops.import_scene.gltf(filepath=str(file_path))
|
||||
except RuntimeError as exc:
|
||||
return {"ok": False, "error": f"import failed: {exc}"}
|
||||
armatures = [o for o in bpy.context.scene.objects if o.type == "ARMATURE"]
|
||||
helpers = {pb.custom_shape.name for arm in armatures for pb in arm.pose.bones
|
||||
if pb.custom_shape is not None}
|
||||
meshes = [o for o in bpy.context.scene.objects
|
||||
if o.type == "MESH" and o.name not in helpers]
|
||||
if not meshes:
|
||||
return {"ok": False, "error": "no meshes in the file"}
|
||||
_apply_imported_transforms(meshes, armatures)
|
||||
before = sum(_triangle_count(o) for o in meshes)
|
||||
|
||||
# Ground: translate so the lowest vertex sits at y = 0.
|
||||
min_y = min((o.matrix_world @ v.co).y for o in meshes for v in o.data.vertices)
|
||||
for o in meshes:
|
||||
o.location.y -= min_y
|
||||
|
||||
# Weld seam-split duplicates left by the glTF importer.
|
||||
for o in meshes:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(o.data)
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
|
||||
bm.to_mesh(o.data)
|
||||
bm.free()
|
||||
|
||||
# Decimate to the target budget (modifier applied so the count is real).
|
||||
total = sum(_triangle_count(o) for o in meshes)
|
||||
if total > target_tris:
|
||||
ratio = max(0.05, target_tris / total)
|
||||
for o in meshes:
|
||||
o.modifiers.new("AdaptDecimate", "DECIMATE").ratio = ratio
|
||||
_apply_all(o)
|
||||
after = sum(_triangle_count(o) for o in meshes)
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for o in meshes:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
out = Path(out_path).expanduser()
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(out), export_format="GLB", use_selection=True,
|
||||
export_apply=True, export_animations=False, export_skins=True,
|
||||
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
|
||||
return {"ok": True, "before_triangles": before, "after_triangles": after,
|
||||
"out": str(out), "bones": [b.name for a in armatures for b in a.data.bones],
|
||||
"meshes": [o.name for o in meshes]}
|
||||
|
||||
|
||||
def _to_godot(co, digits: int = 4) -> list:
|
||||
"""Blender Z-up (x, y, z) -> Godot/glTF Y-up (x, z, -y) so reports match
|
||||
the contract bounds, HEAD_BAND and the audit's coordinate space."""
|
||||
@@ -2449,6 +2517,12 @@ def main(argv=None):
|
||||
insp.add_argument("--json", action="store_true")
|
||||
insp.add_argument("--out", default="",
|
||||
help="with --json, write to this file (Blender prints a startup banner)")
|
||||
ada = sub.add_parser("adapt", help="import + ground + weld + decimate an external GLB")
|
||||
ada.add_argument("--file", required=True,
|
||||
help="external GLB to adapt (absolute path; runs in Blender)")
|
||||
ada.add_argument("--out", required=True, help="where to write the adapted GLB")
|
||||
ada.add_argument("--target-tris", type=int, default=8000,
|
||||
help="decimate target (default 8000)")
|
||||
tex = sub.add_parser("texels", help="per-slot texel density of the character GLBs")
|
||||
tex.add_argument("--only", default="")
|
||||
tex.add_argument("--json", action="store_true")
|
||||
@@ -2498,6 +2572,21 @@ def main(argv=None):
|
||||
ok = all(r.get("ok", False) for r in results)
|
||||
return 0 if ok else 1
|
||||
|
||||
if args.cmd == "adapt":
|
||||
if bpy is None:
|
||||
print("adapt must run inside Blender:\n flatpak run org.blender.Blender "
|
||||
f"--background --python {Path(__file__).resolve()} -- adapt "
|
||||
"--file <in.glb> --out <out.glb>", file=sys.stderr)
|
||||
return 2
|
||||
result = adapt_glb(args.file, args.out, args.target_tris)
|
||||
if not result.get("ok"):
|
||||
print(f"ADAPT FAILED {result.get('error', 'unknown error')}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"adapted {Path(result['out']).name}: "
|
||||
f"{result['before_triangles']} -> {result['after_triangles']} tris"
|
||||
f" (bones={len(result['bones'])}, meshes={len(result['meshes'])})")
|
||||
return 0
|
||||
|
||||
if args.cmd in ("build", "render"):
|
||||
if bpy is None:
|
||||
print(f"{args.cmd} must run inside Blender:\n flatpak run org.blender.Blender "
|
||||
|
||||
Reference in new issue
Block a user