# Tailor measurements from a Quaternius-skinned body GLB.
# Slices the mesh at scanned heights, restricted to vertices whose dominant
# vertex group is in an allowed bone set (so arms don't inflate the chest).
# Run: blender --background --python measure_body.py --
import bpy, sys, json, math
argv = sys.argv[sys.argv.index("--") + 1:]
SRC, OUT = argv[0], argv[1]
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=SRC)
mesh_obj = max((o for o in bpy.data.objects if o.type == "MESH"),
key=lambda o: len(o.data.vertices))
arm_obj = next(o for o in bpy.data.objects if o.type == "ARMATURE")
# --- world-space bone head positions (rest pose) ---
bones = {}
for b in arm_obj.data.bones:
bones[b.name] = arm_obj.matrix_world @ b.head_local
# --- world-space vertex positions + dominant vertex group ---
mw = mesh_obj.matrix_world
group_name = {g.index: g.name for g in mesh_obj.vertex_groups}
verts = [] # (co_world, dominant_group_name)
for v in mesh_obj.data.vertices:
best, bw = None, 0.0
for g in v.groups:
if g.weight > bw:
bw, best = g.weight, group_name.get(g.group)
verts.append((mw @ v.co, best))
edges = [(e.vertices[0], e.vertices[1]) for e in mesh_obj.data.edges]
def hull_perimeter(pts):
"""2D convex hull (monotone chain) perimeter."""
pts = sorted(set(pts))
if len(pts) < 3:
return 0.0
def cross(o, a, b):
return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])
lower, upper = [], []
for p in pts:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)
for p in reversed(pts):
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
upper.pop()
upper.append(p)
hull = lower[:-1] + upper[:-1]
return sum(math.dist(hull[i], hull[(i+1) % len(hull)]) for i in range(len(hull)))
def slice_circ(z, allowed):
"""Perimeter of the mesh cross-section at height z, using edges whose
both endpoints' dominant group is in `allowed` (prefix match)."""
def ok(g):
return g is not None and any(g.startswith(a) for a in allowed)
pts = []
for i0, i1 in edges:
c0, g0 = verts[i0]
c1, g1 = verts[i1]
if not (ok(g0) and ok(g1)):
continue
if (c0.z - z) * (c1.z - z) < 0:
t = (z - c0.z) / (c1.z - c0.z)
pts.append((c0.x + t*(c1.x-c0.x), c0.y + t*(c1.y-c0.y)))
return hull_perimeter(pts)
def scan(z0, z1, allowed, mode, steps=40):
"""Max or min nonzero circumference in [z0,z1]."""
best_c, best_z = (0.0, None) if mode == "max" else (1e9, None)
for i in range(steps + 1):
z = z0 + (z1 - z0) * i / steps
c = slice_circ(z, allowed)
if c <= 0:
continue
if (mode == "max" and c > best_c) or (mode == "min" and c < best_c):
best_c, best_z = c, z
return best_c, best_z
TORSO = ["pelvis", "spine_", "neck_", "clavicle_", "Breast"]
HIPZONE = ["pelvis", "spine_01", "thigh_"]
z_pelvis = bones["pelvis"].z
z_neck = bones["neck_01"].z
z_thigh = bones["thigh_l"].z
z_calf = bones["calf_l"].z
z_foot = bones["foot_l"].z
zmin = min(c.z for c, _ in verts)
zmax = max(c.z for c, _ in verts)
chest, z_chest = scan(z_pelvis + 0.55*(z_neck - z_pelvis), z_neck - 0.05*(z_neck - z_pelvis), TORSO, "max")
waist, z_waist = scan(z_pelvis + 0.05, z_chest - 0.03, TORSO, "min")
hip, z_hip = scan(z_thigh, z_pelvis + 0.06, HIPZONE, "max")
thigh, z_th = scan(z_thigh - 0.6*(z_thigh - z_calf), z_thigh - 0.05*(z_thigh - z_calf), ["thigh_l"], "max")
neckc, z_nk = scan(z_neck, z_neck + 0.5*(zmax - z_neck), ["neck_", "Head", "spine_03", "clavicle_"], "min")
# upper arm circumference (sleeve): slice perpendicular-ish — arms in T-pose are
# horizontal, so slice by X instead of Z for the left arm.
def slice_circ_x(x, allowed):
def ok(g):
return g is not None and any(g.startswith(a) for a in allowed)
pts = []
for i0, i1 in edges:
c0, g0 = verts[i0]
c1, g1 = verts[i1]
if not (ok(g0) and ok(g1)):
continue
if (c0.x - x) * (c1.x - x) < 0:
t = (x - c0.x) / (c1.x - c0.x)
pts.append((c0.y + t*(c1.y-c0.y), c0.z + t*(c1.z-c0.z)))
return hull_perimeter(pts)
x_ua = bones["upperarm_l"].x
x_la = bones["lowerarm_l"].x
bicep = 0.0
xb = None
for i in range(21):
x = x_ua + (x_la - x_ua) * (0.15 + 0.5 * i / 20)
c = slice_circ_x(x, ["upperarm_l"])
if c > bicep:
bicep, xb = c, x
d = math.dist
m = {
"source": SRC,
"units": "meters (glTF)",
"height_total": zmax - zmin,
"chest_circ": chest, "chest_z": z_chest,
"waist_circ": waist, "waist_z": z_waist,
"hip_circ": hip, "hip_z": z_hip,
"thigh_circ": thigh, "thigh_z": z_th,
"neck_circ": neckc, "neck_z": z_nk,
"bicep_circ": bicep,
"shoulder_width": d(bones["upperarm_l"], bones["upperarm_r"]),
"arm_len_shoulder_to_wrist": d(bones["upperarm_l"], bones["lowerarm_l"]) + d(bones["lowerarm_l"], bones["hand_l"]),
"nape_to_pelvis": z_neck - z_pelvis,
"crotch_height": z_thigh,
"pelvis_height": z_pelvis,
"knee_height": z_calf,
"ankle_height": z_foot,
"shoulder_z": bones["clavicle_l"].z,
}
with open(OUT, "w") as f:
json.dump({k: (round(v, 4) if isinstance(v, float) else v) for k, v in m.items()}, f, indent=2)
print("WROTE", OUT)