fix(assets): address PR #96 review — all 8 items
- Fix stale 21→18 segment count in architecture doc, script docstrings, and disk budget table (critical — both reviewers) - Add average_m/README.md noting role as clothing reference body - Add __main__ guards to diagnostic scripts - Preflight-check all source paths in blender_process_bodies.py - Document solid-white hair masks as v0.2 placeholder for multi-region - Add fallback size comment for head mask generation - Add bald_mask.png (1×1 black) for sidecar convention consistency - Document child non-uniform scale rationale (Y=0.72 vs XZ=0.75) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,13 +7,14 @@ Lists all mesh objects in a FullBody GLTF: names, vertex counts, vertex group co
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=args[0])
|
||||
print("Scene objects:")
|
||||
for obj in sorted(bpy.context.scene.objects, key=lambda o: o.name):
|
||||
if obj.type == 'MESH':
|
||||
print(f" MESH: {obj.name!r} verts={len(obj.data.vertices)} vgroups={len(obj.vertex_groups)}")
|
||||
else:
|
||||
print(f" {obj.type}: {obj.name!r}")
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=args[0])
|
||||
print("Scene objects:")
|
||||
for obj in sorted(bpy.context.scene.objects, key=lambda o: o.name):
|
||||
if obj.type == 'MESH':
|
||||
print(f" MESH: {obj.name!r} verts={len(obj.data.vertices)} vgroups={len(obj.vertex_groups)}")
|
||||
else:
|
||||
print(f" {obj.type}: {obj.name!r}")
|
||||
|
||||
@@ -8,17 +8,18 @@ for the segmentation script.
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=args[0])
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type == 'MESH' and obj.vertex_groups:
|
||||
print(f"MESH: {obj.name} — {len(obj.vertex_groups)} vertex groups, {len(obj.data.vertices)} vertices")
|
||||
for vg in sorted(obj.vertex_groups, key=lambda x: x.name):
|
||||
print(f" VG: {vg.name}")
|
||||
break
|
||||
else:
|
||||
print("No skinned mesh found.")
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=args[0])
|
||||
for obj in bpy.context.scene.objects:
|
||||
print(f" {obj.name} ({obj.type})")
|
||||
if obj.type == 'MESH' and obj.vertex_groups:
|
||||
print(f"MESH: {obj.name} — {len(obj.vertex_groups)} vertex groups, {len(obj.data.vertices)} vertices")
|
||||
for vg in sorted(obj.vertex_groups, key=lambda x: x.name):
|
||||
print(f" VG: {vg.name}")
|
||||
break
|
||||
else:
|
||||
print("No skinned mesh found.")
|
||||
for obj in bpy.context.scene.objects:
|
||||
print(f" {obj.name} ({obj.type})")
|
||||
|
||||
@@ -6,12 +6,12 @@ Drives the full body segmentation pipeline for all 11 body types.
|
||||
Calls blender_segment_body.py logic inline (same Blender session, one pass per body type).
|
||||
|
||||
Source GLTF files (pre-exported from Source tier, Godot - UE format):
|
||||
<source_dir>/Regular_Male_FullBody.gltf -> average_m (21 segs)
|
||||
<source_dir>/Regular_Female_FullBody.gltf -> average_f (21 segs)
|
||||
<source_dir>/Superhero_Male_FullBody.gltf -> muscular_m (21 segs)
|
||||
<source_dir>/Superhero_Female_FullBody.gltf -> muscular_f (21 segs)
|
||||
<source_dir>/Teen_Male_FullBody.gltf -> teen_m (21 segs)
|
||||
<source_dir>/Teen_Female_FullBody.gltf -> teen_f (21 segs)
|
||||
<source_dir>/Regular_Male_FullBody.gltf -> average_m (18 segs)
|
||||
<source_dir>/Regular_Female_FullBody.gltf -> average_f (18 segs)
|
||||
<source_dir>/Superhero_Male_FullBody.gltf -> muscular_m (18 segs)
|
||||
<source_dir>/Superhero_Female_FullBody.gltf -> muscular_f (18 segs)
|
||||
<source_dir>/Teen_Male_FullBody.gltf -> teen_m (18 segs)
|
||||
<source_dir>/Teen_Female_FullBody.gltf -> teen_f (18 segs)
|
||||
|
||||
Fork body types (vertex-level scale applied before segmentation):
|
||||
Regular_Male + scale (0.82, 1.0, 0.88) -> thin_m
|
||||
@@ -24,9 +24,9 @@ Fork body types are approximate (auto-generated from mesh scaling). They produce
|
||||
visually distinct silhouettes but may need artist refinement for final quality.
|
||||
A README is placed in each fork directory marking this status.
|
||||
|
||||
Output: <output_base_dir>/{body_type}/seg_{name}.glb (21 files × 11 types = 231 GLBs)
|
||||
Output: <output_base_dir>/{body_type}/seg_{name}.glb (18 files × 11 types = 198 GLBs)
|
||||
|
||||
Decisions: D-159 (11 body types), D-160 (21 segments), D-164 (Source .blends as starting point)
|
||||
Decisions: D-159 (11 body types), D-160 (18 segments), D-164 (Source .blends as starting point)
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -96,16 +96,26 @@ if __name__ == "__main__":
|
||||
source_dir = args[0]
|
||||
output_base_dir = args[1]
|
||||
|
||||
# Preflight: check ALL source files exist before starting any processing,
|
||||
# so all missing files are reported at once rather than failing mid-loop.
|
||||
unique_sources = {gltf_filename for (gltf_filename, _, _, _) in BODY_MANIFEST}
|
||||
missing = [
|
||||
os.path.join(source_dir, f)
|
||||
for f in sorted(unique_sources)
|
||||
if not os.path.exists(os.path.join(source_dir, f))
|
||||
]
|
||||
if missing:
|
||||
print("\nERROR: Missing source GLTF files:")
|
||||
for path in missing:
|
||||
print(f" {path}")
|
||||
sys.exit(1)
|
||||
|
||||
results = []
|
||||
|
||||
for (gltf_filename, body_type, scale, is_fork) in BODY_MANIFEST:
|
||||
gltf_path = os.path.join(source_dir, gltf_filename)
|
||||
output_dir = os.path.join(output_base_dir, body_type)
|
||||
|
||||
if not os.path.exists(gltf_path):
|
||||
print(f"\nERROR: GLTF not found: {gltf_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Body type: {body_type} {'[FORK]' if is_fork else '[DIRECT]'}")
|
||||
print(f" Source: {gltf_filename}")
|
||||
|
||||
@@ -109,7 +109,13 @@ def convert_gltf_to_glb(gltf_path: str, glb_path: str) -> None:
|
||||
|
||||
|
||||
def make_mask_png(mask_path: str, width: int = 1024, height: int = 1024) -> None:
|
||||
"""Generate a solid-white greyscale mask PNG (entire mesh = tintable region)."""
|
||||
"""Generate a solid-white greyscale mask PNG (entire mesh = tintable region).
|
||||
|
||||
Solid white = fully tintable. This is correct for v0.2 where each hair style
|
||||
has a single tint color. The mask format supports greyscale bands for multi-region
|
||||
recoloring (e.g. roots vs tips as separate regions) when needed in future — no
|
||||
re-export required, just a shader update to sample distinct greyscale values.
|
||||
"""
|
||||
mask_img = bpy.data.images.new(
|
||||
name="hair_mask",
|
||||
width=width,
|
||||
|
||||
@@ -54,13 +54,19 @@ def process_head(blend_path: str, output_dir: str, head_id: str) -> None:
|
||||
print("ERROR: No mesh found in the .blend file")
|
||||
sys.exit(1)
|
||||
|
||||
# Detect texture resolution for mask sizing
|
||||
# Detect texture resolution for mask sizing.
|
||||
# NOTE: This picks the first non-HDR texture found in bpy.data.images, which is
|
||||
# fragile — iteration order is not guaranteed and multiple textures may be present.
|
||||
# If no texture is found, falls back to MASK_RESOLUTION (512x512 default) so the
|
||||
# mask is still generated at a sensible size rather than failing.
|
||||
mask_w = mask_h = MASK_RESOLUTION
|
||||
for img in bpy.data.images:
|
||||
if img.size[0] > 0 and img.size[1] > 0 and not img.name.endswith('.hdr'):
|
||||
print(f" Texture found: {img.name} — {img.size[0]}x{img.size[1]}")
|
||||
mask_w, mask_h = img.size[0], img.size[1]
|
||||
break
|
||||
else:
|
||||
print(f" No texture found — using fallback mask size {mask_w}x{mask_h}")
|
||||
|
||||
# Strip all animation data (heads have no runtime animation — BoneAttachment3D moves them)
|
||||
for obj in all_objects:
|
||||
|
||||
@@ -4,7 +4,7 @@ Usage:
|
||||
tooling/blender --background --python tooling/blender_segment_body.py -- \
|
||||
<input.gltf> <output_dir> [--scale sx sy sz]
|
||||
|
||||
Segments a Quaternius FullBody GLTF into 21 production GLBs:
|
||||
Segments a Quaternius FullBody GLTF into 18 production GLBs:
|
||||
seg_head, seg_neck, seg_torso, seg_torso_upper,
|
||||
seg_arm_upper_l/r, seg_arm_lower_l/r, seg_hand_l/r,
|
||||
seg_leg_upper_l/r, seg_leg_lower_l/r, seg_foot_l/r,
|
||||
@@ -16,9 +16,9 @@ plus 1-ring boundary overlap for seam-free deformation.
|
||||
Optional --scale sx sy sz applies a vertex-level scale to the mesh before
|
||||
segmentation (for fork body types: thin, heavy, child).
|
||||
|
||||
Outputs: <output_dir>/seg_{name}.glb (21 files total)
|
||||
Outputs: <output_dir>/seg_{name}.glb (18 files total)
|
||||
|
||||
Design: D-160 (21 segments per body type), D-164 (Source .blends as starting point)
|
||||
Design: D-160 (18 segments per body type), D-164 (Source .blends as starting point)
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -249,7 +249,7 @@ def segment_special_object(special_obj, armature, output_path):
|
||||
|
||||
|
||||
def segment_body(gltf_path, output_dir, scale=(1.0, 1.0, 1.0)):
|
||||
"""Main entry: load GLTF, apply optional scale, produce all 21 segment GLBs."""
|
||||
"""Main entry: load GLTF, apply optional scale, produce all 18 segment GLBs."""
|
||||
print(f"\n Loading: {os.path.basename(gltf_path)}")
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=gltf_path)
|
||||
|
||||
+16
-14
@@ -1,18 +1,20 @@
|
||||
"""List all objects in a Quaternius .blend file."""
|
||||
import sys, bpy
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.open_mainfile(filepath=args[0])
|
||||
|
||||
print("\nAll scene objects:")
|
||||
for o in sorted(bpy.context.scene.objects, key=lambda x: x.name):
|
||||
parent_info = f" (parent: {o.parent.name}" + (f", bone: {o.parent_bone}" if o.parent_bone else "") + ")" if o.parent else ""
|
||||
verts = f", {len(o.data.vertices)} verts" if o.type == 'MESH' else ""
|
||||
loc = tuple(round(v, 3) for v in o.matrix_world.translation)
|
||||
print(f" {o.name:30s} type={o.type:10s} loc={loc}{verts}{parent_info}")
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
args = argv[argv.index("--") + 1:]
|
||||
bpy.ops.wm.open_mainfile(filepath=args[0])
|
||||
|
||||
arm = next((o for o in bpy.context.scene.objects if o.type == 'ARMATURE'), None)
|
||||
if arm:
|
||||
head_bone = arm.data.bones.get('Head')
|
||||
if head_bone:
|
||||
print(f"\nHead bone top: z={round(head_bone.tail_local[2], 4)}")
|
||||
print("\nAll scene objects:")
|
||||
for o in sorted(bpy.context.scene.objects, key=lambda x: x.name):
|
||||
parent_info = f" (parent: {o.parent.name}" + (f", bone: {o.parent_bone}" if o.parent_bone else "") + ")" if o.parent else ""
|
||||
verts = f", {len(o.data.vertices)} verts" if o.type == 'MESH' else ""
|
||||
loc = tuple(round(v, 3) for v in o.matrix_world.translation)
|
||||
print(f" {o.name:30s} type={o.type:10s} loc={loc}{verts}{parent_info}")
|
||||
|
||||
arm = next((o for o in bpy.context.scene.objects if o.type == 'ARMATURE'), None)
|
||||
if arm:
|
||||
head_bone = arm.data.bones.get('Head')
|
||||
if head_bone:
|
||||
print(f"\nHead bone top: z={round(head_bone.tail_local[2], 4)}")
|
||||
|
||||
Reference in New Issue
Block a user