#!/usr/bin/env python3 """Generate the static (unskinned) male test body fixtures. Same procedural boxes as scripts/generate_test_bodies.py in their bind pose, but WITHOUT a skin or skeleton: * static-body-male.obj + .mtl + static-body-albedo.png (OBJ/MTL/PNG). Boxes whose center is above 1.2 m sample the top (red) half of the texture, lower boxes the bottom (blue) half, so UV orientation is testable. * static-body-male.glb: unskinned glTF. The mesh sits under a parent node (translation + uniform scale 2) and a child node (rotation 90 deg about Y); vertices/normals are stored pre-transformed so the baked world geometry equals the skinned body's bind pose. One node animation is included and must NOT be applied by the static loader. Run from the repository root: python3 scripts/generate_static_bodies.py """ from __future__ import annotations import math import struct import sys import zlib from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import generate_test_bodies as gen # noqa: E402 OUT = Path("tests/fixtures/bodies/static") PARENT_T = (0.25, -0.1, 0.4) PARENT_S = 2.0 RED = (220, 40, 30, 255) BLUE = (30, 60, 220, 255) def png_rgba(width: int, height: int, rows: list[list[tuple[int, int, int, int]]]) -> bytes: raw = b"".join(b"\x00" + bytes(c for px in row for c in px) for row in rows) def chunk(kind: bytes, data: bytes) -> bytes: return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF) ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"") def boxes(sex: str): """Yield (triangles, face normals, uv v) per box in world bind pose.""" spec = gen.skeleton(sex) for joint_index, (name, _p, _l, size) in enumerate(spec): center = gen.world_translation(spec, name) positions: list[float] = [] indices: list[int] = [] gen.add_box(positions, indices, [], [], center, size, joint_index) corners = [tuple(positions[i:i + 3]) for i in range(0, len(positions), 3)] tris = [tuple(indices[i:i + 3]) for i in range(0, len(indices), 3)] v = 0.85 if center[1] > 1.2 else 0.15 yield corners, tris, v def face_normal(a, b, c): u = [b[i] - a[i] for i in range(3)] w = [c[i] - a[i] for i in range(3)] n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]] length = math.sqrt(sum(x * x for x in n)) return tuple(x / length for x in n) def write_obj(sex: str) -> None: lines = [ "# Static CC0 test body (no skeleton). Generated by scripts/generate_static_bodies.py", "mtllib static-body-male.mtl", "usemtl body", ] vt = ["vt 0.5 0.85", "vt 0.5 0.15"] lines += vt v_count = 0 n_count = 0 faces = [] for corners, tris, v in boxes(sex): for c in corners: lines.append("v %.6f %.6f %.6f" % c) uv = 1 if v > 0.5 else 2 for t in tris: n = face_normal(corners[t[0]], corners[t[1]], corners[t[2]]) lines.append("vn %.6f %.6f %.6f" % n) n_count += 1 faces.append("f " + " ".join(f"{v_count + i + 1}/{uv}/{n_count}" for i in t)) v_count += len(corners) lines += faces (OUT / "static-body-male.obj").write_text("\n".join(lines) + "\n") (OUT / "static-body-male.mtl").write_text( "# CC0 static test body material\nnewmtl body\nKd 1.0 1.0 1.0\nd 1.0\nmap_Kd static-body-albedo.png\n" ) rows = [[RED] * 4, [RED] * 4, [BLUE] * 4, [BLUE] * 4] (OUT / "static-body-albedo.png").write_bytes(png_rgba(4, 4, rows)) def rot_y(deg: float, p): a = math.radians(deg) c, s = math.cos(a), math.sin(a) return (c * p[0] + s * p[2], p[1], -s * p[0] + c * p[2]) def write_glb(sex: str) -> None: positions: list[float] = [] normals: list[float] = [] indices: list[int] = [] for corners, tris, _v in boxes(sex): for t in tris: a, b, c = (corners[i] for i in t) n = face_normal(a, b, c) for p in (a, b, c): local = tuple((p[i] - PARENT_T[i]) / PARENT_S for i in range(3)) positions.extend(rot_y(-90.0, local)) normals.extend(rot_y(-90.0, n)) indices.append(len(indices)) blob = gen.Blob() index_acc = blob.add_u16(indices, "SCALAR", len(indices)) pmin, pmax = gen.vec_min_max(positions, 3) pos_acc = blob.add_f32(positions, "VEC3", len(positions) // 3, pmin, pmax) nrm_acc = blob.add_f32(normals, "VEC3", len(normals) // 3) times = blob.add_f32([0.0, 1.0], "SCALAR", 2, [0.0], [1.0]) half = math.radians(45.0) rot_out = blob.add_f32([0, 0, 0, 1, 0, math.sin(half), 0, math.cos(half)], "VEC4", 2) half90 = math.radians(45.0) gltf = { "asset": { "version": "2.0", "generator": "scripts/generate_static_bodies.py", "extras": {"testAsset": True, "license": "CC0-1.0", "skin": False}, }, "scene": 0, "scenes": [{"name": "static-body", "nodes": [0]}], "nodes": [ {"name": "static-root", "translation": list(PARENT_T), "scale": [PARENT_S] * 3, "children": [1]}, {"name": "static-body-mesh", "rotation": [0.0, math.sin(half90), 0.0, math.cos(half90)], "mesh": 0}, ], "meshes": [{"name": "static-body", "primitives": [{ "attributes": {"POSITION": pos_acc, "NORMAL": nrm_acc}, "indices": index_acc, "material": 0}]}], "materials": [{"name": "static-body", "pbrMetallicRoughness": { "baseColorFactor": [0.45, 0.55, 0.70, 1.0], "metallicFactor": 0.0, "roughnessFactor": 1.0}}], "animations": [{"name": "spin-not-applied", "channels": [{"sampler": 0, "target": {"node": 0, "path": "rotation"}}], "samplers": [{"input": times, "output": rot_out, "interpolation": "LINEAR"}]}], "buffers": [{"byteLength": len(blob.data)}], "bufferViews": blob.views, "accessors": blob.accessors, } (OUT / "static-body-male.glb").write_bytes(gen.pack_glb(gltf, bytes(blob.data))) def main() -> None: OUT.mkdir(parents=True, exist_ok=True) write_obj("male") write_glb("male") for p in sorted(OUT.iterdir()): print(f"wrote {p} ({p.stat().st_size} bytes)") if __name__ == "__main__": main()