| 1 | # Paperdoll Renderer |
| 2 | |
| 3 | Desktopeditor voor `.ppd`-projecten, bone-gebonden equipment en geanimeerde 3D-preview, met productie-export naar paperdoll-maps. |
| 4 | |
| 5 | ## Starten |
| 6 | |
| 7 | Rust 1.96.0 staat in `rust-toolchain.toml`. Op Debian/Ubuntu gebruik je voor bouwen en het venster `pkg-config`, `libxcb1-dev`, `libx11-dev`, `libxkbcommon-dev`, `libxkbcommon-x11-dev` en `libgl1-mesa-dev`. De native filepicker gebruikt XDG Desktop Portal met een GTK/KDE-backend, of Zenity. Live 3D vereist OpenGL 3.3. |
| 8 | |
| 9 | ```bash |
| 10 | cargo run |
| 11 | cargo run -- "/pad met spaties/character.ppd" |
| 12 | cargo run -- --preview-fixture |
| 13 | cargo run -- --status |
| 14 | ``` |
| 15 | |
| 16 | ## Project en bodies |
| 17 | |
| 18 | Het startvenster biedt **Project laden** en **Nieuw project**, beide met een systeembestandkiezer. Nieuw project maakt `character.ppd` en de mappen `assets/body/`, `assets/equipment/`, `presets/`, `cache/` en `renders/`. De CC0-testlichamen worden in `assets/body/` voorbereid. `--preview-fixture` maakt een tijdelijk voorbeeldproject en opent de mannelijke body. |
| 19 | |
| 20 | De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert een model. Een extern bestand wordt naar de projectmap gekopieerd; bij een bestaande naam krijgt de kopie een eigen naam. Modellen met bijbestanden kun je met hun relatieve mappenstructuur in de projectmap zetten en daar selecteren. **Importoverzicht** toont meshes, materialen, textures, joints en clips van glTF/GLB, FBX en OBJ+MTL. FBX-lichamen worden bij het laden één keer genormaliseerd (rechtshandig Y-up, meters) en daarna net als glTF/GLB in de live 3D-preview en de productierender getekend, met al hun animatiestacks als clips. |
| 21 | |
| 22 | **Open man / Open vrouw** opent de body. Een geskind glTF/GLB- of FBX-model, of een statisch lichaam (zie hieronder), is in de live preview zichtbaar: |
| 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. |
| 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. |
| 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 | |
| 31 |  |
| 32 | |
| 33 | ### Statische lichamen (OBJ, glTF/FBX zonder skin) |
| 34 | |
| 35 | Een lichaam zonder skin kan ook als man- of vrouwmodel gekozen worden: **OBJ+MTL** (met PNG-textures naast het model), **glTF/GLB zonder `skins`** en **FBX zonder skin deformer**. Zo'n lichaam wordt getoond en gerenderd zoals het is opgeslagen, zonder skelet of animatie: |
| 36 | |
| 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. |
| 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. |
| 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 | |
| 43 | ## 2D-preview |
| 44 | |
| 45 | Vanuit **Body** of de bodyweergave opent **2D-preview** de gerenderde compositor over voltooide schema-3 exportsets in `renders/`: |
| 46 | |
| 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. |
| 49 | - **Yaw − / Yaw +** wrappen over 0…330° (stap 30). **Pitch − / Pitch +** kiezen −30 / 0 / +30 met klemmen. Hoekwijzigingen behouden de animatietijd. |
| 50 | - Houd **Azimut / Elevatie** ingedrukt voor continue lichtrotatie in °/s (delta-time); loslaten, focusverlies of pointer-annulering stopt de beweging. Azimut 0° is licht van voren (+Z); elevatie is geklemd. Belichting is alleen voor de preview; opgeslagen diffuse blijft unlit albedo. |
| 51 | - **Randlicht (sterkte)** voegt een subtiel Fresnel-/diepterand-effect toe op het samengestelde frame (standaard aan, begrensd). Op 0 blijft alleen relight over; opgeslagen diffuse wijzigt nooit. |
| 52 | - Het samengestelde frame gebruikt nearest-neighbor weergave met uniforme schaal en gecentreerde letterboxing, gelijk aan de productie-passpreviews. |
| 53 | |
| 54 | ```bash |
| 55 | # Native GUI-smoke met schema-3 fixture-export (Xvfb) |
| 56 | cargo run --example preview2d_smoke --locked -- /tmp/preview2d-smoke |
| 57 | ``` |
| 58 | |
| 59 | ## Equipment en opslaan |
| 60 | |
| 61 | Vanuit een body opent **Equipment** de lijst. Kies een definitie of **Nieuw**. |
| 62 | |
| 63 | 1. Geef een unieke naam van maximaal 64 kleine letters/cijfers/`-`/`_`, zonder spaties; `body` is gereserveerd. |
| 64 | 2. Kies **Man / Vrouw** voor het lichaam dat je bekijkt; **Geldig voor** bepaalt op welke geslachten het preset van toepassing is. |
| 65 | 3. **Onderdeel toevoegen (OBJ/FBX)** opent de systeembestandkiezer. Rigide OBJ-onderdelen worden inclusief MTL en relatieve PNG-bijbestanden ingesloten. Rigide (niet-geskinde) FBX-onderdelen krijgen dezelfde as- en eenheidsnormalisatie als FBX-lichamen; in de FBX ingebedde PNG-textures en relatieve PNG-textures naast het model worden in het preset ingesloten. Een gekoppelde diffuse texture vervangt de FBX-diffusekleur. Geskinde FBX-meshes, morph targets en niet-PNG-textures worden met een melding geweigerd. De preview gebruikt diffuse kleur/texture en alpha-cutout (grens 0,5). Bronbestanden mogen de modelmap niet verlaten. |
| 66 | 4. Vouw **Skeleton / botten** uit en kies het doelbot. Stel relatieve positie XYZ en rotatie XYZ in graden in (Rz·Ry·Rx). Het onderdeel beweegt met dat bot tijdens walk/attack of een andere geïmporteerde bodyclip. Iedere sekse heeft een eigen botbinding; de numerieke offsets zijn gedeeld. |
| 67 | 5. **Kies shinymap (PNG)** moduleert de instelbare glanssterkte. **Wis shinymap** keert terug naar een uniforme glanssterkte. Vul desgewenst **Herkomst / licentie** in. Meerdere onderdelen kunnen verschillende botten gebruiken. |
| 68 | 6. **Opslaan** bewaart het project plus zelfbevattende binaire `.ppe`-snapshots in `presets/` (geometrie, bronpayloads, materialen/textures, transformaties, bindingen, seksevlaggen en herkomst). Er worden geen bronpaden buiten het project nodig om die opnieuw te laden. |
| 69 | |
| 70 | **Preset importeren** leest een `.ppe` uit bijvoorbeeld een ander project. Bij een andere rig moet je de botten opnieuw kiezen; er wordt niet stil geretarget. Onvolledige configuraties kunnen worden opgeslagen, maar onopgeloste onderdelen worden niet op een gegist bot getekend. **Verwijder onderdeel** haalt een onderdeel uit het werkpreset; **Verwijder equipmentdefinitie** haalt de definitie uit de projectlijst. Oude project-snapshots blijven voor herstel bewaard. Opslaan vereist een bestandssysteem met hardlinks; bij een fout wordt een bestaande snapshot niet overschreven. |
| 71 | |
| 72 | Een preset is begrensd op 32 MiB, 32 onderdelen/bestanden en textures van maximaal 2048×2048. Een project heeft maximaal 64 presets en 64 MiB aan presetbestanden. De volledige containerbeschrijving staat in `design.md`. |
| 73 | |
| 74 | ```bash |
| 75 | # Zelfbevattend CC0-zwaardvoorbeeld in een nieuw tijdelijk project |
| 76 | cargo run --example equipment_fixture --locked |
| 77 | ``` |
| 78 | |
| 79 |  |
| 80 | |
| 81 | Bij sluiten of **Ander project / Nieuw project** met wijzigingen kies je **Opslaan**, **Niet opslaan** of **Annuleren**. Als opslaan mislukt, blijft het project open. Annuleren in de bestandkiezer behoudt je huidige werk. |
| 82 | |
| 83 | ## Productie-output |
| 84 | |
| 85 | De bodyknop rendert alleen het lichaam; **Render equipment alle hoeken** in de equipment-editor rendert het geselecteerde preset, geanimeerd op het gekozen lichaam maar **zonder body-occlusie of bodypixels**. De vaste camera van dat geslacht blijft gelijk, ook als je in de live preview vrij hebt ingezoomd. |
| 86 | |
| 87 | 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. |
| 88 | |
| 89 | ```text |
| 90 | renders/run-<tijd>-<pid>/male/body/clip001-walk/f000012/p-30_y330_diffuse.png |
| 91 | ``` |
| 92 | |
| 93 | 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`. |
| 94 | |
| 95 | ### Hervatten |
| 96 | |
| 97 | Een hervatte run schrijft in dezelfde runmap verder; andere runs blijven onaangeroerd. |
| 98 | |
| 99 | - 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. |
| 100 | - 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. |
| 101 | - 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. |
| 102 | - 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. |
| 103 | |
| 104 |  |
| 105 | |
| 106 | ## 2D depth-compositor (bibliotheek) |
| 107 | |
| 108 | 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). |
| 109 | |
| 110 | `relight` past lineaire ambient + Lambert diffuse + Blinn-Phong specular toe. De lichtvektor staat al in view-space, gelijk aan de geëxporteerde normal maps. `apply_rim` / `compose_relight_and_rim` voegen daarna een begrensd Fresnel-randlicht (`(1−N·V)^power`) plus optionele diepte-rand toe; intensiteit is geklemd tot `MAX_RIM_INTENSITY` (0,45) en alpha blijft byte-exact. Opgeslagen diffuse blijft elders onbelichte albedo. |
| 111 | |
| 112 | ```rust |
| 113 | use paperdoll_renderer::compositor::{ |
| 114 | assert_compatible, compose_relight_and_rim, LayerMaps, Light, RimParams, |
| 115 | }; |
| 116 | use paperdoll_renderer::export_set::ExportRun; |
| 117 | |
| 118 | let body = ExportRun::open(&body_run_dir)?; |
| 119 | let sword = ExportRun::open(&sword_run_dir)?; |
| 120 | assert_compatible(&[&body, &sword])?; |
| 121 | let layers = [ |
| 122 | LayerMaps::from_export_run(&body, "body", "clip000-walk", 0, 0, 0)?, |
| 123 | LayerMaps::from_export_run(&sword, "sword", "clip000-walk", 0, 0, 0)?, |
| 124 | ]; |
| 125 | let (maps, lit_rgba) = compose_relight_and_rim(&layers, &Light::default(), &RimParams::default())?; |
| 126 | ``` |
| 127 | |
| 128 | |
| 129 | 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. |
| 130 | |
| 131 | ## Openen met |
| 132 | |
| 133 | - **Windows:** start de uitgepakte `.exe` eenmaal om de editor voor het huidige account aan *Openen met* toe te voegen. Kies daarna bij een `.ppd` **Openen met → Paperdoll Renderer** (of selecteer de `.exe` via Bladeren). |
| 134 | - **Debian/Arch:** de pakketten installeren de `.ppd`-MIME-koppeling, desktopstarter en iconen. Kies de editor in **Openen met**. |
| 135 | - **Linux AppImage/losse binary:** bewaar het bestand op zijn vaste plek en voer `"/pad/Paperdoll.AppImage" --install-desktop` uit. Hiervoor zijn `shared-mime-info` en `desktop-file-utils` nodig. Daarna staat de editor in **Openen met**. Herhaal dit na verplaatsen van de AppImage. |
| 136 | |
| 137 | Het venster, de Linux-starter en de Windows-executable gebruiken Tims applicatie-icoon. |
| 138 | |
| 139 | ## Tests |
| 140 | |
| 141 | ```bash |
| 142 | cargo fmt --check |
| 143 | cargo check --workspace --all-targets --locked |
| 144 | cargo clippy --workspace --all-targets --locked -- -D warnings |
| 145 | cargo test --workspace --locked |
| 146 | # Linux GPU-regressie: echte depth-occlusie, normal/depth/specular en lichtonafhankelijke albedo |
| 147 | xvfb-run -a cargo run --example preview_depth_probe --locked |
| 148 | # Hervatten op echte GPU: setup, render met crash (abort) of annulering, hervatten, export_set-controle |
| 149 | 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 |
| 150 | xvfb-run -a cargo run --release --example resume_fixture --locked -- render /tmp/resume male --abort-after=200 |
| 151 | xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp/resume male |
| 152 | cargo run --release --example resume_fixture --locked -- verify /tmp/resume |
| 153 | # FBX op echte GPU: live view (lichaam + FBX-zwaard) en volledige productie van body- en equipmentset |
| 154 | xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4 |
| 155 | # Statische lichamen op echte GPU: OBJ-man en glTF-vrouw zonder skin, live view, equipment-weigering, productie en export_set |
| 156 | xvfb-run -a -s '-screen 0 1280x1024x24' cargo run --example static_body_fixture --locked -- /tmp/static-body /tmp/static-body-proof 32 48 4 |
| 157 | ``` |
| 158 | |
| 159 | De testlichamen komen uit `scripts/generate_test_bodies.py`. Het FBX-testbestand wordt met Blender via `scripts/convert_test_body_to_fbx.py` gemaakt. De rigide FBX-zwaarden (`sword-fbx.fbx` met ingebedde PNG, `sword-fbx-external.fbx` met PNG ernaast) komen uit `scripts/make_fbx_equipment_fixture.py` (Blender, Z-up geëxporteerd). De statische lichamen in `tests/fixtures/bodies/static/` (OBJ+MTL+PNG en een GLB zonder skin) komen uit `scripts/generate_static_bodies.py`. |