BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / design.md

design.md

design.md 59.6 KiB Download Raw
1# Paperdoll Renderer — design
2
3**Status: design recorded with a foundation shell only.**
4Prepared 6 November 2026 (Europe/Amsterdam). Requirements originate with Tim Falken. Sections marked **proposal** remain future decisions; the appendix preserves the original brief. Current functionality is described in §§5–11: project/equipment authoring, shared-depth live 3D and streaming all-clips/frames/36-view production target maps are implemented. Later corrections (target-only four maps and strictly unlit diffuse) supersede older filename/rim wording in the historical appendix.
5
6Anonymous HTTP requests to `https://git.timfalken.com/groups/golden-age` and `https://git.timfalken.com/golden-age/paperdoll-renderer` returned 404. BlueGit also answers 404 when a private project is not visible to the caller, so those responses do not prove the private repository is absent. This workspace does not create the group or the repository. The requested BlueGit destination stays group **Golden Age**, slug `golden-age`, repository **Paperdoll Renderer**, slug `paperdoll-renderer`.
7
8## 1. Product, scope and non-goals
9
10Build a native Rust desktop asset-authoring application for rendering animated human bodies and bone-attached equipment into aligned 2D paperdoll PNG sequences. A project contains male and female source models, their imported animation inventories, equipment presets, render settings, output paths and output metadata. Either model slot may remain empty while authoring. Missing models must be reported before a render starts, not replaced by invented characters.
11
12The application has three distinct experiences: a project/model workspace, an interactive **3D attachment editor**, and a **rendered 2D paperdoll preview**. The latter must consume actual exported maps; it must not disguise a 3D viewport as a finished sprite renderer.
13
14The later game character-renderer/controller is OUT OF SCOPE. The in-tool compositing preview is in scope because it verifies asset compatibility. No bundled commercial models, automatic retargeting, physics-based cloth, equipment deformation/weight painting, game controllers or multiplayer are promised. Equipment is initially rigid geometry attached to bones. Imported skinned equipment can be a later explicitly scoped extension.
15
16BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`; repository **Paperdoll Renderer**, slug `paperdoll-renderer`. Check existence before creating either. Private visibility is a provisional safe default, not a claim about an existing group's settings. Do not change unrelated group/repository access.
17
18## 2. Requirements and traceability
19
20| ID | Requirement | Verification |
21|---|---|---|
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 |
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 |
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 |
28| R07 | Target-resolution diffuse, depth, normal maps | Round-trip decoding, finite normalized normals and shared depth contract |
29| R08 | Subtle diffuse edge highlights from depth/normals | Deterministic fixture with bounded highlight effect |
30| R09 | Required filename structure | Golden path examples plus sanitization/collision tests |
31| R10 | Bone-relative XYZ position and rotation with live orbitable 3D preview | Transform-chain unit tests and manual viewport test |
32| R11 | Binary equipment presets embedding models and attachment metadata | Round-trip, version/truncation/resource-limit tests |
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 |
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 |
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 |
39| R18 | Tag tests gate AppImage, deb, Arch package, Windows build and release | Packaging smoke tests and complete-artifact release gate |
40| R19 | Minimum import goals OBJ/MTL/textures and FBX; broader support where feasible | Format matrix and fixture-based import tests |
41
42## 3. Proposed architecture and stack
43
44**Proposal, not compatibility certification:** use Bevy for windowing, scene graph, skeletal animation, glTF loading and GPU rendering (wgpu), with a compatible bevy_egui integration for authoring panels. Choose and pin compatible published versions when implementation starts; do not guess latest-version APIs or dependency compatibility. Use ordinary Rust libraries with no GUI/GPU requirements for project validation, naming, render schedules, transforms and image processing. Tests for this core must run headlessly.
45
46A dependency-minimal executable skeleton may precede the full viewport; it must say which capabilities are unimplemented. An application that only prints a placeholder message is not the completed desktop renderer. Rust edition/toolchain must be explicit, version-pinned after confirming installed/build-runner availability. Commit Cargo.lock for reproducibility.
47
48Proposed workspace boundaries:
49- `crates/ppd-core`: project types, validation, portable paths, render schedule, filename codec and format versions.
50- `crates/ppd-import`: import adapters, coordinate normalization, normalized assets and animation discovery.
51- `crates/ppd-render`: shared camera, scene sampling, offscreen maps and readback.
52- `crates/ppd-image`: linear-color downsampling, detail extraction, edge highlight and PNG codecs.
53- `crates/ppd-preview`: deterministic depth compositing and preview/light state independent of UI where possible.
54- `crates/paperdoll-renderer`: desktop shell, model/preset editors, render progress and preview tabs.
55- `tests/fixtures`: tiny synthetic or permissively licensed fixtures, with provenance and licenses.
56- `packaging` and `scripts`: verified platform packaging and CI helpers.
57
58Do not create empty crates solely to imply completion. Establish boundaries as features land. Rendering jobs operate on immutable validated snapshots so editing a project cannot silently change an active batch. Worker messages report progress and cancellation; UI/GPU ownership stays on the appropriate thread.
59
60## 4. Import format advice and support matrix
61
62**Recommendation:** glTF 2.0/GLB is the preferred canonical interchange for animated characters. GLB is convenient for distributing a self-contained body and avoids accidental texture-path breakage. Preserve original sources and conversion/import provenance. OBJ and FBX are minimum product goals, not necessarily native Bevy capabilities. A Bevy glTF loader must not be advertised as an FBX or OBJ loader.
63
64### Import implementation status
65
66The desktop shell stays on eframe/egui. Bevy is not linked. Pulling Bevy in only to parse files would replace the window stack already used for project editing, and it is not required to read glTF or OBJ. Import uses the `gltf` 1.4 crate and `tobj` 4. Both are pure Rust. The live preview loads glTF/GLB on its own and skins the mesh; the inventory records stay the import report.
67
68This build reads glTF 2.0 `.gltf` and `.glb`: mesh positions, triangle indices, normals, UVs, skin weights, materials (base color and named textures), images (external files and buffer views), joint names with parents, and every animation clip (name, duration, channel count, target joints). It reads OBJ+MTL as a static equipment mesh, including referenced texture files, and always records that OBJ has no skeleton and no skeletal clips. External URIs that leave the project directory are rejected. A missing buffer, image or MTL file fails the import and names the missing path.
69
70FBX is read with the `ufbx` crate 0.11.5. That crate compiles the MIT-licensed ufbx C loader into the binary, so AppImage, `.deb`, Arch and the MinGW Windows build do not depend on a system `libassimp`. `russimp` was not used: it needs a native Assimp build whose FBX animation results and Windows packaging are still the open risk above, and ufbx is the loader that actually returns skins, clips and materials for the fixture. A file that is not FBX still fails the parse; the `.fbx` suffix alone is not accepted. The live viewport samples glTF/GLB. FBX stays in the import inventory, and the production orthographic export now renders glTF/GLB, not FBX. The fixture `tests/fixtures/bodies/test-body-male.fbx` is produced from the CC0 GLB by `scripts/convert_test_body_to_fbx.py` (Blender is a fixture tool only; the application does not call it).
71
72| Format | Mesh | Materials/textures | Skeleton | Animation | Intended role / status |
73|---|---|---|---|---|---|
74| glTF 2.0 .gltf/.glb | Yes | PBR metallic/roughness and referenced/embedded textures | Skins/joints | Node/skeletal clips; morph support must be tested | Read by the `gltf` crate in this build: mesh, materials, textures, joints and clips. The live preview draws the skinned body mesh. |
75| OBJ + MTL + texture files | Yes | MTL subset with explicit conversion and relative textures | No | No skeletal clips | Read by `tobj` in this build as a static mesh. The import states that OBJ has no skeletal clips. Not rendered. |
76| FBX ASCII/binary | Yes | Diffuse/PBR color and file or embedded textures that ufbx reports | Skin clusters and bone parents | Takes as animation stacks, with duration and transform channels | Read in this build by vendored ufbx. Not rendered. Not a claim that every FBX dialect matches. |
77| Collada .dae | Usually | Adapter-dependent | Possible | Possible | Optional Assimp-backed candidate, not guaranteed until fixtures pass |
78| PLY / STL | Static geometry | PLY colors possible; STL no conventional textures | No | No | Optional static equipment only; low priority |
79| .blend | Not a portable interchange contract | Requires Blender/version matching | Possible through export | Possible through export | No direct minimum support; explicit external export to GLB preferred |
80
81FBX/OBJ adapter decision remains **open**: evaluate Assimp via maintained Rust bindings (e.g. russimp, native Assimp build dependency), dedicated format loaders, or explicit Blender conversion. Assimp offers a broad-format normalization route but its Rust binding/native ABI, packaging, material conversion, skin weights and animation correctness must be proven on Linux AND Windows. A native library must be bundled/licensed correctly or statically linked where permitted. Do not silently require an installed Blender; if used as an optional converter, show the requirement and invoke it only after explicit user selection. Avoid a heavyweight runtime dependency when an in-process adapter passes tests.
82
83Import normalizes axes, handedness, units, winding, inverse-bind matrices and texture color spaces ONCE, then retains conversion metadata. Detect singular transforms, invalid weights, missing bones/textures and unsupported materials. Imported file paths and embedded archives are untrusted: no traversal, arbitrary script execution or unbounded decompression. Keep parse limits and import failure diagnostics.
84
85OBJ bodies can be displayed as static geometry, but must not pretend to have a skeleton or the requested animations. A UI must clearly disable unavailable bone/clip features. Separate equipment geometry from body rig; bone matching uses stable joint identity/path, not only an ambiguous display name.
86
87Format acceptance gate: fixtures cover mesh topology, alpha materials, normal maps, external relative texture paths, one animated skinned body, multiple clips, bone-local transforms, malformed files and missing resources. A green fixture does not certify every exporter dialect.
88
89## 5. .ppd project and asset ownership
90
91**Proposal:** `.ppd` is a versioned UTF-8 JSON manifest in a project directory, with `format_version`, project identifier, display name, optional male/female model references, import transforms, animation inventory overrides, equipment preset references, render settings, output locations and generated-set metadata. Keep source references and normalized cache separate. Use project-relative paths by default; importing may copy models plus dependencies into `assets/` after user choice. External references need explicit portability warnings and relinking, never silent data loss.
92
93Suggested layout: `character.ppd`, `assets/body/`, `assets/equipment/`, `presets/`, `cache/`, `renders/`. The .ppd manifest is not the binary equipment preset. Cache is disposable and content-addressed; source and preset files are not. Save through a temporary sibling plus atomic rename, preserve valid previous state on failure, reject unsupported newer schema versions with a useful message. Unknown fields should not be silently discarded on rewrite without a documented migration policy.
94
95Validate finite numeric values, dimensions > 0, bounded dimensions and memory estimate, positive FPS, valid clip references, existing model/preset paths and output directories distinct from sources. A missing optional sex slot is a valid project, but cannot render that slot. No absolute-path leakage into exported manifests unless user explicitly chooses external references.
96
97### Implemented format_version 3 (versions 1/2 migration)
98
99`character.ppd` is a strict UTF-8 JSON manifest. Fields remain `format_version`, `id`, `name`, nullable `male_model`/`female_model`, `equipment_presets`, `output_directory`, and `render` (`target_width`, `target_height`, `fps`, `framing`). Version 2 introduced `render.framing`, an array **[male, female]** of `{height_milli, center_y_milli}`: integer world-unit millimetres, default height 2600 and center Y 1000. Version 3 adds optional `pivot_y_milli` on each framing entry (`None` = auto rest-pose neck, `Some` = manual override). Heights 100…10000 and center/pivot −10000…10000 are validated. Each sex has its own fixed camera, shared by every body/equipment export. Opening version 1 or 2 migrates in memory (missing pivot stays auto); the next save writes version 3. Version 4+ and unknown fields are rejected, not silently discarded. This does not add source-axis/import-transform overrides or animation inventory overrides.
100
101Paths use `/` and stay inside the project directory. Save writes/fsyncs a temporary sibling and atomically replaces the manifest; failure preserves the prior manifest. Binary equipment snapshots remain separate. Per-sex framing is part of the normal unsaved-change/save/discard gate, not transient orbit state.
102
103## 6. Camera, axes, alignment and animation sampling
104
105Required 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.
106
107**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.
108
109**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`.
110
111**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.
112
113Highres 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.
114
115## 7. Export naming and metadata
116
117The current hierarchical layout supersedes the earlier flat filename proposal (the original brief is retained verbatim in the historical appendix):
118
119```text
120renders/run-<unix-nanoseconds>-<pid>/<male|female>/<body|equipment-slug>/
121 clip<source-index:03>-<safe-name>/f<frame:06>/p<pitch:+03>_y<yaw:03>_<map>.png
122# example: .../male/body/clip001-walk/f000012/p-30_y330_diffuse.png
123```
124
125`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.
126
127`render.json` (output metadata schema 2, separate from `.ppd` schema 3) records sex/set, target, FPS, highres scale, framing (with resolved `pivot_y_milli`), `pivot_source` (`manual` / `neck` / `fallback`), common near/far, conventions, source clip names/tokens/durations/frame counts, six-pass order, four saved maps and actual counts. Frame timestamps follow the recorded sampling equation. `complete.json` is published ONLY after all PNGs complete; cancellation/failure best-effort writes `cancelled.json`/`failed.json` and leaves valid partial files. `render.json` intentionally remains an incomplete-start record; consumers must require `complete.json`. Source-file checksums, per-image checksums, resumable batches and automatic cleanup are future work, not implemented manifest fields.
128
129## 8. Map contracts and rendering algorithm
130
131**Diffuse (Tim t953u): unlit albedo only.** Linear material base color × sRGB-decoded base-color texture × linear vertex COLOR_0 (white if absent), then sRGB encode at the PNG boundary. Preserve straight alpha/coverage. **No lighting, shading, shadows, specular or rim accents are baked in.** Live 3D may stay lit. A native GPU texture×vertex-color regression checks exact expected albedo/alpha and byte-identical diffuse under three opposed light directions.
132
133**Depth:** actual depth24 GPU attachment readback, resolved to **16-bit grayscale+alpha PNG**, same coverage as other maps; `distance = near + (gray/65535)*(far-near)`. Smaller covered depth is nearer. Transparent zero values are invalid, not a near plane. Fixed near/far and camera distance are identical for body/equipment and every frame. No luminance-derived pseudo-depth or per-frame rescaling.
134
135**Normal:** view-space skinned geometric/shading normal, RGB8 encodes `n*0.5+0.5`, straight coverage alpha. Camera +Y is up; PNG rows are top-down. Normals are transformed by inverse-transpose skin transforms and the common view rotation, then normalized after resolve. Transparent RGB is zero and must be ignored. Tangent-space source normal textures and full PBR material conversion are not implemented.
136
137**Specular:** RGBA8, linear scalar repeated in RGB, shared coverage alpha; body defaults to zero. Equipment uses its editable strength × shinymap luminance (when present), not a baked highlight. Output token is `specular` (not the old provisional `shiny`). The future consumer chooses the lighting/exponent; this is not physically exact metallic/roughness PBR.
138
139Per pose/view, the actual completed-preview order is:
1401. **Highres:** evaluate skeleton/attachments at exact sample time; offscreen MRT draws unlit albedo, view normal, scalar specular and true depth at exact 8× dimensions. Publish highres albedo immediately, no file saved.
1412. **Detail:** color/alpha/depth/normal neighbor discontinuities, bounded weights. Publish actual edge map immediately, no file saved.
1423. **Target-res / diffuse:** for each 8×8 block compute coverage, choose nearest covered depth and a coherent front-surface cluster (depth tolerance 0.0025 of the common near/far interval), apply alpha × `(1+4*detail)` weights, and resolve albedo in linear light. Color/normal/specular all use that same cluster, coverage comes from all covered samples; this explicit front-feature retention policy is an approximation, not unbiased area filtering. Normalize normal. Write target diffuse, publish NN-upscaled preview.
1434. **Depth:** write target 16-bit gray+alpha, publish its gray8 visualization (the saved depth keeps 16-bit precision).
1445. **Normal:** write/publish target normal.
1456. **Specular:** write/publish target scalar map, including body zero.
146
147Every pass uses one uniform scale, centered letterbox, nearest-neighbor display; never stretch target X/Y independently. The progress bar counts actual completed passes out of `images*6`, and a separate label identifies the last completed image while a later pass works. Each pose saves exactly four target PNGs. **Annuleer render** stops between safe units (or after the in-flight GPU readback), keeps partial outputs and never produces a complete marker. Rim lighting and the 2D lighting/compositor remain deferred; any future rim belongs in the consumer, not in pure diffuse albedo.
148
149Current source scope: the first skinned glTF/GLB mesh (all its triangle primitives), first skin, vertex NORMAL attributes when present, otherwise true flat normals from GPU derivatives of the skinned view-space triangle position (never the live-only fallback vector), base-color factors/textures and COLOR_0; albedo images must be 8-bit and ≤2048×2048. Textures currently use nearest/repeat; sampler overrides/texture transforms, morph targets, tangent normal maps, multi-mesh scenes and arbitrary translucent layer ordering are not claimed. Opaque/alpha-cutout uses one winning surface; this is not order-independent blended transparency. GPU limits also reject non-finite geometry, more than 1M vertices/3M indices per draw, invalid texture payloads and invalid camera matrices. FBX remains inventory-only.
150
151Equipment pass hides the body from **all color, depth, normal and specular writes**, while still evaluating its rig and animating attachments. The body is not an invisible occluder; otherwise equipment behind it would be missing when layers are recomposed. Equipment self-occludes normally. Every layer must use the same projection and absolute depth contract. Hair/alpha cutout versus true blended transparency is an open fidelity decision; initial opaque/cutout support must be stated rather than promising arbitrary multilayer transparency from a single depth value.
152
153## 9. Equipment editor and binary presets
154
155The equipment editor is implemented for **rigid OBJ + MTL + PNG** parts over a skinned glTF/GLB body. Select a preview sex, applicability flags, a stable hierarchical bone path, XYZ translation, XYZ Euler degrees, shinymap and specular strength, and optional provenance/license text. The native picker is behind the `Dialogs` interface and runs on a request thread; polling never waits for a modal picker. Material preview uses MTL diffuse color, optional PNG diffuse texture and alpha-cutout at 0.5. Other embedded source-material maps are preserved as declared dependencies, not advertised as applied shading. Equipment FBX/glTF import, deformation/weight painting and a rendered 2D compositor are not implemented by this feature.
156
157Persisted rotation is **Euler degrees with `Rz * Ry * Rx`**, not the earlier proposed quaternion. Reconstruct from stored values each frame; never accumulate rotations. The exact rigid attachment matrix is `animated_joint_world * attachment_local`. The joint-world matrix already includes source root and non-joint ancestors, so neither body/world transforms nor inverse-bind matrices are applied twice. Tests include translated/rotated parents and a nonidentity inverse bind. Each sex has its own exact rig signature (hierarchical paths disambiguated by node indices plus rest-world matrices), with separate target-bone paths per part. Offsets/rotations are shared across sexes; use separate presets if distinct offsets are needed. An explicit bone selection updates that sex's rig binding; changing the body without rebinding yields a visible diagnostic rather than guessed retargeting. Empty/incomplete presets may be saved for authoring; unresolved attachments do not render on guessed joints.
158
159The equipment configuration draws the body plus all meshes in the selected preset once its attachments are configured through the actual Glow callback, sharing the native 24-bit depth attachment. Clear once for the body, never between equipment meshes; depth test/write makes self/body occlusion independent of submission order. Orbit/zoom and body animations continue to work. A grayscale/luminance PNG shinymap multiplies the scalar specular strength in placement shading; this is not a baked production shiny output map. The real GPU probe tests body/equipment front/back ordering, a sampled red albedo texture, alpha discard, and white-versus-black shinymap highlights. The generated CC0 sword OBJ/MTL/checker/shiny fixture and binary golden file are under `tests/fixtures/equipment/`; the example opens an actual seeded project, not a screenshot mock.
160
161### Implemented `.ppe` container, version 1
162
163All integers below are unsigned little-endian. The container is uncompressed (no Rust memory-layout serialization and no decompression stage):
164
165| Bytes | Meaning |
166|---|---|
167| 0–7 | Exact magic `PPEQ\r\n\x1a\n` |
168| 8–11 | Schema version, exactly 1 |
169| 12–15 | UTF-8 JSON metadata length |
170| 16–19 | IEEE CRC32 of the **entire payload** after byte 19 |
171| 20… | Bounded metadata, then one `u32 length + raw bytes` chunk per asset in declared order |
172
173Metadata stores the validated name, provenance, `[male, female]` applicability flags and optional rig signatures, parts with per-sex bone paths/XYZ transforms/scalar specular/shiny index, normalized meshes (positions/normals/UVs/u32 indices/material diffuse/albedo index), and logical asset names. Asset bytes are omitted from JSON and carried in their binary chunks. Each part explicitly owns the indices of its original OBJ/MTL/declared dependency payloads. Removing a part or clearing/replacing a shiny override compacts unused assets and remaps all retained indices; adding another part cannot collide with an existing logical prefix. The reader rejects unknown fields/versions, truncation, checksum mismatches, trailing bytes, invalid links/numerics/geometry and excessive resources. CRC is corruption detection, **not authentication**.
174
175Limits: 32 MiB per complete container; 4 MiB metadata; 32 attachments; 32 raw assets of at most 8 MiB each; 80,000 total normalized vertices; bounded mesh/index counts; textures at most 2048×2048 and 64 MiB decoded total per preset. The shared CPU texture cache is capped at 128 MiB. Logical asset names are portable relative paths of at most 240 bytes; imports canonicalize dependencies within the chosen model directory and reject traversal/absolute references and symlink escapes. Assets are never extracted to disk on decode. Provenance is at most 4096 UTF-8 bytes; names are unique lowercase ASCII slugs of at most 64 bytes, excluding `body`. Imported duplicate names get a valid bounded suffix. Projects allow at most 64 presets, 64 MiB aggregate encoded preset data, a 4 MiB manifest and 64 KiB legacy JSON records; aggregate checks run before installing imports and before writing saved snapshots, and bounded reads preserve unloaded/error references rather than dropping them.
176
177Project save creates immutable `presets/<name>-<file-crc32>-<byte-length>.ppe` snapshots, then atomically saves the `.ppd` references. Flush a unique sibling temporary file, publish with a no-overwrite hardlink, and flush the directory on Unix. A filesystem without hardlink support fails safely; no unsafe overwrite fallback. Existing identical snapshots are reused, conflicting bytes never overwrite an old snapshot. Failure of the final manifest save leaves old references and bytes intact; unreferenced snapshots remain recoverable. Earlier JSON equipment records remain readable and preserved until replaced with a configured binary preset. New snapshots round-trip without consulting external model/texture files.
178
179## 10. Rendered 2D preview
180
181Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory. Play actual exported frames on a loop with elapsed-time accumulation; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation.
182
183Left/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. Clamp elevation/wrap azimuth with clear coordinate convention. These controls are continuous even though model view selection is discrete.
184
185Depth compositor selects the nearest valid surface per pixel from body and chosen equipment, with stable epsilon/tie-break ordering. Chosen pixel supplies diffuse, normal and shiny from the **same layer**. Blend alpha edges using a documented approximation; single-depth compositing cannot reconstruct arbitrary order-dependent translucent surfaces. A simple ambient + diffuse + specular preview proves maps and lighting; it is not the future game controller. Transform the continuous light into the same view space used by each normal map.
186
187## 11. Development workflow, quality and packaging
188
189Each feature starts on its own `feature/...` branch from current master. Implement its tests and acceptance criteria, run local checks, commit, then `git merge --no-ff` into master and push. No initial-phase MRs. Keep branch/feature and merge SHAs in the coordinator status file, report completion before the next feature. For this production-render task the latest explicit instruction overrides publication: merge/cleanup locally only, **no push, tags, releases or package builds**; the parent handles subsequent publication. Setup/design may use `feature/project-design`; establish a baseline master commit first if required to obtain a genuine two-parent merge. Never fabricate a merge on an unborn branch.
190
191After every merge, `README.md` describes only the current behavior: what the program can do and how to use it. It does not list absent features, a roadmap, or negative claims. A screenshot belongs in the README only when it shows how to use the program. This convention applies to every later feature. Design limits stay in this file.
192
193The 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`.
194
195Live 3D preview, as built: the body screen draws the skinned glTF/GLB 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 equipment (§9); FBX is not drawn. 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.
196
197CI 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.
198
199Tests run on MR events (future-proof despite no current MRs), master pushes AND tags. Quality gates: `cargo fmt --check`, `cargo check --workspace --all-targets --locked`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, `cargo test --workspace --locked`; feature combinations should be covered explicitly when introduced. Pin toolchain/components and package tooling. Core tests require no GPU; separate GPU integration tests may use a supported software adapter, with explicit results rather than silent skip-as-pass.
200
201Tag pipeline graph: quality/tests → four packaging jobs → artifact verification → one release publish. A failure of ANY required artifact prevents release. Build Linux x86_64 AppImage, Debian/Ubuntu .deb, Arch .pkg.tar.zst and Windows x86_64 .exe in a .zip. Verify target architecture and contents; packaging a Linux binary into a Windows archive is not a build. Prefer native Windows runner when available, or verified mingw target/toolchain with dependency support. AppImage and distribution packages need desktop integration/icon/license, runtime library audit and version mapping from tag to package version. Include checksums and build provenance; signing is optional future work, never invent signatures.
202
203Release publication uses documented BlueGit release/artifact API and job-scoped least-privilege credentials; no tokens in source/logs/status. All assets attach only after all outputs pass verification, retries are idempotent and do not delete unrelated releases. Distinguish local packaging smoke tests from a real successful tag/release pipeline. **No tag or release is authorized during setup.** A configured future tag trigger is not permission to create a tag now.
204
205Verified BlueGit behaviour, inspected 6 November 2026, is recorded in section 15. The configured tag pipeline calls that API. With the API as it exists today, publishing the same tag that started the pipeline is rejected. That limit is part of the design record, not a claim that a release was created.
206
207## 11a. Tim’s desktop workflow requirements (6 November 2026)
208
209These requirements are durable even where the implementation is staged. They supersede the earlier generic workspace layout.
210
211- Launch without a project shows a small start window with **Project laden** and **Nieuw project**. Both invoke an OS-native picker: choose an existing `.ppd` file or a new project directory. Cancellation keeps the current project and edits unchanged. A new project prepares the CC0 male/female fixtures in `assets/body/`; existing assets are never silently overwritten.
212- The first project view is **Bodies**, with male/female model slots and native asset-pick buttons. Navigation is **Bodies → body → Equipment → equipment configuration**. A body has the live orbit/zoom mesh preview, animation controls, a **Render** action and an **Equipment** action. Equipment list entries open their configuration; **Nieuw** creates a local definition. Keep body context when going back. Remove the old right-hand folder explorer entirely.
213- Closing the OS window or choosing another/new project with unsaved changes asks **Opslaan / Niet opslaan / Annuleren**. Failed saves do not close or switch. A canceled picker does not discard edits, even after choosing Niet opslaan: the actual replacement only happens after another project is loaded successfully. Asset adoption is copy-on-write so discarding metadata cannot secretly change an existing saved body. Equipment config snapshots remain immutable until the manifest’s atomic save succeeds; unreferenced snapshots are retained for recovery.
214- `.ppd` files support the file manager’s **Open with / Openen met** on every supported packaging platform (Windows, Linux AppImage, Debian and Arch). The process accepts a single project path, including spaces, and directly opens that project. Invalid files give an actionable error without corrupting the current session. Linux uses MIME XML, desktop `MimeType` and `%f`; portable binaries/AppImages provide `--install-desktop`. Windows registers a per-user OpenWithProgids entry and a quoted executable/file command, preserving the user’s existing default application. No administrator privileges are required for the Windows entry.
215- 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.
216- 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.
217
218Current 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. 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 2D relighting/compositor, rim effects, resume and FBX drawing remain unimplemented.
219
220## 12. Incremental feature plan and acceptance gates
221
2220. Foundation/design/CI: group and repo existence checks; private repo; single detailed design.md with verbatim appendix; minimal honest compileable Rust shell; locked dependencies/toolchain; genuine two-parent setup merge; tests and verified CI schema. No claim that rendering/import works. Test packaging scripts locally where possible without publishing.
2231. Project model and validation: .ppd open/save/new, portable paths, schema/version checks, model slots and render settings; atomic persistence/invalid paths tests.
2242. 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.
2253. Desktop 3D viewport and animation sampling: visible real imported model, orbit, clip controls, source-axis normalization; transform tests and manual visual test.
2264. Equipment attachments and binary presets: bone tree, sex choice, numeric offsets/rotations, live placement, embedded multi-mesh assets; round-trip and rig mismatch tests.
2275. 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.
2286. Production image pipeline: detail-aware downsampling, depth/normal resolve, rim accents, shiny conversion, filename/manifest export, progress/cancel/resume; compare actual output and memory budgets.
2297. Rendered 2D compositor preview: real exported frames, depth ordering, looping, sex/equipment/clip selection, discrete view controls and continuous held light controls; deterministic state tests and manual lighting test.
2308. 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.
231
232Every feature is independently reported and merged non-FF only when its acceptance gate passes. If compatibility or rendering remains untested, name that limitation and do not merge a false completion claim.
233
234## 13. Open decisions and risks
235
236- Confirm target resolution/FPS defaults, output color treatment, root-motion policy, front-axis and positive-yaw convention with a labelled fixture before production content.
237- FBX exporter variants/native Assimp packaging and skeletal animation fidelity are high-risk; prototype early rather than after UI completion.
238- Eightfold dimensions impose 64x pixels; support tiling or reject unsupported render sizes with estimates, not GPU crashes.
239- Shared framing/depth/normal contracts are essential; independently normalized equipment maps cannot be depth-composited correctly.
240- Alpha-blended materials cannot be fully represented by one surface depth; document cutout/opaque MVP and approximation.
241- Bone attachment supports rigid equipment, not automatic skinning/retargeting; expose that limit.
242- glTF PBR/FBX legacy materials to a single shinymap is lossy; retain source metadata and editable conversion settings.
243- Package version mapping, Windows runner/toolchain and BlueGit group runner availability must be verified, not assumed.
244- Keep model provenance/licensing with embedded presets; importing assets does not grant redistribution rights.
245
246## 14. Research references and verification limits
247
248- Bevy project and documentation: https://bevy.org/ (preferred-stack proposal; exact dependency versions and API compatibility still require implementation verification).
249- Assimp repository: https://github.com/assimp/assimp (candidate broad-format import dependency, not universal file compatibility guarantee).
250- Rust Assimp binding docs inspected: https://docs.rs/russimp/latest/russimp/ (reported russimp 3.2.1 at inspection; no build/compatibility test performed).
251- glTF specification reference: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html (canonical interchange recommendation; implementation must test supported subsets).
252- BlueGit CI specification: `GET https://git.timfalken.com/api/v1/ci/spec` and `docs/CI.md` in `timfa/bluegit` (file name `blue-ci-cd.yml`, job fields, variables, release API).
253- Working pipelines read in public repositories: `timfa/posprint` `blue-ci-cd.yml` and `scripts/ci/release.sh`; `timfa/minecart-trains` `blue-ci-cd.yml`; `timfa/click-tween` `blue-ci-cd.yml` and `scripts/release.sh`.
254- Release implementation read in `timfa/bluegit` `internal/server/ops_ci.go` (`opPublishRelease`) and `internal/server/release_replace.go` (`checkReplace`).
255
256## 15. Verified BlueGit CI and the release-trigger limit
257
258Inspected sources, all public on 6 November 2026:
259
260| Source | What it shows |
261|---|---|
262| `GET /api/v1/ci/spec` and `timfa/bluegit` `docs/CI.md` | The pipeline file is `blue-ci-cd.yml`. Jobs may be top-level. `rules` / `only` / `needs` / `artifacts.paths` / `artifacts.expire_in` / `extends` / `resource_group` / `workflow` are real fields. `CI_COMMIT_TAG` is empty on a branch and `CI_COMMIT_BRANCH` is empty on a tag. `CI_JOB_TOKEN` exists only inside a job container. Image presets include `bluegit/base` and `bluegit/rust` (Rust 1.81). Any other image name, such as `rust:1.83-bookworm`, is a normal Docker image. |
263| `timfa/posprint` `blue-ci-cd.yml` | Tests on merge requests and on `master`. Package jobs use `needs: [test]`. Artifacts are exact file paths, not globs. The release job runs on `master` and calls `POST $CI_API_URL/repos/$CI_PROJECT_PATH/releases` with `Authorization: Bearer $CI_JOB_TOKEN`. Tag pushes are disabled with `if: $CI_COMMIT_TAG != ""` / `when: never`. The comment in that file says tag pipelines must not rebuild old tags. Windows is cross-compiled with MinGW because that comment states there is no Windows runner. |
264| `timfa/posprint` `scripts/ci/release.sh` | The script says the release API creates the tag itself. It refuses to release when `HEAD` is already tagged. |
265| `timfa/minecart-trains` `blue-ci-cd.yml` | States the GitLab project used to release on a new tag, and that on BlueGit the release API creates the tag, so tag pushes start no pipeline. |
266| `timfa/click-tween` `blue-ci-cd.yml` | A tag pipeline may build and verify. The job that publishes the release is manual on `master` and creates the tag through the API. |
267| `timfa/bluegit` `opPublishRelease` | If the tag already exists and `replace` is not set, the API returns HTTP 409 (`err.tagExists`). `replace: true` is checked by `checkReplace`, which rejects a tag pipeline (`err.replaceRef`, HTTP 403). A tag pipeline therefore cannot attach assets to the tag that started it, and cannot replace that release either. |
268| `timfa/legacy-cycles` | The project page redirects to releases and says only the releases are public. `GET /api/v1/projects/timfa/legacy-cycles/repository/tree` and the CI file returned 404. The public release assets for v0.0.38 are a Debian package, an Arch `.pkg.tar.zst`, a Windows zip, and Linux tar archives. No AppImage is listed. Its pipeline YAML was not available to copy. |
269
270Consequences for `blue-ci-cd.yml` in this repository:
271
2721. Syntax is limited to fields confirmed in the CI spec or in the pipelines above. No GitLab-only keyword is added (`pages`, `spec`, child pipelines, `id_tokens`).
2732. Tests run for merge-request pipelines, for pushes to `master`, for pushes to `main`, for `CI_DEFAULT_BRANCH`, and for tags. This workspace's default branch is `main`. Tim's existing BlueGit projects use `master`. Covering both avoids a silent skip after the project moves.
2743. On a tag push the four package jobs run only after `test`, then `verify:packages` checks magic bytes so a Linux executable is not renamed into the Windows zip. `release` needs `verify:packages`, so any failed package skips publication.
2754. `release` runs `scripts/ci/release.sh` after the four packages and `verify:packages`. That script does not call the create-release route and does not send `replace`. A second inspection of public BlueGit master (below) found no way to attach artifacts to the tag that already started the pipeline. The script exits 3 with `release-blocked: no-attach-endpoint`. That is a refused publish, not a created release. The tag order stays a tag push. No tag is created by this repository.
2765. Package scripts map a tag `vX.Y.Z` or `X.Y.Z` to package version `X.Y.Z`. Other tag spellings fail the package job instead of inventing a version.
2776. The Rust image is `rust:1.96-bookworm`. That tag is the one `timfa/posprint` already runs on BlueGit. `rust-toolchain.toml` pins the same compiler, 1.96.0, which was installed here with rustup. The machine default before that pin was 1.83.0. 1.83.0 cannot resolve the current eframe 0.29.1 graph: several transitive crates require edition 2024, which Cargo 1.83 does not parse. `bluegit/rust` is Rust 1.81 on Alpine and is the wrong image for this X11 shell. `bluegit/base` is used for the verify and release jobs, as in posprint.
2787. posprint states BlueGit does not set `CI_PROJECT_DIR` and that jobs start in the checkout. Scripts use `$PWD`.
279
280The toolchain is Rust 1.96.0, edition 2021. Edition 2021 remains explicit; the crate does not opt into edition 2024. `Cargo.lock` is committed. `cargo fmt`, `cargo check`, `cargo clippy -D warnings` and `cargo test` are the test job. No GPU test is required for the foundation shell.
281
282### Release routes that exist, and the missing attach call
283
284Rechecked against public `timfa/bluegit` master (`internal/server/api_routes.go`, `releaseRequest` in `api.go`, `opPublishRelease` in `ops_ci.go`, `checkReplace` in `release_replace.go`, `gitrepo.Tag` in `internal/gitrepo/repo.go`) and `GET /api/v1/openapi.json`. No PUT or PATCH release route exists. `releaseRequest` has `tag`, `commit`, `name`, `body`, `replace` and `assets`. There is no field that means "use the tag that is already there". `encoding/json` drops unknown fields, so a new client flag would still reach `HasTag` and return 409.
285
286| Method | Path | What it does |
287|---|---|---|
288| GET | `/api/v1/projects/{owner}/{repo}/releases` | List releases. |
289| POST | `/api/v1/projects/{owner}/{repo}/releases` | `createRelease` with a user token. `opPublishRelease` copies job artifacts, then `gitrepo.Tag` runs `git tag -a`. |
290| GET | `/api/v1/projects/{owner}/{repo}/releases/{tag}/assets/{name}` | Download one asset. |
291| DELETE | `/api/v1/projects/{owner}/{repo}/releases/{tag}` | Delete the release. Query `delete_tag=true` also deletes the git tag. |
292| DELETE | `/api/v1/projects/{owner}/{repo}/releases/{tag}/assets/{name}` | Delete one asset. The tag stays. This does not upload. |
293| POST | `/api/v1/repos/{owner}/{repo}/releases` | Same `createRelease`, and the route a job token may call. `replace: true` is allowed only by `checkReplace`: running job of this project, pipeline on an unprotected branch, not `forks/*`, actor maintainer or higher, existing release made by a job on that same branch. A tag pipeline is rejected with HTTP 403. |
294| POST | `/api/v1/projects/{owner}/{repo}/repository/tags` | Create a git tag. No release assets. |
295| GET | `/api/v1/projects/{owner}/{repo}/pipelines/{id}/jobs/{job}/artifacts` and `.../artifacts/{path}` | List or download a job artifact. Not a release. |
296
297Minimal server change, not applied in this repository: add `POST /api/v1/repos/{owner}/{repo}/releases/{tag}/assets`, job token only. Body: `commit` (40 hex), optional `name` and `body`, and `assets` as `{job, path, name}` already used by `opPublishRelease`. Do not call `gitrepo.Tag` and do not accept `replace`. Return 404 when the git tag is absent. Return 409 when the tag points at a commit other than `commit`. Require a running job in this repository whose pipeline ref is `refs/tags/{tag}` and whose `CI_COMMIT_SHA` equals `commit`. Resolve each asset with `Store.ArtifactPath` from that same pipeline and copy it under `releases/{owner}/{repo}/{tag}/{name}`. If no release row exists, insert one for the existing tag. If a release row exists from a job on this same tag, replace only those named asset files after the new copies succeed, and leave the git tag and every other release alone. If the release was made by hand or from another ref, return 409 and change nothing. Reject asset names that contain `/` or `..`. Until that route exists, this project's tag job cannot publish.
298
299## 16. Synthetic test bodies
300
301`tests/fixtures/bodies/test-body-male.glb` and `test-body-female.glb` are generated by `scripts/generate_test_bodies.py`. No external model is downloaded. Both files are self-contained glTF 2.0 binary, CC0-1.0, and labelled in `asset.extras` as test assets with `finalCharacter: false`. They are not final characters.
302
303Each file has one skin, 19 named joints, rigid skin weights, and two clips: `walk` (1 second, arms opposite the legs) and `attack` (0.8 seconds, the right arm swings forward). Bind rotations are identity. The documented axes are +Y up and forward -Z. Male and female differ in shoulder width, hip width and hip height so a later importer can tell them apart.
304
305`tests/test_body_assets.rs` checks that both files exist, parse as GLB, contain that skeleton, contain both clips, and byte-match a fresh run of the generator. That test is a fixture check. It is not the product importer, it does not play the clips, and it does not render. The current editor imports and animates these CC0 fixtures; native GPU and batch probes test actual rendering separately from this asset-structure test.
306
307# Appendix A — Tim's original proposal (verbatim)
308
309The following block preserves the original Dutch text including spelling and filename placeholders. It is not a paraphrase or implementation status.
310
311```text
312Ik heb een idee voor een projectje;
313Een Paperdoll Renderer.
314
315Het idee;
316We importeren twee menselijke 3D-modellen; een man en een vrouw. Die hebben een set animaties, zoals lopen, springen, bukken, aanvallen, blokkeren, geraakt worden, etc. (we lezen gewoon alles uit het model)
317
318Bij renderen hebben we twee modussen: Karaktermodel, en uitrusting (hier later meer over). Als je rendert, dan hebben we 3*12 posities; 3 pitches (-30 graden, 0 graden, +30 graden), en 12 yaw (beginnend met recht naar de camera kijken en dan steeds een stapje ronddraaien tot we alle hoeken hebben). We renderen die naar PNG's met de naam: <geslacht>-<settype>-<animatienaam>-<pitch>-<yaw>-<maptype>-f<framenummer>.png met een vooringestelde (hoge, acht keer de doelresolutie) resolutie en framerate.
319We pakken dan die hoge resolutie, en maken een detail-map; dat is effectief een edge-detection algoritme. We schalen de paperdoll dan omlaag naar de doelresolutie, en we gebruiken de detail-map om pixels extra gewicht te geven om te kiezen welke kleur we een pixel geven.
320Daarna maken we een depth-map op de doelresolutie, en een normal map. (de maptypes in de filename zijn dus: diffuse, depth, normal)
321Als we de normalmap en depthmap hebben, doen we nog een pass over de diffuse om randen een beetje te highlighten. Daarna slaan we de hele set op volgens het afgesproken naamformat (allemaal op de doelresolutie). Dit is is settype=body.
322
323Daarna hebben we ook equipment. Je kiest een geslacht, en je moet per lichaamsdeel (op de skeleton van het karaktermodel) een 3D-model kunnen importeren, en op de bone kunnen positioneren. Als je alle modellen van een stuk equipment op de juiste positie gezet hebt, kun je dat opslaan als een preset (binary file met de modellen en posities op bones, alsmede een flag voor of het male of female is). Je kunt hem ook renderen; dan kies je een naam voor je equipment. Die naam is de settype; allemaal lowercase, geen spaties toegestaan. Dit rendert dan nogmaals alles, net als bij de body, alleen is de body dan doorzichtig, waardoor je alleen de equipment-modellen tekent. Equipment heeft ook een shinymap; dat is hoeveel specular een pixel moet hebben voor later bij de renderer.
324
325De bedoeling is dat we die paperdolls later kunnen gebruiken om een renderer te schrijven. Die gebruikt de diffuse maps en laagt met equipments over elkaar; het gebruikt de depth-map om te bepalen welke pixel van welke map (body of equipment) getekent moet worden (soort z-check), en de normal map om bumpmapping en lighting te tekenen. Dat wordt dan deel van een character-renderer en controller.
326
327Als stap 1 wil ik graag de paperdoll renderer maken. Ik heb nog geen modellen, die moet ik dus later zelf in het programma kunnen selecteren. Voor positioneren op bones zijn XYZ voor relatieve positie en rotatie voldoende voor nu, met dan een live preview waarop je het lichaam ziet, die je kunt roteren (3D model view, niet de gerenderde plaatjes want die zijn er dan nog niet).
328
329Wanneer je sets maakt, is dat in een .ppd (PaPerDoll) project; een map met een .ppd file die verteld welke modellen gebruikt worden, welke binary files van equipment sets er zijn, en waar gerenderde output staat.
330Zo kunnen we ook een preview tab maken; kies een geslacht, kies equipments om te layeren, en kies een animatie. Je ziet dan de animatie op loop spelen (de gerenderde 2D paperdolls), en kunt met pijltjes orbiten (boven, midden, onder, en dan voor links en rechts de 12 rotaties). Ook moet je met knopjes die je ingedrukt kan houden de positie van de lichtbron kunnen roteren. Dat gaat in graden, niet in stappen.
331
332Stappenplan:
333
334- Maak een groep op BlueGit: `Golden Age`
335- Maak de repo, Paperdoll Renderer
336
337- Documenteer alle bovenstaande, incl. letterlijke tekst van mij, zeer gedetailleerd in een enkele design.md
338- Bouw dan alle features een voor een in Rust. Elke feature op een nieuwe branch, en als die feature af is, niet-FF mergen naar master (Geen MR's tijdens eerste ontwikkelfase) en door naar de volgende feature. Elke feature krijgt een unit test die we in de CI-CD activeren op een merge request (ook al gebruiken we die nog niet) en op een master push. Een tag push krijgt, als tests slagen, ook een build; AppImage, Ubuntu Package, Arch Package, Windows Build, welke we dan als een release pushen.
339
340Houd me bij elke feature op de hoogte hoe het gaat.
341```
342
343# Appendix B — Additional import requirement (verbatim)
344
345```text
346Bepaal ook welk 3D-model format goed is; liefst ondersteunen we natuurlijk meerdere. In ieder geval .obj (met mtl en texture) en .fbx, maar hoe meer hoe beter.
347```