Merge feature/as-is-gltf-front: as-is loading, glTF +Z front convention
@@ -23,21 +23,13 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert
23
23
24
24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. **Omhullende animaties** berekent eenmalig een renderhoogte en Centrum Y waarbinnen alle clips, alle frames (op project-fps) en alle 60 hoeken vallen, met 5% marge aan elke kant; de pivot blijft staan. Daarna is het kader weer vast: er is geen automatische zoom per frame, clip of laag. Equipment dat verder uitsteekt dan het lichaam vraagt handmatig extra hoogte. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.
25
25
- Kies een animatieclip en gebruik **Pauze / Speel**.
26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z). Vaste oriëntatie (+Y omhoog, het lichaam kijkt naar −Z, zijn rechterzijde is +X): yaw 0 = voorkant (zijn rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.
26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van elk model. Vaste oriëntatie volgens de glTF-standaard (+Y omhoog, de voorkant kijkt naar +Z, de linkerzijde is +X); modellen en animaties worden as-is geladen, zonder automatische correctie: yaw 0 = voorkant (de rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.
27
27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.
28
28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.
29
29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.
30
30
31
31

32
32
33
### Automatische correcties bij het laden
34
35
Na het laden controleert de editor elk lichaam en toont gevonden correcties als blauwe regel *Automatische correctie: …* in het body-scherm. De correcties gelden in de live preview, stills, productie en voor equipment:
36
37
- **Kijkrichting.** De renderer gaat uit van een lichaam dat naar −Z kijkt (yaw 0 = voorkant). Steken de voeten (onderste 4% van de lichaamshoogte) duidelijk verder naar +Z uit dan de enkels (6–10%), zoals bij de glTF-standaard, dan wordt het lichaam 180° om Y gedraaid. Bij minder dan 1000 vertices (blokvormige proxy's) of een onduidelijk verschil blijft de oriëntatie zoals in de bron.
38
- **Losse voeten.** Bij IK-rigs hangen de voetbotten soms onder de root in plaats van onder het onderbeen. Begint zo'n voetketen precies aan het eind van een geanimeerd eindbot (bijvoorbeeld `calf`), en heeft de keten zelf geen animatie, dan beweegt hij star mee met dat bot, zoals bij gewone FK.
39
- **Voor/achter gespiegelde beweging.** Buigen de knieën in vrijwel alle gemeten standen (minstens 4, en meer dan 3× zo vaak verkeerd als goed) naar achteren ten opzichte van de voorkant van het lichaam, dan wordt de beweging voor/achter gespiegeld rond de rustpose. De rustpose zelf verandert niet.
40
41
33
### Meerdere meshes en skins (glTF/GLB)
42
34
43
35
Een geskind glTF/GLB-lichaam wordt volledig geladen: alle mesh-nodes en alle skins. De joints van alle skins worden samengevoegd tot één skelet (joints die in meerdere skins voorkomen tellen één keer; maximaal 128). Een mesh-node zonder skin beweegt star mee met de dichtstbijzijnde voorouder die een joint is; heeft hij die niet, dan hangt hij aan de eerste joint. Zijn rustpositie in de scène blijft daarbij exact behouden (gespiegelde transformaties draaien de driehoeksvolgorde om). Materialen blijven per primitive: basiskleur, albedotexture, alpha-cutoff en vertexkleuren. `KHR_texture_transform` op de basiskleurtexture (offset, rotatie, schaal en `texCoord`) wordt bij het laden één keer in de UV's verwerkt. De sampler-wrapmodus wordt niet gelezen: textures worden altijd met nearest en herhalen (REPEAT) bemonsterd, dus CLAMP/MIRROR geven buiten het UV-bereik 0–1 een ander beeld. Metallic/roughness-, normal-, occlusion- en emissive-textures en morph targets worden niet gebruikt; de normal map komt uit de geometrie en de specular-map van een body is nul (metallic/roughness wordt niet omgerekend; een scalaire specular kan PBR niet fysiek nabootsen).
@@ -23,21 +23,13 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert23
23
24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. **Omhullende animaties** berekent eenmalig een renderhoogte en Centrum Y waarbinnen alle clips, alle frames (op project-fps) en alle 60 hoeken vallen, met 5% marge aan elke kant; de pivot blijft staan. Daarna is het kader weer vast: er is geen automatische zoom per frame, clip of laag. Equipment dat verder uitsteekt dan het lichaam vraagt handmatig extra hoogte. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. **Omhullende animaties** berekent eenmalig een renderhoogte en Centrum Y waarbinnen alle clips, alle frames (op project-fps) en alle 60 hoeken vallen, met 5% marge aan elke kant; de pivot blijft staan. Daarna is het kader weer vast: er is geen automatische zoom per frame, clip of laag. Equipment dat verder uitsteekt dan het lichaam vraagt handmatig extra hoogte. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.25
- Kies een animatieclip en gebruik **Pauze / Speel**.25
- Kies een animatieclip en gebruik **Pauze / Speel**.26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z). Vaste oriëntatie (+Y omhoog, het lichaam kijkt naar −Z, zijn rechterzijde is +X): yaw 0 = voorkant (zijn rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van elk model. Vaste oriëntatie volgens de glTF-standaard (+Y omhoog, de voorkant kijkt naar +Z, de linkerzijde is +X); modellen en animaties worden as-is geladen, zonder automatische correctie: yaw 0 = voorkant (de rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.30
30
31
31
32
32
33
### Automatische correcties bij het laden34
35
Na het laden controleert de editor elk lichaam en toont gevonden correcties als blauwe regel *Automatische correctie: …* in het body-scherm. De correcties gelden in de live preview, stills, productie en voor equipment:36
37
- **Kijkrichting.** De renderer gaat uit van een lichaam dat naar −Z kijkt (yaw 0 = voorkant). Steken de voeten (onderste 4% van de lichaamshoogte) duidelijk verder naar +Z uit dan de enkels (6–10%), zoals bij de glTF-standaard, dan wordt het lichaam 180° om Y gedraaid. Bij minder dan 1000 vertices (blokvormige proxy's) of een onduidelijk verschil blijft de oriëntatie zoals in de bron.38
- **Losse voeten.** Bij IK-rigs hangen de voetbotten soms onder de root in plaats van onder het onderbeen. Begint zo'n voetketen precies aan het eind van een geanimeerd eindbot (bijvoorbeeld `calf`), en heeft de keten zelf geen animatie, dan beweegt hij star mee met dat bot, zoals bij gewone FK.39
- **Voor/achter gespiegelde beweging.** Buigen de knieën in vrijwel alle gemeten standen (minstens 4, en meer dan 3× zo vaak verkeerd als goed) naar achteren ten opzichte van de voorkant van het lichaam, dan wordt de beweging voor/achter gespiegeld rond de rustpose. De rustpose zelf verandert niet.40
41
### Meerdere meshes en skins (glTF/GLB)33
### Meerdere meshes en skins (glTF/GLB)42
34
43
Een geskind glTF/GLB-lichaam wordt volledig geladen: alle mesh-nodes en alle skins. De joints van alle skins worden samengevoegd tot één skelet (joints die in meerdere skins voorkomen tellen één keer; maximaal 128). Een mesh-node zonder skin beweegt star mee met de dichtstbijzijnde voorouder die een joint is; heeft hij die niet, dan hangt hij aan de eerste joint. Zijn rustpositie in de scène blijft daarbij exact behouden (gespiegelde transformaties draaien de driehoeksvolgorde om). Materialen blijven per primitive: basiskleur, albedotexture, alpha-cutoff en vertexkleuren. `KHR_texture_transform` op de basiskleurtexture (offset, rotatie, schaal en `texCoord`) wordt bij het laden één keer in de UV's verwerkt. De sampler-wrapmodus wordt niet gelezen: textures worden altijd met nearest en herhalen (REPEAT) bemonsterd, dus CLAMP/MIRROR geven buiten het UV-bereik 0–1 een ander beeld. Metallic/roughness-, normal-, occlusion- en emissive-textures en morph targets worden niet gebruikt; de normal map komt uit de geometrie en de specular-map van een body is nul (metallic/roughness wordt niet omgerekend; een scalaire specular kan PBR niet fysiek nabootsen).35
Een geskind glTF/GLB-lichaam wordt volledig geladen: alle mesh-nodes en alle skins. De joints van alle skins worden samengevoegd tot één skelet (joints die in meerdere skins voorkomen tellen één keer; maximaal 128). Een mesh-node zonder skin beweegt star mee met de dichtstbijzijnde voorouder die een joint is; heeft hij die niet, dan hangt hij aan de eerste joint. Zijn rustpositie in de scène blijft daarbij exact behouden (gespiegelde transformaties draaien de driehoeksvolgorde om). Materialen blijven per primitive: basiskleur, albedotexture, alpha-cutoff en vertexkleuren. `KHR_texture_transform` op de basiskleurtexture (offset, rotatie, schaal en `texCoord`) wordt bij het laden één keer in de UV's verwerkt. De sampler-wrapmodus wordt niet gelezen: textures worden altijd met nearest en herhalen (REPEAT) bemonsterd, dus CLAMP/MIRROR geven buiten het UV-bereik 0–1 een ander beeld. Metallic/roughness-, normal-, occlusion- en emissive-textures en morph targets worden niet gebruikt; de normal map komt uit de geometrie en de specular-map van een body is nul (metallic/roughness wordt niet omgerekend; een scalaire specular kan PBR niet fysiek nabootsen).@@ -3,7 +3,8 @@
3
3
4
4
The meshes, skeleton and clips are procedural. Nothing is downloaded.
5
5
The figures are labelled test assets, not finished characters. glTF axes:
6
Y is up, the character faces -Z, bind rotations are identity.
6
Y is up, the character faces +Z (the glTF front), its left side is +X,
7
bind rotations are identity.
7
8
8
9
Run from the repository root:
9
10
@@ -82,20 +83,20 @@ def skeleton(sex: str) -> list[tuple[str, str | None, tuple[float, float, float]
82
83
("Chest", "Spine", (0.0, 0.20, 0.0), (p["chest_w"], 0.22, 0.18)),
83
84
("Neck", "Chest", (0.0, 0.16, 0.0), (0.10, 0.08, 0.10)),
84
85
("Head", "Neck", (0.0, 0.14, 0.0), (0.18, 0.22, 0.20)),
85
("LeftShoulder", "Chest", (-shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),
86
("LeftUpperArm", "LeftShoulder", (-0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),
86
("LeftShoulder", "Chest", (shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),
87
("LeftUpperArm", "LeftShoulder", (0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),
87
88
("LeftLowerArm", "LeftUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),
88
89
("LeftHand", "LeftLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),
89
("RightShoulder", "Chest", (shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),
90
("RightUpperArm", "RightShoulder", (0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),
90
("RightShoulder", "Chest", (-shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),
91
("RightUpperArm", "RightShoulder", (-0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),
91
92
("RightLowerArm", "RightUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),
92
93
("RightHand", "RightLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),
93
("LeftUpperLeg", "Hips", (-hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),
94
("LeftUpperLeg", "Hips", (hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),
94
95
("LeftLowerLeg", "LeftUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),
95
("LeftFoot", "LeftLowerLeg", (0.0, -p["lower_leg"], -0.06), (0.10, 0.06, 0.22)),
96
("RightUpperLeg", "Hips", (hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),
96
("LeftFoot", "LeftLowerLeg", (0.0, -p["lower_leg"], 0.06), (0.10, 0.06, 0.22)),
97
("RightUpperLeg", "Hips", (-hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),
97
98
("RightLowerLeg", "RightUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),
98
("RightFoot", "RightLowerLeg", (0.0, -p["lower_leg"], -0.06), (0.10, 0.06, 0.22)),
99
("RightFoot", "RightLowerLeg", (0.0, -p["lower_leg"], 0.06), (0.10, 0.06, 0.22)),
99
100
]
100
101
names = [item[0] for item in spec]
101
102
if names != JOINT_ORDER:
@@ -124,17 +125,17 @@ def walk_clips() -> dict[str, list[tuple[float, float, float, float]]]:
124
125
"RightUpperArm": [-22.0, 0.0, 22.0, 0.0, -22.0],
125
126
"Chest": [4.0, 0.0, -4.0, 0.0, 4.0],
126
127
}
127
return {name: [quat_x(angle) for angle in angles] for name, angles in curves.items()} | {
128
return {name: [quat_x(-angle) for angle in angles] for name, angles in curves.items()} | {
128
129
"_times": [(t, 0.0, 0.0, 0.0) for t in times]
129
130
}
130
131
131
132
132
133
def attack_clips() -> dict[str, list[tuple[float, float, float, float]]]:
133
# Right arm swings toward -Z (negative X rotation). Chest twists about Y.
134
arm = [quat_x(a) for a in (0.0, -20.0, -100.0, -40.0, 0.0)]
135
forearm = [quat_x(a) for a in (0.0, -10.0, -35.0, -15.0, 0.0)]
134
# Right arm swings forward toward +Z (positive X rotation). Chest twists about Y.
135
arm = [quat_x(-a) for a in (0.0, -20.0, -100.0, -40.0, 0.0)]
136
forearm = [quat_x(-a) for a in (0.0, -10.0, -35.0, -15.0, 0.0)]
136
137
chest = [quat_y(a) for a in (0.0, 6.0, 18.0, 8.0, 0.0)]
137
spine = [quat_x(a) for a in (0.0, 4.0, 12.0, 4.0, 0.0)]
138
spine = [quat_x(-a) for a in (0.0, 4.0, 12.0, 4.0, 0.0)]
138
139
return {
139
140
"RightUpperArm": arm,
140
141
"RightLowerArm": forearm,
@@ -376,7 +377,7 @@ def build_glb(sex: str) -> bytes:
376
377
"license": "CC0-1.0",
377
378
"generator": "scripts/generate_test_bodies.py",
378
379
"up": "+Y",
379
"forward": "-Z",
380
"forward": "+Z",
380
381
"note": "Procedureel testlichaam. Geen motion capture en geen definitief karakter.",
381
382
},
382
383
},
@@ -3,7 +3,8 @@3
3
4
The meshes, skeleton and clips are procedural. Nothing is downloaded.4
The meshes, skeleton and clips are procedural. Nothing is downloaded.5
The figures are labelled test assets, not finished characters. glTF axes:5
The figures are labelled test assets, not finished characters. glTF axes:6
Y is up, the character faces -Z, bind rotations are identity.6
Y is up, the character faces +Z (the glTF front), its left side is +X,7
bind rotations are identity.7
8
8
Run from the repository root:9
Run from the repository root:9
10
@@ -82,20 +83,20 @@ def skeleton(sex: str) -> list[tuple[str, str | None, tuple[float, float, float]82
("Chest", "Spine", (0.0, 0.20, 0.0), (p["chest_w"], 0.22, 0.18)),83
("Chest", "Spine", (0.0, 0.20, 0.0), (p["chest_w"], 0.22, 0.18)),83
("Neck", "Chest", (0.0, 0.16, 0.0), (0.10, 0.08, 0.10)),84
("Neck", "Chest", (0.0, 0.16, 0.0), (0.10, 0.08, 0.10)),84
("Head", "Neck", (0.0, 0.14, 0.0), (0.18, 0.22, 0.20)),85
("Head", "Neck", (0.0, 0.14, 0.0), (0.18, 0.22, 0.20)),85
("LeftShoulder", "Chest", (-shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),86
("LeftShoulder", "Chest", (shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),86
("LeftUpperArm", "LeftShoulder", (-0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),87
("LeftUpperArm", "LeftShoulder", (0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),87
("LeftLowerArm", "LeftUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),88
("LeftLowerArm", "LeftUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),88
("LeftHand", "LeftLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),89
("LeftHand", "LeftLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),89
("RightShoulder", "Chest", (shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),90
("RightShoulder", "Chest", (-shoulder, 0.08, 0.0), (0.10, 0.08, 0.08)),90
("RightUpperArm", "RightShoulder", (0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),91
("RightUpperArm", "RightShoulder", (-0.08, 0.0, 0.0), (0.08, p["upper_arm"], 0.08)),91
("RightLowerArm", "RightUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),92
("RightLowerArm", "RightUpperArm", (0.0, -p["upper_arm"], 0.0), (0.07, p["lower_arm"], 0.07)),92
("RightHand", "RightLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),93
("RightHand", "RightLowerArm", (0.0, -p["lower_arm"], 0.0), (0.08, 0.10, 0.04)),93
("LeftUpperLeg", "Hips", (-hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),94
("LeftUpperLeg", "Hips", (hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),94
("LeftLowerLeg", "LeftUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),95
("LeftLowerLeg", "LeftUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),95
("LeftFoot", "LeftLowerLeg", (0.0, -p["lower_leg"], -0.06), (0.10, 0.06, 0.22)),96
("LeftFoot", "LeftLowerLeg", (0.0, -p["lower_leg"], 0.06), (0.10, 0.06, 0.22)),96
("RightUpperLeg", "Hips", (hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),97
("RightUpperLeg", "Hips", (-hip, -0.06, 0.0), (0.12, p["upper_leg"], 0.12)),97
("RightLowerLeg", "RightUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),98
("RightLowerLeg", "RightUpperLeg", (0.0, -p["upper_leg"], 0.0), (0.09, p["lower_leg"], 0.09)),98
("RightFoot", "RightLowerLeg", (0.0, -p["lower_leg"], -0.06), (0.10, 0.06, 0.22)),99
("RightFoot", "RightLowerLeg", (0.0, -p["lower_leg"], 0.06), (0.10, 0.06, 0.22)),99
]100
]100
names = [item[0] for item in spec]101
names = [item[0] for item in spec]101
if names != JOINT_ORDER:102
if names != JOINT_ORDER:@@ -124,17 +125,17 @@ def walk_clips() -> dict[str, list[tuple[float, float, float, float]]]:124
"RightUpperArm": [-22.0, 0.0, 22.0, 0.0, -22.0],125
"RightUpperArm": [-22.0, 0.0, 22.0, 0.0, -22.0],125
"Chest": [4.0, 0.0, -4.0, 0.0, 4.0],126
"Chest": [4.0, 0.0, -4.0, 0.0, 4.0],126
}127
}127
return {name: [quat_x(angle) for angle in angles] for name, angles in curves.items()} | {128
return {name: [quat_x(-angle) for angle in angles] for name, angles in curves.items()} | {128
"_times": [(t, 0.0, 0.0, 0.0) for t in times]129
"_times": [(t, 0.0, 0.0, 0.0) for t in times]129
}130
}130
131
131
132
132
def attack_clips() -> dict[str, list[tuple[float, float, float, float]]]:133
def attack_clips() -> dict[str, list[tuple[float, float, float, float]]]:133
# Right arm swings toward -Z (negative X rotation). Chest twists about Y.134
# Right arm swings forward toward +Z (positive X rotation). Chest twists about Y.134
arm = [quat_x(a) for a in (0.0, -20.0, -100.0, -40.0, 0.0)]135
arm = [quat_x(-a) for a in (0.0, -20.0, -100.0, -40.0, 0.0)]135
forearm = [quat_x(a) for a in (0.0, -10.0, -35.0, -15.0, 0.0)]136
forearm = [quat_x(-a) for a in (0.0, -10.0, -35.0, -15.0, 0.0)]136
chest = [quat_y(a) for a in (0.0, 6.0, 18.0, 8.0, 0.0)]137
chest = [quat_y(a) for a in (0.0, 6.0, 18.0, 8.0, 0.0)]137
spine = [quat_x(a) for a in (0.0, 4.0, 12.0, 4.0, 0.0)]138
spine = [quat_x(-a) for a in (0.0, 4.0, 12.0, 4.0, 0.0)]138
return {139
return {139
"RightUpperArm": arm,140
"RightUpperArm": arm,140
"RightLowerArm": forearm,141
"RightLowerArm": forearm,@@ -376,7 +377,7 @@ def build_glb(sex: str) -> bytes:376
"license": "CC0-1.0",377
"license": "CC0-1.0",377
"generator": "scripts/generate_test_bodies.py",378
"generator": "scripts/generate_test_bodies.py",378
"up": "+Y",379
"up": "+Y",379
"forward": "-Z",380
"forward": "+Z",380
"note": "Procedureel testlichaam. Geen motion capture en geen definitief karakter.",381
"note": "Procedureel testlichaam. Geen motion capture en geen definitief karakter.",381
},382
},382
},383
},@@ -171,8 +171,8 @@ pub fn matrices(framing: CameraFraming, yaw: f32, pitch: f32, aspect: f32) -> (M
171
171
}
172
172
173
173
/// Labelled orientation convention (§6/§13), verified by
174
/// `orientation_convention_is_labelled`: the character faces −Z, +Y is up,
175
/// its right hand side is +X. Camera eye = `eye_offset(yaw, pitch)`.
174
/// `orientation_convention_is_labelled`: the glTF convention — the character
175
/// faces +Z, +Y is up, its left hand side is +X (−X is right). Camera eye = `eye_offset(yaw, pitch)`.
176
176
pub const ORIENTATION_VIEWS: [(f32, &str); 4] = [
177
177
(0., "voorkant"),
178
178
(90., "linkerzijde"),
@@ -564,12 +564,12 @@ pub fn arm_measure(body: &PreviewCharacter) -> Option<(f32, f32)> {
564
564
mod tests {
565
565
#[test]
566
566
fn orientation_convention_is_labelled() {
567
// Labelled markers on a body facing −Z with its right side on +X.
567
// Labelled markers on a body facing +Z (glTF front), its left side on +X.
568
568
let markers = [
569
("voorkant", [0., 1., -0.3]),
570
("achterkant", [0., 1., 0.3]),
571
("linkerzijde", [-0.3, 1., 0.]),
572
("rechterzijde", [0.3, 1., 0.]),
569
("voorkant", [0., 1., 0.3]),
570
("achterkant", [0., 1., -0.3]),
571
("linkerzijde", [0.3, 1., 0.]),
572
("rechterzijde", [-0.3, 1., 0.]),
573
573
];
574
574
let framing = crate::project::CameraFraming::default();
575
575
for (yaw, label) in ORIENTATION_VIEWS {
@@ -583,14 +583,14 @@ mod tests {
583
583
.0;
584
584
assert_eq!(nearest, label, "yaw {yaw}");
585
585
}
586
// Yaw 0 (front): the character's right (+X) is on the image's left.
586
// Yaw 0 (front): the character's right (−X) is on the image's left.
587
587
let (view, _) = matrices(framing, 0., 0., 1.);
588
588
let x = |p: [f32; 3]| view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];
589
assert!(x([0.3, 1., 0.]) < 0. && x([-0.3, 1., 0.]) > 0.);
589
assert!(x([-0.3, 1., 0.]) < 0. && x([0.3, 1., 0.]) > 0.);
590
590
// Positive pitch looks down from above: the front marker sits lower.
591
591
let (view, _) = matrices(framing, 0., 60., 1.);
592
592
let y = |p: [f32; 3]| view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];
593
assert!(y([0., 1., -0.3]) < y([0., 1., 0.3]));
593
assert!(y([0., 1., 0.3]) < y([0., 1., -0.3]));
594
594
}
595
595
596
596
#[test]
@@ -669,7 +669,6 @@ mod tests {
669
669
surfaces: Vec::new(),
670
670
clips: Vec::new(),
671
671
rig: preview::BodyRig::Skinned,
672
fixups: Default::default(),
673
672
}
674
673
}
675
674
@@ -171,8 +171,8 @@ pub fn matrices(framing: CameraFraming, yaw: f32, pitch: f32, aspect: f32) -> (M171
}171
}172
172
173
/// Labelled orientation convention (§6/§13), verified by173
/// Labelled orientation convention (§6/§13), verified by174
/// `orientation_convention_is_labelled`: the character faces −Z, +Y is up,174
/// `orientation_convention_is_labelled`: the glTF convention — the character175
/// its right hand side is +X. Camera eye = `eye_offset(yaw, pitch)`.175
/// faces +Z, +Y is up, its left hand side is +X (−X is right). Camera eye = `eye_offset(yaw, pitch)`.176
pub const ORIENTATION_VIEWS: [(f32, &str); 4] = [176
pub const ORIENTATION_VIEWS: [(f32, &str); 4] = [177
(0., "voorkant"),177
(0., "voorkant"),178
(90., "linkerzijde"),178
(90., "linkerzijde"),@@ -564,12 +564,12 @@ pub fn arm_measure(body: &PreviewCharacter) -> Option<(f32, f32)> {564
mod tests {564
mod tests {565
#[test]565
#[test]566
fn orientation_convention_is_labelled() {566
fn orientation_convention_is_labelled() {567
// Labelled markers on a body facing −Z with its right side on +X.567
// Labelled markers on a body facing +Z (glTF front), its left side on +X.568
let markers = [568
let markers = [569
("voorkant", [0., 1., -0.3]),569
("voorkant", [0., 1., 0.3]),570
("achterkant", [0., 1., 0.3]),570
("achterkant", [0., 1., -0.3]),571
("linkerzijde", [-0.3, 1., 0.]),571
("linkerzijde", [0.3, 1., 0.]),572
("rechterzijde", [0.3, 1., 0.]),572
("rechterzijde", [-0.3, 1., 0.]),573
];573
];574
let framing = crate::project::CameraFraming::default();574
let framing = crate::project::CameraFraming::default();575
for (yaw, label) in ORIENTATION_VIEWS {575
for (yaw, label) in ORIENTATION_VIEWS {@@ -583,14 +583,14 @@ mod tests {583
.0;583
.0;584
assert_eq!(nearest, label, "yaw {yaw}");584
assert_eq!(nearest, label, "yaw {yaw}");585
}585
}586
// Yaw 0 (front): the character's right (+X) is on the image's left.586
// Yaw 0 (front): the character's right (−X) is on the image's left.587
let (view, _) = matrices(framing, 0., 0., 1.);587
let (view, _) = matrices(framing, 0., 0., 1.);588
let x = |p: [f32; 3]| view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];588
let x = |p: [f32; 3]| view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];589
assert!(x([0.3, 1., 0.]) < 0. && x([-0.3, 1., 0.]) > 0.);589
assert!(x([-0.3, 1., 0.]) < 0. && x([0.3, 1., 0.]) > 0.);590
// Positive pitch looks down from above: the front marker sits lower.590
// Positive pitch looks down from above: the front marker sits lower.591
let (view, _) = matrices(framing, 0., 60., 1.);591
let (view, _) = matrices(framing, 0., 60., 1.);592
let y = |p: [f32; 3]| view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];592
let y = |p: [f32; 3]| view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];593
assert!(y([0., 1., -0.3]) < y([0., 1., 0.3]));593
assert!(y([0., 1., 0.3]) < y([0., 1., -0.3]));594
}594
}595
595
596
#[test]596
#[test]@@ -669,7 +669,6 @@ mod tests {669
surfaces: Vec::new(),669
surfaces: Vec::new(),670
clips: Vec::new(),670
clips: Vec::new(),671
rig: preview::BodyRig::Skinned,671
rig: preview::BodyRig::Skinned,672
fixups: Default::default(),673
}672
}674
}673
}675
674
@@ -1381,14 +1381,6 @@ impl EditorApp {
1381
1381
),
1382
1382
);
1383
1383
}
1384
if let Some(body) = self.character() {
1385
for note in &body.fixups.notes {
1386
ui.colored_label(
1387
egui::Color32::from_rgb(150, 200, 255),
1388
format!("Automatische correctie: {note}"),
1389
);
1390
}
1391
}
1392
1384
self.framing_ui(ui, usize::from(sex == Sex::Female));
1393
1385
self.render_progress_ui(ui);
1394
1386
self.resume_ui(ui);
@@ -1381,14 +1381,6 @@ impl EditorApp {1381
),1381
),1382
);1382
);1383
}1383
}1384
if let Some(body) = self.character() {1385
for note in &body.fixups.notes {1386
ui.colored_label(1387
egui::Color32::from_rgb(150, 200, 255),1388
format!("Automatische correctie: {note}"),1389
);1390
}1391
}1392
self.framing_ui(ui, usize::from(sex == Sex::Female));1384
self.framing_ui(ui, usize::from(sex == Sex::Female));1393
self.render_progress_ui(ui);1385
self.render_progress_ui(ui);1394
self.resume_ui(ui);1386
self.resume_ui(ui);@@ -122,7 +122,7 @@ impl SyntheticRun {
122
122
"depth_bits": self.depth_format.bits,
123
123
"normal_space": "view (+Y up), PNG rows top-down",
124
124
"camera_distance": 10,
125
"yaw_zero": "camera on -Z",
125
"yaw_zero": "camera on +Z",
126
126
"pitches": PRODUCTION_PITCHES,
127
127
"yaws": production::production_yaws(),
128
128
"yaw_step": 30,
@@ -122,7 +122,7 @@ impl SyntheticRun {122
"depth_bits": self.depth_format.bits,122
"depth_bits": self.depth_format.bits,123
"normal_space": "view (+Y up), PNG rows top-down",123
"normal_space": "view (+Y up), PNG rows top-down",124
"camera_distance": 10,124
"camera_distance": 10,125
"yaw_zero": "camera on -Z",125
"yaw_zero": "camera on +Z",126
"pitches": PRODUCTION_PITCHES,126
"pitches": PRODUCTION_PITCHES,127
"yaws": production::production_yaws(),127
"yaws": production::production_yaws(),128
"yaw_step": 30,128
"yaw_step": 30,@@ -1033,7 +1033,7 @@ mod tests {
1033
1033
"depth_encoding": "gray",
1034
1034
"depth_bits": 16,
1035
1035
"camera_distance": 10.0,
1036
"yaw_zero": "camera on -Z",
1036
"yaw_zero": "camera on +Z",
1037
1037
"normal_space": "view (+Y up), PNG rows top-down",
1038
1038
"pitches": crate::preview::PRODUCTION_PITCHES,
1039
1039
"yaws": crate::production::production_yaws(),
@@ -1033,7 +1033,7 @@ mod tests {1033
"depth_encoding": "gray",1033
"depth_encoding": "gray",1034
"depth_bits": 16,1034
"depth_bits": 16,1035
"camera_distance": 10.0,1035
"camera_distance": 10.0,1036
"yaw_zero": "camera on -Z",1036
"yaw_zero": "camera on +Z",1037
"normal_space": "view (+Y up), PNG rows top-down",1037
"normal_space": "view (+Y up), PNG rows top-down",1038
"pitches": crate::preview::PRODUCTION_PITCHES,1038
"pitches": crate::preview::PRODUCTION_PITCHES,1039
"yaws": crate::production::production_yaws(),1039
"yaws": crate::production::production_yaws(),@@ -227,7 +227,6 @@ pub fn load_fbx_drawable(path: &Path) -> Result<FbxDrawableScene, String> {
227
227
surfaces,
228
228
clips,
229
229
rig: crate::preview::BodyRig::Skinned,
230
fixups: Default::default(),
231
230
},
232
231
provenance,
233
232
materials,
@@ -227,7 +227,6 @@ pub fn load_fbx_drawable(path: &Path) -> Result<FbxDrawableScene, String> {227
surfaces,227
surfaces,228
clips,228
clips,229
rig: crate::preview::BodyRig::Skinned,229
rig: crate::preview::BodyRig::Skinned,230
fixups: Default::default(),231
},230
},232
provenance,231
provenance,233
materials,232
materials,@@ -23,7 +23,6 @@ pub mod file_assoc;
23
23
pub mod foundation;
24
24
pub mod icon;
25
25
pub mod import;
26
pub mod orient;
27
26
pub mod preview;
28
27
pub mod preview2d;
29
28
pub mod preview_gl;
@@ -23,7 +23,6 @@ pub mod file_assoc;23
pub mod foundation;23
pub mod foundation;24
pub mod icon;24
pub mod icon;25
pub mod import;25
pub mod import;26
pub mod orient;27
pub mod preview;26
pub mod preview;28
pub mod preview2d;27
pub mod preview2d;29
pub mod preview_gl;28
pub mod preview_gl;@@ -1,543 +0,0 @@
1
//! Load-time orientation and animation corrections for source bodies.
2
//!
3
//! Applied once per body after loading, inside `preview::posed_worlds`, so
4
//! the live view, stills, production and equipment all see the same pose.
5
//!
6
//! * **Forward axis.** The renderer's convention is "character faces −Z"
7
//! (yaw 0 = front). A body whose feet point to +Z (toes in front of the
8
//! ankles, the glTF default) is turned 180° about Y.
9
//! * **Detached feet.** IK rigs often keep the foot bones under the root
10
//! instead of under the shin. When such a foot chain starts exactly at the
11
//! end of an animated leaf bone and has no animation of its own, it follows
12
//! that bone rigidly (as forward kinematics would).
13
//! * **Front/back mirrored motion.** When knees bend the wrong way relative
14
//! to the body's own front in almost every frame, the motion is mirrored
15
//! front/back (per-joint world deltas reflected through the frontal plane);
16
//! the rest pose is unchanged.
17
use crate::preview::{self, affine_inverse, mul, Mat4, PreviewCharacter};
18
19
#[derive(Debug, Clone, Default, PartialEq)]
20
pub struct BodyFixups {
21
/// Turn the whole body 180° about +Y (source faces +Z).
22
pub turn_180: bool,
23
/// Reflect the motion front/back (source-local Z) around the rest pose.
24
pub mirror_motion: bool,
25
/// (node, driver node): node's subtree follows the driver rigidly.
26
pub follow: Vec<(usize, usize)>,
27
/// Human-readable (Dutch) explanations for the editor/render log.
28
pub notes: Vec<String>,
29
}
30
31
impl BodyFixups {
32
pub fn is_identity(&self) -> bool {
33
!self.turn_180 && !self.mirror_motion && self.follow.is_empty()
34
}
35
}
36
37
/// Which way a body faces, as decided by `detect`.
38
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39
pub enum Forward {
40
MinusZ,
41
PlusZ,
42
}
43
44
fn diag(x: f32, y: f32, z: f32) -> Mat4 {
45
let mut m = preview::identity();
46
m[0] = x;
47
m[5] = y;
48
m[10] = z;
49
m
50
}
51
52
fn descendants(character: &PreviewCharacter, root: usize) -> Vec<usize> {
53
let mut out = vec![root];
54
let mut i = 0;
55
while i < out.len() {
56
let n = out[i];
57
for (c, node) in character.nodes.iter().enumerate() {
58
if node.parent == Some(n) && !out.contains(&c) {
59
out.push(c);
60
}
61
}
62
i += 1;
63
}
64
out
65
}
66
67
fn is_ancestor(character: &PreviewCharacter, a: usize, mut n: usize) -> bool {
68
let mut guard = 0;
69
while let Some(p) = character.nodes.get(n).and_then(|x| x.parent) {
70
if p == a {
71
return true;
72
}
73
n = p;
74
guard += 1;
75
if guard > character.nodes.len() {
76
break;
77
}
78
}
79
false
80
}
81
82
/// Apply `character.fixups` to raw posed world matrices.
83
pub fn apply(character: &PreviewCharacter, worlds: Vec<Mat4>) -> Vec<Mat4> {
84
apply_with(character, &character.fixups, worlds)
85
}
86
87
fn apply_with(character: &PreviewCharacter, f: &BodyFixups, mut w: Vec<Mat4>) -> Vec<Mat4> {
88
if f.is_identity() {
89
return w;
90
}
91
let rest = preview::rest_worlds(character);
92
for &(node, driver) in &f.follow {
93
let (Some(rd), Some(rn)) = (affine_inverse(&rest[driver]), affine_inverse(&w[node])) else {
94
continue;
95
};
96
let delta = mul(w[driver], rd);
97
let new_node = mul(delta, rest[node]);
98
for d in descendants(character, node) {
99
w[d] = mul(new_node, mul(rn, w[d]));
100
}
101
}
102
if f.mirror_motion {
103
let s = diag(1., 1., -1.);
104
for (i, m) in w.iter_mut().enumerate() {
105
if let Some(ri) = affine_inverse(&rest[i]) {
106
*m = mul(s, mul(*m, mul(ri, mul(s, rest[i]))));
107
}
108
}
109
}
110
if f.turn_180 {
111
let r = diag(-1., 1., -1.);
112
for m in w.iter_mut() {
113
*m = mul(r, *m);
114
}
115
}
116
w
117
}
118
119
fn point(m: &Mat4, p: [f32; 3]) -> [f32; 3] {
120
[
121
m[0] * p[0] + m[4] * p[1] + m[8] * p[2] + m[12],
122
m[1] * p[0] + m[5] * p[1] + m[9] * p[2] + m[13],
123
m[2] * p[0] + m[6] * p[1] + m[10] * p[2] + m[14],
124
]
125
}
126
127
/// Rest-pose vertex positions (bind positions through the rest skin).
128
fn rest_positions(character: &PreviewCharacter) -> Vec<[f32; 3]> {
129
if character.is_static() || character.joint_nodes.is_empty() {
130
return character.positions.clone();
131
}
132
let rest = preview::rest_worlds(character);
133
let skin: Vec<Mat4> = character
134
.joint_nodes
135
.iter()
136
.zip(&character.inverse_binds)
137
.map(|(n, ib)| mul(rest[*n], *ib))
138
.collect();
139
character
140
.positions
141
.iter()
142
.enumerate()
143
.map(|(i, p)| {
144
let w = character
145
.weights
146
.get(i)
147
.copied()
148
.unwrap_or([1., 0., 0., 0.]);
149
let j = character.joints.get(i).copied().unwrap_or([0; 4]);
150
let mut out = [0.; 3];
151
let mut sum = 0.;
152
for k in 0..4 {
153
if w[k] > 0. {
154
if let Some(m) = skin.get(j[k] as usize) {
155
let q = point(m, *p);
156
for a in 0..3 {
157
out[a] += w[k] * q[a];
158
}
159
sum += w[k];
160
}
161
}
162
}
163
if sum > 0. {
164
out.map(|v| v / sum)
165
} else {
166
*p
167
}
168
})
169
.collect()
170
}
171
172
/// Feet heuristic: the vertices of the lowest 4% of the body (feet) extend
173
/// further forward than those at 6–10% (ankles/shins). None when unclear
174
/// (meshes under 1000 vertices such as box proxies, fewer than 12 vertices per
175
/// band, or a difference under 1% of height).
176
pub fn detect_forward(positions: &[[f32; 3]]) -> Option<Forward> {
177
let (lo, hi) = positions
178
.iter()
179
.fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {
180
(a.min(p[1]), b.max(p[1]))
181
});
182
let h = hi - lo;
183
if !(h.is_finite() && h > 0.) || positions.len() < 1000 {
184
return None;
185
}
186
let mean = |a: f32, b: f32| {
187
let v: Vec<f32> = positions
188
.iter()
189
.filter(|p| p[1] >= lo + a * h && p[1] <= lo + b * h)
190
.map(|p| p[2])
191
.collect();
192
// Too few vertices (box-like proxies) give no reliable feet shape.
193
(v.len() >= 12).then(|| v.iter().sum::<f32>() / v.len() as f32)
194
};
195
let feet = mean(0., 0.04)?;
196
let ankles = mean(0.06, 0.10)?;
197
let d = feet - ankles;
198
if d > 0.01 * h {
199
Some(Forward::PlusZ)
200
} else if d < -0.01 * h {
201
Some(Forward::MinusZ)
202
} else {
203
None
204
}
205
}
206
207
fn channel_static(ch: &preview::AnimChannel) -> bool {
208
let n = ch.property.width();
209
let first: Vec<f32> = ch.values.iter().take(n).copied().collect();
210
ch.values
211
.chunks(n.max(1))
212
.all(|k| k.iter().zip(&first).all(|(a, b)| (a - b).abs() < 1e-5))
213
}
214
215
fn node_animated(character: &PreviewCharacter, node: usize) -> bool {
216
character.clips.iter().any(|c| {
217
c.channels
218
.iter()
219
.any(|ch| ch.node == node && !channel_static(ch))
220
})
221
}
222
223
fn node_matches_rest(character: &PreviewCharacter, node: usize) -> bool {
224
// Static channels must also equal the rest pose (else the bone is posed).
225
let n = &character.nodes[node];
226
character.clips.iter().all(|c| {
227
c.channels.iter().filter(|ch| ch.node == node).all(|ch| {
228
let rest: Vec<f32> = match ch.property {
229
preview::ChannelProperty::Translation => n.translation.to_vec(),
230
preview::ChannelProperty::Rotation => n.rotation.to_vec(),
231
preview::ChannelProperty::Scale => n.scale.to_vec(),
232
};
233
let k = &ch.values[..rest.len().min(ch.values.len())];
234
let dot: f32 = k.iter().zip(&rest).map(|(a, b)| a * b).sum();
235
match ch.property {
236
preview::ChannelProperty::Rotation => dot.abs() > 0.9999,
237
_ => k.iter().zip(&rest).all(|(a, b)| (a - b).abs() < 1e-4),
238
}
239
})
240
})
241
}
242
243
/// Detect all fixups for a freshly loaded body.
244
pub fn detect(character: &PreviewCharacter) -> BodyFixups {
245
let mut f = BodyFixups::default();
246
let positions = rest_positions(character);
247
let forward = detect_forward(&positions);
248
if character.is_static() {
249
return f;
250
}
251
let rest = preview::rest_worlds(character);
252
let (lo, hi) = positions
253
.iter()
254
.fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {
255
(a.min(p[1]), b.max(p[1]))
256
});
257
let h = (hi - lo).max(1e-3);
258
let joints = &character.joint_nodes;
259
let has_joint_child = |n: usize| joints.iter().any(|c| character.nodes[*c].parent == Some(n));
260
// Detached feet.
261
for &driver in joints {
262
if has_joint_child(driver) || !node_animated(character, driver) {
263
continue;
264
}
265
let Some(inv) = affine_inverse(&rest[driver]) else {
266
continue;
267
};
268
for &cand in joints {
269
if cand == driver
270
|| is_ancestor(character, cand, driver)
271
|| is_ancestor(character, driver, cand)
272
{
273
continue;
274
}
275
let o = [rest[cand][12], rest[cand][13], rest[cand][14]];
276
let l = point(&inv, o);
277
// Rest origin on the driver's bone axis (+Y), beyond its head.
278
let len = (rest[driver][4].powi(2) + rest[driver][5].powi(2) + rest[driver][6].powi(2))
279
.sqrt();
280
let along = l[1] * len;
281
if !(l[0].abs() * len < 0.003 * h
282
&& l[2].abs() * len < 0.003 * h
283
&& along > 0.05 * h
284
&& along < 0.5 * h)
285
{
286
continue;
287
}
288
// Climb to the topmost ancestor that is not above the driver.
289
let mut top = cand;
290
while let Some(p) = character.nodes[top].parent {
291
if is_ancestor(character, p, driver) || p == driver {
292
break;
293
}
294
top = p;
295
}
296
let subtree = descendants(character, top);
297
if subtree
298
.iter()
299
.any(|n| node_animated(character, *n) || !node_matches_rest(character, *n))
300
{
301
continue;
302
}
303
if !f.follow.iter().any(|(n, _)| *n == top) {
304
f.follow.push((top, driver));
305
f.notes.push(format!(
306
"{} volgt {} (losse voetketen zonder eigen animatie)",
307
character.nodes[top].name, character.nodes[driver].name
308
));
309
}
310
}
311
}
312
// Knee direction vs. the body's own front.
313
let front = match forward {
314
Some(Forward::PlusZ) => 1.,
315
_ => -1.,
316
};
317
let mut legs: Vec<(usize, usize, [f32; 3])> = Vec::new(); // (thigh node, shin node, ankle in shin space)
318
for &(node, driver) in &f.follow {
319
if let (Some(p), Some(inv)) = (
320
character.nodes[driver].parent,
321
affine_inverse(&rest[driver]),
322
) {
323
legs.push((
324
p,
325
driver,
326
point(&inv, [rest[node][12], rest[node][13], rest[node][14]]),
327
));
328
}
329
}
330
let (mut good, mut bad) = (0, 0);
331
if !legs.is_empty() {
332
let partial = BodyFixups {
333
follow: f.follow.clone(),
334
..Default::default()
335
};
336
for clip in &character.clips {
337
let frames = ((clip.duration * 24.).ceil() as usize).max(1);
338
for i in 0..frames {
339
let w = apply_with(
340
character,
341
&partial,
342
preview::raw_posed_worlds(character, &clip.name, i as f32 / 24.),
343
);
344
for (thigh, shin, ankle_local) in &legs {
345
let hip = [w[*thigh][12], w[*thigh][13], w[*thigh][14]];
346
let knee = [w[*shin][12], w[*shin][13], w[*shin][14]];
347
let ankle = point(&w[*shin], *ankle_local);
348
let dy = ankle[1] - hip[1];
349
if dy.abs() < 1e-4 {
350
continue;
351
}
352
let t = (knee[1] - hip[1]) / dy;
353
let line_z = hip[2] + t * (ankle[2] - hip[2]);
354
let off = (knee[2] - line_z) * front;
355
if off > 0.02 * h {
356
good += 1;
357
} else if off < -0.02 * h {
358
bad += 1;
359
}
360
}
361
}
362
}
363
}
364
if bad >= 4 && bad > 3 * good {
365
f.mirror_motion = true;
366
f.notes.push(format!(
367
"beweging voor/achter gespiegeld: knieën bogen in {bad} van {} gemeten standen de verkeerde kant op",
368
bad + good
369
));
370
}
371
if forward == Some(Forward::PlusZ) {
372
f.turn_180 = true;
373
f.notes.push("lichaam kijkt naar +Z (tenen voor de enkels): 180° gedraaid zodat yaw 0 de voorkant toont".into());
374
}
375
f
376
}
377
378
/// Detect and store the fixups on a loaded body. Static bodies (no bones)
379
/// get their +Z-facing geometry turned directly.
380
pub fn finalize(mut character: PreviewCharacter) -> PreviewCharacter {
381
if character.is_static() {
382
if detect_forward(&character.positions) == Some(Forward::PlusZ) {
383
for p in character
384
.positions
385
.iter_mut()
386
.chain(character.normals.iter_mut())
387
{
388
p[0] = -p[0];
389
p[2] = -p[2];
390
}
391
character.fixups.notes.push(
392
"lichaam kijkt naar +Z (tenen voor de enkels): 180° gedraaid zodat yaw 0 de voorkant toont".into(),
393
);
394
}
395
return character;
396
}
397
character.fixups = detect(&character);
398
character
399
}
400
401
/// Forward axis of a body's rest pose (None when unclear).
402
pub fn forward_of(character: &PreviewCharacter) -> Option<Forward> {
403
detect_forward(&rest_positions(character))
404
}
405
406
#[cfg(test)]
407
mod tests {
408
use super::*;
409
use crate::preview::{
410
AnimChannel, BodyRig, ChannelProperty, Interpolation, PreviewClip, SceneNode,
411
};
412
413
fn node(name: &str, parent: Option<usize>, t: [f32; 3], r: [f32; 4]) -> SceneNode {
414
SceneNode {
415
name: name.into(),
416
parent,
417
translation: t,
418
rotation: r,
419
scale: [1.; 3],
420
}
421
}
422
423
/// Leg whose foot control hangs under the root (IK style), with a knee
424
/// that bends by `knee_deg` (negative = shin back = natural for −Z front).
425
fn ik_leg(knee_deg: f32) -> PreviewCharacter {
426
let half = (knee_deg.to_radians() / 2.).sin_cos();
427
let nodes = vec![
428
node("root", None, [0., 0., 0.], [0., 0., 0., 1.]),
429
node("thigh", Some(0), [0., 1., 0.], [1., 0., 0., 0.]),
430
node("calf", Some(1), [0., 0.5, 0.], [0., 0., 0., 1.]),
431
node("footControl", Some(0), [0., 0.05, 0.], [0., 0., 0., 1.]),
432
node("toes", Some(3), [0., 0., -0.1], [0., 0., 0., 1.]),
433
];
434
let clip = PreviewClip {
435
name: "walk".into(),
436
duration: 1.,
437
channels: vec![AnimChannel {
438
node: 2,
439
property: ChannelProperty::Rotation,
440
times: vec![0., 0.5, 1.],
441
values: vec![0., 0., 0., 1., half.0, 0., 0., half.1, 0., 0., 0., 1.],
442
interpolation: Interpolation::Linear,
443
}],
444
skipped: Vec::new(),
445
};
446
PreviewCharacter {
447
nodes,
448
joint_nodes: (0..5).collect(),
449
joint_names: ["root", "thigh", "calf", "footControl", "toes"]
450
.map(String::from)
451
.to_vec(),
452
inverse_binds: vec![preview::identity(); 5],
453
positions: vec![[0., 0., 0.], [0., 1.8, 0.]],
454
normals: vec![[0., 1., 0.]; 2],
455
source_normals_complete: true,
456
joints: vec![[0; 4]; 2],
457
weights: vec![[1., 0., 0., 0.]; 2],
458
indices: Vec::new(),
459
base_color: [1.; 4],
460
colors: Vec::new(),
461
uvs: Vec::new(),
462
surfaces: Vec::new(),
463
clips: vec![clip],
464
rig: BodyRig::Skinned,
465
fixups: BodyFixups::default(),
466
}
467
}
468
469
#[test]
470
fn detached_foot_follows_the_shin() {
471
let mut leg = ik_leg(-40.);
472
leg.fixups = detect(&leg);
473
assert_eq!(leg.fixups.follow, vec![(3, 2)]);
474
assert!(!leg.fixups.mirror_motion && !leg.fixups.turn_180);
475
let w = preview::posed_worlds(&leg, "walk", 0.5);
476
// Foot origin = shin end (0.45 m along the shin), toes keep their offset.
477
let end = point(&w[2], [0., 0.45, 0.]);
478
for a in 0..3 {
479
assert!(
480
(w[3][12 + a] - end[a]).abs() < 1e-4,
481
"{:?} vs {end:?}",
482
&w[3][12..15]
483
);
484
}
485
assert!(
486
end[2] > 0.2,
487
"shin swings back (+Z) for a −Z front: {end:?}"
488
);
489
let toes = [w[4][12], w[4][13], w[4][14]];
490
assert!((toes[2] - end[2] + 0.1).abs() < 0.05 || (toes[1] - end[1]).abs() > 0.01);
491
// At rest nothing moves.
492
let r = preview::posed_worlds(&leg, "", 0.);
493
assert!((r[3][13] - 0.05).abs() < 1e-5);
494
}
495
496
#[test]
497
fn backwards_knees_mirror_the_motion_front_back() {
498
let mut leg = ik_leg(40.);
499
leg.fixups = detect(&leg);
500
assert!(leg.fixups.mirror_motion, "{:?}", leg.fixups.notes);
501
let w = preview::posed_worlds(&leg, "walk", 0.5);
502
let ankle = [w[3][12], w[3][13], w[3][14]];
503
assert!(
504
ankle[2] > 0.2,
505
"after the mirror the shin swings back: {ankle:?}"
506
);
507
// The rest pose is unchanged by the mirror.
508
let r = preview::posed_worlds(&leg, "", 0.);
509
assert!((r[2][13] - 0.5).abs() < 1e-5 && r[2][14].abs() < 1e-5);
510
}
511
512
#[test]
513
fn plus_z_bodies_turn_and_test_bodies_stay() {
514
let mut feet = Vec::new();
515
for i in 0..1200 {
516
let f = i as f32 / 1200.;
517
// Feet (lowest 4%) reach +Z, ankles (6–10% of height) sit behind.
518
if i % 3 == 0 {
519
feet.push([f * 0.1, 0.02, 0.1 * f]);
520
} else if i % 3 == 1 {
521
feet.push([f * 0.1, 0.16, -0.05]);
522
} else {
523
feet.push([f * 0.1, 0.2 + 1.5 * f, 0.]);
524
}
525
}
526
assert_eq!(detect_forward(&feet), Some(Forward::PlusZ));
527
let mirrored: Vec<[f32; 3]> = feet.iter().map(|p| [p[0], p[1], -p[2]]).collect();
528
assert_eq!(detect_forward(&mirrored), Some(Forward::MinusZ));
529
for f in [
530
"test-body-male.glb",
531
"test-body-female.glb",
532
"test-body-male.fbx",
533
] {
534
let b = preview::load_body_file(
535
&std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
536
.join("tests/fixtures/bodies")
537
.join(f),
538
)
539
.unwrap();
540
assert!(b.fixups.is_identity(), "{f}: {:?}", b.fixups.notes);
541
}
542
}
543
}
@@ -1,543 +0,0 @@1
//! Load-time orientation and animation corrections for source bodies.2
//!3
//! Applied once per body after loading, inside `preview::posed_worlds`, so4
//! the live view, stills, production and equipment all see the same pose.5
//!6
//! * **Forward axis.** The renderer's convention is "character faces −Z"7
//! (yaw 0 = front). A body whose feet point to +Z (toes in front of the8
//! ankles, the glTF default) is turned 180° about Y.9
//! * **Detached feet.** IK rigs often keep the foot bones under the root10
//! instead of under the shin. When such a foot chain starts exactly at the11
//! end of an animated leaf bone and has no animation of its own, it follows12
//! that bone rigidly (as forward kinematics would).13
//! * **Front/back mirrored motion.** When knees bend the wrong way relative14
//! to the body's own front in almost every frame, the motion is mirrored15
//! front/back (per-joint world deltas reflected through the frontal plane);16
//! the rest pose is unchanged.17
use crate::preview::{self, affine_inverse, mul, Mat4, PreviewCharacter};18
19
#[derive(Debug, Clone, Default, PartialEq)]20
pub struct BodyFixups {21
/// Turn the whole body 180° about +Y (source faces +Z).22
pub turn_180: bool,23
/// Reflect the motion front/back (source-local Z) around the rest pose.24
pub mirror_motion: bool,25
/// (node, driver node): node's subtree follows the driver rigidly.26
pub follow: Vec<(usize, usize)>,27
/// Human-readable (Dutch) explanations for the editor/render log.28
pub notes: Vec<String>,29
}30
31
impl BodyFixups {32
pub fn is_identity(&self) -> bool {33
!self.turn_180 && !self.mirror_motion && self.follow.is_empty()34
}35
}36
37
/// Which way a body faces, as decided by `detect`.38
#[derive(Debug, Clone, Copy, PartialEq, Eq)]39
pub enum Forward {40
MinusZ,41
PlusZ,42
}43
44
fn diag(x: f32, y: f32, z: f32) -> Mat4 {45
let mut m = preview::identity();46
m[0] = x;47
m[5] = y;48
m[10] = z;49
m50
}51
52
fn descendants(character: &PreviewCharacter, root: usize) -> Vec<usize> {53
let mut out = vec![root];54
let mut i = 0;55
while i < out.len() {56
let n = out[i];57
for (c, node) in character.nodes.iter().enumerate() {58
if node.parent == Some(n) && !out.contains(&c) {59
out.push(c);60
}61
}62
i += 1;63
}64
out65
}66
67
fn is_ancestor(character: &PreviewCharacter, a: usize, mut n: usize) -> bool {68
let mut guard = 0;69
while let Some(p) = character.nodes.get(n).and_then(|x| x.parent) {70
if p == a {71
return true;72
}73
n = p;74
guard += 1;75
if guard > character.nodes.len() {76
break;77
}78
}79
false80
}81
82
/// Apply `character.fixups` to raw posed world matrices.83
pub fn apply(character: &PreviewCharacter, worlds: Vec<Mat4>) -> Vec<Mat4> {84
apply_with(character, &character.fixups, worlds)85
}86
87
fn apply_with(character: &PreviewCharacter, f: &BodyFixups, mut w: Vec<Mat4>) -> Vec<Mat4> {88
if f.is_identity() {89
return w;90
}91
let rest = preview::rest_worlds(character);92
for &(node, driver) in &f.follow {93
let (Some(rd), Some(rn)) = (affine_inverse(&rest[driver]), affine_inverse(&w[node])) else {94
continue;95
};96
let delta = mul(w[driver], rd);97
let new_node = mul(delta, rest[node]);98
for d in descendants(character, node) {99
w[d] = mul(new_node, mul(rn, w[d]));100
}101
}102
if f.mirror_motion {103
let s = diag(1., 1., -1.);104
for (i, m) in w.iter_mut().enumerate() {105
if let Some(ri) = affine_inverse(&rest[i]) {106
*m = mul(s, mul(*m, mul(ri, mul(s, rest[i]))));107
}108
}109
}110
if f.turn_180 {111
let r = diag(-1., 1., -1.);112
for m in w.iter_mut() {113
*m = mul(r, *m);114
}115
}116
w117
}118
119
fn point(m: &Mat4, p: [f32; 3]) -> [f32; 3] {120
[121
m[0] * p[0] + m[4] * p[1] + m[8] * p[2] + m[12],122
m[1] * p[0] + m[5] * p[1] + m[9] * p[2] + m[13],123
m[2] * p[0] + m[6] * p[1] + m[10] * p[2] + m[14],124
]125
}126
127
/// Rest-pose vertex positions (bind positions through the rest skin).128
fn rest_positions(character: &PreviewCharacter) -> Vec<[f32; 3]> {129
if character.is_static() || character.joint_nodes.is_empty() {130
return character.positions.clone();131
}132
let rest = preview::rest_worlds(character);133
let skin: Vec<Mat4> = character134
.joint_nodes135
.iter()136
.zip(&character.inverse_binds)137
.map(|(n, ib)| mul(rest[*n], *ib))138
.collect();139
character140
.positions141
.iter()142
.enumerate()143
.map(|(i, p)| {144
let w = character145
.weights146
.get(i)147
.copied()148
.unwrap_or([1., 0., 0., 0.]);149
let j = character.joints.get(i).copied().unwrap_or([0; 4]);150
let mut out = [0.; 3];151
let mut sum = 0.;152
for k in 0..4 {153
if w[k] > 0. {154
if let Some(m) = skin.get(j[k] as usize) {155
let q = point(m, *p);156
for a in 0..3 {157
out[a] += w[k] * q[a];158
}159
sum += w[k];160
}161
}162
}163
if sum > 0. {164
out.map(|v| v / sum)165
} else {166
*p167
}168
})169
.collect()170
}171
172
/// Feet heuristic: the vertices of the lowest 4% of the body (feet) extend173
/// further forward than those at 6–10% (ankles/shins). None when unclear174
/// (meshes under 1000 vertices such as box proxies, fewer than 12 vertices per175
/// band, or a difference under 1% of height).176
pub fn detect_forward(positions: &[[f32; 3]]) -> Option<Forward> {177
let (lo, hi) = positions178
.iter()179
.fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {180
(a.min(p[1]), b.max(p[1]))181
});182
let h = hi - lo;183
if !(h.is_finite() && h > 0.) || positions.len() < 1000 {184
return None;185
}186
let mean = |a: f32, b: f32| {187
let v: Vec<f32> = positions188
.iter()189
.filter(|p| p[1] >= lo + a * h && p[1] <= lo + b * h)190
.map(|p| p[2])191
.collect();192
// Too few vertices (box-like proxies) give no reliable feet shape.193
(v.len() >= 12).then(|| v.iter().sum::<f32>() / v.len() as f32)194
};195
let feet = mean(0., 0.04)?;196
let ankles = mean(0.06, 0.10)?;197
let d = feet - ankles;198
if d > 0.01 * h {199
Some(Forward::PlusZ)200
} else if d < -0.01 * h {201
Some(Forward::MinusZ)202
} else {203
None204
}205
}206
207
fn channel_static(ch: &preview::AnimChannel) -> bool {208
let n = ch.property.width();209
let first: Vec<f32> = ch.values.iter().take(n).copied().collect();210
ch.values211
.chunks(n.max(1))212
.all(|k| k.iter().zip(&first).all(|(a, b)| (a - b).abs() < 1e-5))213
}214
215
fn node_animated(character: &PreviewCharacter, node: usize) -> bool {216
character.clips.iter().any(|c| {217
c.channels218
.iter()219
.any(|ch| ch.node == node && !channel_static(ch))220
})221
}222
223
fn node_matches_rest(character: &PreviewCharacter, node: usize) -> bool {224
// Static channels must also equal the rest pose (else the bone is posed).225
let n = &character.nodes[node];226
character.clips.iter().all(|c| {227
c.channels.iter().filter(|ch| ch.node == node).all(|ch| {228
let rest: Vec<f32> = match ch.property {229
preview::ChannelProperty::Translation => n.translation.to_vec(),230
preview::ChannelProperty::Rotation => n.rotation.to_vec(),231
preview::ChannelProperty::Scale => n.scale.to_vec(),232
};233
let k = &ch.values[..rest.len().min(ch.values.len())];234
let dot: f32 = k.iter().zip(&rest).map(|(a, b)| a * b).sum();235
match ch.property {236
preview::ChannelProperty::Rotation => dot.abs() > 0.9999,237
_ => k.iter().zip(&rest).all(|(a, b)| (a - b).abs() < 1e-4),238
}239
})240
})241
}242
243
/// Detect all fixups for a freshly loaded body.244
pub fn detect(character: &PreviewCharacter) -> BodyFixups {245
let mut f = BodyFixups::default();246
let positions = rest_positions(character);247
let forward = detect_forward(&positions);248
if character.is_static() {249
return f;250
}251
let rest = preview::rest_worlds(character);252
let (lo, hi) = positions253
.iter()254
.fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {255
(a.min(p[1]), b.max(p[1]))256
});257
let h = (hi - lo).max(1e-3);258
let joints = &character.joint_nodes;259
let has_joint_child = |n: usize| joints.iter().any(|c| character.nodes[*c].parent == Some(n));260
// Detached feet.261
for &driver in joints {262
if has_joint_child(driver) || !node_animated(character, driver) {263
continue;264
}265
let Some(inv) = affine_inverse(&rest[driver]) else {266
continue;267
};268
for &cand in joints {269
if cand == driver270
|| is_ancestor(character, cand, driver)271
|| is_ancestor(character, driver, cand)272
{273
continue;274
}275
let o = [rest[cand][12], rest[cand][13], rest[cand][14]];276
let l = point(&inv, o);277
// Rest origin on the driver's bone axis (+Y), beyond its head.278
let len = (rest[driver][4].powi(2) + rest[driver][5].powi(2) + rest[driver][6].powi(2))279
.sqrt();280
let along = l[1] * len;281
if !(l[0].abs() * len < 0.003 * h282
&& l[2].abs() * len < 0.003 * h283
&& along > 0.05 * h284
&& along < 0.5 * h)285
{286
continue;287
}288
// Climb to the topmost ancestor that is not above the driver.289
let mut top = cand;290
while let Some(p) = character.nodes[top].parent {291
if is_ancestor(character, p, driver) || p == driver {292
break;293
}294
top = p;295
}296
let subtree = descendants(character, top);297
if subtree298
.iter()299
.any(|n| node_animated(character, *n) || !node_matches_rest(character, *n))300
{301
continue;302
}303
if !f.follow.iter().any(|(n, _)| *n == top) {304
f.follow.push((top, driver));305
f.notes.push(format!(306
"{} volgt {} (losse voetketen zonder eigen animatie)",307
character.nodes[top].name, character.nodes[driver].name308
));309
}310
}311
}312
// Knee direction vs. the body's own front.313
let front = match forward {314
Some(Forward::PlusZ) => 1.,315
_ => -1.,316
};317
let mut legs: Vec<(usize, usize, [f32; 3])> = Vec::new(); // (thigh node, shin node, ankle in shin space)318
for &(node, driver) in &f.follow {319
if let (Some(p), Some(inv)) = (320
character.nodes[driver].parent,321
affine_inverse(&rest[driver]),322
) {323
legs.push((324
p,325
driver,326
point(&inv, [rest[node][12], rest[node][13], rest[node][14]]),327
));328
}329
}330
let (mut good, mut bad) = (0, 0);331
if !legs.is_empty() {332
let partial = BodyFixups {333
follow: f.follow.clone(),334
..Default::default()335
};336
for clip in &character.clips {337
let frames = ((clip.duration * 24.).ceil() as usize).max(1);338
for i in 0..frames {339
let w = apply_with(340
character,341
&partial,342
preview::raw_posed_worlds(character, &clip.name, i as f32 / 24.),343
);344
for (thigh, shin, ankle_local) in &legs {345
let hip = [w[*thigh][12], w[*thigh][13], w[*thigh][14]];346
let knee = [w[*shin][12], w[*shin][13], w[*shin][14]];347
let ankle = point(&w[*shin], *ankle_local);348
let dy = ankle[1] - hip[1];349
if dy.abs() < 1e-4 {350
continue;351
}352
let t = (knee[1] - hip[1]) / dy;353
let line_z = hip[2] + t * (ankle[2] - hip[2]);354
let off = (knee[2] - line_z) * front;355
if off > 0.02 * h {356
good += 1;357
} else if off < -0.02 * h {358
bad += 1;359
}360
}361
}362
}363
}364
if bad >= 4 && bad > 3 * good {365
f.mirror_motion = true;366
f.notes.push(format!(367
"beweging voor/achter gespiegeld: knieën bogen in {bad} van {} gemeten standen de verkeerde kant op",368
bad + good369
));370
}371
if forward == Some(Forward::PlusZ) {372
f.turn_180 = true;373
f.notes.push("lichaam kijkt naar +Z (tenen voor de enkels): 180° gedraaid zodat yaw 0 de voorkant toont".into());374
}375
f376
}377
378
/// Detect and store the fixups on a loaded body. Static bodies (no bones)379
/// get their +Z-facing geometry turned directly.380
pub fn finalize(mut character: PreviewCharacter) -> PreviewCharacter {381
if character.is_static() {382
if detect_forward(&character.positions) == Some(Forward::PlusZ) {383
for p in character384
.positions385
.iter_mut()386
.chain(character.normals.iter_mut())387
{388
p[0] = -p[0];389
p[2] = -p[2];390
}391
character.fixups.notes.push(392
"lichaam kijkt naar +Z (tenen voor de enkels): 180° gedraaid zodat yaw 0 de voorkant toont".into(),393
);394
}395
return character;396
}397
character.fixups = detect(&character);398
character399
}400
401
/// Forward axis of a body's rest pose (None when unclear).402
pub fn forward_of(character: &PreviewCharacter) -> Option<Forward> {403
detect_forward(&rest_positions(character))404
}405
406
#[cfg(test)]407
mod tests {408
use super::*;409
use crate::preview::{410
AnimChannel, BodyRig, ChannelProperty, Interpolation, PreviewClip, SceneNode,411
};412
413
fn node(name: &str, parent: Option<usize>, t: [f32; 3], r: [f32; 4]) -> SceneNode {414
SceneNode {415
name: name.into(),416
parent,417
translation: t,418
rotation: r,419
scale: [1.; 3],420
}421
}422
423
/// Leg whose foot control hangs under the root (IK style), with a knee424
/// that bends by `knee_deg` (negative = shin back = natural for −Z front).425
fn ik_leg(knee_deg: f32) -> PreviewCharacter {426
let half = (knee_deg.to_radians() / 2.).sin_cos();427
let nodes = vec![428
node("root", None, [0., 0., 0.], [0., 0., 0., 1.]),429
node("thigh", Some(0), [0., 1., 0.], [1., 0., 0., 0.]),430
node("calf", Some(1), [0., 0.5, 0.], [0., 0., 0., 1.]),431
node("footControl", Some(0), [0., 0.05, 0.], [0., 0., 0., 1.]),432
node("toes", Some(3), [0., 0., -0.1], [0., 0., 0., 1.]),433
];434
let clip = PreviewClip {435
name: "walk".into(),436
duration: 1.,437
channels: vec![AnimChannel {438
node: 2,439
property: ChannelProperty::Rotation,440
times: vec![0., 0.5, 1.],441
values: vec![0., 0., 0., 1., half.0, 0., 0., half.1, 0., 0., 0., 1.],442
interpolation: Interpolation::Linear,443
}],444
skipped: Vec::new(),445
};446
PreviewCharacter {447
nodes,448
joint_nodes: (0..5).collect(),449
joint_names: ["root", "thigh", "calf", "footControl", "toes"]450
.map(String::from)451
.to_vec(),452
inverse_binds: vec![preview::identity(); 5],453
positions: vec![[0., 0., 0.], [0., 1.8, 0.]],454
normals: vec![[0., 1., 0.]; 2],455
source_normals_complete: true,456
joints: vec![[0; 4]; 2],457
weights: vec![[1., 0., 0., 0.]; 2],458
indices: Vec::new(),459
base_color: [1.; 4],460
colors: Vec::new(),461
uvs: Vec::new(),462
surfaces: Vec::new(),463
clips: vec![clip],464
rig: BodyRig::Skinned,465
fixups: BodyFixups::default(),466
}467
}468
469
#[test]470
fn detached_foot_follows_the_shin() {471
let mut leg = ik_leg(-40.);472
leg.fixups = detect(&leg);473
assert_eq!(leg.fixups.follow, vec![(3, 2)]);474
assert!(!leg.fixups.mirror_motion && !leg.fixups.turn_180);475
let w = preview::posed_worlds(&leg, "walk", 0.5);476
// Foot origin = shin end (0.45 m along the shin), toes keep their offset.477
let end = point(&w[2], [0., 0.45, 0.]);478
for a in 0..3 {479
assert!(480
(w[3][12 + a] - end[a]).abs() < 1e-4,481
"{:?} vs {end:?}",482
&w[3][12..15]483
);484
}485
assert!(486
end[2] > 0.2,487
"shin swings back (+Z) for a −Z front: {end:?}"488
);489
let toes = [w[4][12], w[4][13], w[4][14]];490
assert!((toes[2] - end[2] + 0.1).abs() < 0.05 || (toes[1] - end[1]).abs() > 0.01);491
// At rest nothing moves.492
let r = preview::posed_worlds(&leg, "", 0.);493
assert!((r[3][13] - 0.05).abs() < 1e-5);494
}495
496
#[test]497
fn backwards_knees_mirror_the_motion_front_back() {498
let mut leg = ik_leg(40.);499
leg.fixups = detect(&leg);500
assert!(leg.fixups.mirror_motion, "{:?}", leg.fixups.notes);501
let w = preview::posed_worlds(&leg, "walk", 0.5);502
let ankle = [w[3][12], w[3][13], w[3][14]];503
assert!(504
ankle[2] > 0.2,505
"after the mirror the shin swings back: {ankle:?}"506
);507
// The rest pose is unchanged by the mirror.508
let r = preview::posed_worlds(&leg, "", 0.);509
assert!((r[2][13] - 0.5).abs() < 1e-5 && r[2][14].abs() < 1e-5);510
}511
512
#[test]513
fn plus_z_bodies_turn_and_test_bodies_stay() {514
let mut feet = Vec::new();515
for i in 0..1200 {516
let f = i as f32 / 1200.;517
// Feet (lowest 4%) reach +Z, ankles (6–10% of height) sit behind.518
if i % 3 == 0 {519
feet.push([f * 0.1, 0.02, 0.1 * f]);520
} else if i % 3 == 1 {521
feet.push([f * 0.1, 0.16, -0.05]);522
} else {523
feet.push([f * 0.1, 0.2 + 1.5 * f, 0.]);524
}525
}526
assert_eq!(detect_forward(&feet), Some(Forward::PlusZ));527
let mirrored: Vec<[f32; 3]> = feet.iter().map(|p| [p[0], p[1], -p[2]]).collect();528
assert_eq!(detect_forward(&mirrored), Some(Forward::MinusZ));529
for f in [530
"test-body-male.glb",531
"test-body-female.glb",532
"test-body-male.fbx",533
] {534
let b = preview::load_body_file(535
&std::path::Path::new(env!("CARGO_MANIFEST_DIR"))536
.join("tests/fixtures/bodies")537
.join(f),538
)539
.unwrap();540
assert!(b.fixups.is_identity(), "{f}: {:?}", b.fixups.notes);541
}542
}543
}@@ -3,7 +3,7 @@
3
3
//! This is not the paperdoll export. The export camera is orthographic and
4
4
//! still out of scope. The interactive camera can snap to the same five
5
5
//! pitches and twelve yaws. The character forward axis of the test bodies is -Z,
6
//! so yaw 0 places the camera on -Z, looking at the body.
6
//! so yaw 0 places the camera on +Z (the glTF front), looking at the body.
7
7
8
8
use std::path::Path;
9
9
@@ -103,8 +103,6 @@ pub struct PreviewCharacter {
103
103
pub clips: Vec<PreviewClip>,
104
104
/// Skinned (skeleton + clips) or static geometry without a skeleton.
105
105
pub rig: BodyRig,
106
/// Load-time orientation/animation corrections (see `orient`).
107
pub fixups: crate::orient::BodyFixups,
108
106
}
109
107
110
108
/// Whether a body has a real skin. A static body keeps an empty skeleton and
@@ -311,11 +309,6 @@ pub fn is_drawable_body(path: &Path) -> bool {
311
309
/// deformer) load as [`BodyRig::Static`]: geometry in its stored pose, no
312
310
/// skeleton and no clips.
313
311
pub fn load_body_file(path: &Path) -> Result<PreviewCharacter, String> {
314
load_body_file_raw(path).map(crate::orient::finalize)
315
}
316
317
/// Load without orientation/animation fixups (diagnostics and tests).
318
pub fn load_body_file_raw(path: &Path) -> Result<PreviewCharacter, String> {
319
312
match path
320
313
.extension()
321
314
.and_then(|e| e.to_str())
@@ -493,7 +486,6 @@ impl StaticAcc {
493
486
format: format.to_string(),
494
487
reason,
495
488
},
496
fixups: Default::default(),
497
489
})
498
490
}
499
491
}
@@ -779,7 +771,6 @@ fn load_gltf_file_unguarded(path: &Path) -> Result<PreviewCharacter, String> {
779
771
surfaces,
780
772
clips,
781
773
rig: BodyRig::Skinned,
782
fixups: Default::default(),
783
774
})
784
775
}
785
776
@@ -1031,7 +1022,7 @@ fn apply_uv(m: &[[f32; 3]; 2], uv: [f32; 2]) -> [f32; 2] {
1031
1022
}
1032
1023
1033
1024
/// Inverse of an affine matrix (None when singular).
1034
pub(crate) fn affine_inverse(m: &Mat4) -> Option<Mat4> {
1025
fn affine_inverse(m: &Mat4) -> Option<Mat4> {
1035
1026
let it = normal_matrix(m)?; // inverse-transpose of the 3×3
1036
1027
// inverse 3×3 (row r, col c) = it[c][r]
1037
1028
let mut out = identity();
@@ -1097,37 +1088,9 @@ fn mat3_mul(m: &[[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {
1097
1088
]
1098
1089
}
1099
1090
1100
/// World matrices of every node for a clip time, with the body's load-time
1101
/// fixups applied (feet following the shin, motion mirror, 180° turn). Every
1102
/// render path (live, still, production, equipment) goes through here.
1103
pub fn posed_worlds(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
1091
pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
1104
1092
let locals = sample_local_matrices(character, clip_name, time);
1105
1093
let worlds = world_matrices(&character.nodes, &locals);
1106
crate::orient::apply(character, worlds)
1107
}
1108
1109
pub(crate) fn rest_worlds(character: &PreviewCharacter) -> Vec<Mat4> {
1110
let locals: Vec<Mat4> = character
1111
.nodes
1112
.iter()
1113
.map(|n| trs(n.translation, n.rotation, n.scale))
1114
.collect();
1115
world_matrices(&character.nodes, &locals)
1116
}
1117
1118
pub(crate) fn raw_posed_worlds(
1119
character: &PreviewCharacter,
1120
clip_name: &str,
1121
time: f32,
1122
) -> Vec<Mat4> {
1123
world_matrices(
1124
&character.nodes,
1125
&sample_local_matrices(character, clip_name, time),
1126
)
1127
}
1128
1129
pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
1130
let worlds = posed_worlds(character, clip_name, time);
1131
1094
character
1132
1095
.joint_nodes
1133
1096
.iter()
@@ -1185,7 +1148,8 @@ pub fn animated_joint_world(
1185
1148
time: f32,
1186
1149
joint: usize,
1187
1150
) -> Option<Mat4> {
1188
let worlds = posed_worlds(character, clip, time);
1151
let locals = sample_local_matrices(character, clip, time);
1152
let worlds = world_matrices(&character.nodes, &locals);
1189
1153
character
1190
1154
.joint_nodes
1191
1155
.get(joint)
@@ -1204,7 +1168,8 @@ pub fn joint_world_translation(
1204
1168
.iter()
1205
1169
.position(|name| name == joint_name)?;
1206
1170
let node = character.joint_nodes[joint];
1207
let worlds = posed_worlds(character, clip_name, time);
1171
let locals = sample_local_matrices(character, clip_name, time);
1172
let worlds = world_matrices(&character.nodes, &locals);
1208
1173
Some([worlds[node][12], worlds[node][13], worlds[node][14]])
1209
1174
}
1210
1175
@@ -1223,9 +1188,9 @@ pub fn eye_offset(yaw_deg: f32, pitch_deg: f32, distance: f32) -> [f32; 3] {
1223
1188
let yaw = yaw_deg.to_radians();
1224
1189
let pitch = pitch_deg.to_radians();
1225
1190
[
1226
-distance * pitch.cos() * yaw.sin(),
1191
distance * pitch.cos() * yaw.sin(),
1227
1192
distance * pitch.sin(),
1228
-distance * pitch.cos() * yaw.cos(),
1193
distance * pitch.cos() * yaw.cos(),
1229
1194
]
1230
1195
}
1231
1196
@@ -1962,7 +1927,10 @@ mod tests {
1962
1927
#[test]
1963
1928
fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {
1964
1929
let front = eye_offset(0.0, 0.0, 4.0);
1965
assert!(front[2] < -3.5, "yaw 0 camera belongs on -Z, got {front:?}");
1930
assert!(
1931
front[2] > 3.5,
1932
"yaw 0 camera belongs on +Z (glTF front), got {front:?}"
1933
);
1966
1934
assert!(front[0].abs() < 0.05);
1967
1935
let mut seen = Vec::new();
1968
1936
for yaw in paperdoll_yaws_deg() {
@@ -3,7 +3,7 @@3
//! This is not the paperdoll export. The export camera is orthographic and3
//! This is not the paperdoll export. The export camera is orthographic and4
//! still out of scope. The interactive camera can snap to the same five4
//! still out of scope. The interactive camera can snap to the same five5
//! pitches and twelve yaws. The character forward axis of the test bodies is -Z,5
//! pitches and twelve yaws. The character forward axis of the test bodies is -Z,6
//! so yaw 0 places the camera on -Z, looking at the body.6
//! so yaw 0 places the camera on +Z (the glTF front), looking at the body.7
7
8
use std::path::Path;8
use std::path::Path;9
9
@@ -103,8 +103,6 @@ pub struct PreviewCharacter {103
pub clips: Vec<PreviewClip>,103
pub clips: Vec<PreviewClip>,104
/// Skinned (skeleton + clips) or static geometry without a skeleton.104
/// Skinned (skeleton + clips) or static geometry without a skeleton.105
pub rig: BodyRig,105
pub rig: BodyRig,106
/// Load-time orientation/animation corrections (see `orient`).107
pub fixups: crate::orient::BodyFixups,108
}106
}109
107
110
/// Whether a body has a real skin. A static body keeps an empty skeleton and108
/// Whether a body has a real skin. A static body keeps an empty skeleton and@@ -311,11 +309,6 @@ pub fn is_drawable_body(path: &Path) -> bool {311
/// deformer) load as [`BodyRig::Static`]: geometry in its stored pose, no309
/// deformer) load as [`BodyRig::Static`]: geometry in its stored pose, no312
/// skeleton and no clips.310
/// skeleton and no clips.313
pub fn load_body_file(path: &Path) -> Result<PreviewCharacter, String> {311
pub fn load_body_file(path: &Path) -> Result<PreviewCharacter, String> {314
load_body_file_raw(path).map(crate::orient::finalize)315
}316
317
/// Load without orientation/animation fixups (diagnostics and tests).318
pub fn load_body_file_raw(path: &Path) -> Result<PreviewCharacter, String> {319
match path312
match path320
.extension()313
.extension()321
.and_then(|e| e.to_str())314
.and_then(|e| e.to_str())@@ -493,7 +486,6 @@ impl StaticAcc {493
format: format.to_string(),486
format: format.to_string(),494
reason,487
reason,495
},488
},496
fixups: Default::default(),497
})489
})498
}490
}499
}491
}@@ -779,7 +771,6 @@ fn load_gltf_file_unguarded(path: &Path) -> Result<PreviewCharacter, String> {779
surfaces,771
surfaces,780
clips,772
clips,781
rig: BodyRig::Skinned,773
rig: BodyRig::Skinned,782
fixups: Default::default(),783
})774
})784
}775
}785
776
@@ -1031,7 +1022,7 @@ fn apply_uv(m: &[[f32; 3]; 2], uv: [f32; 2]) -> [f32; 2] {1031
}1022
}1032
1023
1033
/// Inverse of an affine matrix (None when singular).1024
/// Inverse of an affine matrix (None when singular).1034
pub(crate) fn affine_inverse(m: &Mat4) -> Option<Mat4> {1025
fn affine_inverse(m: &Mat4) -> Option<Mat4> {1035
let it = normal_matrix(m)?; // inverse-transpose of the 3×31026
let it = normal_matrix(m)?; // inverse-transpose of the 3×31036
// inverse 3×3 (row r, col c) = it[c][r]1027
// inverse 3×3 (row r, col c) = it[c][r]1037
let mut out = identity();1028
let mut out = identity();@@ -1097,37 +1088,9 @@ fn mat3_mul(m: &[[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {1097
]1088
]1098
}1089
}1099
1090
1100
/// World matrices of every node for a clip time, with the body's load-time1091
pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {1101
/// fixups applied (feet following the shin, motion mirror, 180° turn). Every1102
/// render path (live, still, production, equipment) goes through here.1103
pub fn posed_worlds(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {1104
let locals = sample_local_matrices(character, clip_name, time);1092
let locals = sample_local_matrices(character, clip_name, time);1105
let worlds = world_matrices(&character.nodes, &locals);1093
let worlds = world_matrices(&character.nodes, &locals);1106
crate::orient::apply(character, worlds)1107
}1108
1109
pub(crate) fn rest_worlds(character: &PreviewCharacter) -> Vec<Mat4> {1110
let locals: Vec<Mat4> = character1111
.nodes1112
.iter()1113
.map(|n| trs(n.translation, n.rotation, n.scale))1114
.collect();1115
world_matrices(&character.nodes, &locals)1116
}1117
1118
pub(crate) fn raw_posed_worlds(1119
character: &PreviewCharacter,1120
clip_name: &str,1121
time: f32,1122
) -> Vec<Mat4> {1123
world_matrices(1124
&character.nodes,1125
&sample_local_matrices(character, clip_name, time),1126
)1127
}1128
1129
pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {1130
let worlds = posed_worlds(character, clip_name, time);1131
character1094
character1132
.joint_nodes1095
.joint_nodes1133
.iter()1096
.iter()@@ -1185,7 +1148,8 @@ pub fn animated_joint_world(1185
time: f32,1148
time: f32,1186
joint: usize,1149
joint: usize,1187
) -> Option<Mat4> {1150
) -> Option<Mat4> {1188
let worlds = posed_worlds(character, clip, time);1151
let locals = sample_local_matrices(character, clip, time);1152
let worlds = world_matrices(&character.nodes, &locals);1189
character1153
character1190
.joint_nodes1154
.joint_nodes1191
.get(joint)1155
.get(joint)@@ -1204,7 +1168,8 @@ pub fn joint_world_translation(1204
.iter()1168
.iter()1205
.position(|name| name == joint_name)?;1169
.position(|name| name == joint_name)?;1206
let node = character.joint_nodes[joint];1170
let node = character.joint_nodes[joint];1207
let worlds = posed_worlds(character, clip_name, time);1171
let locals = sample_local_matrices(character, clip_name, time);1172
let worlds = world_matrices(&character.nodes, &locals);1208
Some([worlds[node][12], worlds[node][13], worlds[node][14]])1173
Some([worlds[node][12], worlds[node][13], worlds[node][14]])1209
}1174
}1210
1175
@@ -1223,9 +1188,9 @@ pub fn eye_offset(yaw_deg: f32, pitch_deg: f32, distance: f32) -> [f32; 3] {1223
let yaw = yaw_deg.to_radians();1188
let yaw = yaw_deg.to_radians();1224
let pitch = pitch_deg.to_radians();1189
let pitch = pitch_deg.to_radians();1225
[1190
[1226
-distance * pitch.cos() * yaw.sin(),1191
distance * pitch.cos() * yaw.sin(),1227
distance * pitch.sin(),1192
distance * pitch.sin(),1228
-distance * pitch.cos() * yaw.cos(),1193
distance * pitch.cos() * yaw.cos(),1229
]1194
]1230
}1195
}1231
1196
@@ -1962,7 +1927,10 @@ mod tests {1962
#[test]1927
#[test]1963
fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {1928
fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {1964
let front = eye_offset(0.0, 0.0, 4.0);1929
let front = eye_offset(0.0, 0.0, 4.0);1965
assert!(front[2] < -3.5, "yaw 0 camera belongs on -Z, got {front:?}");1930
assert!(1931
front[2] > 3.5,1932
"yaw 0 camera belongs on +Z (glTF front), got {front:?}"1933
);1966
assert!(front[0].abs() < 0.05);1934
assert!(front[0].abs() < 0.05);1967
let mut seen = Vec::new();1935
let mut seen = Vec::new();1968
for yaw in paperdoll_yaws_deg() {1936
for yaw in paperdoll_yaws_deg() {@@ -137,7 +137,7 @@ impl Viewport {
137
137
));
138
138
});
139
139
ui.label(
140
"Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (−Z). De knoppen zetten de vijf pitches en de twaalf yaws.",
140
"Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (+Z, glTF-standaard). De knoppen zetten de vijf pitches en de twaalf yaws.",
141
141
);
142
142
let available = ui.available_rect_before_wrap();
143
143
let rect = egui::Rect::from_min_size(
@@ -137,7 +137,7 @@ impl Viewport {137
));137
));138
});138
});139
ui.label(139
ui.label(140
"Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (−Z). De knoppen zetten de vijf pitches en de twaalf yaws.",140
"Sleep om te draaien, scroll om te zoomen. Yaw 0 kijkt op de voorkant (+Z, glTF-standaard). De knoppen zetten de vijf pitches en de twaalf yaws.",141
);141
);142
let available = ui.available_rect_before_wrap();142
let available = ui.available_rect_before_wrap();143
let rect = egui::Rect::from_min_size(143
let rect = egui::Rect::from_min_size(@@ -1474,7 +1474,7 @@ pub fn contract(mut job: RenderJob) -> Result<Contract, String> {
1474
1474
"depth_bits": job.depth_format.bits,
1475
1475
"normal_space": "view (+Y up), PNG rows top-down",
1476
1476
"camera_distance": crate::camera::CAMERA_DISTANCE,
1477
"yaw_zero": "camera on -Z",
1477
"yaw_zero": "camera on +Z",
1478
1478
"pitches": preview::PRODUCTION_PITCHES,
1479
1479
"yaws": production_yaws(),
1480
1480
"yaw_step": 30,
@@ -1474,7 +1474,7 @@ pub fn contract(mut job: RenderJob) -> Result<Contract, String> {1474
"depth_bits": job.depth_format.bits,1474
"depth_bits": job.depth_format.bits,1475
"normal_space": "view (+Y up), PNG rows top-down",1475
"normal_space": "view (+Y up), PNG rows top-down",1476
"camera_distance": crate::camera::CAMERA_DISTANCE,1476
"camera_distance": crate::camera::CAMERA_DISTANCE,1477
"yaw_zero": "camera on -Z",1477
"yaw_zero": "camera on +Z",1478
"pitches": preview::PRODUCTION_PITCHES,1478
"pitches": preview::PRODUCTION_PITCHES,1479
"yaws": production_yaws(),1479
"yaws": production_yaws(),1480
"yaw_step": 30,1480
"yaw_step": 30,@@ -185,12 +185,12 @@ mod tests {
185
185
.join("tests/fixtures/bodies/test-body-male.glb");
186
186
let mut character = load_gltf_file(&path).unwrap();
187
187
character.positions = vec![
188
[-0.5, 0.4, -0.25],
189
[0.5, 0.4, -0.25],
190
[0., 1.5, -0.25],
191
188
[-0.5, 0.4, 0.25],
192
189
[0.5, 0.4, 0.25],
193
[0., 1.5, 0.95],
190
[0., 1.5, 0.25],
191
[-0.5, 0.4, -0.25],
192
[0.5, 0.4, -0.25],
193
[0., 1.5, -0.95],
194
194
];
195
195
character.joint_nodes.clear();
196
196
character.inverse_binds.clear();
@@ -185,12 +185,12 @@ mod tests {185
.join("tests/fixtures/bodies/test-body-male.glb");185
.join("tests/fixtures/bodies/test-body-male.glb");186
let mut character = load_gltf_file(&path).unwrap();186
let mut character = load_gltf_file(&path).unwrap();187
character.positions = vec![187
character.positions = vec![188
[-0.5, 0.4, -0.25],189
[0.5, 0.4, -0.25],190
[0., 1.5, -0.25],191
[-0.5, 0.4, 0.25],188
[-0.5, 0.4, 0.25],192
[0.5, 0.4, 0.25],189
[0.5, 0.4, 0.25],193
[0., 1.5, 0.95],190
[0., 1.5, 0.25],191
[-0.5, 0.4, -0.25],192
[0.5, 0.4, -0.25],193
[0., 1.5, -0.95],194
];194
];195
character.joint_nodes.clear();195
character.joint_nodes.clear();196
character.inverse_binds.clear();196
character.inverse_binds.clear();@@ -12,7 +12,7 @@ Beide hebben een skin met 19 joints en twee clips:
12
12
- `walk` — loopcyclus van 1 seconde, armen tegengesteld aan de benen, eerste en laatste sample gelijk
13
13
- `attack` — zwaai van de rechterarm naar voren, 0,8 seconde, eindigt weer in de bindpose
14
14
15
De bindpose heeft identiteitsrotaties. glTF-assen: +Y is omhoog, het gezicht wijst naar -Z. Elke vertex hoort met gewicht 1 bij één joint. De inverse-bindmatrix is de inverse translatie van de wereldpositie in die bindpose.
15
De bindpose heeft identiteitsrotaties. glTF-assen: +Y is omhoog, het gezicht wijst naar +Z (glTF-standaard), de linkerzijde ligt op +X. Elke vertex hoort met gewicht 1 bij één joint. De inverse-bindmatrix is de inverse translatie van de wereldpositie in die bindpose.
16
16
17
17
Opnieuw genereren, vanaf de repositoryroot:
18
18
@@ -12,7 +12,7 @@ Beide hebben een skin met 19 joints en twee clips:12
- `walk` — loopcyclus van 1 seconde, armen tegengesteld aan de benen, eerste en laatste sample gelijk12
- `walk` — loopcyclus van 1 seconde, armen tegengesteld aan de benen, eerste en laatste sample gelijk13
- `attack` — zwaai van de rechterarm naar voren, 0,8 seconde, eindigt weer in de bindpose13
- `attack` — zwaai van de rechterarm naar voren, 0,8 seconde, eindigt weer in de bindpose14
14
15
De bindpose heeft identiteitsrotaties. glTF-assen: +Y is omhoog, het gezicht wijst naar -Z. Elke vertex hoort met gewicht 1 bij één joint. De inverse-bindmatrix is de inverse translatie van de wereldpositie in die bindpose.15
De bindpose heeft identiteitsrotaties. glTF-assen: +Y is omhoog, het gezicht wijst naar +Z (glTF-standaard), de linkerzijde ligt op +X. Elke vertex hoort met gewicht 1 bij één joint. De inverse-bindmatrix is de inverse translatie van de wereldpositie in die bindpose.16
16
17
Opnieuw genereren, vanaf de repositoryroot:17
Opnieuw genereren, vanaf de repositoryroot:18
18
The contents of this binary file are not shown.
@@ -103,86 +103,6 @@ vn 0.000000 1.000000 0.000000
103
103
vn 0.000000 1.000000 0.000000
104
104
vn 0.000000 -1.000000 0.000000
105
105
vn 0.000000 -1.000000 0.000000
106
v -0.250000 1.340000 -0.040000
107
v -0.150000 1.340000 -0.040000
108
v -0.150000 1.420000 -0.040000
109
v -0.250000 1.420000 -0.040000
110
v -0.250000 1.340000 0.040000
111
v -0.150000 1.340000 0.040000
112
v -0.150000 1.420000 0.040000
113
v -0.250000 1.420000 0.040000
114
vn 0.000000 0.000000 -1.000000
115
vn 0.000000 0.000000 -1.000000
116
vn 0.000000 0.000000 1.000000
117
vn 0.000000 -0.000000 1.000000
118
vn -1.000000 0.000000 0.000000
119
vn -1.000000 0.000000 0.000000
120
vn 1.000000 0.000000 0.000000
121
vn 1.000000 0.000000 0.000000
122
vn 0.000000 1.000000 0.000000
123
vn 0.000000 1.000000 0.000000
124
vn 0.000000 -1.000000 0.000000
125
vn 0.000000 -1.000000 0.000000
126
v -0.320000 1.240000 -0.040000
127
v -0.240000 1.240000 -0.040000
128
v -0.240000 1.520000 -0.040000
129
v -0.320000 1.520000 -0.040000
130
v -0.320000 1.240000 0.040000
131
v -0.240000 1.240000 0.040000
132
v -0.240000 1.520000 0.040000
133
v -0.320000 1.520000 0.040000
134
vn 0.000000 0.000000 -1.000000
135
vn 0.000000 0.000000 -1.000000
136
vn 0.000000 0.000000 1.000000
137
vn 0.000000 -0.000000 1.000000
138
vn -1.000000 0.000000 0.000000
139
vn -1.000000 0.000000 0.000000
140
vn 1.000000 0.000000 0.000000
141
vn 1.000000 0.000000 0.000000
142
vn 0.000000 1.000000 0.000000
143
vn 0.000000 1.000000 0.000000
144
vn 0.000000 -1.000000 0.000000
145
vn 0.000000 -1.000000 0.000000
146
v -0.315000 0.970000 -0.035000
147
v -0.245000 0.970000 -0.035000
148
v -0.245000 1.230000 -0.035000
149
v -0.315000 1.230000 -0.035000
150
v -0.315000 0.970000 0.035000
151
v -0.245000 0.970000 0.035000
152
v -0.245000 1.230000 0.035000
153
v -0.315000 1.230000 0.035000
154
vn 0.000000 0.000000 -1.000000
155
vn 0.000000 0.000000 -1.000000
156
vn 0.000000 0.000000 1.000000
157
vn 0.000000 -0.000000 1.000000
158
vn -1.000000 0.000000 0.000000
159
vn -1.000000 0.000000 0.000000
160
vn 1.000000 0.000000 0.000000
161
vn 1.000000 0.000000 0.000000
162
vn 0.000000 1.000000 0.000000
163
vn 0.000000 1.000000 0.000000
164
vn 0.000000 -1.000000 0.000000
165
vn 0.000000 -1.000000 0.000000
166
v -0.320000 0.790000 -0.020000
167
v -0.240000 0.790000 -0.020000
168
v -0.240000 0.890000 -0.020000
169
v -0.320000 0.890000 -0.020000
170
v -0.320000 0.790000 0.020000
171
v -0.240000 0.790000 0.020000
172
v -0.240000 0.890000 0.020000
173
v -0.320000 0.890000 0.020000
174
vn 0.000000 0.000000 -1.000000
175
vn 0.000000 0.000000 -1.000000
176
vn 0.000000 0.000000 1.000000
177
vn 0.000000 -0.000000 1.000000
178
vn -1.000000 0.000000 0.000000
179
vn -1.000000 0.000000 0.000000
180
vn 1.000000 0.000000 0.000000
181
vn 1.000000 0.000000 0.000000
182
vn 0.000000 1.000000 0.000000
183
vn 0.000000 1.000000 0.000000
184
vn 0.000000 -1.000000 0.000000
185
vn 0.000000 -1.000000 0.000000
186
106
v 0.150000 1.340000 -0.040000
187
107
v 0.250000 1.340000 -0.040000
188
108
v 0.250000 1.420000 -0.040000
@@ -263,14 +183,14 @@ vn 0.000000 1.000000 0.000000
263
183
vn 0.000000 1.000000 0.000000
264
184
vn 0.000000 -1.000000 0.000000
265
185
vn 0.000000 -1.000000 0.000000
266
v -0.160000 0.690000 -0.060000
267
v -0.040000 0.690000 -0.060000
268
v -0.040000 1.110000 -0.060000
269
v -0.160000 1.110000 -0.060000
270
v -0.160000 0.690000 0.060000
271
v -0.040000 0.690000 0.060000
272
v -0.040000 1.110000 0.060000
273
v -0.160000 1.110000 0.060000
186
v -0.250000 1.340000 -0.040000
187
v -0.150000 1.340000 -0.040000
188
v -0.150000 1.420000 -0.040000
189
v -0.250000 1.420000 -0.040000
190
v -0.250000 1.340000 0.040000
191
v -0.150000 1.340000 0.040000
192
v -0.150000 1.420000 0.040000
193
v -0.250000 1.420000 0.040000
274
194
vn 0.000000 0.000000 -1.000000
275
195
vn 0.000000 0.000000 -1.000000
276
196
vn 0.000000 0.000000 1.000000
@@ -283,14 +203,34 @@ vn 0.000000 1.000000 0.000000
283
203
vn 0.000000 1.000000 0.000000
284
204
vn 0.000000 -1.000000 0.000000
285
205
vn 0.000000 -1.000000 0.000000
286
v -0.145000 0.280000 -0.045000
287
v -0.055000 0.280000 -0.045000
288
v -0.055000 0.680000 -0.045000
289
v -0.145000 0.680000 -0.045000
290
v -0.145000 0.280000 0.045000
291
v -0.055000 0.280000 0.045000
292
v -0.055000 0.680000 0.045000
293
v -0.145000 0.680000 0.045000
206
v -0.320000 1.240000 -0.040000
207
v -0.240000 1.240000 -0.040000
208
v -0.240000 1.520000 -0.040000
209
v -0.320000 1.520000 -0.040000
210
v -0.320000 1.240000 0.040000
211
v -0.240000 1.240000 0.040000
212
v -0.240000 1.520000 0.040000
213
v -0.320000 1.520000 0.040000
214
vn 0.000000 0.000000 -1.000000
215
vn 0.000000 0.000000 -1.000000
216
vn 0.000000 0.000000 1.000000
217
vn 0.000000 -0.000000 1.000000
218
vn -1.000000 0.000000 0.000000
219
vn -1.000000 0.000000 0.000000
220
vn 1.000000 0.000000 0.000000
221
vn 1.000000 0.000000 0.000000
222
vn 0.000000 1.000000 0.000000
223
vn 0.000000 1.000000 0.000000
224
vn 0.000000 -1.000000 0.000000
225
vn 0.000000 -1.000000 0.000000
226
v -0.315000 0.970000 -0.035000
227
v -0.245000 0.970000 -0.035000
228
v -0.245000 1.230000 -0.035000
229
v -0.315000 1.230000 -0.035000
230
v -0.315000 0.970000 0.035000
231
v -0.245000 0.970000 0.035000
232
v -0.245000 1.230000 0.035000
233
v -0.315000 1.230000 0.035000
294
234
vn 0.000000 0.000000 -1.000000
295
235
vn 0.000000 0.000000 -1.000000
296
236
vn 0.000000 0.000000 1.000000
@@ -303,14 +243,14 @@ vn 0.000000 1.000000 0.000000
303
243
vn 0.000000 1.000000 0.000000
304
244
vn 0.000000 -1.000000 0.000000
305
245
vn 0.000000 -1.000000 0.000000
306
v -0.150000 0.050000 -0.170000
307
v -0.050000 0.050000 -0.170000
308
v -0.050000 0.110000 -0.170000
309
v -0.150000 0.110000 -0.170000
310
v -0.150000 0.050000 0.050000
311
v -0.050000 0.050000 0.050000
312
v -0.050000 0.110000 0.050000
313
v -0.150000 0.110000 0.050000
246
v -0.320000 0.790000 -0.020000
247
v -0.240000 0.790000 -0.020000
248
v -0.240000 0.890000 -0.020000
249
v -0.320000 0.890000 -0.020000
250
v -0.320000 0.790000 0.020000
251
v -0.240000 0.790000 0.020000
252
v -0.240000 0.890000 0.020000
253
v -0.320000 0.890000 0.020000
314
254
vn 0.000000 0.000000 -1.000000
315
255
vn 0.000000 0.000000 -1.000000
316
256
vn 0.000000 0.000000 1.000000
@@ -363,14 +303,74 @@ vn 0.000000 1.000000 0.000000
363
303
vn 0.000000 1.000000 0.000000
364
304
vn 0.000000 -1.000000 0.000000
365
305
vn 0.000000 -1.000000 0.000000
366
v 0.050000 0.050000 -0.170000
367
v 0.150000 0.050000 -0.170000
368
v 0.150000 0.110000 -0.170000
369
v 0.050000 0.110000 -0.170000
370
v 0.050000 0.050000 0.050000
371
v 0.150000 0.050000 0.050000
372
v 0.150000 0.110000 0.050000
373
v 0.050000 0.110000 0.050000
306
v 0.050000 0.050000 -0.050000
307
v 0.150000 0.050000 -0.050000
308
v 0.150000 0.110000 -0.050000
309
v 0.050000 0.110000 -0.050000
310
v 0.050000 0.050000 0.170000
311
v 0.150000 0.050000 0.170000
312
v 0.150000 0.110000 0.170000
313
v 0.050000 0.110000 0.170000
314
vn 0.000000 0.000000 -1.000000
315
vn 0.000000 0.000000 -1.000000
316
vn 0.000000 0.000000 1.000000
317
vn 0.000000 -0.000000 1.000000
318
vn -1.000000 0.000000 0.000000
319
vn -1.000000 0.000000 0.000000
320
vn 1.000000 0.000000 0.000000
321
vn 1.000000 0.000000 0.000000
322
vn 0.000000 1.000000 0.000000
323
vn 0.000000 1.000000 0.000000
324
vn 0.000000 -1.000000 0.000000
325
vn 0.000000 -1.000000 0.000000
326
v -0.160000 0.690000 -0.060000
327
v -0.040000 0.690000 -0.060000
328
v -0.040000 1.110000 -0.060000
329
v -0.160000 1.110000 -0.060000
330
v -0.160000 0.690000 0.060000
331
v -0.040000 0.690000 0.060000
332
v -0.040000 1.110000 0.060000
333
v -0.160000 1.110000 0.060000
334
vn 0.000000 0.000000 -1.000000
335
vn 0.000000 0.000000 -1.000000
336
vn 0.000000 0.000000 1.000000
337
vn 0.000000 -0.000000 1.000000
338
vn -1.000000 0.000000 0.000000
339
vn -1.000000 0.000000 0.000000
340
vn 1.000000 0.000000 0.000000
341
vn 1.000000 0.000000 0.000000
342
vn 0.000000 1.000000 0.000000
343
vn 0.000000 1.000000 0.000000
344
vn 0.000000 -1.000000 0.000000
345
vn 0.000000 -1.000000 0.000000
346
v -0.145000 0.280000 -0.045000
347
v -0.055000 0.280000 -0.045000
348
v -0.055000 0.680000 -0.045000
349
v -0.145000 0.680000 -0.045000
350
v -0.145000 0.280000 0.045000
351
v -0.055000 0.280000 0.045000
352
v -0.055000 0.680000 0.045000
353
v -0.145000 0.680000 0.045000
354
vn 0.000000 0.000000 -1.000000
355
vn 0.000000 0.000000 -1.000000
356
vn 0.000000 0.000000 1.000000
357
vn 0.000000 -0.000000 1.000000
358
vn -1.000000 0.000000 0.000000
359
vn -1.000000 0.000000 0.000000
360
vn 1.000000 0.000000 0.000000
361
vn 1.000000 0.000000 0.000000
362
vn 0.000000 1.000000 0.000000
363
vn 0.000000 1.000000 0.000000
364
vn 0.000000 -1.000000 0.000000
365
vn 0.000000 -1.000000 0.000000
366
v -0.150000 0.050000 -0.050000
367
v -0.050000 0.050000 -0.050000
368
v -0.050000 0.110000 -0.050000
369
v -0.150000 0.110000 -0.050000
370
v -0.150000 0.050000 0.170000
371
v -0.050000 0.050000 0.170000
372
v -0.050000 0.110000 0.170000
373
v -0.150000 0.110000 0.170000
374
374
vn 0.000000 0.000000 -1.000000
375
375
vn 0.000000 0.000000 -1.000000
376
376
vn 0.000000 0.000000 1.000000
@@ -103,86 +103,6 @@ vn 0.000000 1.000000 0.000000103
vn 0.000000 1.000000 0.000000103
vn 0.000000 1.000000 0.000000104
vn 0.000000 -1.000000 0.000000104
vn 0.000000 -1.000000 0.000000105
vn 0.000000 -1.000000 0.000000105
vn 0.000000 -1.000000 0.000000106
v -0.250000 1.340000 -0.040000107
v -0.150000 1.340000 -0.040000108
v -0.150000 1.420000 -0.040000109
v -0.250000 1.420000 -0.040000110
v -0.250000 1.340000 0.040000111
v -0.150000 1.340000 0.040000112
v -0.150000 1.420000 0.040000113
v -0.250000 1.420000 0.040000114
vn 0.000000 0.000000 -1.000000115
vn 0.000000 0.000000 -1.000000116
vn 0.000000 0.000000 1.000000117
vn 0.000000 -0.000000 1.000000118
vn -1.000000 0.000000 0.000000119
vn -1.000000 0.000000 0.000000120
vn 1.000000 0.000000 0.000000121
vn 1.000000 0.000000 0.000000122
vn 0.000000 1.000000 0.000000123
vn 0.000000 1.000000 0.000000124
vn 0.000000 -1.000000 0.000000125
vn 0.000000 -1.000000 0.000000126
v -0.320000 1.240000 -0.040000127
v -0.240000 1.240000 -0.040000128
v -0.240000 1.520000 -0.040000129
v -0.320000 1.520000 -0.040000130
v -0.320000 1.240000 0.040000131
v -0.240000 1.240000 0.040000132
v -0.240000 1.520000 0.040000133
v -0.320000 1.520000 0.040000134
vn 0.000000 0.000000 -1.000000135
vn 0.000000 0.000000 -1.000000136
vn 0.000000 0.000000 1.000000137
vn 0.000000 -0.000000 1.000000138
vn -1.000000 0.000000 0.000000139
vn -1.000000 0.000000 0.000000140
vn 1.000000 0.000000 0.000000141
vn 1.000000 0.000000 0.000000142
vn 0.000000 1.000000 0.000000143
vn 0.000000 1.000000 0.000000144
vn 0.000000 -1.000000 0.000000145
vn 0.000000 -1.000000 0.000000146
v -0.315000 0.970000 -0.035000147
v -0.245000 0.970000 -0.035000148
v -0.245000 1.230000 -0.035000149
v -0.315000 1.230000 -0.035000150
v -0.315000 0.970000 0.035000151
v -0.245000 0.970000 0.035000152
v -0.245000 1.230000 0.035000153
v -0.315000 1.230000 0.035000154
vn 0.000000 0.000000 -1.000000155
vn 0.000000 0.000000 -1.000000156
vn 0.000000 0.000000 1.000000157
vn 0.000000 -0.000000 1.000000158
vn -1.000000 0.000000 0.000000159
vn -1.000000 0.000000 0.000000160
vn 1.000000 0.000000 0.000000161
vn 1.000000 0.000000 0.000000162
vn 0.000000 1.000000 0.000000163
vn 0.000000 1.000000 0.000000164
vn 0.000000 -1.000000 0.000000165
vn 0.000000 -1.000000 0.000000166
v -0.320000 0.790000 -0.020000167
v -0.240000 0.790000 -0.020000168
v -0.240000 0.890000 -0.020000169
v -0.320000 0.890000 -0.020000170
v -0.320000 0.790000 0.020000171
v -0.240000 0.790000 0.020000172
v -0.240000 0.890000 0.020000173
v -0.320000 0.890000 0.020000174
vn 0.000000 0.000000 -1.000000175
vn 0.000000 0.000000 -1.000000176
vn 0.000000 0.000000 1.000000177
vn 0.000000 -0.000000 1.000000178
vn -1.000000 0.000000 0.000000179
vn -1.000000 0.000000 0.000000180
vn 1.000000 0.000000 0.000000181
vn 1.000000 0.000000 0.000000182
vn 0.000000 1.000000 0.000000183
vn 0.000000 1.000000 0.000000184
vn 0.000000 -1.000000 0.000000185
vn 0.000000 -1.000000 0.000000186
v 0.150000 1.340000 -0.040000106
v 0.150000 1.340000 -0.040000187
v 0.250000 1.340000 -0.040000107
v 0.250000 1.340000 -0.040000188
v 0.250000 1.420000 -0.040000108
v 0.250000 1.420000 -0.040000@@ -263,14 +183,14 @@ vn 0.000000 1.000000 0.000000263
vn 0.000000 1.000000 0.000000183
vn 0.000000 1.000000 0.000000264
vn 0.000000 -1.000000 0.000000184
vn 0.000000 -1.000000 0.000000265
vn 0.000000 -1.000000 0.000000185
vn 0.000000 -1.000000 0.000000266
v -0.160000 0.690000 -0.060000186
v -0.250000 1.340000 -0.040000267
v -0.040000 0.690000 -0.060000187
v -0.150000 1.340000 -0.040000268
v -0.040000 1.110000 -0.060000188
v -0.150000 1.420000 -0.040000269
v -0.160000 1.110000 -0.060000189
v -0.250000 1.420000 -0.040000270
v -0.160000 0.690000 0.060000190
v -0.250000 1.340000 0.040000271
v -0.040000 0.690000 0.060000191
v -0.150000 1.340000 0.040000272
v -0.040000 1.110000 0.060000192
v -0.150000 1.420000 0.040000273
v -0.160000 1.110000 0.060000193
v -0.250000 1.420000 0.040000274
vn 0.000000 0.000000 -1.000000194
vn 0.000000 0.000000 -1.000000275
vn 0.000000 0.000000 -1.000000195
vn 0.000000 0.000000 -1.000000276
vn 0.000000 0.000000 1.000000196
vn 0.000000 0.000000 1.000000@@ -283,14 +203,34 @@ vn 0.000000 1.000000 0.000000283
vn 0.000000 1.000000 0.000000203
vn 0.000000 1.000000 0.000000284
vn 0.000000 -1.000000 0.000000204
vn 0.000000 -1.000000 0.000000285
vn 0.000000 -1.000000 0.000000205
vn 0.000000 -1.000000 0.000000286
v -0.145000 0.280000 -0.045000206
v -0.320000 1.240000 -0.040000287
v -0.055000 0.280000 -0.045000207
v -0.240000 1.240000 -0.040000288
v -0.055000 0.680000 -0.045000208
v -0.240000 1.520000 -0.040000289
v -0.145000 0.680000 -0.045000209
v -0.320000 1.520000 -0.040000290
v -0.145000 0.280000 0.045000210
v -0.320000 1.240000 0.040000291
v -0.055000 0.280000 0.045000211
v -0.240000 1.240000 0.040000292
v -0.055000 0.680000 0.045000212
v -0.240000 1.520000 0.040000293
v -0.145000 0.680000 0.045000213
v -0.320000 1.520000 0.040000214
vn 0.000000 0.000000 -1.000000215
vn 0.000000 0.000000 -1.000000216
vn 0.000000 0.000000 1.000000217
vn 0.000000 -0.000000 1.000000218
vn -1.000000 0.000000 0.000000219
vn -1.000000 0.000000 0.000000220
vn 1.000000 0.000000 0.000000221
vn 1.000000 0.000000 0.000000222
vn 0.000000 1.000000 0.000000223
vn 0.000000 1.000000 0.000000224
vn 0.000000 -1.000000 0.000000225
vn 0.000000 -1.000000 0.000000226
v -0.315000 0.970000 -0.035000227
v -0.245000 0.970000 -0.035000228
v -0.245000 1.230000 -0.035000229
v -0.315000 1.230000 -0.035000230
v -0.315000 0.970000 0.035000231
v -0.245000 0.970000 0.035000232
v -0.245000 1.230000 0.035000233
v -0.315000 1.230000 0.035000294
vn 0.000000 0.000000 -1.000000234
vn 0.000000 0.000000 -1.000000295
vn 0.000000 0.000000 -1.000000235
vn 0.000000 0.000000 -1.000000296
vn 0.000000 0.000000 1.000000236
vn 0.000000 0.000000 1.000000@@ -303,14 +243,14 @@ vn 0.000000 1.000000 0.000000303
vn 0.000000 1.000000 0.000000243
vn 0.000000 1.000000 0.000000304
vn 0.000000 -1.000000 0.000000244
vn 0.000000 -1.000000 0.000000305
vn 0.000000 -1.000000 0.000000245
vn 0.000000 -1.000000 0.000000306
v -0.150000 0.050000 -0.170000246
v -0.320000 0.790000 -0.020000307
v -0.050000 0.050000 -0.170000247
v -0.240000 0.790000 -0.020000308
v -0.050000 0.110000 -0.170000248
v -0.240000 0.890000 -0.020000309
v -0.150000 0.110000 -0.170000249
v -0.320000 0.890000 -0.020000310
v -0.150000 0.050000 0.050000250
v -0.320000 0.790000 0.020000311
v -0.050000 0.050000 0.050000251
v -0.240000 0.790000 0.020000312
v -0.050000 0.110000 0.050000252
v -0.240000 0.890000 0.020000313
v -0.150000 0.110000 0.050000253
v -0.320000 0.890000 0.020000314
vn 0.000000 0.000000 -1.000000254
vn 0.000000 0.000000 -1.000000315
vn 0.000000 0.000000 -1.000000255
vn 0.000000 0.000000 -1.000000316
vn 0.000000 0.000000 1.000000256
vn 0.000000 0.000000 1.000000@@ -363,14 +303,74 @@ vn 0.000000 1.000000 0.000000363
vn 0.000000 1.000000 0.000000303
vn 0.000000 1.000000 0.000000364
vn 0.000000 -1.000000 0.000000304
vn 0.000000 -1.000000 0.000000365
vn 0.000000 -1.000000 0.000000305
vn 0.000000 -1.000000 0.000000366
v 0.050000 0.050000 -0.170000306
v 0.050000 0.050000 -0.050000367
v 0.150000 0.050000 -0.170000307
v 0.150000 0.050000 -0.050000368
v 0.150000 0.110000 -0.170000308
v 0.150000 0.110000 -0.050000369
v 0.050000 0.110000 -0.170000309
v 0.050000 0.110000 -0.050000370
v 0.050000 0.050000 0.050000310
v 0.050000 0.050000 0.170000371
v 0.150000 0.050000 0.050000311
v 0.150000 0.050000 0.170000372
v 0.150000 0.110000 0.050000312
v 0.150000 0.110000 0.170000373
v 0.050000 0.110000 0.050000313
v 0.050000 0.110000 0.170000314
vn 0.000000 0.000000 -1.000000315
vn 0.000000 0.000000 -1.000000316
vn 0.000000 0.000000 1.000000317
vn 0.000000 -0.000000 1.000000318
vn -1.000000 0.000000 0.000000319
vn -1.000000 0.000000 0.000000320
vn 1.000000 0.000000 0.000000321
vn 1.000000 0.000000 0.000000322
vn 0.000000 1.000000 0.000000323
vn 0.000000 1.000000 0.000000324
vn 0.000000 -1.000000 0.000000325
vn 0.000000 -1.000000 0.000000326
v -0.160000 0.690000 -0.060000327
v -0.040000 0.690000 -0.060000328
v -0.040000 1.110000 -0.060000329
v -0.160000 1.110000 -0.060000330
v -0.160000 0.690000 0.060000331
v -0.040000 0.690000 0.060000332
v -0.040000 1.110000 0.060000333
v -0.160000 1.110000 0.060000334
vn 0.000000 0.000000 -1.000000335
vn 0.000000 0.000000 -1.000000336
vn 0.000000 0.000000 1.000000337
vn 0.000000 -0.000000 1.000000338
vn -1.000000 0.000000 0.000000339
vn -1.000000 0.000000 0.000000340
vn 1.000000 0.000000 0.000000341
vn 1.000000 0.000000 0.000000342
vn 0.000000 1.000000 0.000000343
vn 0.000000 1.000000 0.000000344
vn 0.000000 -1.000000 0.000000345
vn 0.000000 -1.000000 0.000000346
v -0.145000 0.280000 -0.045000347
v -0.055000 0.280000 -0.045000348
v -0.055000 0.680000 -0.045000349
v -0.145000 0.680000 -0.045000350
v -0.145000 0.280000 0.045000351
v -0.055000 0.280000 0.045000352
v -0.055000 0.680000 0.045000353
v -0.145000 0.680000 0.045000354
vn 0.000000 0.000000 -1.000000355
vn 0.000000 0.000000 -1.000000356
vn 0.000000 0.000000 1.000000357
vn 0.000000 -0.000000 1.000000358
vn -1.000000 0.000000 0.000000359
vn -1.000000 0.000000 0.000000360
vn 1.000000 0.000000 0.000000361
vn 1.000000 0.000000 0.000000362
vn 0.000000 1.000000 0.000000363
vn 0.000000 1.000000 0.000000364
vn 0.000000 -1.000000 0.000000365
vn 0.000000 -1.000000 0.000000366
v -0.150000 0.050000 -0.050000367
v -0.050000 0.050000 -0.050000368
v -0.050000 0.110000 -0.050000369
v -0.150000 0.110000 -0.050000370
v -0.150000 0.050000 0.170000371
v -0.050000 0.050000 0.170000372
v -0.050000 0.110000 0.170000373
v -0.150000 0.110000 0.170000374
vn 0.000000 0.000000 -1.000000374
vn 0.000000 0.000000 -1.000000375
vn 0.000000 0.000000 -1.000000375
vn 0.000000 0.000000 -1.000000376
vn 0.000000 0.000000 1.000000376
vn 0.000000 0.000000 1.000000The contents of this binary file are not shown.
The contents of this binary file are not shown.
The contents of this binary file are not shown.
The contents of this binary file are not shown.
@@ -203,7 +203,29 @@ fn assert_body(json: &Value, bin: &[u8], sex: &str) {
203
203
assert_eq!(extras["sex"], sex);
204
204
assert_eq!(extras["license"], "CC0-1.0");
205
205
assert_eq!(extras["up"], "+Y");
206
assert_eq!(extras["forward"], "-Z");
206
assert_eq!(extras["forward"], "+Z");
207
// glTF convention: front faces +Z, the character's left side is +X.
208
let node = |name: &str| {
209
json["nodes"]
210
.as_array()
211
.unwrap()
212
.iter()
213
.find(|n| n["name"] == name)
214
.unwrap_or_else(|| panic!("{name} missing"))
215
.clone()
216
};
217
assert!(
218
node("LeftFoot")["translation"][2].as_f64().unwrap() > 0.0,
219
"toes point to +Z"
220
);
221
assert!(
222
node("LeftUpperLeg")["translation"][0].as_f64().unwrap() > 0.0,
223
"left leg on +X"
224
);
225
assert!(
226
node("RightUpperLeg")["translation"][0].as_f64().unwrap() < 0.0,
227
"right leg on -X"
228
);
207
229
let label = extras["label"].as_str().unwrap();
208
230
assert!(
209
231
label.contains("geen definitief karakter"),
@@ -339,14 +361,16 @@ fn male_and_female_test_bodies_have_a_skeleton_and_both_clips() {
339
361
assert_body(&male, &male_bin, "male");
340
362
assert_body(&female, &female_bin, "female");
341
363
342
let male_shoulder = translation(&male, "LeftShoulder")[0];
343
let female_shoulder = translation(&female, "LeftShoulder")[0];
364
let male_shoulder = translation(&male, "LeftShoulder")[0].abs();
365
let female_shoulder = translation(&female, "LeftShoulder")[0].abs();
344
366
assert!(
345
male_shoulder < female_shoulder,
367
male_shoulder > female_shoulder,
346
368
"male shoulders must be wider ({male_shoulder} vs {female_shoulder})"
347
369
);
348
370
assert!(translation(&male, "Hips")[1] > translation(&female, "Hips")[1]);
349
assert!(translation(&female, "LeftUpperLeg")[0] < translation(&male, "LeftUpperLeg")[0]);
371
assert!(
372
translation(&female, "LeftUpperLeg")[0].abs() > translation(&male, "LeftUpperLeg")[0].abs()
373
);
350
374
351
375
let readme =
352
376
std::fs::read_to_string(repo_root().join("tests/fixtures/bodies/README.md")).unwrap();
@@ -203,7 +203,29 @@ fn assert_body(json: &Value, bin: &[u8], sex: &str) {203
assert_eq!(extras["sex"], sex);203
assert_eq!(extras["sex"], sex);204
assert_eq!(extras["license"], "CC0-1.0");204
assert_eq!(extras["license"], "CC0-1.0");205
assert_eq!(extras["up"], "+Y");205
assert_eq!(extras["up"], "+Y");206
assert_eq!(extras["forward"], "-Z");206
assert_eq!(extras["forward"], "+Z");207
// glTF convention: front faces +Z, the character's left side is +X.208
let node = |name: &str| {209
json["nodes"]210
.as_array()211
.unwrap()212
.iter()213
.find(|n| n["name"] == name)214
.unwrap_or_else(|| panic!("{name} missing"))215
.clone()216
};217
assert!(218
node("LeftFoot")["translation"][2].as_f64().unwrap() > 0.0,219
"toes point to +Z"220
);221
assert!(222
node("LeftUpperLeg")["translation"][0].as_f64().unwrap() > 0.0,223
"left leg on +X"224
);225
assert!(226
node("RightUpperLeg")["translation"][0].as_f64().unwrap() < 0.0,227
"right leg on -X"228
);207
let label = extras["label"].as_str().unwrap();229
let label = extras["label"].as_str().unwrap();208
assert!(230
assert!(209
label.contains("geen definitief karakter"),231
label.contains("geen definitief karakter"),@@ -339,14 +361,16 @@ fn male_and_female_test_bodies_have_a_skeleton_and_both_clips() {339
assert_body(&male, &male_bin, "male");361
assert_body(&male, &male_bin, "male");340
assert_body(&female, &female_bin, "female");362
assert_body(&female, &female_bin, "female");341
363
342
let male_shoulder = translation(&male, "LeftShoulder")[0];364
let male_shoulder = translation(&male, "LeftShoulder")[0].abs();343
let female_shoulder = translation(&female, "LeftShoulder")[0];365
let female_shoulder = translation(&female, "LeftShoulder")[0].abs();344
assert!(366
assert!(345
male_shoulder < female_shoulder,367
male_shoulder > female_shoulder,346
"male shoulders must be wider ({male_shoulder} vs {female_shoulder})"368
"male shoulders must be wider ({male_shoulder} vs {female_shoulder})"347
);369
);348
assert!(translation(&male, "Hips")[1] > translation(&female, "Hips")[1]);370
assert!(translation(&male, "Hips")[1] > translation(&female, "Hips")[1]);349
assert!(translation(&female, "LeftUpperLeg")[0] < translation(&male, "LeftUpperLeg")[0]);371
assert!(372
translation(&female, "LeftUpperLeg")[0].abs() > translation(&male, "LeftUpperLeg")[0].abs()373
);350
374
351
let readme =375
let readme =352
std::fs::read_to_string(repo_root().join("tests/fixtures/bodies/README.md")).unwrap();376
std::fs::read_to_string(repo_root().join("tests/fixtures/bodies/README.md")).unwrap();