BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / scripts / generate_static_bodies.py

generate_static_bodies.py

generate_static_bodies.py 6.4 KiB Download Raw
1#!/usr/bin/env python3
2"""Generate the static (unskinned) male test body fixtures.
3
4Same procedural boxes as scripts/generate_test_bodies.py in their bind pose,
5but WITHOUT a skin or skeleton:
6
7* static-body-male.obj + .mtl + static-body-albedo.png (OBJ/MTL/PNG). Boxes
8 whose center is above 1.2 m sample the top (red) half of the texture,
9 lower boxes the bottom (blue) half, so UV orientation is testable.
10* static-body-male.glb: unskinned glTF. The mesh sits under a parent node
11 (translation + uniform scale 2) and a child node (rotation 90 deg about Y);
12 vertices/normals are stored pre-transformed so the baked world geometry
13 equals the skinned body's bind pose. One node animation is included and
14 must NOT be applied by the static loader.
15
16Run from the repository root:
17
18 python3 scripts/generate_static_bodies.py
19"""
20
21from __future__ import annotations
22
23import math
24import struct
25import sys
26import zlib
27from pathlib import Path
28
29sys.path.insert(0, str(Path(__file__).resolve().parent))
30import generate_test_bodies as gen # noqa: E402
31
32OUT = Path("tests/fixtures/bodies/static")
33PARENT_T = (0.25, -0.1, 0.4)
34PARENT_S = 2.0
35RED = (220, 40, 30, 255)
36BLUE = (30, 60, 220, 255)
37
38
39def png_rgba(width: int, height: int, rows: list[list[tuple[int, int, int, int]]]) -> bytes:
40 raw = b"".join(b"\x00" + bytes(c for px in row for c in px) for row in rows)
41 def chunk(kind: bytes, data: bytes) -> bytes:
42 return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
43 ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
44 return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")
45
46
47def boxes(sex: str):
48 """Yield (triangles, face normals, uv v) per box in world bind pose."""
49 spec = gen.skeleton(sex)
50 for joint_index, (name, _p, _l, size) in enumerate(spec):
51 center = gen.world_translation(spec, name)
52 positions: list[float] = []
53 indices: list[int] = []
54 gen.add_box(positions, indices, [], [], center, size, joint_index)
55 corners = [tuple(positions[i:i + 3]) for i in range(0, len(positions), 3)]
56 tris = [tuple(indices[i:i + 3]) for i in range(0, len(indices), 3)]
57 v = 0.85 if center[1] > 1.2 else 0.15
58 yield corners, tris, v
59
60
61def face_normal(a, b, c):
62 u = [b[i] - a[i] for i in range(3)]
63 w = [c[i] - a[i] for i in range(3)]
64 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]]
65 length = math.sqrt(sum(x * x for x in n))
66 return tuple(x / length for x in n)
67
68
69def write_obj(sex: str) -> None:
70 lines = [
71 "# Static CC0 test body (no skeleton). Generated by scripts/generate_static_bodies.py",
72 "mtllib static-body-male.mtl",
73 "usemtl body",
74 ]
75 vt = ["vt 0.5 0.85", "vt 0.5 0.15"]
76 lines += vt
77 v_count = 0
78 n_count = 0
79 faces = []
80 for corners, tris, v in boxes(sex):
81 for c in corners:
82 lines.append("v %.6f %.6f %.6f" % c)
83 uv = 1 if v > 0.5 else 2
84 for t in tris:
85 n = face_normal(corners[t[0]], corners[t[1]], corners[t[2]])
86 lines.append("vn %.6f %.6f %.6f" % n)
87 n_count += 1
88 faces.append("f " + " ".join(f"{v_count + i + 1}/{uv}/{n_count}" for i in t))
89 v_count += len(corners)
90 lines += faces
91 (OUT / "static-body-male.obj").write_text("\n".join(lines) + "\n")
92 (OUT / "static-body-male.mtl").write_text(
93 "# CC0 static test body material\nnewmtl body\nKd 1.0 1.0 1.0\nd 1.0\nmap_Kd static-body-albedo.png\n"
94 )
95 rows = [[RED] * 4, [RED] * 4, [BLUE] * 4, [BLUE] * 4]
96 (OUT / "static-body-albedo.png").write_bytes(png_rgba(4, 4, rows))
97
98
99def rot_y(deg: float, p):
100 a = math.radians(deg)
101 c, s = math.cos(a), math.sin(a)
102 return (c * p[0] + s * p[2], p[1], -s * p[0] + c * p[2])
103
104
105def write_glb(sex: str) -> None:
106 positions: list[float] = []
107 normals: list[float] = []
108 indices: list[int] = []
109 for corners, tris, _v in boxes(sex):
110 for t in tris:
111 a, b, c = (corners[i] for i in t)
112 n = face_normal(a, b, c)
113 for p in (a, b, c):
114 local = tuple((p[i] - PARENT_T[i]) / PARENT_S for i in range(3))
115 positions.extend(rot_y(-90.0, local))
116 normals.extend(rot_y(-90.0, n))
117 indices.append(len(indices))
118 blob = gen.Blob()
119 index_acc = blob.add_u16(indices, "SCALAR", len(indices))
120 pmin, pmax = gen.vec_min_max(positions, 3)
121 pos_acc = blob.add_f32(positions, "VEC3", len(positions) // 3, pmin, pmax)
122 nrm_acc = blob.add_f32(normals, "VEC3", len(normals) // 3)
123 times = blob.add_f32([0.0, 1.0], "SCALAR", 2, [0.0], [1.0])
124 half = math.radians(45.0)
125 rot_out = blob.add_f32([0, 0, 0, 1, 0, math.sin(half), 0, math.cos(half)], "VEC4", 2)
126 half90 = math.radians(45.0)
127 gltf = {
128 "asset": {
129 "version": "2.0",
130 "generator": "scripts/generate_static_bodies.py",
131 "extras": {"testAsset": True, "license": "CC0-1.0", "skin": False},
132 },
133 "scene": 0,
134 "scenes": [{"name": "static-body", "nodes": [0]}],
135 "nodes": [
136 {"name": "static-root", "translation": list(PARENT_T), "scale": [PARENT_S] * 3, "children": [1]},
137 {"name": "static-body-mesh", "rotation": [0.0, math.sin(half90), 0.0, math.cos(half90)], "mesh": 0},
138 ],
139 "meshes": [{"name": "static-body", "primitives": [{
140 "attributes": {"POSITION": pos_acc, "NORMAL": nrm_acc},
141 "indices": index_acc, "material": 0}]}],
142 "materials": [{"name": "static-body", "pbrMetallicRoughness": {
143 "baseColorFactor": [0.45, 0.55, 0.70, 1.0], "metallicFactor": 0.0, "roughnessFactor": 1.0}}],
144 "animations": [{"name": "spin-not-applied", "channels": [{"sampler": 0, "target": {"node": 0, "path": "rotation"}}],
145 "samplers": [{"input": times, "output": rot_out, "interpolation": "LINEAR"}]}],
146 "buffers": [{"byteLength": len(blob.data)}],
147 "bufferViews": blob.views,
148 "accessors": blob.accessors,
149 }
150 (OUT / "static-body-male.glb").write_bytes(gen.pack_glb(gltf, bytes(blob.data)))
151
152
153def main() -> None:
154 OUT.mkdir(parents=True, exist_ok=True)
155 write_obj("male")
156 write_glb("male")
157 for p in sorted(OUT.iterdir()):
158 print(f"wrote {p} ({p.stat().st_size} bytes)")
159
160
161if __name__ == "__main__":
162 main()