9b: 60 views, per-map atlases + .ppda, Preview2d via .ppda, fps default 4; docs
@@ -23,9 +23,9 @@ 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. 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 -30 / 0 / 30** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z).
27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 36 hoeken. 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.
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).
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.
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

@@ -37,24 +37,28 @@ Een lichaam zonder skin kan ook als man- of vrouwmodel gekozen worden: **OBJ+MTL
37
37
- OBJ en FBX lopen door dezelfde rigide lezer als equipment (FBX met dezelfde as-/eenheidsnormalisatie, één keer); bij glTF worden de node-transformaties van de scène één keer in de geometrie verwerkt (gespiegelde transformaties draaien de driehoeksvolgorde om). Animaties in een glTF zonder skin worden niet toegepast; de melding noemt hoeveel.
38
38
- De bodyweergave meldt dat het lichaam statisch is. De animatiekeuze en **Speel/Pauze** zijn uitgeschakeld.
39
39
- **Pivot Y:** zonder nekbot gebruikt de camera **Centrum Y** als orbitpunt (diagnose *Statisch lichaam zonder skeleton*); een handmatige **Pivot Y** werkt zoals bij geskinde lichamen. Vaste renderhoogte/zoom per lichaam blijft gelijk.
40
- **Render alle hoeken** maakt één bindpose-sample `clip000-bind` × 36 hoeken met dezelfde vier maps (diffuse als onbelichte albedo, normal, depth, specular) op doelresolutie; de set opent via `export_set` en de 2D-preview.
40
- **Render alle hoeken** maakt één bindpose-sample `clip000-bind` × 60 hoeken met dezelfde vier maps (diffuse als onbelichte albedo, normal, depth, specular) op doelresolutie, plus atlassen en `.ppda`; de set opent via `export_set` en de 2D-preview.
41
41
- Equipment heeft een bot nodig: in de equipment-editor zijn **Skeleton / botten** en **Render equipment · alle hoeken** uitgeschakeld met uitleg, en de live view tekent geen equipment op een statisch lichaam. Een equipmentrender op een statisch lichaam wordt geweigerd.
42
42
43
43
## 2D-preview
44
44
45
Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor over voltooide schema-3 exportsets in `renders/`:
45
Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor over voltooide exports in `renders/`. De preview leest iedere export via zijn `.ppda` en de vier atlassen (gevalideerd, zie *Atlassen en .ppda*), niet via de losse PNG's. Runs zonder geldige `.ppda` worden overgeslagen; het aantal staat onder de exportteller, met de reden bij hover. De gedecodeerde atlassen van de huidige selectie blijven in het geheugen tot je een andere selectie kiest of **Vernieuw exportsets** gebruikt.
46
46
47
47
- Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.
48
48
- Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.
49
- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −30 / 0 / +30 met klemmen. Hoekwijzigingen behouden de animatietijd.
49
- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −60 / −30 / 0 / +30 / +60 met klemmen (start op 0). Hoekwijzigingen behouden de animatietijd.
50
50
- Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen de diepte-gecompositeerde winnende laag onbelicht (depth als grijswaarde, normal als opgeslagen RGB). Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.
51
51
- Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. Loslaten, focusverlies of pointer-annulering stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.
52
52
- Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).
53
53
- Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.
54
54
55

56
55
57
```bash
56
# Native GUI-smoke met schema-3 fixture-export (Xvfb)
58
# Native GUI-smoke (Xvfb): synthetische export, losse PNG's verwijderd, preview via .ppda/atlassen
57
59
cargo run --example preview2d_smoke --locked -- /tmp/preview2d-smoke
60
# Zelfde smoke op een bestaand project met echte exports
61
cargo run --example preview2d_smoke --locked -- /tmp/preview2d-real /tmp/fbx-live
58
62
```
59
63
60
64
## Equipment en opslaan
@@ -88,10 +92,22 @@ De bodyknop rendert alleen het lichaam; **Render equipment alle hoeken** in de e
88
92
Per beeld worden **alleen vier target-res PNGs** opgeslagen: `diffuse`, `normal`, `depth`, `specular`. Diffuse is **onbelichte albedo** (basiskleur/texture × vertexkleur, alpha behouden), nooit ingebakken lighting/schaduw/glans. Depth is lineaire 16-bit grayscale+alpha met gemeenschappelijke near/far; normal is view-space RGB+alpha; specular is lineaire sterkte (body nul, equipment inclusief ingestelde shinymap). Highres en detail zijn uitsluitend zichtbare hulpstappen en worden niet opgeslagen.
89
93
90
94
```text
91
renders/run-<tijd>-<pid>/male/body/clip001-walk/f000012/p-30_y330_diffuse.png
95
renders/run-<tijd>-<pid>/male/body/clip001-walk/f000012/p-60_y330_diffuse.png
96
renders/run-<tijd>-<pid>/male-body-diffuse-atlas.png (en -normal, -depth, -specular)
97
renders/run-<tijd>-<pid>/male-body-atlas.ppda
92
98
```
93
99
94
Ieder nieuw renderverzoek krijgt een eigen runmap; bestaande uitvoer wordt niet overschreven. `render.json` (schema 3) beschrijft projectidentiteit, bronfingerprints (SHA-256 van bodybestand en optioneel encoded equipment), camera/pivot/sample/materiaalcontract, clipinventaris en telling. Alleen `complete.json` (zelfde schema, met per-PNG SHA-256) betekent een volledige set. `cancelled.json`/`failed.json` markeren een gedeeltelijke run zonder compleet-marker. De bibliotheekmodule `export_set` opent uitsluitend voltooide runs met overeenkomende identiteit en geverifieerde checksums, en leest de echte RGBA8-/depth16-maps met strikte pad-/formaatvalidatie. De map-/camera-/pixelcontracten staan in `design.md`.
100
Ieder nieuw renderverzoek krijgt een eigen runmap; bestaande uitvoer wordt niet overschreven. `render.json` (schema 4) beschrijft projectidentiteit, bronfingerprints (SHA-256 van bodybestand en optioneel encoded equipment), camera/pivot/sample/materiaalcontract (pitches −60…+60, yaws 0…330), clipinventaris en telling. Alleen `complete.json` (zelfde schema, met per-PNG SHA-256 en de namen en SHA-256 van atlassen en `.ppda`) betekent een volledige set. Exports met een ouder schema worden door de lezers geweigerd. `cancelled.json`/`failed.json` markeren een gedeeltelijke run zonder compleet-marker. De bibliotheekmodule `export_set` opent uitsluitend voltooide runs met overeenkomende identiteit en geverifieerde checksums, en leest de echte RGBA8-/depth16-maps met strikte pad-/formaatvalidatie. De map-/camera-/pixelcontracten staan in `design.md`.
101
102
### Atlassen en .ppda
103
104
Als alle losse maps klaar en geverifieerd zijn, ook na hervatten, schrijft de render per maptype één atlas in de runmap: `<geslacht>-<settype>-<maptype>-atlas.png` (bijv. `male-sword-normal-atlas.png`). Daarna volgt `<geslacht>-<settype>-atlas.ppda`, en pas daarna `complete.json`.
105
106
- Een cel is exact de doelresolutie, zonder rand of schaling. Iedere rij is één (clip, frame) in bronvolgorde. De 60 kolommen zijn pitch −60→+60 en daarbinnen yaw 0→330. De vier atlassen hebben dezelfde celcoördinaten. Diffuse houdt het sRGB-kenmerk; depth blijft 16-bit grijs+alpha.
107
- De atlassen worden rij voor rij uit de geverifieerde losse PNG's opgebouwd, atomair gepubliceerd en overschrijven niets. Staat er na een crash al een atlas, dan blijft die staan als hij byte-gelijk is aan de nieuwe; anders wordt hij als `.corrupt-aNNN-K` bewaard en opnieuw geschreven.
108
- Wordt een atlas groter dan 16384 pixels in breedte of hoogte, dan weigert de render vóór de start met een duidelijke melding (verlaag fps, resolutie of animatieduur). Atlassen worden nooit gesplitst.
109
- `.ppda` (PaPerDollAtlas) is strikte UTF-8 JSON met `format_version` 1, `sex`, `settype`, `project` (id/naam), `identity` (body- en equipment-SHA-256, framing, pivotbron, near/far, cameraafstand, normal- en depthcodering), `fps`, `cell_width`/`cell_height`, `pitches`, `yaws`, `maps` (per maptype bestandsnaam, SHA-256, breedte, hoogte) en `clips` (bronindex, naam, token, `frame_count`, `duration` en per frame alle 60 hoeken als `{pitch, yaw, x, y, w, h}` in atlaspixels).
110
- De bibliotheeklezer `atlas::AtlasSet` weigert onbekende velden of versies, bestandsnamen die niet de verwachte naam in dezelfde map zijn, symlinks, afwijkende atlasafmetingen of kleurtypes, celcoördinaten die niet op het raster liggen en SHA-256-fouten. `load()` decodeert de vier atlassen met dezelfde pixelregels als de losse maps; `cell()` knipt één hoek eruit. `atlas::cross_check_run` vergelijkt iedere atlascel met de losse PNG.
95
111
96
112
### Hervatten
97
113
@@ -100,13 +116,14 @@ Een hervatte run schrijft in dezelfde runmap verder; andere runs blijven onaange
100
116
- Elke run houdt `progress.jsonl` bij: per gepubliceerde PNG het pad en de SHA-256. `run.lock` voorkomt dat twee renders tegelijk in dezelfde run schrijven.
101
117
- Hervatten kan alleen als `render.json` exact overeenkomt met wat de huidige job zou schrijven: project, geslacht/set, SHA-256 van body en equipment, target, fps, framing/pivot, camera, clips en maps. Een voltooide, vergrendelde of afwijkende run wordt geweigerd.
102
118
- Vóór het renderen wordt iedere geplande PNG gecontroleerd. Bestanden met een kloppende checksum blijven ongewijzigd en worden niet opnieuw gerenderd. Ontbrekende bestanden worden opnieuw gemaakt. Beschadigde bestanden worden hernoemd naar `<naam>.corrupt-aNNN-K` en daarna opnieuw gemaakt; ze worden niet verwijderd. Een bestaand bestand zonder journalregel (crash tussen schrijven en vastleggen) blijft staan als de opnieuw gerenderde bytes gelijk zijn. Achtergebleven `.part-*`-bestanden worden niet aangeraakt.
119
- Na de losse maps worden de atlassen en `.ppda` geschreven (zie hierboven).
103
120
- De voortgang begint bij het werk dat al klopt; overgeslagen beelden heten *al aanwezig, checksums kloppen*. Je kunt opnieuw annuleren en later weer hervatten. Een vorige `cancelled.json`/`failed.json` wordt bewaard als `attempt-NNN-…json`, en `resume.jsonl` legt per poging vast wat geverifieerd, ontbrekend en in quarantaine gezet is. Pas als alles klopt, wordt `complete.json` met alle checksums geschreven.
104
121
105
122

