refactor(tooling): T-1290 — the character domain, and six payloads the map misfiled
`reach character {logo, strip-glb, qa, qa-analyze}` replaces make_logo.py,
glb_strip_utility_nodes.py, analyze_captures.py and the run-garment-qa bash
driver. The QA configs and method doc move beside the domain (qa_configs/,
GARMENT_QA.md), and `qa` takes a config name (`reach character qa hoodie_modern`)
or a path.
Parity, from baselines taken before anything moved:
- the logo PNG is byte-identical
- a synthetic GLB with three real utility nodes strips to identical bytes
(the committed bodies strip 0 nodes, so they proved nothing)
- re-analyzing a cached capture set gives a byte-identical report.json and
summary
run-garment-qa is rewritten, not wrapped (D-263). Its decisions — which config,
which Godot ($GODOT, then ~/bin/godot4, then PATH), and whether xvfb-run is
needed — are capture_plan(), pinned by tooling/test_character.py without
launching Godot. The bash exit codes are kept: 2 for a missing config, 3 for
no Godot.
The T-1271 domain map was wrong about this domain. Six of its ten files import
bpy: convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere,
render_quaternius_test and test_quaternius_raw. They are Blender payloads and
joined the carve-out as blender_* (41 payloads now). The 22 existing payloads'
docstrings still cited tooling/garment-fit/ from before T-1273; fixed.
Archived, with reasons in tooling/archive/README.md:
- setup_clothing_metadata.py wrote coverage data for five garments that no
longer exist in the 24-garment wardrobe
- wipe-bodies.sh ran raw DELETEs on systems.db
segment_reference_distribution.md moved to docs/assets/visual/.
Behaviour changes:
- The QA analyzer exited 0 whatever it found, though its own README says
clip-through "is the real defect and it gates". qa and qa-analyze now exit 1
on clip-through, and the remedy names --min-pixels (Wave 1/2 were accepted
at 150). The cached peasant set has 33 failures at the default 8 px.
- glb strip re-reported the same nodes as stripped on every re-run and
rewrote an unchanged file: it left them as orphans and then found them
again. Only nodes still linked into the graph count now, and a first pass
writes the same bytes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ default_tints (sole near-black, upper work brown, laces dark).
|
||||
|
||||
Usage (boots_modern reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_boots.py -- \
|
||||
tooling/scripts/blender/blender_author_boots.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/boots_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.013] [--shaft-frac 0.30] \
|
||||
|
||||
@@ -29,7 +29,7 @@ channel is still authored (costs nothing; enables future brand variants).
|
||||
Run (per-body only — this garment ships the per-body route):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_buttondown.py -- \
|
||||
tooling/scripts/blender/blender_author_buttondown.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/buttondown_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.009]
|
||||
|
||||
@@ -39,7 +39,7 @@ modelled — no 3D pocket geometry (texture carries identity).
|
||||
|
||||
Usage (cargo_pants reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_cargo_pants.py -- \
|
||||
tooling/scripts/blender/blender_author_cargo_pants.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/cargo_pants \
|
||||
[--bodies average_m,child,...] [--offset 0.013] [--hem-frac 1.0] \
|
||||
|
||||
@@ -52,7 +52,7 @@ bodies. Per-body mode only (offset shells author per body, Q-060).
|
||||
|
||||
Usage (jeans_modern reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_denim_pants.py -- \
|
||||
tooling/scripts/blender/blender_author_denim_pants.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/jeans_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.012] [--hem-frac 1.0] \
|
||||
|
||||
@@ -28,7 +28,7 @@ Hoodie deltas over the base t-shirt shell:
|
||||
PER-BODY ONLY (Q-060: offset-shells are authored per body, never SD-fit):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_hoodie.py -- \
|
||||
tooling/scripts/blender/blender_author_hoodie.py -- \
|
||||
client/assets/characters/bodies client/assets/characters/clothing/hoodie_modern \
|
||||
[--bodies a,b,c] [--offset M]
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ PER-BODY ONLY: offset-shell garments are authored per body (Q-060 route
|
||||
guidance) — there is no single-reference mode here.
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_jacket_shell.py -- \
|
||||
tooling/scripts/blender/blender_author_jacket_shell.py -- \
|
||||
<bodies_root> <out_dir> \
|
||||
[--bodies average_m,child,...] [--offset 0.018] [--wrist-frac 0.85]
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ boundaries give the waist opening and ankle hems for free. Per-body mode only
|
||||
|
||||
Usage (joggers_modern reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_joggers.py -- \
|
||||
tooling/scripts/blender/blender_author_joggers.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/joggers_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.014] [--mid-offset 0.010] \
|
||||
|
||||
@@ -30,7 +30,7 @@ offset-shell garments author per body; weights inherited by construction).
|
||||
|
||||
Usage:
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_lower_shell.py -- \
|
||||
tooling/scripts/blender/blender_author_lower_shell.py -- \
|
||||
<bodies_root> <out_dir> \
|
||||
[--bodies a,b,c] [--offset 0.010] [--leg-frac 1.0] \
|
||||
[--waistband-frac 1.0] [--cuff-frac 0.0] \
|
||||
|
||||
@@ -38,7 +38,7 @@ Painted albedo details (flat, toon-friendly; identity lives in the texture):
|
||||
Per-body only (this garment ships per-body per the Q-060 route guidance):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_offset_coverall.py -- \
|
||||
tooling/scripts/blender/blender_author_offset_coverall.py -- \
|
||||
<bodies_root> <out_dir> [--bodies a,b,c] [--offset 0.012]
|
||||
|
||||
Writes per body:
|
||||
|
||||
@@ -34,7 +34,7 @@ SINGLE-REFERENCE (default) — author on one body (average_m); G1
|
||||
derived/hand-authored garments that share one UV layout + one mask.
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_offset_shell.py -- \
|
||||
tooling/scripts/blender/blender_author_offset_shell.py -- \
|
||||
<bodies_dir>/average_m <out_dir> [--offset 0.020] [--sleeve-frac 0.40]
|
||||
|
||||
Writes:
|
||||
@@ -52,7 +52,7 @@ from each body's OWN bone landmarks using the same proportional ratios
|
||||
regions stay consistent across bodies.
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_offset_shell.py -- \
|
||||
tooling/scripts/blender/blender_author_offset_shell.py -- \
|
||||
<bodies_dir> <out_dir> --per-body \
|
||||
[--bodies average_m,child,...] [--offset 0.012] [--sleeve-frac 0.40]
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Modes mirror the base script:
|
||||
|
||||
PER-BODY (preferred for offset shells, Q-060):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_offset_shell_legs.py -- \
|
||||
tooling/scripts/blender/blender_author_offset_shell_legs.py -- \
|
||||
<bodies_dir> <out_dir> --per-body \
|
||||
[--bodies a,b,c] [--offset 0.012] [--hem-frac 0.78] [--waistband-frac 0.35]
|
||||
|
||||
@@ -47,7 +47,7 @@ PER-BODY (preferred for offset shells, Q-060):
|
||||
|
||||
SINGLE-REFERENCE:
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_offset_shell_legs.py -- \
|
||||
tooling/scripts/blender/blender_author_offset_shell_legs.py -- \
|
||||
<bodies_dir>/average_m <out_dir> [--offset 0.020] [...]
|
||||
|
||||
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house wardrobe).
|
||||
|
||||
@@ -26,7 +26,7 @@ What it adds over the base t-shirt shell:
|
||||
Per-body only (this family is the poster child for the Q-060 per-body route):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_outerwear.py -- \
|
||||
tooling/scripts/blender/blender_author_outerwear.py -- \
|
||||
<bodies_root> <out_dir> \
|
||||
[--bodies a,b,c] [--offset 0.022] [--cuff-keep 0.88]
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ top, combining the proven practices of the wave-1 family:
|
||||
Per-body only (Q-060: offset shells author per body, never SD-fit):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_parka.py -- \
|
||||
tooling/scripts/blender/blender_author_parka.py -- \
|
||||
client/assets/characters/bodies client/assets/characters/clothing/parka_thrds \
|
||||
[--bodies a,b,c] [--offset 0.026] [--hem-frac 0.15]
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ only (offset shells author per body, Q-060).
|
||||
|
||||
Usage (shoes_formal reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_shoes_formal.py -- \
|
||||
tooling/scripts/blender/blender_author_shoes_formal.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/shoes_formal \
|
||||
[--bodies average_m,child,...] [--offset 0.006] [--sole-mm 0.008] \
|
||||
|
||||
@@ -53,7 +53,7 @@ torso/leg companion provides, as reusable parameters:
|
||||
|
||||
Usage (per-body only — offset shells author per body, Q-060):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_slides.py -- \
|
||||
tooling/scripts/blender/blender_author_slides.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/slides \
|
||||
[--bodies average_m,child,...] [--offset 0.005] \
|
||||
|
||||
@@ -57,7 +57,7 @@ symmetric, so overlapping texels agree by construction.
|
||||
|
||||
Usage (sneakers_modern reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_sneakers.py -- \
|
||||
tooling/scripts/blender/blender_author_sneakers.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/sneakers_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.009] [--sole-drop 0.015] \
|
||||
|
||||
@@ -51,7 +51,7 @@ as reusable parameters:
|
||||
PER-BODY ONLY (Q-060: offset shells are authored per body, never SD-fit):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_sweater.py -- \
|
||||
tooling/scripts/blender/blender_author_sweater.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/sweater_modern \
|
||||
[--bodies average_m,child,...] [--offset 0.014] [--hem-drop 0.30] \
|
||||
|
||||
@@ -41,7 +41,7 @@ proportional-ratio philosophy as base.derive_thresholds. Per-body mode only
|
||||
|
||||
Usage (swim_trunks reference invocation):
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_swim_trunks.py -- \
|
||||
tooling/scripts/blender/blender_author_swim_trunks.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/swim_trunks \
|
||||
[--bodies average_m,child,...] [--offset 0.012] [--hem-frac 0.60] \
|
||||
|
||||
@@ -46,7 +46,7 @@ logo_uv TEXCOORD_1 layer ships for channel consistency (not logo-capable).
|
||||
Per-body mode only (offset shells author per body, Q-060):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_swimsuit.py -- \
|
||||
tooling/scripts/blender/blender_author_swimsuit.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/swimsuit_onepiece \
|
||||
[--bodies average_m,child,...] [--offset 0.012]
|
||||
|
||||
@@ -42,7 +42,7 @@ texture carries identity, flat toon-friendly).
|
||||
Run (per-body only — offset shells author per body, Q-060):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_tank_top.py -- \
|
||||
tooling/scripts/blender/blender_author_tank_top.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/tank_top \
|
||||
[--bodies average_m,child,...] [--offset 0.012] \
|
||||
|
||||
@@ -40,7 +40,7 @@ kit needs beyond that script is delivered as reusable parameters, not hacks:
|
||||
PER-BODY ONLY (Q-060: offset shells are authored per body, never SD-fit):
|
||||
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_author_track_jacket.py -- \
|
||||
tooling/scripts/blender/blender_author_track_jacket.py -- \
|
||||
client/assets/characters/bodies \
|
||||
client/assets/characters/clothing/track_jacket \
|
||||
[--bodies average_m,child,...] [--offset 0.018] [--wrist-keep 0.90]
|
||||
|
||||
@@ -26,7 +26,7 @@ Per body type:
|
||||
|
||||
Run:
|
||||
tooling/blender --background --python \
|
||||
tooling/garment-fit/blender_batch_fit_skinned.py -- \
|
||||
tooling/scripts/blender/blender_batch_fit_skinned.py -- \
|
||||
<reference_glb> <bodies_dir> <output_dir> \
|
||||
[--only average_m,average_f,...] [--solidify 0.0] [--self-check]
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Check if a hair mesh is symmetric across the X axis."""
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
if "--" not in argv:
|
||||
sys.exit(1)
|
||||
path = argv[argv.index("--") + 1]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=path)
|
||||
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type != 'MESH' or 'Hair' not in obj.name:
|
||||
continue
|
||||
verts = obj.data.vertices
|
||||
xs = [v.co.x for v in verts]
|
||||
zs = [v.co.z for v in verts]
|
||||
print(f"{obj.name}: {len(verts)} verts")
|
||||
print(f" X range: {min(xs):.4f} to {max(xs):.4f}")
|
||||
print(f" Z range: {min(zs):.4f} to {max(zs):.4f}")
|
||||
left = sum(1 for x in xs if x > 0.01)
|
||||
right = sum(1 for x in xs if x < -0.01)
|
||||
center = sum(1 for x in xs if abs(x) <= 0.01)
|
||||
print(f" left(+X): {left}, right(-X): {right}, center: {center}")
|
||||
|
||||
# Check normals for consistency
|
||||
normals = [v.normal for v in verts]
|
||||
outward = sum(1 for n in normals if n.length() > 0.5)
|
||||
print(f" verts with normals: {outward}/{len(verts)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""List all objects in a Quaternius .blend file."""
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
if __name__ == "__main__":
|
||||
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}")
|
||||
|
||||
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)}")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Convert a Quaternius outfit gltf to glb with optional solidify."""
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
CLOTHING_THICKNESS = 0.025 # 25mm outward solidify
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
if "--" not in argv:
|
||||
print("Usage: reach blender run blender_convert_outfit <input.gltf> <output.glb>")
|
||||
sys.exit(1)
|
||||
args = argv[argv.index("--") + 1:]
|
||||
input_path = args[0]
|
||||
output_path = args[1]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=input_path)
|
||||
|
||||
# Solidify clothing meshes for thickness
|
||||
meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH']
|
||||
for m in meshes:
|
||||
if m.name.startswith("Icosphere") or len(m.data.vertices) < 50:
|
||||
continue
|
||||
bpy.context.view_layer.objects.active = m
|
||||
m.select_set(True)
|
||||
# Recalculate normals for consistent outward direction
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.normals_make_consistent(inside=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
# Solidify
|
||||
sol = m.modifiers.new(name="Solidify", type='SOLIDIFY')
|
||||
sol.thickness = CLOTHING_THICKNESS
|
||||
sol.offset = 1.0 # grow outward only
|
||||
sol.use_rim = True
|
||||
bpy.ops.object.modifier_apply(modifier=sol.name)
|
||||
m.select_set(False)
|
||||
print(f" Solidified: {m.name} ({CLOTHING_THICKNESS*1000:.0f}mm)")
|
||||
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=output_path,
|
||||
use_selection=True,
|
||||
export_format='GLB',
|
||||
export_animations=False,
|
||||
export_yup=True,
|
||||
export_texcoords=True,
|
||||
export_normals=True,
|
||||
export_skins=True,
|
||||
export_materials='EXPORT',
|
||||
)
|
||||
print(f"Exported: {output_path}")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Inspect a GLB file's objects, vertex groups, and parent hierarchy."""
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
if "--" not in argv:
|
||||
print("Usage: reach blender run blender_inspect_glb <input.glb>")
|
||||
sys.exit(1)
|
||||
args = argv[argv.index("--") + 1:]
|
||||
input_path = args[0]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.gltf(filepath=input_path)
|
||||
|
||||
for obj in bpy.context.scene.objects:
|
||||
print(f"OBJ: {obj.name} type={obj.type} pos={tuple(round(v,3) for v in obj.location)}")
|
||||
if obj.type == 'MESH':
|
||||
vg_names = [vg.name for vg in obj.vertex_groups]
|
||||
print(f" vertex_groups ({len(vg_names)}): {vg_names[:10]}")
|
||||
print(f" parent: {obj.parent.name if obj.parent else 'None'}")
|
||||
print(f" verts: {len(obj.data.vertices)}")
|
||||
# Check skin modifier
|
||||
for mod in obj.modifiers:
|
||||
print(f" modifier: {mod.name} type={mod.type} object={mod.object.name if hasattr(mod, 'object') and mod.object else 'None'}")
|
||||
if obj.type == 'ARMATURE':
|
||||
bones = [b.name for b in obj.data.bones]
|
||||
print(f" bones ({len(bones)}): {bones[:8]}...")
|
||||
# Print head bone position if exists
|
||||
for b in obj.data.bones:
|
||||
if b.name == 'Head':
|
||||
print(f" Head bone head_local: {tuple(round(v,3) for v in b.head_local)}")
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Load original Quaternius body + hair as-is and export a combined GLB for visual inspection."""
|
||||
import sys
|
||||
import bpy
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
if "--" not in argv:
|
||||
print("Usage: reach blender run blender_quaternius_raw <output.glb>")
|
||||
sys.exit(1)
|
||||
output = argv[argv.index("--") + 1]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
# Load the Superhero Female body (original, unmodified)
|
||||
body_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Base Characters/Exports/Godot - UE/Superhero_Female_FullBody.gltf"
|
||||
bpy.ops.import_scene.gltf(filepath=body_path)
|
||||
|
||||
print("=== After body import ===")
|
||||
for obj in bpy.context.scene.objects:
|
||||
print(f" {obj.name} type={obj.type}")
|
||||
|
||||
# Load bob hair (Rigged to Head Bone, Female)
|
||||
hair_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Hairstyles/Rigged to Head Bone/glTF (Godot -Unreal)/Female/Hair_Bob.gltf"
|
||||
bpy.ops.import_scene.gltf(filepath=hair_path)
|
||||
|
||||
print("=== After hair import ===")
|
||||
for obj in bpy.context.scene.objects:
|
||||
print(f" {obj.name} type={obj.type} parent={obj.parent.name if obj.parent else 'None'}")
|
||||
|
||||
# Export everything as one GLB
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=output,
|
||||
use_selection=True,
|
||||
export_format='GLB',
|
||||
export_animations=False,
|
||||
export_yup=True,
|
||||
export_skins=True,
|
||||
export_materials='EXPORT',
|
||||
)
|
||||
print(f"Exported: {output}")
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Render the raw Quaternius body + hair from multiple angles to check for clipping."""
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import bpy
|
||||
|
||||
if __name__ == "__main__":
|
||||
argv = sys.argv
|
||||
if "--" not in argv:
|
||||
sys.exit(1)
|
||||
out_dir = argv[argv.index("--") + 1]
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
# Load body
|
||||
body_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Base Characters/Exports/Godot - UE/Superhero_Female_FullBody.gltf"
|
||||
bpy.ops.import_scene.gltf(filepath=body_path)
|
||||
|
||||
# Load bob hair
|
||||
hair_path = "/var/mnt/data/projects/settled-reach/main/docs/assets/downloads/Universal Base Characters[Source]/Hairstyles/Rigged to Head Bone/glTF (Godot -Unreal)/Female/Hair_Bob.gltf"
|
||||
bpy.ops.import_scene.gltf(filepath=hair_path)
|
||||
|
||||
# Remove icospheres
|
||||
for obj in list(bpy.context.scene.objects):
|
||||
if "Icosphere" in obj.name:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
# Setup camera
|
||||
cam_data = bpy.data.cameras.new("TestCam")
|
||||
cam_data.lens = 85
|
||||
cam = bpy.data.objects.new("TestCam", cam_data)
|
||||
bpy.context.scene.collection.objects.link(cam)
|
||||
bpy.context.scene.camera = cam
|
||||
|
||||
# Light
|
||||
light_data = bpy.data.lights.new("TestLight", type='SUN')
|
||||
light_data.energy = 3.0
|
||||
light = bpy.data.objects.new("TestLight", light_data)
|
||||
light.rotation_euler = (math.radians(45), 0, math.radians(30))
|
||||
bpy.context.scene.collection.objects.link(light)
|
||||
|
||||
# Render settings
|
||||
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||
bpy.context.scene.render.resolution_x = 800
|
||||
bpy.context.scene.render.resolution_y = 600
|
||||
bpy.context.scene.render.film_transparent = True
|
||||
|
||||
# Head center (approx)
|
||||
target = (0.0, 0.0, 1.65)
|
||||
dist = 0.5
|
||||
|
||||
angles = {
|
||||
"front": 0,
|
||||
"right": 90,
|
||||
"back": 180,
|
||||
"left": 270,
|
||||
}
|
||||
|
||||
for name, deg in angles.items():
|
||||
rad = math.radians(deg)
|
||||
cx = target[0] + dist * math.sin(rad)
|
||||
cy = target[1] - dist * math.cos(rad)
|
||||
cz = target[2]
|
||||
cam.location = (cx, cy, cz)
|
||||
# Point at target
|
||||
direction = (target[0] - cx, target[1] - cy, target[2] - cz)
|
||||
rot_z = math.atan2(direction[0], -direction[1])
|
||||
rot_x = math.atan2(math.sqrt(direction[0]**2 + direction[1]**2), direction[2])
|
||||
cam.rotation_euler = (rot_x, 0, rot_z)
|
||||
|
||||
filepath = os.path.join(out_dir, f"raw_bob_{name}.png")
|
||||
bpy.context.scene.render.filepath = filepath
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print(f"Rendered: {filepath}")
|
||||
Reference in New Issue
Block a user