# Skull shell + hair-fit numbers for any 65-joint Quaternius GLB/glTF. # # The game rebinds Quaternius hair (100% rigid to the single `Head` bone) onto whatever body # loaded, correcting it with CharacterBuildData.HeadFit = a diagonal scale about the Head rest # pivot plus a bind translation. Those constants are only valid against the skull they were # tuned on, and they go stale silently every time a body is rebaked. This measures the skull # they SHOULD be tuned against, and predicts where a given hair style's crown actually lands. # # python tools/measure_head_shell.py [ ...] # python tools/measure_head_shell.py --hair --on --scale X,Y,Z --offset X,Y,Z # # Two shell measures are printed. The FULL Head-weighted shell is what Ozan's MakoHeadFit # comment quotes; it is inflated by jaw/ear/neck weight bleed and is NOT the surface a hair cap # sits on. The CRANIUM band (Head-weighted verts within 8 cm of the skull top) is the honest # one — use it for any scale ratio. # # See docs/characters/head-features-contract.md. import json, struct, sys, os, array CT = {5120: ('b', 1), 5121: ('B', 1), 5122: ('h', 2), 5123: ('H', 2), 5125: ('I', 4), 5126: ('f', 4)} NW = {'SCALAR': 1, 'VEC2': 2, 'VEC3': 3, 'VEC4': 4, 'MAT4': 16} def load(path): if path.endswith('.glb'): d = open(path, 'rb').read() n = struct.unpack_from('= weight_min: pts.append(p) if not pts: raise SystemExit(f"{os.path.basename(path)}: no Head-weighted verts") return pivot, pts, total def _pct(v, p): v = sorted(v) k = (len(v) - 1) * p f = int(k) return v[f] + (v[min(f + 1, len(v) - 1)] - v[f]) * (k - f) def landmarks(path, slab=0.01): """Two robust cranium landmarks in Head-pivot-relative metres, plus spans at the wider one. `top` = skull crown. `wide` = the height where the cranium is widest (temple/ear line). Two landmarks is exactly what a scale-about-pivot + translate has degrees of freedom for, per axis. """ pivot, pts, _ = head_points(path) ys = [p[1] for p in pts] top = _pct(ys, 0.999) best = None y = top while y > top - 0.16: # search the top 16 cm — cranium, never the jaw slabpts = [p for p in pts if y - slab <= p[1] < y] if len(slabpts) >= 8: xs = [p[0] for p in slabpts] span = _pct(xs, 0.98) - _pct(xs, 0.02) if best is None or span > best[1]: zs = [p[2] for p in slabpts] best = (y - slab / 2, span, _pct(zs, 0.98) - _pct(zs, 0.02), (_pct(zs, 0.98) + _pct(zs, 0.02)) / 2) y -= slab if best is None: raise SystemExit(f"{os.path.basename(path)}: cranium slabs too sparse") wide_y, wide_w, wide_d, wide_zc = best return { 'name': os.path.basename(path), 'pivot_y': pivot[1], 'top': top - pivot[1], 'wide': wide_y - pivot[1], 'wide_w': wide_w, 'wide_d': wide_d, 'wide_zc': wide_zc, } def fit(stock_path, body_path): """Derive HeadScale / HairOffset that carry the stock cranium onto this body's cranium. Runtime maps an authored vert at Head-local h to shipPivot + h*scale + offset, so two matched landmarks per axis determine both constants exactly — no bed guessing. """ s, t = landmarks(stock_path), landmarks(body_path) sy = (t['top'] - t['wide']) / (s['top'] - s['wide']) oy = t['top'] - s['top'] * sy sx = t['wide_w'] / s['wide_w'] sz = t['wide_d'] / s['wide_d'] oz = t['wide_zc'] - s['wide_zc'] * sz for d in (s, t): print(f" {d['name']:42s} top {d['top']*100:6.2f} wide-line {d['wide']*100:6.2f}" f" w {d['wide_w']*100:6.2f} d {d['wide_d']*100:6.2f} (cm above pivot)") print(f" HeadScale = new Vector3({sx:.3f}f, {sy:.3f}f, {sz:.3f}f)") print(f" HairOffset = new Vector3(0f, {oy:.4f}f, {oz:.4f}f)") return (sx, sy, sz), (0.0, oy, oz) def measure(path): pivot, pts, total = head_points(path) ys = [p[1] for p in pts] top, chin = max(ys), min(ys) band = [p for p in pts if p[1] >= top - CRANIUM_BAND_M] def span(v): return (max(v) - min(v)) * 100 print(f"{os.path.basename(path):46s} headverts {len(pts):6d}/{total}") print(f" Head pivot Y {pivot[1]:.4f} skull top Y {top:.4f} chin Y {chin:.4f}") print(f" full shell w {span([p[0] for p in pts]):6.2f} d {span([p[2] for p in pts]):6.2f} (cm)") print(f" cranium band w {span([p[0] for p in band]):6.2f} d {span([p[2] for p in band]):6.2f}" f" n={len(band)} <- use THIS for scale ratios") print(f" skull top above pivot {(top - pivot[1]) * 100:6.2f} cm" f" pivot above chin {(pivot[1] - chin) * 100:6.2f} cm") def predict(hair_path, body_path, stock_path, scale, offset, terse=False): """Does this style land on this body the way it was authored to land on the stock head? Runtime (HeadAccessoryBindMath.RestToWorldStepwise) on a vert rigid to Head: world = bodyHeadRest + ScaleAbout(authoredLocal, 0, headScale) + offset The crown's ABSOLUTE height over the skull is not the test — a mohawk stands proud and a beard hangs at the chin by design. The test is whether the style keeps the clearance it was drawn with on its own authoring rig. """ hpivot, hpts, _ = head_points(hair_path) bpivot, bpts, _ = head_points(body_path) spivot, spts, _ = head_points(stock_path) crown_local = max(p[1] for p in hpts) - hpivot[1] authored = crown_local - (_pct([p[1] for p in spts], 0.999) - spivot[1]) skull_top = _pct([p[1] for p in bpts], 0.999) landed = bpivot[1] + crown_local * scale[1] + offset[1] got = landed - skull_top err = (got - authored) * 100 verdict = "ok" if abs(err) <= 1.0 else ("HIGH" if err > 0 else "LOW") if terse: print(f" {os.path.basename(hair_path):26s} authored {authored*100:+6.2f} " f"got {got*100:+6.2f} err {err:+6.2f} cm {verdict}") return err print(f"{os.path.basename(hair_path)} on {os.path.basename(body_path)}") print(f" authored crown {crown_local*100:6.2f} cm above its pivot" f" = {authored*100:+.2f} cm clearance over the stock skull") print(f" scale {scale} offset {offset}") print(f" lands Y {landed:.4f} skull top Y {skull_top:.4f}" f" = {got*100:+.2f} cm clearance") print(f" error vs authored {err:+6.2f} cm {verdict}") return err def sweep(stock_path, body_path, hair_dir, scale, offset): print(f" {'style':26s} {'authored':>8s} {'got':>8s} {'err':>10s}") errs = [] for f in sorted(os.listdir(hair_dir)): if not f.startswith('Hair_') or not f.endswith('.gltf') or 'Teen' in f: continue errs.append(abs(predict(os.path.join(hair_dir, f), body_path, stock_path, scale, offset, terse=True))) print(f" -> worst {max(errs):.2f} cm, mean {sum(errs)/len(errs):.2f} cm over {len(errs)} styles") def _vec(s): return tuple(float(v) for v in s.split(',')) if __name__ == '__main__': a = sys.argv[1:] sc = _vec(a[a.index('--scale') + 1]) if '--scale' in a else (1, 1, 1) off = _vec(a[a.index('--offset') + 1]) if '--offset' in a else (0, 0, 0) if '--fit' in a: fit(a[a.index('--fit') + 1], a[a.index('--on') + 1]) elif '--sweep' in a: sweep(a[a.index('--stock') + 1], a[a.index('--on') + 1], a[a.index('--sweep') + 1], sc, off) elif '--hair' in a: predict(a[a.index('--hair') + 1], a[a.index('--on') + 1], a[a.index('--stock') + 1], sc, off) else: for p in a: measure(p)