106
123
107
124
## 2D depth-compositor (bibliotheek)
108
125
109
De module `compositor` combineert een body-export met nul of meer compatibele equipment-exports (zelfde project-id, geslacht, body-fingerprint, target, fps, framing, near/far en clipinventaris via `export_set`). Per pixel wint het dichtstbijzijnde geldige oppervlak (depth gray dichterbij = kleiner; epsilon `DEPTH_EPSILON_U16 = 1` depthcode; bij gelijke diepte wint de laagste laagindex — geef body eerst, daarna equipment op vaste naamvolgorde). Diffuse, normal en specular komen altijd uit **dezelfde** winnende laag. Soft alpha van die winnaar blijft behouden; verliezende lagen mixen niet mee (single-depth kan geen willekeurige doorschijnendheid reconstrueren).
126
De module `compositor` combineert een body-export met nul of meer compatibele equipment-exports (zelfde project-id, geslacht, body-fingerprint, target, fps, framing, near/far en clipinventaris, gelezen uit `export_set` of uit de `.ppda`). Per pixel wint het dichtstbijzijnde geldige oppervlak (depth gray dichterbij = kleiner; epsilon `DEPTH_EPSILON_U16 = 1` depthcode; bij gelijke diepte wint de laagste laagindex — geef body eerst, daarna equipment op vaste naamvolgorde). Diffuse, normal en specular komen altijd uit **dezelfde** winnende laag. Soft alpha van die winnaar blijft behouden; verliezende lagen mixen niet mee (single-depth kan geen willekeurige doorschijnendheid reconstrueren).
110
127
111
128
`relight` / `compose_and_relight` past lineaire ambient + Lambert diffuse + Blinn-Phong specular toe. De lichtvektor staat al in view-space, gelijk aan de geëxporteerde normal maps. Er is geen randlicht: de preview toont alleen deze relight in stand Resultaat. Opgeslagen diffuse blijft elders onbelichte albedo.
112
129
@@ -124,10 +141,15 @@ let layers = [
124
141
LayerMaps::from_export_run(&sword, "sword", "clip000-walk", 0, 0, 0)?,
125
142
];
126
143
let (maps, lit_rgba) = compose_and_relight(&layers, &Light::default())?;
144
145
// Zelfde lagen via de consumentenvorm (.ppda + atlassen):
146
use paperdoll_renderer::atlas::AtlasSet;
147
let body = AtlasSet::open_run(&body_run_dir)?.load()?;
148
let layer = LayerMaps::from_atlas(&body, "body", "clip000-walk", 0, 60, 330)?;
127
149
```
128
150
129
151
130
Highres is exact 8× beide assen; batch-targets zijn momenteel **1–256 pixels per as** (highres max 2048). Grotere projectinstellingen geven een duidelijke renderfout, geen stil verkleinde supersampling. Maximaal 500000 PNGs / 16 GiB geschatte ongecomprimeerde targetdata per run; output vereist hardlinks. Productie rendert de eerste skinned glTF/GLB-mesh met driehoekprimitieven, basiskleur/8-bit albedotextures en vertexkleuren. Een FBX-lichaam loopt via de genormaliseerde scene (`fbx_scene`) door dezelfde batch: alle animatiestacks × frames × 36 hoeken, dezelfde vaste camera, nekpivot en vaste zoom per lichaam, en dezelfde vier maps. Equipment hangt op de geanimeerde botten van dat FBX-skelet. Statische lichamen renderen één bindpose-sample per hoek (zie *Statische lichamen*). Live placement blijft een belichte, vereenvoudigde weergave.
152
Highres is exact 8× beide assen; batch-targets zijn momenteel **1–256 pixels per as** (highres max 2048). Grotere projectinstellingen geven een duidelijke renderfout, geen stil verkleinde supersampling. Maximaal 500000 PNGs / 16 GiB geschatte ongecomprimeerde targetdata per run; output vereist hardlinks. Productie rendert de eerste skinned glTF/GLB-mesh met driehoekprimitieven, basiskleur/8-bit albedotextures en vertexkleuren. Een FBX-lichaam loopt via de genormaliseerde scene (`fbx_scene`) door dezelfde batch: alle animatiestacks × frames × 60 hoeken, dezelfde vaste camera, nekpivot en vaste zoom per lichaam, en dezelfde vier maps. Equipment hangt op de geanimeerde botten van dat FBX-skelet. Statische lichamen renderen één bindpose-sample per hoek (zie *Statische lichamen*). Live placement blijft een belichte, vereenvoudigde weergave.
131
153
132
154
## Openen met
133
155
@@ -146,7 +168,9 @@ cargo clippy --workspace --all-targets --locked -- -D warnings
146
168
cargo test --workspace --locked
147
169
# Linux GPU-regressie: echte depth-occlusie, normal/depth/specular en lichtonafhankelijke albedo
148
170
xvfb-run -a cargo run --example preview_depth_probe --locked
149
# Hervatten op echte GPU: setup, render met crash (abort) of annulering, hervatten, export_set-controle
171
# Productie op echte GPU (body of equipment); controleert daarna iedere atlascel tegen de losse PNG
172
xvfb-run -a cargo run --example production_fixture --locked -- /tmp/prod tests/fixtures/bodies/test-body-male.glb tests/fixtures/bodies/test-body-female.glb male 32 48 4
173
# Hervatten op echte GPU: setup, render met crash (abort) of annulering, hervatten, export_set- en atlascontrole
150
174
cargo run --release --example resume_fixture --locked -- setup /tmp/resume tests/fixtures/bodies/test-body-male.glb tests/fixtures/bodies/test-body-female.glb 32 32 2
151
175
xvfb-run -a cargo run --release --example resume_fixture --locked -- render /tmp/resume male --abort-after=200
152
176
xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp/resume male
@@ -23,9 +23,9 @@ 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. 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. 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 -30 / 0 / 30** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z).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).27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 36 hoeken. 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.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
@@ -37,24 +37,28 @@ Een lichaam zonder skin kan ook als man- of vrouwmodel gekozen worden: **OBJ+MTL37
- OBJ en FBX lopen door dezelfde rigide lezer als equipment (FBX met dezelfde as-/eenheidsnormalisatie, één keer); bij glTF worden de node-transformaties van de scène één keer in de geometrie verwerkt (gespiegelde transformaties draaien de driehoeksvolgorde om). Animaties in een glTF zonder skin worden niet toegepast; de melding noemt hoeveel.37
- OBJ en FBX lopen door dezelfde rigide lezer als equipment (FBX met dezelfde as-/eenheidsnormalisatie, één keer); bij glTF worden de node-transformaties van de scène één keer in de geometrie verwerkt (gespiegelde transformaties draaien de driehoeksvolgorde om). Animaties in een glTF zonder skin worden niet toegepast; de melding noemt hoeveel.38
- De bodyweergave meldt dat het lichaam statisch is. De animatiekeuze en **Speel/Pauze** zijn uitgeschakeld.38
- De bodyweergave meldt dat het lichaam statisch is. De animatiekeuze en **Speel/Pauze** zijn uitgeschakeld.39
- **Pivot Y:** zonder nekbot gebruikt de camera **Centrum Y** als orbitpunt (diagnose *Statisch lichaam zonder skeleton*); een handmatige **Pivot Y** werkt zoals bij geskinde lichamen. Vaste renderhoogte/zoom per lichaam blijft gelijk.39
- **Pivot Y:** zonder nekbot gebruikt de camera **Centrum Y** als orbitpunt (diagnose *Statisch lichaam zonder skeleton*); een handmatige **Pivot Y** werkt zoals bij geskinde lichamen. Vaste renderhoogte/zoom per lichaam blijft gelijk.40
- **Render alle hoeken** maakt één bindpose-sample `clip000-bind` × 36 hoeken met dezelfde vier maps (diffuse als onbelichte albedo, normal, depth, specular) op doelresolutie; de set opent via `export_set` en de 2D-preview.40
- **Render alle hoeken** maakt één bindpose-sample `clip000-bind` × 60 hoeken met dezelfde vier maps (diffuse als onbelichte albedo, normal, depth, specular) op doelresolutie, plus atlassen en `.ppda`; de set opent via `export_set` en de 2D-preview.41
- Equipment heeft een bot nodig: in de equipment-editor zijn **Skeleton / botten** en **Render equipment · alle hoeken** uitgeschakeld met uitleg, en de live view tekent geen equipment op een statisch lichaam. Een equipmentrender op een statisch lichaam wordt geweigerd.41
- Equipment heeft een bot nodig: in de equipment-editor zijn **Skeleton / botten** en **Render equipment · alle hoeken** uitgeschakeld met uitleg, en de live view tekent geen equipment op een statisch lichaam. Een equipmentrender op een statisch lichaam wordt geweigerd.42
42
43
## 2D-preview43
## 2D-preview44
44
45
Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor over voltooide schema-3 exportsets in `renders/`:45
Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor over voltooide exports in `renders/`. De preview leest iedere export via zijn `.ppda` en de vier atlassen (gevalideerd, zie *Atlassen en .ppda*), niet via de losse PNG's. Runs zonder geldige `.ppda` worden overgeslagen; het aantal staat onder de exportteller, met de reden bij hover. De gedecodeerde atlassen van de huidige selectie blijven in het geheugen tot je een andere selectie kiest of **Vernieuw exportsets** gebruikt.46
46
47
- Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.47
- Kies **Man / Vrouw**. Alleen exportsets met dezelfde project-/rig-/exportcontract (`CompatKey`) zijn combineerbaar; equipment verschijnt als multiselect wanneer die contracten overeenkomen.48
- Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.48
- Kies een clip uit de body-exportinventaris. **Speel / Pauze**, snelheid en export-fps sturen een looping frame-tijd (elapsed × fps); ontbrekende maps/frames tonen een diagnose in plaats van een stille vervanging.49
- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −30 / 0 / +30 met klemmen. Hoekwijzigingen behouden de animatietijd.49
- **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −60 / −30 / 0 / +30 / +60 met klemmen (start op 0). Hoekwijzigingen behouden de animatietijd.50
- Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen de diepte-gecompositeerde winnende laag onbelicht (depth als grijswaarde, normal als opgeslagen RGB). Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.50
- Kies weergavestand **Resultaat** (standaard; belichte compositie), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map** of **Alleen specular map**. Mapstanden tonen de diepte-gecompositeerde winnende laag onbelicht (depth als grijswaarde, normal als opgeslagen RGB). Playback, hoek en equipment werken in elke stand; licht alleen in Resultaat.51
- Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. Loslaten, focusverlies of pointer-annulering stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.51
- Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); azimut wrapt over 360° zonder klem, elevatie blijft −80…+80. Loslaten, focusverlies of pointer-annulering stopt de beweging. Belichting is alleen voor Resultaat; opgeslagen diffuse blijft unlit albedo.52
- Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).52
- Playback loopt over exact `frames / fps` seconden, zodat ieder geëxporteerd frame (inclusief het laatste) even lang duurt (`1/fps`).53
- Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.53
- Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews.54
54
55
56
55
```bash57
```bash56
# Native GUI-smoke met schema-3 fixture-export (Xvfb)58
# Native GUI-smoke (Xvfb): synthetische export, losse PNG's verwijderd, preview via .ppda/atlassen57
cargo run --example preview2d_smoke --locked -- /tmp/preview2d-smoke59
cargo run --example preview2d_smoke --locked -- /tmp/preview2d-smoke60
# Zelfde smoke op een bestaand project met echte exports61
cargo run --example preview2d_smoke --locked -- /tmp/preview2d-real /tmp/fbx-live58
```62
```59
63
60
## Equipment en opslaan64
## Equipment en opslaan@@ -88,10 +92,22 @@ De bodyknop rendert alleen het lichaam; **Render equipment alle hoeken** in de e88
Per beeld worden **alleen vier target-res PNGs** opgeslagen: `diffuse`, `normal`, `depth`, `specular`. Diffuse is **onbelichte albedo** (basiskleur/texture × vertexkleur, alpha behouden), nooit ingebakken lighting/schaduw/glans. Depth is lineaire 16-bit grayscale+alpha met gemeenschappelijke near/far; normal is view-space RGB+alpha; specular is lineaire sterkte (body nul, equipment inclusief ingestelde shinymap). Highres en detail zijn uitsluitend zichtbare hulpstappen en worden niet opgeslagen.92
Per beeld worden **alleen vier target-res PNGs** opgeslagen: `diffuse`, `normal`, `depth`, `specular`. Diffuse is **onbelichte albedo** (basiskleur/texture × vertexkleur, alpha behouden), nooit ingebakken lighting/schaduw/glans. Depth is lineaire 16-bit grayscale+alpha met gemeenschappelijke near/far; normal is view-space RGB+alpha; specular is lineaire sterkte (body nul, equipment inclusief ingestelde shinymap). Highres en detail zijn uitsluitend zichtbare hulpstappen en worden niet opgeslagen.89
93
90
```text94
```text91
renders/run-<tijd>-<pid>/male/body/clip001-walk/f000012/p-30_y330_diffuse.png95
renders/run-<tijd>-<pid>/male/body/clip001-walk/f000012/p-60_y330_diffuse.png96
renders/run-<tijd>-<pid>/male-body-diffuse-atlas.png (en -normal, -depth, -specular)97
renders/run-<tijd>-<pid>/male-body-atlas.ppda92
```98
```93
99
94
Ieder nieuw renderverzoek krijgt een eigen runmap; bestaande uitvoer wordt niet overschreven. `render.json` (schema 3) beschrijft projectidentiteit, bronfingerprints (SHA-256 van bodybestand en optioneel encoded equipment), camera/pivot/sample/materiaalcontract, clipinventaris en telling. Alleen `complete.json` (zelfde schema, met per-PNG SHA-256) betekent een volledige set. `cancelled.json`/`failed.json` markeren een gedeeltelijke run zonder compleet-marker. De bibliotheekmodule `export_set` opent uitsluitend voltooide runs met overeenkomende identiteit en geverifieerde checksums, en leest de echte RGBA8-/depth16-maps met strikte pad-/formaatvalidatie. De map-/camera-/pixelcontracten staan in `design.md`.100
Ieder nieuw renderverzoek krijgt een eigen runmap; bestaande uitvoer wordt niet overschreven. `render.json` (schema 4) beschrijft projectidentiteit, bronfingerprints (SHA-256 van bodybestand en optioneel encoded equipment), camera/pivot/sample/materiaalcontract (pitches −60…+60, yaws 0…330), clipinventaris en telling. Alleen `complete.json` (zelfde schema, met per-PNG SHA-256 en de namen en SHA-256 van atlassen en `.ppda`) betekent een volledige set. Exports met een ouder schema worden door de lezers geweigerd. `cancelled.json`/`failed.json` markeren een gedeeltelijke run zonder compleet-marker. De bibliotheekmodule `export_set` opent uitsluitend voltooide runs met overeenkomende identiteit en geverifieerde checksums, en leest de echte RGBA8-/depth16-maps met strikte pad-/formaatvalidatie. De map-/camera-/pixelcontracten staan in `design.md`.101
102
### Atlassen en .ppda103
104
Als alle losse maps klaar en geverifieerd zijn, ook na hervatten, schrijft de render per maptype één atlas in de runmap: `<geslacht>-<settype>-<maptype>-atlas.png` (bijv. `male-sword-normal-atlas.png`). Daarna volgt `<geslacht>-<settype>-atlas.ppda`, en pas daarna `complete.json`.105
106
- Een cel is exact de doelresolutie, zonder rand of schaling. Iedere rij is één (clip, frame) in bronvolgorde. De 60 kolommen zijn pitch −60→+60 en daarbinnen yaw 0→330. De vier atlassen hebben dezelfde celcoördinaten. Diffuse houdt het sRGB-kenmerk; depth blijft 16-bit grijs+alpha.107
- De atlassen worden rij voor rij uit de geverifieerde losse PNG's opgebouwd, atomair gepubliceerd en overschrijven niets. Staat er na een crash al een atlas, dan blijft die staan als hij byte-gelijk is aan de nieuwe; anders wordt hij als `.corrupt-aNNN-K` bewaard en opnieuw geschreven.108
- Wordt een atlas groter dan 16384 pixels in breedte of hoogte, dan weigert de render vóór de start met een duidelijke melding (verlaag fps, resolutie of animatieduur). Atlassen worden nooit gesplitst.109
- `.ppda` (PaPerDollAtlas) is strikte UTF-8 JSON met `format_version` 1, `sex`, `settype`, `project` (id/naam), `identity` (body- en equipment-SHA-256, framing, pivotbron, near/far, cameraafstand, normal- en depthcodering), `fps`, `cell_width`/`cell_height`, `pitches`, `yaws`, `maps` (per maptype bestandsnaam, SHA-256, breedte, hoogte) en `clips` (bronindex, naam, token, `frame_count`, `duration` en per frame alle 60 hoeken als `{pitch, yaw, x, y, w, h}` in atlaspixels).110
- De bibliotheeklezer `atlas::AtlasSet` weigert onbekende velden of versies, bestandsnamen die niet de verwachte naam in dezelfde map zijn, symlinks, afwijkende atlasafmetingen of kleurtypes, celcoördinaten die niet op het raster liggen en SHA-256-fouten. `load()` decodeert de vier atlassen met dezelfde pixelregels als de losse maps; `cell()` knipt één hoek eruit. `atlas::cross_check_run` vergelijkt iedere atlascel met de losse PNG.95
111
96
### Hervatten112
### Hervatten97
113
@@ -100,13 +116,14 @@ Een hervatte run schrijft in dezelfde runmap verder; andere runs blijven onaange100
- Elke run houdt `progress.jsonl` bij: per gepubliceerde PNG het pad en de SHA-256. `run.lock` voorkomt dat twee renders tegelijk in dezelfde run schrijven.116
- Elke run houdt `progress.jsonl` bij: per gepubliceerde PNG het pad en de SHA-256. `run.lock` voorkomt dat twee renders tegelijk in dezelfde run schrijven.101
- Hervatten kan alleen als `render.json` exact overeenkomt met wat de huidige job zou schrijven: project, geslacht/set, SHA-256 van body en equipment, target, fps, framing/pivot, camera, clips en maps. Een voltooide, vergrendelde of afwijkende run wordt geweigerd.117
- Hervatten kan alleen als `render.json` exact overeenkomt met wat de huidige job zou schrijven: project, geslacht/set, SHA-256 van body en equipment, target, fps, framing/pivot, camera, clips en maps. Een voltooide, vergrendelde of afwijkende run wordt geweigerd.102
- Vóór het renderen wordt iedere geplande PNG gecontroleerd. Bestanden met een kloppende checksum blijven ongewijzigd en worden niet opnieuw gerenderd. Ontbrekende bestanden worden opnieuw gemaakt. Beschadigde bestanden worden hernoemd naar `<naam>.corrupt-aNNN-K` en daarna opnieuw gemaakt; ze worden niet verwijderd. Een bestaand bestand zonder journalregel (crash tussen schrijven en vastleggen) blijft staan als de opnieuw gerenderde bytes gelijk zijn. Achtergebleven `.part-*`-bestanden worden niet aangeraakt.118
- Vóór het renderen wordt iedere geplande PNG gecontroleerd. Bestanden met een kloppende checksum blijven ongewijzigd en worden niet opnieuw gerenderd. Ontbrekende bestanden worden opnieuw gemaakt. Beschadigde bestanden worden hernoemd naar `<naam>.corrupt-aNNN-K` en daarna opnieuw gemaakt; ze worden niet verwijderd. Een bestaand bestand zonder journalregel (crash tussen schrijven en vastleggen) blijft staan als de opnieuw gerenderde bytes gelijk zijn. Achtergebleven `.part-*`-bestanden worden niet aangeraakt.119
- Na de losse maps worden de atlassen en `.ppda` geschreven (zie hierboven).103
- De voortgang begint bij het werk dat al klopt; overgeslagen beelden heten *al aanwezig, checksums kloppen*. Je kunt opnieuw annuleren en later weer hervatten. Een vorige `cancelled.json`/`failed.json` wordt bewaard als `attempt-NNN-…json`, en `resume.jsonl` legt per poging vast wat geverifieerd, ontbrekend en in quarantaine gezet is. Pas als alles klopt, wordt `complete.json` met alle checksums geschreven.120
- De voortgang begint bij het werk dat al klopt; overgeslagen beelden heten *al aanwezig, checksums kloppen*. Je kunt opnieuw annuleren en later weer hervatten. Een vorige `cancelled.json`/`failed.json` wordt bewaard als `attempt-NNN-…json`, en `resume.jsonl` legt per poging vast wat geverifieerd, ontbrekend en in quarantaine gezet is. Pas als alles klopt, wordt `complete.json` met alle checksums geschreven.104
121
105
122
106
123
107
## 2D depth-compositor (bibliotheek)124
## 2D depth-compositor (bibliotheek)108
125
109
De module `compositor` combineert een body-export met nul of meer compatibele equipment-exports (zelfde project-id, geslacht, body-fingerprint, target, fps, framing, near/far en clipinventaris via `export_set`). Per pixel wint het dichtstbijzijnde geldige oppervlak (depth gray dichterbij = kleiner; epsilon `DEPTH_EPSILON_U16 = 1` depthcode; bij gelijke diepte wint de laagste laagindex — geef body eerst, daarna equipment op vaste naamvolgorde). Diffuse, normal en specular komen altijd uit **dezelfde** winnende laag. Soft alpha van die winnaar blijft behouden; verliezende lagen mixen niet mee (single-depth kan geen willekeurige doorschijnendheid reconstrueren).126
De module `compositor` combineert een body-export met nul of meer compatibele equipment-exports (zelfde project-id, geslacht, body-fingerprint, target, fps, framing, near/far en clipinventaris, gelezen uit `export_set` of uit de `.ppda`). Per pixel wint het dichtstbijzijnde geldige oppervlak (depth gray dichterbij = kleiner; epsilon `DEPTH_EPSILON_U16 = 1` depthcode; bij gelijke diepte wint de laagste laagindex — geef body eerst, daarna equipment op vaste naamvolgorde). Diffuse, normal en specular komen altijd uit **dezelfde** winnende laag. Soft alpha van die winnaar blijft behouden; verliezende lagen mixen niet mee (single-depth kan geen willekeurige doorschijnendheid reconstrueren).110
127
111
`relight` / `compose_and_relight` past lineaire ambient + Lambert diffuse + Blinn-Phong specular toe. De lichtvektor staat al in view-space, gelijk aan de geëxporteerde normal maps. Er is geen randlicht: de preview toont alleen deze relight in stand Resultaat. Opgeslagen diffuse blijft elders onbelichte albedo.128
`relight` / `compose_and_relight` past lineaire ambient + Lambert diffuse + Blinn-Phong specular toe. De lichtvektor staat al in view-space, gelijk aan de geëxporteerde normal maps. Er is geen randlicht: de preview toont alleen deze relight in stand Resultaat. Opgeslagen diffuse blijft elders onbelichte albedo.112
129
@@ -124,10 +141,15 @@ let layers = [124
LayerMaps::from_export_run(&sword, "sword", "clip000-walk", 0, 0, 0)?,141
LayerMaps::from_export_run(&sword, "sword", "clip000-walk", 0, 0, 0)?,125
];142
];126
let (maps, lit_rgba) = compose_and_relight(&layers, &Light::default())?;143
let (maps, lit_rgba) = compose_and_relight(&layers, &Light::default())?;144
145
// Zelfde lagen via de consumentenvorm (.ppda + atlassen):146
use paperdoll_renderer::atlas::AtlasSet;147
let body = AtlasSet::open_run(&body_run_dir)?.load()?;148
let layer = LayerMaps::from_atlas(&body, "body", "clip000-walk", 0, 60, 330)?;127
```149
```128
150
129
151
130
Highres is exact 8× beide assen; batch-targets zijn momenteel **1–256 pixels per as** (highres max 2048). Grotere projectinstellingen geven een duidelijke renderfout, geen stil verkleinde supersampling. Maximaal 500000 PNGs / 16 GiB geschatte ongecomprimeerde targetdata per run; output vereist hardlinks. Productie rendert de eerste skinned glTF/GLB-mesh met driehoekprimitieven, basiskleur/8-bit albedotextures en vertexkleuren. Een FBX-lichaam loopt via de genormaliseerde scene (`fbx_scene`) door dezelfde batch: alle animatiestacks × frames × 36 hoeken, dezelfde vaste camera, nekpivot en vaste zoom per lichaam, en dezelfde vier maps. Equipment hangt op de geanimeerde botten van dat FBX-skelet. Statische lichamen renderen één bindpose-sample per hoek (zie *Statische lichamen*). Live placement blijft een belichte, vereenvoudigde weergave.152
Highres is exact 8× beide assen; batch-targets zijn momenteel **1–256 pixels per as** (highres max 2048). Grotere projectinstellingen geven een duidelijke renderfout, geen stil verkleinde supersampling. Maximaal 500000 PNGs / 16 GiB geschatte ongecomprimeerde targetdata per run; output vereist hardlinks. Productie rendert de eerste skinned glTF/GLB-mesh met driehoekprimitieven, basiskleur/8-bit albedotextures en vertexkleuren. Een FBX-lichaam loopt via de genormaliseerde scene (`fbx_scene`) door dezelfde batch: alle animatiestacks × frames × 60 hoeken, dezelfde vaste camera, nekpivot en vaste zoom per lichaam, en dezelfde vier maps. Equipment hangt op de geanimeerde botten van dat FBX-skelet. Statische lichamen renderen één bindpose-sample per hoek (zie *Statische lichamen*). Live placement blijft een belichte, vereenvoudigde weergave.131
153
132
## Openen met154
## Openen met133
155
@@ -146,7 +168,9 @@ cargo clippy --workspace --all-targets --locked -- -D warnings146
cargo test --workspace --locked168
cargo test --workspace --locked147
# Linux GPU-regressie: echte depth-occlusie, normal/depth/specular en lichtonafhankelijke albedo169
# Linux GPU-regressie: echte depth-occlusie, normal/depth/specular en lichtonafhankelijke albedo148
xvfb-run -a cargo run --example preview_depth_probe --locked170
xvfb-run -a cargo run --example preview_depth_probe --locked149
# Hervatten op echte GPU: setup, render met crash (abort) of annulering, hervatten, export_set-controle171
# Productie op echte GPU (body of equipment); controleert daarna iedere atlascel tegen de losse PNG172
xvfb-run -a cargo run --example production_fixture --locked -- /tmp/prod tests/fixtures/bodies/test-body-male.glb tests/fixtures/bodies/test-body-female.glb male 32 48 4173
# Hervatten op echte GPU: setup, render met crash (abort) of annulering, hervatten, export_set- en atlascontrole150
cargo run --release --example resume_fixture --locked -- setup /tmp/resume tests/fixtures/bodies/test-body-male.glb tests/fixtures/bodies/test-body-female.glb 32 32 2174
cargo run --release --example resume_fixture --locked -- setup /tmp/resume tests/fixtures/bodies/test-body-male.glb tests/fixtures/bodies/test-body-female.glb 32 32 2151
xvfb-run -a cargo run --release --example resume_fixture --locked -- render /tmp/resume male --abort-after=200175
xvfb-run -a cargo run --release --example resume_fixture --locked -- render /tmp/resume male --abort-after=200152
xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp/resume male176
xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp/resume male@@ -22,7 +22,7 @@ BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`;
22
22
| R01 | Rust application with user-selected male/female models | Open/save missing and populated slots; later manual desktop smoke test |
23
23
| R02 | Discover all supported embedded animation clips | Synthetic licensed fixture inventory, including unnamed/duplicate clips |
24
24
| R03 | Body and equipment modes | Export body plus isolated equipment from identical scene state |
25
| R04 | Three pitches times twelve yaw positions | Exactly 36 unique orientations; yaw starts facing viewer |
25
| R04 | Five pitches times twelve yaw positions (t955u; was three) | Exactly 60 unique orientations; yaw starts facing viewer |
26
26
| R05 | Initial geometry render at 8x target dimensions and configured FPS | Explicit render manifest and dimension/frame-count tests |
27
27
| R06 | Edge/detail-guided downsampling | Image fixtures retain thin details better than baseline without alpha fringes |
28
28
| R07 | Target-resolution diffuse, depth, normal maps | Round-trip decoding, finite normalized normals and shared depth contract |
@@ -33,7 +33,7 @@ BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`;
33
33
| R12 | Equipment set name lowercase without spaces | Validation; reject reserved body name and path traversal |
34
34
| R13 | Equipment-only rendering with shinymap | No body occlusion/write contamination; specular fixture |
35
35
| R14 | Folder plus .ppd manifest describing assets, presets, outputs | Portable relative paths and safe atomic saves |
36
| R15 | 2D looping animation, sex/equipment/clip selection and 36-angle navigation | State-machine and depth-compositing tests |
36
| R15 | 2D looping animation, sex/equipment/clip selection and 60-angle navigation | State-machine and depth-compositing tests |
37
37
| R16 | Held light-control buttons rotate continuously in degrees | Delta-time tests including key/button release/focus loss |
38
38
| R17 | Per-feature branch, tests, non-FF master merge, no initial MRs | Two-parent merge topology and CI checks |
39
39
| R18 | Tag tests gate AppImage, deb, Arch package, Windows build and release | Packaging smoke tests and complete-artifact release gate |
@@ -104,13 +104,13 @@ Paths use `/` and stay inside the project directory. Save writes/fsyncs a tempor
104
104
105
105
## 6. Camera, axes, alignment and animation sampling
106
106
107
Required views: pitches -30, 0, +30 degrees; yaw 0, 30, 60, ..., 330 degrees. **Proposal:** yaw zero means the standardized character front faces the camera, world +Y is up, and positive yaw is defined by a documented right-hand convention. Distinguish camera elevation from model rotation: use orbiting orthographic camera around fixed character origin. Golden orientation fixture must show labelled front/back/left/right to remove ambiguity before production export.
107
Required views: pitches −60, −30, 0, +30, +60 degrees (t955u; originally −30/0/+30); yaw 0, 30, 60, ..., 330 degrees. **Proposal:** yaw zero means the standardized character front faces the camera, world +Y is up, and positive yaw is defined by a documented right-hand convention. Distinguish camera elevation from model rotation: use orbiting orthographic camera around fixed character origin. Golden orientation fixture must show labelled front/back/left/right to remove ambiguity before production export.
108
108
109
109
**Implemented fixed framing (Tim t952u):** orthographic projection, +Y world up, yaw 0 camera on −Z looking toward the character; positive yaw follows the existing orbit convention. The **orbit pivot** is independent of the vertical image center: target is `(0, pivot_y/1000, 0)` with camera distance **10**. `pivot_y_milli = None` resolves the unique rest-pose neck joint (name tokens such as `Neck`, `mixamorigNeck`, `Bip001 Neck`, `CC_Base_Neck`); missing/ambiguous necks fall back to `center_y_milli` with a UI diagnostic; an explicit `Some` value is a manual override. Vertical world extent is `height_milli/1000`, horizontal extent vertical × target aspect. Orthographic Y translation `(pivot − center) / half_height` keeps zero-pitch image framing (feet / image center) stable when the pivot is raised to the neck. Near **0.05**, far **40** are shared absolutely across all layers, frames and views. Body, equipment layered preview, fixed live frame and production `camera()` all share `camera::resolve` + `camera::matrices`. No per-frame/per-equipment auto-framing or depth normalization. Source root motion is retained as imported (no extraction). User adjusts height/center/pivot in the body view; automatic animation envelopes are not promised.
110
110
111
111
**Vast renderkader tonen** switches live placement to this same orthographic camera and target-aspect, centered yellow frame. Drag still changes yaw/pitch; orbit-scroll zoom is ignored while the fixed frame is shown. Uncheck it for the independent perspective orbit/scroll camera. Neither free orbit zoom nor live selected clip/time changes a production job. Render snapshots the current project settings at job start (including resolved pivot and `pivot_source`); later edits do not change that in-flight batch. **Opslaan** persists male and female fixed framing (including optional manual pivot) in `.ppd`.
112
112
113
**Implemented batch nesting:** source clip order → sampled frame → pitch **−30,0,+30** → yaw **0…330 step30** → six completed passes. Samples are `t=frame/FPS`, `0 <= frame < max(1, ceil(duration*FPS))`. To avoid exporter float noise creating a near-duplicate terminal frame, the product snaps to its nearest integer when within `4*f32::EPSILON*max(1,product)` before ceiling; a regression covers an exported 1-second endpoint one ULP above 1. Otherwise the full fractional duration is sampled; a zero-duration clip has one static sample; no clips produces one bind-pose clip. Duplicate display names get indexed tokens and internally unique sampling names, not accidental reuse of the first clip. All clips/frames/views are rendered regardless of current live selection. FPS is sampling, not playback rate.
113
**Implemented batch nesting:** source clip order → sampled frame → pitch **−60,−30,0,+30,+60** → yaw **0…330 step30** → six completed passes. Samples are `t=frame/FPS`, `0 <= frame < max(1, ceil(duration*FPS))`. To avoid exporter float noise creating a near-duplicate terminal frame, the product snaps to its nearest integer when within `4*f32::EPSILON*max(1,product)` before ceiling; a regression covers an exported 1-second endpoint one ULP above 1. Otherwise the full fractional duration is sampled; a zero-duration clip has one static sample; no clips produces one bind-pose clip. Duplicate display names get indexed tokens and internally unique sampling names, not accidental reuse of the first clip. All clips/frames/views are rendered regardless of current live selection. FPS is sampling, not playback rate.
114
114
115
115
Highres is exactly **8× width AND height** (64× pixels); targets currently **1…256 per axis**, highres max **2048 per axis**. Larger projects may be opened but their batch render is explicitly rejected, never silently reduced to 1×. A batch is limited to 500000 PNGs and 16 GiB estimated uncompressed target data. Render streams one pose/view at a time; no whole-batch image allocation. GPU service is one offscreen draw/readback unit per native UI update, with real depth24 attachment/test/write/clear; skinning, detail/resolve and PNG writes run on a background worker. The GUI keeps updating; a large individual GPU unit can still take a frame. The latest-pass mailbox retains **one** completed image, one progress update, and a terminal event; no unbounded preview backlog. No sleeps, pass playback delay or preview acknowledgment. Fast passes can be visually skipped, never faked.
116
116
@@ -126,9 +126,9 @@ renders/run-<unix-nanoseconds>-<pid>/<male|female>/<body|equipment-slug>/
126
126
127
127
`output_directory` may change the `renders/` root. Safe lowercase clip slugs replace non-ASCII/non-alphanumerics with `-`, truncate to 48 characters, and prefix source index (so duplicate names cannot collide). Equipment slugs use the existing strict preset validator; `body` is reserved. Frame index is zero-based. Map tokens are **only `diffuse`, `depth`, `normal`, `specular`**, for body AND equipment, all exactly target width/height. **Highres and detail are preview-only intermediates: never PNG exports.** Each run is a fresh directory, never replaces any previous run or named image. Canonical parent checks reject traversal/outside symlinks; sibling temporary file + fsync + no-clobber hardlink publication prevents a partial PNG appearing as a valid final file. Filesystems must support hardlinks; unsupported filesystems fail explicitly. Existing or foreign temporary files are never deleted on creation failure.
128
128
129
`render.json` (output metadata schema **3**, separate from `.ppd` schema 3) is written at run start with `status: "incomplete"` and records the full export identity/contract: `project.{id,name}`, sex/set, `body_model.{path,sha256}` (SHA-256 of the body file bytes used for the job), optional `equipment_preset.{name,sha256}` (SHA-256 of the encoded `.ppe` bytes), target size, FPS, highres scale, resolved framing (including `pivot_y_milli`), `pivot_source` (`manual` / `neck` / `fallback`), common near/far, camera distance, conventions, clip inventory (names/tokens/durations/frames), six-pass order, four saved map tokens and planned counts. Frame timestamps follow the recorded sampling equation. Each published target PNG is hashed (SHA-256 of the on-disk PNG bytes) as it is written. `complete.json` (schema 3) is published ONLY after all PNGs complete and contains a matching `identity` object plus `png_sha256` (relative path → hex digest) for every file; cancellation/failure best-effort writes `cancelled.json`/`failed.json` and leaves valid partial files without a complete marker. `render.json` intentionally remains an incomplete-start record; consumers must require `complete.json` and must not infer compatibility from filenames alone.
129
`render.json` (output metadata schema **4** since t955u — adds `pitches`/`yaws` and the atlas record; schema 3 introduced identity/checksums; separate from `.ppd` schema 3) is written at run start with `status: "incomplete"` and records the full export identity/contract: `project.{id,name}`, sex/set, `body_model.{path,sha256}` (SHA-256 of the body file bytes used for the job), optional `equipment_preset.{name,sha256}` (SHA-256 of the encoded `.ppe` bytes), target size, FPS, highres scale, resolved framing (including `pivot_y_milli`), `pivot_source` (`manual` / `neck` / `fallback`), common near/far, camera distance, conventions, clip inventory (names/tokens/durations/frames), six-pass order, four saved map tokens and planned counts. Frame timestamps follow the recorded sampling equation. Each published target PNG is hashed (SHA-256 of the on-disk PNG bytes) as it is written. `complete.json` (schema 4) is published ONLY after all PNGs complete and the atlases/`.ppda` are written, and contains a matching `identity` object, `png_sha256` (relative path → hex digest) for every file and `atlas` (`ppda`, `ppda_sha256`, `maps.<map>.{file,sha256}`); cancellation/failure best-effort writes `cancelled.json`/`failed.json` and leaves valid partial files without a complete marker. `render.json` intentionally remains an incomplete-start record; consumers must require `complete.json` and must not infer compatibility from filenames alone.
130
130
131
**Safe export-set reader (`export_set` module):** `ExportRun::open` accepts a completed run directory only when both manifests are present, schema 3 matches, identity fields agree, every listed PNG is a regular file inside the run (no path traversal, no escaping symlinks), each checksum verifies, and resource caps hold (target ≤256, ≤500000 files, bounded PNG/manifest sizes). `load_map` / `load_relative` decode actual RGBA8 diffuse/normal/specular and gray-alpha16 depth with strict color-type/bit-depth/dimension checks; normals require near-unit length on covered pixels and zero RGB when transparent; specular RGB channels must be equal; transparent depth gray must be zero. Incompatible, stale (schema <3), incomplete or corrupted sets are rejected. The 2D depth compositor consumes this reader rather than inventing a parallel contract. Automatic cleanup of old runs is still future work.
131
**Safe export-set reader (`export_set` module):** `ExportRun::open` accepts a completed run directory only when both manifests are present, schema 4 matches (pitches/yaws exactly −60…+60 × 0…330, atlas names match sex/set), identity fields agree, every listed PNG is a regular file inside the run (no path traversal, no escaping symlinks), each checksum verifies, and resource caps hold (target ≤256, ≤500000 files, bounded PNG/manifest sizes). `load_map` / `load_relative` decode actual RGBA8 diffuse/normal/specular and gray-alpha16 depth with strict color-type/bit-depth/dimension checks; normals require near-unit length on covered pixels and zero RGB when transparent; specular RGB channels must be equal; transparent depth gray must be zero. Incompatible, stale (schema <4), incomplete or corrupted sets are rejected. `ExportRun` is the loose-PNG reader; atlas bytes are verified by the `.ppda` reader (`atlas::AtlasSet`). The 2D depth compositor consumes this reader rather than inventing a parallel contract. Automatic cleanup of old runs is still future work.
132
132
133
133
**Implemented resume (`resume` module, decision 7 November 2026):** every run directory holds `run.lock` (exclusive OS file lock held by the writing worker for the whole attempt; released by the OS on a crash) and an append-only `progress.jsonl` with one `{"path","sha256"}` line per published target PNG, appended and fsynced *after* the PNG's no-clobber publication. A selected incomplete run (no `complete.json`; marker `cancelled.json`, `failed.json` or none = crash) is resumable only when it is a direct `run-*` child of the output directory, its lock is free and its stored `render.json` equals, as JSON, exactly the document the current job would write (project id/name, sex/set, body path+SHA-256, optional equipment SHA-256, target, FPS, highres scale, resolved framing/pivot/pivot_source, near/far/camera, conventions, clip inventory, pass/map lists and counts). Any difference refuses the resume and names the differing top-level fields; the run is left untouched. Before rendering, every planned PNG is classified: journaled with a matching on-disk SHA-256 → kept and never rewritten (same inode); absent → regenerated; corrupt, oversized or non-regular (e.g. symlink) → moved aside with a no-clobber hard link to `<name>.corrupt-aNNN-K` and regenerated (never deleted); a directory in place of a PNG refuses the resume; an existing file without a journal entry (crash between publication and journal append) is compared with the regenerated bytes and kept when identical, otherwise moved aside. Journal lines naming a path outside the plan refuse the resume; torn/unparseable lines only make that file unverified. Images whose four maps are all verified skip the GPU; progress starts at the verified pass count and labels them *al aanwezig, checksums kloppen*. A previous top-level `cancelled.json`/`failed.json` is archived as `attempt-NNN-<marker>.json` (attempt 0 = original run), so a resumed run can be cancelled again (new `cancelled.json` with `attempt`, `completed_passes`, `png_files_done`) and `export_set` still opens it once complete. `resume.jsonl` appends one record per resume attempt (verified/missing/quarantined/unjournaled/damaged-journal counts and archived marker). `complete.json` is written only when all planned PNGs have verified checksums and is identical in form to an uninterrupted run. Foreign `.part-*` temporaries are never deleted; atomic writes now use per-write unique temp names (`part-<pid>-<seq>`) so a stale temp from a crashed process with a reused PID cannot block publication. Other runs are never opened for writing. Runs created before this change have no journal: all their existing PNGs are treated as unjournaled and verified by byte comparison with regenerated output. The editor lists matching incomplete runs (same sex/set, newest first, at most 20) under **Onderbroken renders** in the body and equipment views; mismatched or locked runs are shown with the reason and no **Hervat** action. Real Xvfb/Mesa proof: an `abort()`-crashed run, resumed, cancelled again, damaged and resumed to completion produced a `png_sha256` map identical to an uninterrupted run of the same project, while the earlier completed run stayed byte-identical (`examples/resume_fixture.rs`).
134
134
@@ -184,9 +184,9 @@ Project save creates immutable `presets/<name>-<file-crc32>-<byte-length>.ppe` s
184
184
185
185
## 10. Rendered 2D preview
186
186
187
Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory through the completed-run `export_set` reader (schema 3 identity + checksums), not by guessing from filenames. Play actual exported frames on a loop with elapsed-time accumulation over exactly `frames/fps` seconds so every frame lasts `1/fps`; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation. The **2D-preview** editor tab (`preview2d`) discovers completed runs under `renders/`, filters equipment by `CompatKey` / `assert_compatible`, and shows the frame with aspect-preserving nearest-neighbor letterboxing. View modes: **Resultaat** (relit composite), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map**, **Alleen specular map** (map modes show the depth-composited winning-layer map unlit). Light azimuth wraps full 360°; elevation stays −80…+80; light affects Resultaat only. Rim/edge lighting is removed (t958u). The library depth compositor and relighting API (`compositor`) consumes the safe map reader; lighting is preview-only and does not alter saved unlit diffuse.
187
Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory through the validating `.ppda`/atlas reader (`atlas::AtlasSet`; identity + SHA-256), not by guessing from filenames. Play actual exported frames on a loop with elapsed-time accumulation over exactly `frames/fps` seconds so every frame lasts `1/fps`; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation. The **2D-preview** editor tab (`preview2d`) discovers completed runs under `renders/`, filters equipment by `CompatKey` / `assert_compatible`, and shows the frame with aspect-preserving nearest-neighbor letterboxing. View modes: **Resultaat** (relit composite), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map**, **Alleen specular map** (map modes show the depth-composited winning-layer map unlit). Light azimuth wraps full 360°; elevation stays −80…+80; light affects Resultaat only. Rim/edge lighting is removed (t958u). The library depth compositor and relighting API (`compositor`) consumes the safe map reader; lighting is preview-only and does not alter saved unlit diffuse.
188
188
189
Left/right controls wrap among twelve yaw values. Up/down selects one of three pitch rows with clamping, not cyclic hidden extra angles. The orientation zero/positive direction agrees with export. Changing angle preserves animation time. Holding light buttons rotates azimuth/elevation continuously in degrees per second using delta time; releasing, focus loss or pointer cancellation stops motion. Azimuth 0° is light from +Z (front); positive azimuth toward +X; elevation clamps −80…+80. These controls are continuous even though model view selection is discrete.
189
Left/right controls wrap among twelve yaw values. Up/down selects one of five pitch rows (−60…+60, start 0) with clamping, not cyclic hidden extra angles. The orientation zero/positive direction agrees with export. Changing angle preserves animation time. Holding light buttons rotates azimuth/elevation continuously in degrees per second using delta time; releasing, focus loss or pointer cancellation stops motion. Azimuth 0° is light from +Z (front); positive azimuth toward +X; elevation clamps −80…+80. These controls are continuous even though model view selection is discrete.
190
190
191
191
Depth compositor (`src/compositor.rs`) selects the nearest valid surface per pixel from body and chosen equipment. Valid samples need non-zero depth alpha and non-zero diffuse alpha. Depths within `DEPTH_EPSILON_U16` (1 code) are ties; the lower caller-supplied layer index wins (body first, then equipment in stable name order). Chosen pixel supplies diffuse, normal and shiny from the **same layer**. Alpha-edge policy: winner straight alpha is kept; losing layers never blend in — single-depth compositing cannot reconstruct arbitrary order-dependent translucency. `relight` / `compose_and_relight` applies linear ambient + Lambertian diffuse + Blinn-Phong specular with the light already in view space (matching exported normals; view = +Z). Rim/edge lighting was removed (Tim t958u). It is a consumer proof, not the future game controller. Saved production diffuse remains unlit albedo (Tim t953u).
192
192
@@ -198,7 +198,7 @@ After every merge, `README.md` describes only the current behavior: what the pro
198
198
199
199
The application icon is artwork supplied by Tim Falken. The original file is `assets/icons/paperdoll-renderer.png` (57×60 RGBA). Package bitmaps are scaled from that file with a premultiplied Lanczos filter, so the drawing is not redrawn. The eframe window uses the 128×128 raster as its window icon. Larger hicolor sizes stay in the packages. Linux AppImage, `.deb` and Arch packages install the hicolor sizes and name the icon `paperdoll-renderer` from both the desktop file and the AppStream metainfo. The MinGW Windows build embeds the same artwork in `paperdoll-renderer.exe` as `RT_GROUP_ICON`; the zip does not carry a separate `.ico`.
200
200
201
Live 3D preview, as built: the body screen draws the skinned glTF/GLB or FBX mesh with an independent perspective orbit camera or the visible fixed orthographic render frame (§6). Drag orbits, scroll zooms, and buttons snap to pitches -30/0/+30 and the twelve yaw steps of 30 degrees. Yaw 0 places the camera on -Z because the test bodies face -Z. Walk and attack are sampled on the CPU and uploaded as joint matrices. The preview light is a placement shade. The placement preview is lit, unlike exported albedo. It overlays bone-attached OBJ or FBX equipment (§9); FBX bodies are drawn from the normalized `fbx_scene`. The shell explicitly requests a native 24-bit depth buffer in eframe's **Glow/OpenGL** backend (not wgpu). Every body callback owns depth-test LESS, depth writes and clear-to-1 each frame; subsequent equipment layers must share that attachment without clearing it. A real GPU overlap/readback probe checks draw-order independence and depth clearing between frames. The CPU still rasterizer also depth-tests finite in-frustum fragments; neither preview is allowed to use painter's submission order as occlusion.
201
Live 3D preview, as built: the body screen draws the skinned glTF/GLB or FBX mesh with an independent perspective orbit camera or the visible fixed orthographic render frame (§6). Drag orbits, scroll zooms, and buttons snap to pitches −60/−30/0/+30/+60 and the twelve yaw steps of 30 degrees. Yaw 0 places the camera on -Z because the test bodies face -Z. Walk and attack are sampled on the CPU and uploaded as joint matrices. The preview light is a placement shade. The placement preview is lit, unlike exported albedo. It overlays bone-attached OBJ or FBX equipment (§9); FBX bodies are drawn from the normalized `fbx_scene`. The shell explicitly requests a native 24-bit depth buffer in eframe's **Glow/OpenGL** backend (not wgpu). Every body callback owns depth-test LESS, depth writes and clear-to-1 each frame; subsequent equipment layers must share that attachment without clearing it. A real GPU overlap/readback probe checks draw-order independence and depth clearing between frames. The CPU still rasterizer also depth-tests finite in-frustum fragments; neither preview is allowed to use painter's submission order as occlusion.
202
202
203
203
CI filename requested: `blue-ci-cd.yml`. **Unverified infrastructure detail:** do not invent BlueGit YAML syntax. Inspect documented BlueGit runner schema and narrow existing `timfa/posprint` and `legacy-cycles` pipeline/release examples before writing configuration. Respect runner/image selection, artifact retention and release API semantics. No GitLab mirror is requested for this new project.
204
204
@@ -221,7 +221,7 @@ These requirements are durable even where the implementation is staged. They sup
221
221
- Tim’s PNG is the authoritative app/window/executable/package icon; the original is retained and derived sizes are tested. After every merge the README describes only current abilities and their usage. Add screenshots only when they clarify usage. Merged feature branches may be removed after ancestry verification; never remove master or active work.
222
222
- During a body render, a single preview area is replaced immediately after each completed pass: high-resolution image, internal detail map, target-resolution image enlarged with **nearest-neighbor**, preserving aspect ratio with one uniform scale, centered in the preview box with letterboxing (never independently stretch X/Y; applies to every pass/map), then each produced output map, then the next sampled pose/view. Show progress across actual completed work. Do not add sleeps, playback delays or wait for UI acknowledgment; fast passes may be visually skipped. Never count an unimplemented map as rendered, invent a map image or substitute a 3D viewport for exported maps.
223
223
224
Current implementation checkpoint: native project/pickers, equipment authoring/presets, shared-depth animated placement preview, fixed per-sex framing and rest-pose neck-height orbit pivot are present. **Render alle hoeken** now produces every source clip × sampled frame × 36 views with the six real pass previews and four target maps described above, streaming, progress and cancellation, with schema-3 export identity, source fingerprints and per-PNG SHA-256 checksums plus the safe `export_set` reader. The former two-pass shaded color still is no longer the Render action. The older CPU still helper remains regression-tested but is not the production path. The library 2D depth compositor/relighting and the 2D-preview GUI/tab/playback (view modes Resultaat/map-only, full-circle light azimuth, loop on frames/fps; rim removed per t958u) are implemented, as is checksum-verified resume of cancelled/failed/crashed runs (§7). FBX bodies (normalized `fbx_scene`) are drawn in the live viewport and rendered by the production four-map batch with the same camera, neck pivot and fixed per-body zoom; rigid FBX equipment is imported with embedded/relative PNG textures.
224
Current implementation checkpoint: native project/pickers, equipment authoring/presets, shared-depth animated placement preview, fixed per-sex framing and rest-pose neck-height orbit pivot are present. **Render alle hoeken** now produces every source clip × sampled frame × 60 views with the six real pass previews and four target maps described above, then per-map atlases plus `.ppda`, streaming, progress and cancellation, with schema-3 export identity, source fingerprints and per-PNG SHA-256 checksums plus the safe `export_set` reader. The former two-pass shaded color still is no longer the Render action. The older CPU still helper remains regression-tested but is not the production path. The library 2D depth compositor/relighting and the 2D-preview GUI/tab/playback (view modes Resultaat/map-only, full-circle light azimuth, loop on frames/fps; rim removed per t958u) are implemented, as is checksum-verified resume of cancelled/failed/crashed runs (§7). FBX bodies (normalized `fbx_scene`) are drawn in the live viewport and rendered by the production four-map batch with the same camera, neck pivot and fixed per-body zoom; rigid FBX equipment is imported with embedded/relative PNG textures.
225
225
226
226
## 12. Incremental feature plan and acceptance gates
227
227
@@ -230,7 +230,7 @@ Current implementation checkpoint: native project/pickers, equipment authoring/p
230
230
2. Normalized import and animation inventory: glTF/GLB fixtures first, required OBJ+MTL/textures and FBX adapter proof; format-specific truthful UI/errors and skeleton/clip inventories; integration fixture tests before declaring format support.
231
231
3. Desktop 3D viewport and animation sampling: visible real imported model, orbit, clip controls, source-axis normalization; transform tests and manual visual test.
232
232
4. Equipment attachments and binary presets: bone tree, sex choice, numeric offsets/rotations, live placement, embedded multi-mesh assets; round-trip and rig mismatch tests.
233
5. Shared batch camera/sampling and raw buffers: 36 views, common framing, 8x geometry output/readback, diffuse/depth/normal alignment; orientation/occlusion fixture before aesthetic postprocessing.
233
5. Shared batch camera/sampling and raw buffers: 60 views (t955u; was 36), common framing, 8x geometry output/readback, diffuse/depth/normal alignment; orientation/occlusion fixture before aesthetic postprocessing.
234
234
6. Production image pipeline: detail-aware downsampling, depth/normal resolve, shiny conversion, filename/manifest export, progress/cancel/resume (resume **done**, §7); compare actual output and memory budgets. (No rim accents; preview uses relight only — t958u.)
235
235
7. Rendered 2D compositor preview (**done**): real exported frames, depth ordering, looping on frames/fps, sex/equipment/clip selection, view modes (Resultaat + map-only), discrete view controls, continuous held light controls with full-circle azimuth; deterministic state tests and GUI smoke.
236
236
8. Packaging/hardening: verify all platform packages and install/run behavior, imports with native dependencies, accessibility and failure recovery; actual releases only on later explicit tag authorization.
@@ -360,6 +360,16 @@ Bepaal ook welk 3D-model format goed is; liefst ondersteunen we natuurlijk meerd
360
360
- **Default framerate:** new projects default to **4 fps** instead of 12. Existing `.ppd` files keep their stored `fps`.
361
361
- **`.ppda` as the consumer format (Tim t957u):** the `.ppda` plus its atlases is the primary format for consumers. The editor's 2D previewer loads an export through the `.ppda` and its atlases rather than the loose PNGs, and the eventual game renderer uses the same format. The loose PNGs stay available as export output. The `.ppda` reader validates the version, filenames (no paths outside the export folder), atlas dimensions, cell bounds and SHA-256 before use.
362
362
363
**Implemented (9b, `atlas` module):**
364
- Production plans 60 views per sample (`preview::PRODUCTION_PITCHES` × `production_yaws()`); `render.json`/`complete.json` are output schema 4. New projects default to `project::DEFAULT_FPS = 4`. The legacy `.ppd` shape-validation memory heuristic keeps its factor 36 so 256×256 targets stay valid (production streams one view at a time).
365
- Location: atlases and `.ppda` sit in the run root (`renders/run-…/male-body-diffuse-atlas.png`, `…/male-body-atlas.ppda`); one run holds one sex/set.
366
- Write order after the last loose PNG is verified (fresh or resumed run): four atlases (diffuse, normal, depth, specular) → `.ppda` → `complete.json` (lists the atlas/`.ppda` names + SHA-256). Each atlas is streamed one cell row at a time from the checksum-verified loose PNGs (re-decoded under the map pixel contract), hashed while written, published via temp file + fsync + no-clobber hard link. If an atlas or `.ppda` already exists (crash after publication), it is kept when byte-identical to the regenerated output (encoding is deterministic) and otherwise moved aside as `<name>.corrupt-aNNN-K`. Cancellation is checked per row; `cancelled.json`/`failed.json` follow the normal rules.
367
- Limit: `MAX_ATLAS_DIM = 16384` per side. `production::contract` refuses the job before rendering when 60 × cell width or rows × cell height would exceed it; atlases are never split.
368
- Diffuse atlas carries the sRGB chunk like the loose diffuse maps; depth atlas is 16-bit gray+alpha; normal/specular RGBA8.
369
- `.ppda` fields as listed above, with two additions needed by consumers: `identity` (body/equipment SHA-256, framing, pivot-source kind, near/far, camera distance, normal and depth encoding) so body and equipment atlases can be checked for compositing compatibility without the run manifests, and per-clip `token`; each frame is `{index, views:[{pitch,yaw,x,y,w,h}×60]}`; `maps.<map>` also records `width`/`height`. All structs use `deny_unknown_fields`.
370
- Reader `atlas::AtlasSet::open`/`open_run` (exactly one `*-atlas.ppda` per folder): bounded read, strict JSON, version 1, name = `<sex>-<settype>-atlas.ppda`, sex/settype/project/identity/fps/cell/pitch/yaw checks, every view must equal the deterministic grid cell (implies in-bounds), map file names must equal `<sex>-<settype>-<map>-atlas.png` (no paths), regular non-symlink files inside the folder, SHA-256, PNG header size/colour type. `load()` decodes all four with the loose-map pixel rules (2 GiB decoded budget per set); `LoadedAtlas::cell` crops one view. `CompatKey::from_ppda` + `assert_compatible_keys` and `LayerMaps::from_atlas` feed the compositor. `atlas::cross_check_run` compares every cell with its loose PNG (used by the GPU proofs).
371
- Preview2d discovers runs through `AtlasSet::open_run` (runs without a valid `.ppda`, including older schema-3 runs, are skipped and counted with reasons), decodes the selected body + equipment once and caches them until the selection or catalog changes.
372
363
373
## Decision 2026-11-07 (Tim t958u): full-circle light azimuth, rim lighting removed
364
374
365
375
- **Light azimuth:** in the 2D previewer, holding the light buttons rotates the azimuth through the full 360 degrees and keeps going (wrapping modulo 360, no clamp). Elevation keeps its −80…+80 clamp.
@@ -22,7 +22,7 @@ BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`;22
| R01 | Rust application with user-selected male/female models | Open/save missing and populated slots; later manual desktop smoke test |22
| R01 | Rust application with user-selected male/female models | Open/save missing and populated slots; later manual desktop smoke test |23
| R02 | Discover all supported embedded animation clips | Synthetic licensed fixture inventory, including unnamed/duplicate clips |23
| R02 | Discover all supported embedded animation clips | Synthetic licensed fixture inventory, including unnamed/duplicate clips |24
| R03 | Body and equipment modes | Export body plus isolated equipment from identical scene state |24
| R03 | Body and equipment modes | Export body plus isolated equipment from identical scene state |25
| R04 | Three pitches times twelve yaw positions | Exactly 36 unique orientations; yaw starts facing viewer |25
| R04 | Five pitches times twelve yaw positions (t955u; was three) | Exactly 60 unique orientations; yaw starts facing viewer |26
| R05 | Initial geometry render at 8x target dimensions and configured FPS | Explicit render manifest and dimension/frame-count tests |26
| R05 | Initial geometry render at 8x target dimensions and configured FPS | Explicit render manifest and dimension/frame-count tests |27
| R06 | Edge/detail-guided downsampling | Image fixtures retain thin details better than baseline without alpha fringes |27
| R06 | Edge/detail-guided downsampling | Image fixtures retain thin details better than baseline without alpha fringes |28
| R07 | Target-resolution diffuse, depth, normal maps | Round-trip decoding, finite normalized normals and shared depth contract |28
| R07 | Target-resolution diffuse, depth, normal maps | Round-trip decoding, finite normalized normals and shared depth contract |@@ -33,7 +33,7 @@ BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`;33
| R12 | Equipment set name lowercase without spaces | Validation; reject reserved body name and path traversal |33
| R12 | Equipment set name lowercase without spaces | Validation; reject reserved body name and path traversal |34
| R13 | Equipment-only rendering with shinymap | No body occlusion/write contamination; specular fixture |34
| R13 | Equipment-only rendering with shinymap | No body occlusion/write contamination; specular fixture |35
| R14 | Folder plus .ppd manifest describing assets, presets, outputs | Portable relative paths and safe atomic saves |35
| R14 | Folder plus .ppd manifest describing assets, presets, outputs | Portable relative paths and safe atomic saves |36
| R15 | 2D looping animation, sex/equipment/clip selection and 36-angle navigation | State-machine and depth-compositing tests |36
| R15 | 2D looping animation, sex/equipment/clip selection and 60-angle navigation | State-machine and depth-compositing tests |37
| R16 | Held light-control buttons rotate continuously in degrees | Delta-time tests including key/button release/focus loss |37
| R16 | Held light-control buttons rotate continuously in degrees | Delta-time tests including key/button release/focus loss |38
| R17 | Per-feature branch, tests, non-FF master merge, no initial MRs | Two-parent merge topology and CI checks |38
| R17 | Per-feature branch, tests, non-FF master merge, no initial MRs | Two-parent merge topology and CI checks |39
| R18 | Tag tests gate AppImage, deb, Arch package, Windows build and release | Packaging smoke tests and complete-artifact release gate |39
| R18 | Tag tests gate AppImage, deb, Arch package, Windows build and release | Packaging smoke tests and complete-artifact release gate |@@ -104,13 +104,13 @@ Paths use `/` and stay inside the project directory. Save writes/fsyncs a tempor104
104
105
## 6. Camera, axes, alignment and animation sampling105
## 6. Camera, axes, alignment and animation sampling106
106
107
Required views: pitches -30, 0, +30 degrees; yaw 0, 30, 60, ..., 330 degrees. **Proposal:** yaw zero means the standardized character front faces the camera, world +Y is up, and positive yaw is defined by a documented right-hand convention. Distinguish camera elevation from model rotation: use orbiting orthographic camera around fixed character origin. Golden orientation fixture must show labelled front/back/left/right to remove ambiguity before production export.107
Required views: pitches −60, −30, 0, +30, +60 degrees (t955u; originally −30/0/+30); yaw 0, 30, 60, ..., 330 degrees. **Proposal:** yaw zero means the standardized character front faces the camera, world +Y is up, and positive yaw is defined by a documented right-hand convention. Distinguish camera elevation from model rotation: use orbiting orthographic camera around fixed character origin. Golden orientation fixture must show labelled front/back/left/right to remove ambiguity before production export.108
108
109
**Implemented fixed framing (Tim t952u):** orthographic projection, +Y world up, yaw 0 camera on −Z looking toward the character; positive yaw follows the existing orbit convention. The **orbit pivot** is independent of the vertical image center: target is `(0, pivot_y/1000, 0)` with camera distance **10**. `pivot_y_milli = None` resolves the unique rest-pose neck joint (name tokens such as `Neck`, `mixamorigNeck`, `Bip001 Neck`, `CC_Base_Neck`); missing/ambiguous necks fall back to `center_y_milli` with a UI diagnostic; an explicit `Some` value is a manual override. Vertical world extent is `height_milli/1000`, horizontal extent vertical × target aspect. Orthographic Y translation `(pivot − center) / half_height` keeps zero-pitch image framing (feet / image center) stable when the pivot is raised to the neck. Near **0.05**, far **40** are shared absolutely across all layers, frames and views. Body, equipment layered preview, fixed live frame and production `camera()` all share `camera::resolve` + `camera::matrices`. No per-frame/per-equipment auto-framing or depth normalization. Source root motion is retained as imported (no extraction). User adjusts height/center/pivot in the body view; automatic animation envelopes are not promised.109
**Implemented fixed framing (Tim t952u):** orthographic projection, +Y world up, yaw 0 camera on −Z looking toward the character; positive yaw follows the existing orbit convention. The **orbit pivot** is independent of the vertical image center: target is `(0, pivot_y/1000, 0)` with camera distance **10**. `pivot_y_milli = None` resolves the unique rest-pose neck joint (name tokens such as `Neck`, `mixamorigNeck`, `Bip001 Neck`, `CC_Base_Neck`); missing/ambiguous necks fall back to `center_y_milli` with a UI diagnostic; an explicit `Some` value is a manual override. Vertical world extent is `height_milli/1000`, horizontal extent vertical × target aspect. Orthographic Y translation `(pivot − center) / half_height` keeps zero-pitch image framing (feet / image center) stable when the pivot is raised to the neck. Near **0.05**, far **40** are shared absolutely across all layers, frames and views. Body, equipment layered preview, fixed live frame and production `camera()` all share `camera::resolve` + `camera::matrices`. No per-frame/per-equipment auto-framing or depth normalization. Source root motion is retained as imported (no extraction). User adjusts height/center/pivot in the body view; automatic animation envelopes are not promised.110
110
111
**Vast renderkader tonen** switches live placement to this same orthographic camera and target-aspect, centered yellow frame. Drag still changes yaw/pitch; orbit-scroll zoom is ignored while the fixed frame is shown. Uncheck it for the independent perspective orbit/scroll camera. Neither free orbit zoom nor live selected clip/time changes a production job. Render snapshots the current project settings at job start (including resolved pivot and `pivot_source`); later edits do not change that in-flight batch. **Opslaan** persists male and female fixed framing (including optional manual pivot) in `.ppd`.111
**Vast renderkader tonen** switches live placement to this same orthographic camera and target-aspect, centered yellow frame. Drag still changes yaw/pitch; orbit-scroll zoom is ignored while the fixed frame is shown. Uncheck it for the independent perspective orbit/scroll camera. Neither free orbit zoom nor live selected clip/time changes a production job. Render snapshots the current project settings at job start (including resolved pivot and `pivot_source`); later edits do not change that in-flight batch. **Opslaan** persists male and female fixed framing (including optional manual pivot) in `.ppd`.112
112
113
**Implemented batch nesting:** source clip order → sampled frame → pitch **−30,0,+30** → yaw **0…330 step30** → six completed passes. Samples are `t=frame/FPS`, `0 <= frame < max(1, ceil(duration*FPS))`. To avoid exporter float noise creating a near-duplicate terminal frame, the product snaps to its nearest integer when within `4*f32::EPSILON*max(1,product)` before ceiling; a regression covers an exported 1-second endpoint one ULP above 1. Otherwise the full fractional duration is sampled; a zero-duration clip has one static sample; no clips produces one bind-pose clip. Duplicate display names get indexed tokens and internally unique sampling names, not accidental reuse of the first clip. All clips/frames/views are rendered regardless of current live selection. FPS is sampling, not playback rate.113
**Implemented batch nesting:** source clip order → sampled frame → pitch **−60,−30,0,+30,+60** → yaw **0…330 step30** → six completed passes. Samples are `t=frame/FPS`, `0 <= frame < max(1, ceil(duration*FPS))`. To avoid exporter float noise creating a near-duplicate terminal frame, the product snaps to its nearest integer when within `4*f32::EPSILON*max(1,product)` before ceiling; a regression covers an exported 1-second endpoint one ULP above 1. Otherwise the full fractional duration is sampled; a zero-duration clip has one static sample; no clips produces one bind-pose clip. Duplicate display names get indexed tokens and internally unique sampling names, not accidental reuse of the first clip. All clips/frames/views are rendered regardless of current live selection. FPS is sampling, not playback rate.114
114
115
Highres is exactly **8× width AND height** (64× pixels); targets currently **1…256 per axis**, highres max **2048 per axis**. Larger projects may be opened but their batch render is explicitly rejected, never silently reduced to 1×. A batch is limited to 500000 PNGs and 16 GiB estimated uncompressed target data. Render streams one pose/view at a time; no whole-batch image allocation. GPU service is one offscreen draw/readback unit per native UI update, with real depth24 attachment/test/write/clear; skinning, detail/resolve and PNG writes run on a background worker. The GUI keeps updating; a large individual GPU unit can still take a frame. The latest-pass mailbox retains **one** completed image, one progress update, and a terminal event; no unbounded preview backlog. No sleeps, pass playback delay or preview acknowledgment. Fast passes can be visually skipped, never faked.115
Highres is exactly **8× width AND height** (64× pixels); targets currently **1…256 per axis**, highres max **2048 per axis**. Larger projects may be opened but their batch render is explicitly rejected, never silently reduced to 1×. A batch is limited to 500000 PNGs and 16 GiB estimated uncompressed target data. Render streams one pose/view at a time; no whole-batch image allocation. GPU service is one offscreen draw/readback unit per native UI update, with real depth24 attachment/test/write/clear; skinning, detail/resolve and PNG writes run on a background worker. The GUI keeps updating; a large individual GPU unit can still take a frame. The latest-pass mailbox retains **one** completed image, one progress update, and a terminal event; no unbounded preview backlog. No sleeps, pass playback delay or preview acknowledgment. Fast passes can be visually skipped, never faked.116
116
@@ -126,9 +126,9 @@ renders/run-<unix-nanoseconds>-<pid>/<male|female>/<body|equipment-slug>/126
126
127
`output_directory` may change the `renders/` root. Safe lowercase clip slugs replace non-ASCII/non-alphanumerics with `-`, truncate to 48 characters, and prefix source index (so duplicate names cannot collide). Equipment slugs use the existing strict preset validator; `body` is reserved. Frame index is zero-based. Map tokens are **only `diffuse`, `depth`, `normal`, `specular`**, for body AND equipment, all exactly target width/height. **Highres and detail are preview-only intermediates: never PNG exports.** Each run is a fresh directory, never replaces any previous run or named image. Canonical parent checks reject traversal/outside symlinks; sibling temporary file + fsync + no-clobber hardlink publication prevents a partial PNG appearing as a valid final file. Filesystems must support hardlinks; unsupported filesystems fail explicitly. Existing or foreign temporary files are never deleted on creation failure.127
`output_directory` may change the `renders/` root. Safe lowercase clip slugs replace non-ASCII/non-alphanumerics with `-`, truncate to 48 characters, and prefix source index (so duplicate names cannot collide). Equipment slugs use the existing strict preset validator; `body` is reserved. Frame index is zero-based. Map tokens are **only `diffuse`, `depth`, `normal`, `specular`**, for body AND equipment, all exactly target width/height. **Highres and detail are preview-only intermediates: never PNG exports.** Each run is a fresh directory, never replaces any previous run or named image. Canonical parent checks reject traversal/outside symlinks; sibling temporary file + fsync + no-clobber hardlink publication prevents a partial PNG appearing as a valid final file. Filesystems must support hardlinks; unsupported filesystems fail explicitly. Existing or foreign temporary files are never deleted on creation failure.128
128
129
`render.json` (output metadata schema **3**, separate from `.ppd` schema 3) is written at run start with `status: "incomplete"` and records the full export identity/contract: `project.{id,name}`, sex/set, `body_model.{path,sha256}` (SHA-256 of the body file bytes used for the job), optional `equipment_preset.{name,sha256}` (SHA-256 of the encoded `.ppe` bytes), target size, FPS, highres scale, resolved framing (including `pivot_y_milli`), `pivot_source` (`manual` / `neck` / `fallback`), common near/far, camera distance, conventions, clip inventory (names/tokens/durations/frames), six-pass order, four saved map tokens and planned counts. Frame timestamps follow the recorded sampling equation. Each published target PNG is hashed (SHA-256 of the on-disk PNG bytes) as it is written. `complete.json` (schema 3) is published ONLY after all PNGs complete and contains a matching `identity` object plus `png_sha256` (relative path → hex digest) for every file; cancellation/failure best-effort writes `cancelled.json`/`failed.json` and leaves valid partial files without a complete marker. `render.json` intentionally remains an incomplete-start record; consumers must require `complete.json` and must not infer compatibility from filenames alone.129
`render.json` (output metadata schema **4** since t955u — adds `pitches`/`yaws` and the atlas record; schema 3 introduced identity/checksums; separate from `.ppd` schema 3) is written at run start with `status: "incomplete"` and records the full export identity/contract: `project.{id,name}`, sex/set, `body_model.{path,sha256}` (SHA-256 of the body file bytes used for the job), optional `equipment_preset.{name,sha256}` (SHA-256 of the encoded `.ppe` bytes), target size, FPS, highres scale, resolved framing (including `pivot_y_milli`), `pivot_source` (`manual` / `neck` / `fallback`), common near/far, camera distance, conventions, clip inventory (names/tokens/durations/frames), six-pass order, four saved map tokens and planned counts. Frame timestamps follow the recorded sampling equation. Each published target PNG is hashed (SHA-256 of the on-disk PNG bytes) as it is written. `complete.json` (schema 4) is published ONLY after all PNGs complete and the atlases/`.ppda` are written, and contains a matching `identity` object, `png_sha256` (relative path → hex digest) for every file and `atlas` (`ppda`, `ppda_sha256`, `maps.<map>.{file,sha256}`); cancellation/failure best-effort writes `cancelled.json`/`failed.json` and leaves valid partial files without a complete marker. `render.json` intentionally remains an incomplete-start record; consumers must require `complete.json` and must not infer compatibility from filenames alone.130
130
131
**Safe export-set reader (`export_set` module):** `ExportRun::open` accepts a completed run directory only when both manifests are present, schema 3 matches, identity fields agree, every listed PNG is a regular file inside the run (no path traversal, no escaping symlinks), each checksum verifies, and resource caps hold (target ≤256, ≤500000 files, bounded PNG/manifest sizes). `load_map` / `load_relative` decode actual RGBA8 diffuse/normal/specular and gray-alpha16 depth with strict color-type/bit-depth/dimension checks; normals require near-unit length on covered pixels and zero RGB when transparent; specular RGB channels must be equal; transparent depth gray must be zero. Incompatible, stale (schema <3), incomplete or corrupted sets are rejected. The 2D depth compositor consumes this reader rather than inventing a parallel contract. Automatic cleanup of old runs is still future work.131
**Safe export-set reader (`export_set` module):** `ExportRun::open` accepts a completed run directory only when both manifests are present, schema 4 matches (pitches/yaws exactly −60…+60 × 0…330, atlas names match sex/set), identity fields agree, every listed PNG is a regular file inside the run (no path traversal, no escaping symlinks), each checksum verifies, and resource caps hold (target ≤256, ≤500000 files, bounded PNG/manifest sizes). `load_map` / `load_relative` decode actual RGBA8 diffuse/normal/specular and gray-alpha16 depth with strict color-type/bit-depth/dimension checks; normals require near-unit length on covered pixels and zero RGB when transparent; specular RGB channels must be equal; transparent depth gray must be zero. Incompatible, stale (schema <4), incomplete or corrupted sets are rejected. `ExportRun` is the loose-PNG reader; atlas bytes are verified by the `.ppda` reader (`atlas::AtlasSet`). The 2D depth compositor consumes this reader rather than inventing a parallel contract. Automatic cleanup of old runs is still future work.132
132
133
**Implemented resume (`resume` module, decision 7 November 2026):** every run directory holds `run.lock` (exclusive OS file lock held by the writing worker for the whole attempt; released by the OS on a crash) and an append-only `progress.jsonl` with one `{"path","sha256"}` line per published target PNG, appended and fsynced *after* the PNG's no-clobber publication. A selected incomplete run (no `complete.json`; marker `cancelled.json`, `failed.json` or none = crash) is resumable only when it is a direct `run-*` child of the output directory, its lock is free and its stored `render.json` equals, as JSON, exactly the document the current job would write (project id/name, sex/set, body path+SHA-256, optional equipment SHA-256, target, FPS, highres scale, resolved framing/pivot/pivot_source, near/far/camera, conventions, clip inventory, pass/map lists and counts). Any difference refuses the resume and names the differing top-level fields; the run is left untouched. Before rendering, every planned PNG is classified: journaled with a matching on-disk SHA-256 → kept and never rewritten (same inode); absent → regenerated; corrupt, oversized or non-regular (e.g. symlink) → moved aside with a no-clobber hard link to `<name>.corrupt-aNNN-K` and regenerated (never deleted); a directory in place of a PNG refuses the resume; an existing file without a journal entry (crash between publication and journal append) is compared with the regenerated bytes and kept when identical, otherwise moved aside. Journal lines naming a path outside the plan refuse the resume; torn/unparseable lines only make that file unverified. Images whose four maps are all verified skip the GPU; progress starts at the verified pass count and labels them *al aanwezig, checksums kloppen*. A previous top-level `cancelled.json`/`failed.json` is archived as `attempt-NNN-<marker>.json` (attempt 0 = original run), so a resumed run can be cancelled again (new `cancelled.json` with `attempt`, `completed_passes`, `png_files_done`) and `export_set` still opens it once complete. `resume.jsonl` appends one record per resume attempt (verified/missing/quarantined/unjournaled/damaged-journal counts and archived marker). `complete.json` is written only when all planned PNGs have verified checksums and is identical in form to an uninterrupted run. Foreign `.part-*` temporaries are never deleted; atomic writes now use per-write unique temp names (`part-<pid>-<seq>`) so a stale temp from a crashed process with a reused PID cannot block publication. Other runs are never opened for writing. Runs created before this change have no journal: all their existing PNGs are treated as unjournaled and verified by byte comparison with regenerated output. The editor lists matching incomplete runs (same sex/set, newest first, at most 20) under **Onderbroken renders** in the body and equipment views; mismatched or locked runs are shown with the reason and no **Hervat** action. Real Xvfb/Mesa proof: an `abort()`-crashed run, resumed, cancelled again, damaged and resumed to completion produced a `png_sha256` map identical to an uninterrupted run of the same project, while the earlier completed run stayed byte-identical (`examples/resume_fixture.rs`).133
**Implemented resume (`resume` module, decision 7 November 2026):** every run directory holds `run.lock` (exclusive OS file lock held by the writing worker for the whole attempt; released by the OS on a crash) and an append-only `progress.jsonl` with one `{"path","sha256"}` line per published target PNG, appended and fsynced *after* the PNG's no-clobber publication. A selected incomplete run (no `complete.json`; marker `cancelled.json`, `failed.json` or none = crash) is resumable only when it is a direct `run-*` child of the output directory, its lock is free and its stored `render.json` equals, as JSON, exactly the document the current job would write (project id/name, sex/set, body path+SHA-256, optional equipment SHA-256, target, FPS, highres scale, resolved framing/pivot/pivot_source, near/far/camera, conventions, clip inventory, pass/map lists and counts). Any difference refuses the resume and names the differing top-level fields; the run is left untouched. Before rendering, every planned PNG is classified: journaled with a matching on-disk SHA-256 → kept and never rewritten (same inode); absent → regenerated; corrupt, oversized or non-regular (e.g. symlink) → moved aside with a no-clobber hard link to `<name>.corrupt-aNNN-K` and regenerated (never deleted); a directory in place of a PNG refuses the resume; an existing file without a journal entry (crash between publication and journal append) is compared with the regenerated bytes and kept when identical, otherwise moved aside. Journal lines naming a path outside the plan refuse the resume; torn/unparseable lines only make that file unverified. Images whose four maps are all verified skip the GPU; progress starts at the verified pass count and labels them *al aanwezig, checksums kloppen*. A previous top-level `cancelled.json`/`failed.json` is archived as `attempt-NNN-<marker>.json` (attempt 0 = original run), so a resumed run can be cancelled again (new `cancelled.json` with `attempt`, `completed_passes`, `png_files_done`) and `export_set` still opens it once complete. `resume.jsonl` appends one record per resume attempt (verified/missing/quarantined/unjournaled/damaged-journal counts and archived marker). `complete.json` is written only when all planned PNGs have verified checksums and is identical in form to an uninterrupted run. Foreign `.part-*` temporaries are never deleted; atomic writes now use per-write unique temp names (`part-<pid>-<seq>`) so a stale temp from a crashed process with a reused PID cannot block publication. Other runs are never opened for writing. Runs created before this change have no journal: all their existing PNGs are treated as unjournaled and verified by byte comparison with regenerated output. The editor lists matching incomplete runs (same sex/set, newest first, at most 20) under **Onderbroken renders** in the body and equipment views; mismatched or locked runs are shown with the reason and no **Hervat** action. Real Xvfb/Mesa proof: an `abort()`-crashed run, resumed, cancelled again, damaged and resumed to completion produced a `png_sha256` map identical to an uninterrupted run of the same project, while the earlier completed run stayed byte-identical (`examples/resume_fixture.rs`).134
134
@@ -184,9 +184,9 @@ Project save creates immutable `presets/<name>-<file-crc32>-<byte-length>.ppe` s184
184
185
## 10. Rendered 2D preview185
## 10. Rendered 2D preview186
186
187
Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory through the completed-run `export_set` reader (schema 3 identity + checksums), not by guessing from filenames. Play actual exported frames on a loop with elapsed-time accumulation over exactly `frames/fps` seconds so every frame lasts `1/fps`; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation. The **2D-preview** editor tab (`preview2d`) discovers completed runs under `renders/`, filters equipment by `CompatKey` / `assert_compatible`, and shows the frame with aspect-preserving nearest-neighbor letterboxing. View modes: **Resultaat** (relit composite), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map**, **Alleen specular map** (map modes show the depth-composited winning-layer map unlit). Light azimuth wraps full 360°; elevation stays −80…+80; light affects Resultaat only. Rim/edge lighting is removed (t958u). The library depth compositor and relighting API (`compositor`) consumes the safe map reader; lighting is preview-only and does not alter saved unlit diffuse.187
Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory through the validating `.ppda`/atlas reader (`atlas::AtlasSet`; identity + SHA-256), not by guessing from filenames. Play actual exported frames on a loop with elapsed-time accumulation over exactly `frames/fps` seconds so every frame lasts `1/fps`; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation. The **2D-preview** editor tab (`preview2d`) discovers completed runs under `renders/`, filters equipment by `CompatKey` / `assert_compatible`, and shows the frame with aspect-preserving nearest-neighbor letterboxing. View modes: **Resultaat** (relit composite), **Alleen diffuse**, **Alleen normal map**, **Alleen depth map**, **Alleen specular map** (map modes show the depth-composited winning-layer map unlit). Light azimuth wraps full 360°; elevation stays −80…+80; light affects Resultaat only. Rim/edge lighting is removed (t958u). The library depth compositor and relighting API (`compositor`) consumes the safe map reader; lighting is preview-only and does not alter saved unlit diffuse.188
188
189
Left/right controls wrap among twelve yaw values. Up/down selects one of three pitch rows with clamping, not cyclic hidden extra angles. The orientation zero/positive direction agrees with export. Changing angle preserves animation time. Holding light buttons rotates azimuth/elevation continuously in degrees per second using delta time; releasing, focus loss or pointer cancellation stops motion. Azimuth 0° is light from +Z (front); positive azimuth toward +X; elevation clamps −80…+80. These controls are continuous even though model view selection is discrete.189
Left/right controls wrap among twelve yaw values. Up/down selects one of five pitch rows (−60…+60, start 0) with clamping, not cyclic hidden extra angles. The orientation zero/positive direction agrees with export. Changing angle preserves animation time. Holding light buttons rotates azimuth/elevation continuously in degrees per second using delta time; releasing, focus loss or pointer cancellation stops motion. Azimuth 0° is light from +Z (front); positive azimuth toward +X; elevation clamps −80…+80. These controls are continuous even though model view selection is discrete.190
190
191
Depth compositor (`src/compositor.rs`) selects the nearest valid surface per pixel from body and chosen equipment. Valid samples need non-zero depth alpha and non-zero diffuse alpha. Depths within `DEPTH_EPSILON_U16` (1 code) are ties; the lower caller-supplied layer index wins (body first, then equipment in stable name order). Chosen pixel supplies diffuse, normal and shiny from the **same layer**. Alpha-edge policy: winner straight alpha is kept; losing layers never blend in — single-depth compositing cannot reconstruct arbitrary order-dependent translucency. `relight` / `compose_and_relight` applies linear ambient + Lambertian diffuse + Blinn-Phong specular with the light already in view space (matching exported normals; view = +Z). Rim/edge lighting was removed (Tim t958u). It is a consumer proof, not the future game controller. Saved production diffuse remains unlit albedo (Tim t953u).191
Depth compositor (`src/compositor.rs`) selects the nearest valid surface per pixel from body and chosen equipment. Valid samples need non-zero depth alpha and non-zero diffuse alpha. Depths within `DEPTH_EPSILON_U16` (1 code) are ties; the lower caller-supplied layer index wins (body first, then equipment in stable name order). Chosen pixel supplies diffuse, normal and shiny from the **same layer**. Alpha-edge policy: winner straight alpha is kept; losing layers never blend in — single-depth compositing cannot reconstruct arbitrary order-dependent translucency. `relight` / `compose_and_relight` applies linear ambient + Lambertian diffuse + Blinn-Phong specular with the light already in view space (matching exported normals; view = +Z). Rim/edge lighting was removed (Tim t958u). It is a consumer proof, not the future game controller. Saved production diffuse remains unlit albedo (Tim t953u).192
192
@@ -198,7 +198,7 @@ After every merge, `README.md` describes only the current behavior: what the pro198
198
199
The application icon is artwork supplied by Tim Falken. The original file is `assets/icons/paperdoll-renderer.png` (57×60 RGBA). Package bitmaps are scaled from that file with a premultiplied Lanczos filter, so the drawing is not redrawn. The eframe window uses the 128×128 raster as its window icon. Larger hicolor sizes stay in the packages. Linux AppImage, `.deb` and Arch packages install the hicolor sizes and name the icon `paperdoll-renderer` from both the desktop file and the AppStream metainfo. The MinGW Windows build embeds the same artwork in `paperdoll-renderer.exe` as `RT_GROUP_ICON`; the zip does not carry a separate `.ico`.199
The application icon is artwork supplied by Tim Falken. The original file is `assets/icons/paperdoll-renderer.png` (57×60 RGBA). Package bitmaps are scaled from that file with a premultiplied Lanczos filter, so the drawing is not redrawn. The eframe window uses the 128×128 raster as its window icon. Larger hicolor sizes stay in the packages. Linux AppImage, `.deb` and Arch packages install the hicolor sizes and name the icon `paperdoll-renderer` from both the desktop file and the AppStream metainfo. The MinGW Windows build embeds the same artwork in `paperdoll-renderer.exe` as `RT_GROUP_ICON`; the zip does not carry a separate `.ico`.200
200
201
Live 3D preview, as built: the body screen draws the skinned glTF/GLB or FBX mesh with an independent perspective orbit camera or the visible fixed orthographic render frame (§6). Drag orbits, scroll zooms, and buttons snap to pitches -30/0/+30 and the twelve yaw steps of 30 degrees. Yaw 0 places the camera on -Z because the test bodies face -Z. Walk and attack are sampled on the CPU and uploaded as joint matrices. The preview light is a placement shade. The placement preview is lit, unlike exported albedo. It overlays bone-attached OBJ or FBX equipment (§9); FBX bodies are drawn from the normalized `fbx_scene`. The shell explicitly requests a native 24-bit depth buffer in eframe's **Glow/OpenGL** backend (not wgpu). Every body callback owns depth-test LESS, depth writes and clear-to-1 each frame; subsequent equipment layers must share that attachment without clearing it. A real GPU overlap/readback probe checks draw-order independence and depth clearing between frames. The CPU still rasterizer also depth-tests finite in-frustum fragments; neither preview is allowed to use painter's submission order as occlusion.201
Live 3D preview, as built: the body screen draws the skinned glTF/GLB or FBX mesh with an independent perspective orbit camera or the visible fixed orthographic render frame (§6). Drag orbits, scroll zooms, and buttons snap to pitches −60/−30/0/+30/+60 and the twelve yaw steps of 30 degrees. Yaw 0 places the camera on -Z because the test bodies face -Z. Walk and attack are sampled on the CPU and uploaded as joint matrices. The preview light is a placement shade. The placement preview is lit, unlike exported albedo. It overlays bone-attached OBJ or FBX equipment (§9); FBX bodies are drawn from the normalized `fbx_scene`. The shell explicitly requests a native 24-bit depth buffer in eframe's **Glow/OpenGL** backend (not wgpu). Every body callback owns depth-test LESS, depth writes and clear-to-1 each frame; subsequent equipment layers must share that attachment without clearing it. A real GPU overlap/readback probe checks draw-order independence and depth clearing between frames. The CPU still rasterizer also depth-tests finite in-frustum fragments; neither preview is allowed to use painter's submission order as occlusion.202
202
203
CI filename requested: `blue-ci-cd.yml`. **Unverified infrastructure detail:** do not invent BlueGit YAML syntax. Inspect documented BlueGit runner schema and narrow existing `timfa/posprint` and `legacy-cycles` pipeline/release examples before writing configuration. Respect runner/image selection, artifact retention and release API semantics. No GitLab mirror is requested for this new project.203
CI filename requested: `blue-ci-cd.yml`. **Unverified infrastructure detail:** do not invent BlueGit YAML syntax. Inspect documented BlueGit runner schema and narrow existing `timfa/posprint` and `legacy-cycles` pipeline/release examples before writing configuration. Respect runner/image selection, artifact retention and release API semantics. No GitLab mirror is requested for this new project.204
204
@@ -221,7 +221,7 @@ These requirements are durable even where the implementation is staged. They sup221
- Tim’s PNG is the authoritative app/window/executable/package icon; the original is retained and derived sizes are tested. After every merge the README describes only current abilities and their usage. Add screenshots only when they clarify usage. Merged feature branches may be removed after ancestry verification; never remove master or active work.221
- Tim’s PNG is the authoritative app/window/executable/package icon; the original is retained and derived sizes are tested. After every merge the README describes only current abilities and their usage. Add screenshots only when they clarify usage. Merged feature branches may be removed after ancestry verification; never remove master or active work.222
- During a body render, a single preview area is replaced immediately after each completed pass: high-resolution image, internal detail map, target-resolution image enlarged with **nearest-neighbor**, preserving aspect ratio with one uniform scale, centered in the preview box with letterboxing (never independently stretch X/Y; applies to every pass/map), then each produced output map, then the next sampled pose/view. Show progress across actual completed work. Do not add sleeps, playback delays or wait for UI acknowledgment; fast passes may be visually skipped. Never count an unimplemented map as rendered, invent a map image or substitute a 3D viewport for exported maps.222
- During a body render, a single preview area is replaced immediately after each completed pass: high-resolution image, internal detail map, target-resolution image enlarged with **nearest-neighbor**, preserving aspect ratio with one uniform scale, centered in the preview box with letterboxing (never independently stretch X/Y; applies to every pass/map), then each produced output map, then the next sampled pose/view. Show progress across actual completed work. Do not add sleeps, playback delays or wait for UI acknowledgment; fast passes may be visually skipped. Never count an unimplemented map as rendered, invent a map image or substitute a 3D viewport for exported maps.223
223
224
Current implementation checkpoint: native project/pickers, equipment authoring/presets, shared-depth animated placement preview, fixed per-sex framing and rest-pose neck-height orbit pivot are present. **Render alle hoeken** now produces every source clip × sampled frame × 36 views with the six real pass previews and four target maps described above, streaming, progress and cancellation, with schema-3 export identity, source fingerprints and per-PNG SHA-256 checksums plus the safe `export_set` reader. The former two-pass shaded color still is no longer the Render action. The older CPU still helper remains regression-tested but is not the production path. The library 2D depth compositor/relighting and the 2D-preview GUI/tab/playback (view modes Resultaat/map-only, full-circle light azimuth, loop on frames/fps; rim removed per t958u) are implemented, as is checksum-verified resume of cancelled/failed/crashed runs (§7). FBX bodies (normalized `fbx_scene`) are drawn in the live viewport and rendered by the production four-map batch with the same camera, neck pivot and fixed per-body zoom; rigid FBX equipment is imported with embedded/relative PNG textures.224
Current implementation checkpoint: native project/pickers, equipment authoring/presets, shared-depth animated placement preview, fixed per-sex framing and rest-pose neck-height orbit pivot are present. **Render alle hoeken** now produces every source clip × sampled frame × 60 views with the six real pass previews and four target maps described above, then per-map atlases plus `.ppda`, streaming, progress and cancellation, with schema-3 export identity, source fingerprints and per-PNG SHA-256 checksums plus the safe `export_set` reader. The former two-pass shaded color still is no longer the Render action. The older CPU still helper remains regression-tested but is not the production path. The library 2D depth compositor/relighting and the 2D-preview GUI/tab/playback (view modes Resultaat/map-only, full-circle light azimuth, loop on frames/fps; rim removed per t958u) are implemented, as is checksum-verified resume of cancelled/failed/crashed runs (§7). FBX bodies (normalized `fbx_scene`) are drawn in the live viewport and rendered by the production four-map batch with the same camera, neck pivot and fixed per-body zoom; rigid FBX equipment is imported with embedded/relative PNG textures.225
225
226
## 12. Incremental feature plan and acceptance gates226
## 12. Incremental feature plan and acceptance gates227
227
@@ -230,7 +230,7 @@ Current implementation checkpoint: native project/pickers, equipment authoring/p230
2. Normalized import and animation inventory: glTF/GLB fixtures first, required OBJ+MTL/textures and FBX adapter proof; format-specific truthful UI/errors and skeleton/clip inventories; integration fixture tests before declaring format support.230
2. Normalized import and animation inventory: glTF/GLB fixtures first, required OBJ+MTL/textures and FBX adapter proof; format-specific truthful UI/errors and skeleton/clip inventories; integration fixture tests before declaring format support.231
3. Desktop 3D viewport and animation sampling: visible real imported model, orbit, clip controls, source-axis normalization; transform tests and manual visual test.231
3. Desktop 3D viewport and animation sampling: visible real imported model, orbit, clip controls, source-axis normalization; transform tests and manual visual test.232
4. Equipment attachments and binary presets: bone tree, sex choice, numeric offsets/rotations, live placement, embedded multi-mesh assets; round-trip and rig mismatch tests.232
4. Equipment attachments and binary presets: bone tree, sex choice, numeric offsets/rotations, live placement, embedded multi-mesh assets; round-trip and rig mismatch tests.233
5. Shared batch camera/sampling and raw buffers: 36 views, common framing, 8x geometry output/readback, diffuse/depth/normal alignment; orientation/occlusion fixture before aesthetic postprocessing.233
5. Shared batch camera/sampling and raw buffers: 60 views (t955u; was 36), common framing, 8x geometry output/readback, diffuse/depth/normal alignment; orientation/occlusion fixture before aesthetic postprocessing.234
6. Production image pipeline: detail-aware downsampling, depth/normal resolve, shiny conversion, filename/manifest export, progress/cancel/resume (resume **done**, §7); compare actual output and memory budgets. (No rim accents; preview uses relight only — t958u.)234
6. Production image pipeline: detail-aware downsampling, depth/normal resolve, shiny conversion, filename/manifest export, progress/cancel/resume (resume **done**, §7); compare actual output and memory budgets. (No rim accents; preview uses relight only — t958u.)235
7. Rendered 2D compositor preview (**done**): real exported frames, depth ordering, looping on frames/fps, sex/equipment/clip selection, view modes (Resultaat + map-only), discrete view controls, continuous held light controls with full-circle azimuth; deterministic state tests and GUI smoke.235
7. Rendered 2D compositor preview (**done**): real exported frames, depth ordering, looping on frames/fps, sex/equipment/clip selection, view modes (Resultaat + map-only), discrete view controls, continuous held light controls with full-circle azimuth; deterministic state tests and GUI smoke.236
8. Packaging/hardening: verify all platform packages and install/run behavior, imports with native dependencies, accessibility and failure recovery; actual releases only on later explicit tag authorization.236
8. Packaging/hardening: verify all platform packages and install/run behavior, imports with native dependencies, accessibility and failure recovery; actual releases only on later explicit tag authorization.@@ -360,6 +360,16 @@ Bepaal ook welk 3D-model format goed is; liefst ondersteunen we natuurlijk meerd360
- **Default framerate:** new projects default to **4 fps** instead of 12. Existing `.ppd` files keep their stored `fps`.360
- **Default framerate:** new projects default to **4 fps** instead of 12. Existing `.ppd` files keep their stored `fps`.361
- **`.ppda` as the consumer format (Tim t957u):** the `.ppda` plus its atlases is the primary format for consumers. The editor's 2D previewer loads an export through the `.ppda` and its atlases rather than the loose PNGs, and the eventual game renderer uses the same format. The loose PNGs stay available as export output. The `.ppda` reader validates the version, filenames (no paths outside the export folder), atlas dimensions, cell bounds and SHA-256 before use.361
- **`.ppda` as the consumer format (Tim t957u):** the `.ppda` plus its atlases is the primary format for consumers. The editor's 2D previewer loads an export through the `.ppda` and its atlases rather than the loose PNGs, and the eventual game renderer uses the same format. The loose PNGs stay available as export output. The `.ppda` reader validates the version, filenames (no paths outside the export folder), atlas dimensions, cell bounds and SHA-256 before use.362
362
363
**Implemented (9b, `atlas` module):**364
- Production plans 60 views per sample (`preview::PRODUCTION_PITCHES` × `production_yaws()`); `render.json`/`complete.json` are output schema 4. New projects default to `project::DEFAULT_FPS = 4`. The legacy `.ppd` shape-validation memory heuristic keeps its factor 36 so 256×256 targets stay valid (production streams one view at a time).365
- Location: atlases and `.ppda` sit in the run root (`renders/run-…/male-body-diffuse-atlas.png`, `…/male-body-atlas.ppda`); one run holds one sex/set.366
- Write order after the last loose PNG is verified (fresh or resumed run): four atlases (diffuse, normal, depth, specular) → `.ppda` → `complete.json` (lists the atlas/`.ppda` names + SHA-256). Each atlas is streamed one cell row at a time from the checksum-verified loose PNGs (re-decoded under the map pixel contract), hashed while written, published via temp file + fsync + no-clobber hard link. If an atlas or `.ppda` already exists (crash after publication), it is kept when byte-identical to the regenerated output (encoding is deterministic) and otherwise moved aside as `<name>.corrupt-aNNN-K`. Cancellation is checked per row; `cancelled.json`/`failed.json` follow the normal rules.367
- Limit: `MAX_ATLAS_DIM = 16384` per side. `production::contract` refuses the job before rendering when 60 × cell width or rows × cell height would exceed it; atlases are never split.368
- Diffuse atlas carries the sRGB chunk like the loose diffuse maps; depth atlas is 16-bit gray+alpha; normal/specular RGBA8.369
- `.ppda` fields as listed above, with two additions needed by consumers: `identity` (body/equipment SHA-256, framing, pivot-source kind, near/far, camera distance, normal and depth encoding) so body and equipment atlases can be checked for compositing compatibility without the run manifests, and per-clip `token`; each frame is `{index, views:[{pitch,yaw,x,y,w,h}×60]}`; `maps.<map>` also records `width`/`height`. All structs use `deny_unknown_fields`.370
- Reader `atlas::AtlasSet::open`/`open_run` (exactly one `*-atlas.ppda` per folder): bounded read, strict JSON, version 1, name = `<sex>-<settype>-atlas.ppda`, sex/settype/project/identity/fps/cell/pitch/yaw checks, every view must equal the deterministic grid cell (implies in-bounds), map file names must equal `<sex>-<settype>-<map>-atlas.png` (no paths), regular non-symlink files inside the folder, SHA-256, PNG header size/colour type. `load()` decodes all four with the loose-map pixel rules (2 GiB decoded budget per set); `LoadedAtlas::cell` crops one view. `CompatKey::from_ppda` + `assert_compatible_keys` and `LayerMaps::from_atlas` feed the compositor. `atlas::cross_check_run` compares every cell with its loose PNG (used by the GPU proofs).371
- Preview2d discovers runs through `AtlasSet::open_run` (runs without a valid `.ppda`, including older schema-3 runs, are skipped and counted with reasons), decodes the selected body + equipment once and caches them until the selection or catalog changes.372
363
## Decision 2026-11-07 (Tim t958u): full-circle light azimuth, rim lighting removed373
## Decision 2026-11-07 (Tim t958u): full-circle light azimuth, rim lighting removed364
374
365
- **Light azimuth:** in the 2D previewer, holding the light buttons rotates the azimuth through the full 360 degrees and keeps going (wrapping modulo 360, no clamp). Elevation keeps its −80…+80 clamp.375
- **Light azimuth:** in the 2D previewer, holding the light buttons rotates the azimuth through the full 360 degrees and keeps going (wrapping modulo 360, no clamp). Elevation keeps its −80…+80 clamp.The contents of this binary file are not shown.
@@ -1,6 +1,6 @@
1
1
//! Native GUI smoke for the rendered 2D preview tab: it loads a synthetic
2
2
//! export through its `.ppda` + atlases (loose PNGs are deleted first).
3
//! Usage: preview2d_smoke OUTPUT_DIR
3
//! Usage: preview2d_smoke OUTPUT_DIR [EXISTING_PROJECT_DIR]
4
4
5
5
use eframe::App;
6
6
use paperdoll_renderer::editor::EditorApp;
@@ -18,6 +18,8 @@ struct Smoke {
18
18
started: Instant,
19
19
frames: u64,
20
20
done: bool,
21
shot_requested: bool,
22
shot_saved: bool,
21
23
}
22
24
23
25
impl App for Smoke {
@@ -27,13 +29,44 @@ impl App for Smoke {
27
29
if self.frames == 2 {
28
30
self.editor.open_preview2d();
29
31
}
32
if self.frames == 4 {
33
// Steepest pitch row (+60) and yaw 30 prove the five-row atlas layout.
34
let state = self.editor.preview2d_state_mut();
35
state.step_pitch(2);
36
state.step_yaw(1);
37
}
38
ctx.request_repaint();
39
if self.frames >= 30 && !self.shot_saved {
40
if !self.shot_requested {
41
self.shot_requested = true;
42
ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot);
43
} else if let Some(image) = ctx.input(|i| {
44
i.raw.events.iter().find_map(|e| match e {
45
eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()),
46
_ => None,
47
})
48
}) {
49
let [w, h] = image.size;
50
let rgba: Vec<u8> = image.pixels.iter().flat_map(|p| p.to_array()).collect();
51
write_png(
52
&self.out.join("preview2d-gui.png"),
53
&ImageBuf {
54
width: w as u32,
55
height: h as u32,
56
rgba,
57
},
58
)
59
.expect("png");
60
self.shot_saved = true;
61
}
62
}
30
63
let ready = self.editor.preview2d_image().is_some()
31
64
|| self.editor.preview2d_state().diagnostic.is_some();
32
let timeout = self.started.elapsed() > Duration::from_secs(8);
65
let timeout = self.started.elapsed() > Duration::from_secs(20);
33
66
if self.done {
34
67
return;
35
68
}
36
if (self.frames >= 12 && ready) || timeout {
69
if (self.shot_saved && ready) || timeout {
37
70
self.done = true;
38
71
let state = self.editor.preview2d_state();
39
72
let image_ok = if let Some(img) = self.editor.preview2d_image() {
@@ -43,7 +76,8 @@ impl App for Smoke {
43
76
false
44
77
};
45
78
let value = serde_json::json!({
46
"success": image_ok,
79
"success": image_ok && self.shot_saved,
80
"screenshot": self.shot_saved,
47
81
"gui_updates": self.frames,
48
82
"sex": state.sex_token(),
49
83
"clip": state.clip_token,
@@ -109,13 +143,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
109
143
.ok_or("usage: preview2d_smoke OUTPUT_DIR")?,
110
144
);
111
145
fs::create_dir_all(&out)?;
112
let project = out.join("project");
113
let mut session = Session::new();
114
session.install_new_project(&project)?;
115
session.save()?;
116
install_body_export(&project)?;
117
// Prove the preview reads the atlases: remove every loose PNG.
118
fs::remove_dir_all(project.join("renders/run-preview2d-body/male"))?;
146
// Optional second argument: an existing project with real production
147
// exports (its loose PNGs are left alone).
148
let project = match std::env::args().nth(2) {
149
Some(existing) => PathBuf::from(existing),
150
None => {
151
let project = out.join("project");
152
let mut session = Session::new();
153
session.install_new_project(&project)?;
154
session.save()?;
155
install_body_export(&project)?;
156
// Prove the preview reads the atlases: remove every loose PNG.
157
fs::remove_dir_all(project.join("renders/run-preview2d-body/male"))?;
158
project
159
}
160
};
119
161
let catalog = discover_completed_runs(&project);
120
162
fs::write(
121
163
out.join("catalog.json"),
@@ -144,6 +186,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
144
186
started: Instant::now(),
145
187
frames: 0,
146
188
done: false,
189
shot_requested: false,
190
shot_saved: false,
147
191
}) as Box<dyn App>)
148
192
}),
149
193
)
@@ -1,6 +1,6 @@1
//! Native GUI smoke for the rendered 2D preview tab: it loads a synthetic1
//! Native GUI smoke for the rendered 2D preview tab: it loads a synthetic2
//! export through its `.ppda` + atlases (loose PNGs are deleted first).2
//! export through its `.ppda` + atlases (loose PNGs are deleted first).3
//! Usage: preview2d_smoke OUTPUT_DIR3
//! Usage: preview2d_smoke OUTPUT_DIR [EXISTING_PROJECT_DIR]4
4
5
use eframe::App;5
use eframe::App;6
use paperdoll_renderer::editor::EditorApp;6
use paperdoll_renderer::editor::EditorApp;@@ -18,6 +18,8 @@ struct Smoke {18
started: Instant,18
started: Instant,19
frames: u64,19
frames: u64,20
done: bool,20
done: bool,21
shot_requested: bool,22
shot_saved: bool,21
}23
}22
24
23
impl App for Smoke {25
impl App for Smoke {@@ -27,13 +29,44 @@ impl App for Smoke {27
if self.frames == 2 {29
if self.frames == 2 {28
self.editor.open_preview2d();30
self.editor.open_preview2d();29
}31
}32
if self.frames == 4 {33
// Steepest pitch row (+60) and yaw 30 prove the five-row atlas layout.34
let state = self.editor.preview2d_state_mut();35
state.step_pitch(2);36
state.step_yaw(1);37
}38
ctx.request_repaint();39
if self.frames >= 30 && !self.shot_saved {40
if !self.shot_requested {41
self.shot_requested = true;42
ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot);43
} else if let Some(image) = ctx.input(|i| {44
i.raw.events.iter().find_map(|e| match e {45
eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()),46
_ => None,47
})48
}) {49
let [w, h] = image.size;50
let rgba: Vec<u8> = image.pixels.iter().flat_map(|p| p.to_array()).collect();51
write_png(52
&self.out.join("preview2d-gui.png"),53
&ImageBuf {54
width: w as u32,55
height: h as u32,56
rgba,57
},58
)59
.expect("png");60
self.shot_saved = true;61
}62
}30
let ready = self.editor.preview2d_image().is_some()63
let ready = self.editor.preview2d_image().is_some()31
|| self.editor.preview2d_state().diagnostic.is_some();64
|| self.editor.preview2d_state().diagnostic.is_some();32
let timeout = self.started.elapsed() > Duration::from_secs(8);65
let timeout = self.started.elapsed() > Duration::from_secs(20);33
if self.done {66
if self.done {34
return;67
return;35
}68
}36
if (self.frames >= 12 && ready) || timeout {69
if (self.shot_saved && ready) || timeout {37
self.done = true;70
self.done = true;38
let state = self.editor.preview2d_state();71
let state = self.editor.preview2d_state();39
let image_ok = if let Some(img) = self.editor.preview2d_image() {72
let image_ok = if let Some(img) = self.editor.preview2d_image() {@@ -43,7 +76,8 @@ impl App for Smoke {43
false76
false44
};77
};45
let value = serde_json::json!({78
let value = serde_json::json!({46
"success": image_ok,79
"success": image_ok && self.shot_saved,80
"screenshot": self.shot_saved,47
"gui_updates": self.frames,81
"gui_updates": self.frames,48
"sex": state.sex_token(),82
"sex": state.sex_token(),49
"clip": state.clip_token,83
"clip": state.clip_token,@@ -109,13 +143,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {109
.ok_or("usage: preview2d_smoke OUTPUT_DIR")?,143
.ok_or("usage: preview2d_smoke OUTPUT_DIR")?,110
);144
);111
fs::create_dir_all(&out)?;145
fs::create_dir_all(&out)?;112
let project = out.join("project");146
// Optional second argument: an existing project with real production113
let mut session = Session::new();147
// exports (its loose PNGs are left alone).114
session.install_new_project(&project)?;148
let project = match std::env::args().nth(2) {115
session.save()?;149
Some(existing) => PathBuf::from(existing),116
install_body_export(&project)?;150
None => {117
// Prove the preview reads the atlases: remove every loose PNG.151
let project = out.join("project");118
fs::remove_dir_all(project.join("renders/run-preview2d-body/male"))?;152
let mut session = Session::new();153
session.install_new_project(&project)?;154
session.save()?;155
install_body_export(&project)?;156
// Prove the preview reads the atlases: remove every loose PNG.157
fs::remove_dir_all(project.join("renders/run-preview2d-body/male"))?;158
project159
}160
};119
let catalog = discover_completed_runs(&project);161
let catalog = discover_completed_runs(&project);120
fs::write(162
fs::write(121
out.join("catalog.json"),163
out.join("catalog.json"),@@ -144,6 +186,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {144
started: Instant::now(),186
started: Instant::now(),145
frames: 0,187
frames: 0,146
done: false,188
done: false,189
shot_requested: false,190
shot_saved: false,147
}) as Box<dyn App>)191
}) as Box<dyn App>)148
}),192
}),149
)193
)@@ -411,7 +411,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
411
411
&& coverage_mismatch * 200 <= covered
412
412
&& obj_red > 0
413
413
&& obj_blue > 0
414
&& obj_other * 20 <= obj_red + obj_blue
414
// Mixed red/blue seam pixels from the coverage resolve; the steep
415
// ±60° pitch rows (t955u) see the seam over more pixels than ±30°.
416
&& obj_other * 10 <= obj_red + obj_blue
415
417
&& gltf_colors.len() <= 2
416
418
&& normal_bad == 0;
417
419
if !(gui_ok && runs_ok && maps_ok) {
@@ -411,7 +411,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {411
&& coverage_mismatch * 200 <= covered411
&& coverage_mismatch * 200 <= covered412
&& obj_red > 0412
&& obj_red > 0413
&& obj_blue > 0413
&& obj_blue > 0414
&& obj_other * 20 <= obj_red + obj_blue414
// Mixed red/blue seam pixels from the coverage resolve; the steep415
// ±60° pitch rows (t955u) see the seam over more pixels than ±30°.416
&& obj_other * 10 <= obj_red + obj_blue415
&& gltf_colors.len() <= 2417
&& gltf_colors.len() <= 2416
&& normal_bad == 0;418
&& normal_bad == 0;417
if !(gui_ok && runs_ok && maps_ok) {419
if !(gui_ok && runs_ok && maps_ok) {@@ -1,6 +1,7 @@
1
1
//! Deterministic 2D depth compositor and linear relighting.
2
2
//!
3
//! Consumes already-exported maps through [`crate::export_set`] (schema 3).
3
//! Consumes already-exported maps through [`crate::export_set`] (loose PNGs) or
4
//! [`crate::atlas`] (`.ppda` + atlases, the consumer format).
4
5
//! Saved diffuse remains unlit albedo elsewhere; this module only applies
5
6
//! consumer-side lighting to composed layers.
6
7
//!
@@ -1,6 +1,7 @@1
//! Deterministic 2D depth compositor and linear relighting.1
//! Deterministic 2D depth compositor and linear relighting.2
//!2
//!3
//! Consumes already-exported maps through [`crate::export_set`] (schema 3).3
//! Consumes already-exported maps through [`crate::export_set`] (loose PNGs) or4
//! [`crate::atlas`] (`.ppda` + atlases, the consumer format).4
//! Saved diffuse remains unlit albedo elsewhere; this module only applies5
//! Saved diffuse remains unlit albedo elsewhere; this module only applies5
//! consumer-side lighting to composed layers.6
//! consumer-side lighting to composed layers.6
//!7
//!@@ -1399,6 +1399,11 @@ impl EditorApp {
1399
1399
&self.preview2d
1400
1400
}
1401
1401
1402
/// Mutable 2D-preview state (view/playback), e.g. for GUI smokes.
1403
pub fn preview2d_state_mut(&mut self) -> &mut Preview2dState {
1404
&mut self.preview2d
1405
}
1406
1402
1407
pub fn preview2d_image(&self) -> Option<&ImageBuf> {
1403
1408
self.preview2d_image.as_ref()
1404
1409
}
@@ -1399,6 +1399,11 @@ impl EditorApp {1399
&self.preview2d1399
&self.preview2d1400
}1400
}1401
1401
1402
/// Mutable 2D-preview state (view/playback), e.g. for GUI smokes.1403
pub fn preview2d_state_mut(&mut self) -> &mut Preview2dState {1404
&mut self.preview2d1405
}1406
1402
pub fn preview2d_image(&self) -> Option<&ImageBuf> {1407
pub fn preview2d_image(&self) -> Option<&ImageBuf> {1403
self.preview2d_image.as_ref()1408
self.preview2d_image.as_ref()1404
}1409
}@@ -118,7 +118,7 @@ pub fn dutch_state(availability: Availability) -> &'static str {
118
118
}
119
119
120
120
pub fn dutch_summary() -> &'static str {
121
"Deze build leest glTF/GLB, FBX en OBJ+MTL, opent een .ppd-project en toont een live geskinde 3D-preview van glTF/GLB- en FBX-lichamen (FBX via een eenmalig genormaliseerde tekenbare/animeerbare scene) met al hun clips (zoals walk en attack), rigide bone-gebonden OBJ- of FBX-equipment en binaire .ppe-presets. Render maakt voor glTF/GLB- en FBX-lichamen alle clips × frames × 36 hoeken met vaste orthografische camera en diffuse/depth/normal/specular op doelresolutie. De 2D-preview composeert voltooide exportsets met depth-ordering, looping-playback, hoeknavigatie en continue lichtbediening."
121
"Deze build leest glTF/GLB, FBX en OBJ+MTL, opent een .ppd-project en toont een live geskinde 3D-preview van glTF/GLB- en FBX-lichamen (FBX via een eenmalig genormaliseerde tekenbare/animeerbare scene) met al hun clips (zoals walk en attack), rigide bone-gebonden OBJ- of FBX-equipment en binaire .ppe-presets. Render maakt voor glTF/GLB- en FBX-lichamen alle clips × frames × 60 hoeken (5 pitches × 12 yaws) met vaste orthografische camera en diffuse/depth/normal/specular op doelresolutie, plus per maptype een atlas en een .ppda. De 2D-preview leest exports via .ppda en atlassen en composeert ze met depth-ordering, looping-playback, hoeknavigatie en continue lichtbediening."
122
122
}
123
123
124
124
/// Stable status text shared by `--status` and the window.
@@ -118,7 +118,7 @@ pub fn dutch_state(availability: Availability) -> &'static str {118
}118
}119
119
120
pub fn dutch_summary() -> &'static str {120
pub fn dutch_summary() -> &'static str {121
"Deze build leest glTF/GLB, FBX en OBJ+MTL, opent een .ppd-project en toont een live geskinde 3D-preview van glTF/GLB- en FBX-lichamen (FBX via een eenmalig genormaliseerde tekenbare/animeerbare scene) met al hun clips (zoals walk en attack), rigide bone-gebonden OBJ- of FBX-equipment en binaire .ppe-presets. Render maakt voor glTF/GLB- en FBX-lichamen alle clips × frames × 36 hoeken met vaste orthografische camera en diffuse/depth/normal/specular op doelresolutie. De 2D-preview composeert voltooide exportsets met depth-ordering, looping-playback, hoeknavigatie en continue lichtbediening."121
"Deze build leest glTF/GLB, FBX en OBJ+MTL, opent een .ppd-project en toont een live geskinde 3D-preview van glTF/GLB- en FBX-lichamen (FBX via een eenmalig genormaliseerde tekenbare/animeerbare scene) met al hun clips (zoals walk en attack), rigide bone-gebonden OBJ- of FBX-equipment en binaire .ppe-presets. Render maakt voor glTF/GLB- en FBX-lichamen alle clips × frames × 60 hoeken (5 pitches × 12 yaws) met vaste orthografische camera en diffuse/depth/normal/specular op doelresolutie, plus per maptype een atlas en een .ppda. De 2D-preview leest exports via .ppda en atlassen en composeert ze met depth-ordering, looping-playback, hoeknavigatie en continue lichtbediening."122
}122
}123
123
124
/// Stable status text shared by `--status` and the window.124
/// Stable status text shared by `--status` and the window.@@ -5,7 +5,7 @@
5
5
//! draws skinned glTF/FBX bodies, static (unskinned) OBJ/glTF/FBX bodies and
6
6
//! equipment previews, and streams production target maps.
7
7
//! Completed export runs are opened through `export_set` with checksum/identity
8
//! validation; `compositor` depth-composites compatible layers and relights them; `preview2d` drives the rendered 2D GUI/tab/playback with view modes; `resume` recovers incomplete runs after contract and checksum verification.
8
//! validation; `atlas` writes and validates the per-map atlases + `.ppda` consumer format; `compositor` depth-composites compatible layers and relights them; `preview2d` drives the rendered 2D GUI/tab/playback with view modes; `resume` recovers incomplete runs after contract and checksum verification.
9
9
10
10
pub mod atlas;
11
11
pub mod camera;
@@ -5,7 +5,7 @@5
//! draws skinned glTF/FBX bodies, static (unskinned) OBJ/glTF/FBX bodies and5
//! draws skinned glTF/FBX bodies, static (unskinned) OBJ/glTF/FBX bodies and6
//! equipment previews, and streams production target maps.6
//! equipment previews, and streams production target maps.7
//! Completed export runs are opened through `export_set` with checksum/identity7
//! Completed export runs are opened through `export_set` with checksum/identity8
//! validation; `compositor` depth-composites compatible layers and relights them; `preview2d` drives the rendered 2D GUI/tab/playback with view modes; `resume` recovers incomplete runs after contract and checksum verification.8
//! validation; `atlas` writes and validates the per-map atlases + `.ppda` consumer format; `compositor` depth-composites compatible layers and relights them; `preview2d` drives the rendered 2D GUI/tab/playback with view modes; `resume` recovers incomplete runs after contract and checksum verification.9
9
10
pub mod atlas;10
pub mod atlas;11
pub mod camera;11
pub mod camera;