Merge verified preview depth/aspect fixes into equipment feature
@@ -21,10 +21,10 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert
21
21
22
22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB-model is in de live preview zichtbaar:
23
23
24
- Sleep om te orbiten, scroll om te zoomen.
24
- Sleep om te orbiten, scroll om te zoomen. Een 24-bits depthbuffer zorgt dat voorste oppervlakken achterliggende delen afdekken.
25
25
- Kies een animatieclip en gebruik **Pauze / Speel**.
26
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** maakt een gekleurd PNG-stilbeeld van de huidige pose en camera in `renders/body-male-still.png` of `body-female-still.png`. De preview toont achtereenvolgens de hoge resolutie en de ingestelde doelresolutie, met nearest-neighbor uitvergroot en een voortgangsbalk. **Live 3D** keert terug naar het model.
27
- **Render** maakt een gekleurd PNG-stilbeeld van de huidige pose en camera in `renders/body-male-still.png` of `body-female-still.png`. De preview toont achtereenvolgens de hoge resolutie en de ingestelde doelresolutie, met nearest-neighbor uitvergroot en een voortgangsbalk. Iedere pass behoudt zijn beeldverhouding en staat gecentreerd met zwarte balken waar nodig. **Live 3D** keert terug naar het model.
28
28
- Stel breedte, hoogte en fps in de Bodies-weergave in.
29
29
30
30

@@ -50,6 +50,8 @@ cargo fmt --check
50
50
cargo check --workspace --all-targets --locked
51
51
cargo clippy --workspace --all-targets --locked -- -D warnings
52
52
cargo test --workspace --locked
53
# Linux GPU-regressie (Xvfb + Mesa): echte overlappende geometrie en pixelreadback
54
xvfb-run -a cargo run --example preview_depth_probe --locked
53
55
```
54
56
55
57
De testlichamen komen uit `scripts/generate_test_bodies.py`. Het FBX-testbestand wordt met Blender via `scripts/convert_test_body_to_fbx.py` gemaakt.
@@ -21,10 +21,10 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert21
21
22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB-model is in de live preview zichtbaar:22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB-model is in de live preview zichtbaar:23
23
24
- Sleep om te orbiten, scroll om te zoomen.24
- Sleep om te orbiten, scroll om te zoomen. Een 24-bits depthbuffer zorgt dat voorste oppervlakken achterliggende delen afdekken.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 -30 / 0 / 30** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z).27
- **Render** maakt een gekleurd PNG-stilbeeld van de huidige pose en camera in `renders/body-male-still.png` of `body-female-still.png`. De preview toont achtereenvolgens de hoge resolutie en de ingestelde doelresolutie, met nearest-neighbor uitvergroot en een voortgangsbalk. **Live 3D** keert terug naar het model.27
- **Render** maakt een gekleurd PNG-stilbeeld van de huidige pose en camera in `renders/body-male-still.png` of `body-female-still.png`. De preview toont achtereenvolgens de hoge resolutie en de ingestelde doelresolutie, met nearest-neighbor uitvergroot en een voortgangsbalk. Iedere pass behoudt zijn beeldverhouding en staat gecentreerd met zwarte balken waar nodig. **Live 3D** keert terug naar het model.28
- Stel breedte, hoogte en fps in de Bodies-weergave in.28
- Stel breedte, hoogte en fps in de Bodies-weergave in.29
29
30
30
@@ -50,6 +50,8 @@ cargo fmt --check50
cargo check --workspace --all-targets --locked50
cargo check --workspace --all-targets --locked51
cargo clippy --workspace --all-targets --locked -- -D warnings51
cargo clippy --workspace --all-targets --locked -- -D warnings52
cargo test --workspace --locked52
cargo test --workspace --locked53
# Linux GPU-regressie (Xvfb + Mesa): echte overlappende geometrie en pixelreadback54
xvfb-run -a cargo run --example preview_depth_probe --locked53
```55
```54
56
55
De testlichamen komen uit `scripts/generate_test_bodies.py`. Het FBX-testbestand wordt met Blender via `scripts/convert_test_body_to_fbx.py` gemaakt.57
De testlichamen komen uit `scripts/generate_test_bodies.py`. Het FBX-testbestand wordt met Blender via `scripts/convert_test_body_to_fbx.py` gemaakt.@@ -161,7 +161,7 @@ After every merge, `README.md` describes only the current behavior: what the pro
161
161
162
162
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`.
163
163
164
Live 3D preview, as built: the body screen of the eframe window draws the skinned glTF/GLB mesh with a perspective orbit camera. 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. It is not the orthographic paperdoll export, and it does not draw OBJ or FBX.
164
Live 3D preview, as built: the body screen of the eframe window draws the skinned glTF/GLB mesh with a perspective orbit camera. 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. It is not the orthographic paperdoll export, and it does not draw OBJ or FBX. 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.
165
165
166
166
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.
167
167
@@ -182,7 +182,7 @@ These requirements are durable even where the implementation is staged. They sup
182
182
- 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.
183
183
- `.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.
184
184
- 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.
185
- 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** to fill the preview box, 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.
185
- 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.
186
186
187
187
Current implementation checkpoint: startup/native pickers, seeded projects, body-centric navigation, equipment name/model CRUD, unsaved-change gate, CLI project opening, platform integration assets, icon and live animated glTF preview are implemented. Equipment records currently store name and model in immutable project-side JSON snapshots; bone attachment controls and the binary embedded preset format remain stage 4. The current **Render** action exports a shaded color still of the selected glTF pose/camera through high-resolution rasterization and target-resolution nearest sampling, with two real pass previews and progress. It does not claim production sprite exports: full 36-view/animation batching, orthographic framing, detail-aware reconstruction and diffuse/depth/normal/shiny/rim maps remain stages 5–6 above. The complete requested preview order is preserved here and in `PASS_ORDER` for that work; unimplemented passes are excluded from the running job’s progress. High-resolution stills use up to 8× dimensions within a 1024-pixel high-resolution staging limit for small targets; larger valid targets use scale 1. This is a preview/still renderer, not the production contract in §8.
188
188
@@ -161,7 +161,7 @@ After every merge, `README.md` describes only the current behavior: what the pro161
161
162
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`.162
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`.163
163
164
Live 3D preview, as built: the body screen of the eframe window draws the skinned glTF/GLB mesh with a perspective orbit camera. 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. It is not the orthographic paperdoll export, and it does not draw OBJ or FBX.164
Live 3D preview, as built: the body screen of the eframe window draws the skinned glTF/GLB mesh with a perspective orbit camera. 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. It is not the orthographic paperdoll export, and it does not draw OBJ or FBX. 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.165
165
166
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.166
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.167
167
@@ -182,7 +182,7 @@ These requirements are durable even where the implementation is staged. They sup182
- 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.182
- 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.183
- `.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.183
- `.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.184
- 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.184
- 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.185
- 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** to fill the preview box, 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.185
- 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.186
186
187
Current implementation checkpoint: startup/native pickers, seeded projects, body-centric navigation, equipment name/model CRUD, unsaved-change gate, CLI project opening, platform integration assets, icon and live animated glTF preview are implemented. Equipment records currently store name and model in immutable project-side JSON snapshots; bone attachment controls and the binary embedded preset format remain stage 4. The current **Render** action exports a shaded color still of the selected glTF pose/camera through high-resolution rasterization and target-resolution nearest sampling, with two real pass previews and progress. It does not claim production sprite exports: full 36-view/animation batching, orthographic framing, detail-aware reconstruction and diffuse/depth/normal/shiny/rim maps remain stages 5–6 above. The complete requested preview order is preserved here and in `PASS_ORDER` for that work; unimplemented passes are excluded from the running job’s progress. High-resolution stills use up to 8× dimensions within a 1024-pixel high-resolution staging limit for small targets; larger valid targets use scale 1. This is a preview/still renderer, not the production contract in §8.187
Current implementation checkpoint: startup/native pickers, seeded projects, body-centric navigation, equipment name/model CRUD, unsaved-change gate, CLI project opening, platform integration assets, icon and live animated glTF preview are implemented. Equipment records currently store name and model in immutable project-side JSON snapshots; bone attachment controls and the binary embedded preset format remain stage 4. The current **Render** action exports a shaded color still of the selected glTF pose/camera through high-resolution rasterization and target-resolution nearest sampling, with two real pass previews and progress. It does not claim production sprite exports: full 36-view/animation batching, orthographic framing, detail-aware reconstruction and diffuse/depth/normal/shiny/rim maps remain stages 5–6 above. The complete requested preview order is preserved here and in `PASS_ORDER` for that work; unimplemented passes are excluded from the running job’s progress. High-resolution stills use up to 8× dimensions within a 1024-pixel high-resolution staging limit for small targets; larger valid targets use scale 1. This is a preview/still renderer, not the production contract in §8.188
188
The contents of this binary file are not shown.
The contents of this binary file are not shown.
@@ -0,0 +1,37 @@
1
//! Run with a real GL context: xvfb-run -a cargo run --example preview_depth_probe.
2
use std::sync::{Arc, Mutex};
3
struct Quit;
4
impl eframe::App for Quit {
5
fn update(&mut self, ctx: &eframe::egui::Context, _: &mut eframe::Frame) {
6
ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
7
}
8
}
9
fn main() -> Result<(), Box<dyn std::error::Error>> {
10
let outcome = Arc::new(Mutex::new(None));
11
let result = outcome.clone();
12
let options = eframe::NativeOptions {
13
viewport: eframe::egui::ViewportBuilder::default().with_inner_size([64., 64.]),
14
..paperdoll_renderer::preview_gl::native_options()
15
};
16
eframe::run_native(
17
"Paperdoll depth regression",
18
options,
19
Box::new(move |creation| {
20
let test = creation
21
.gl
22
.as_ref()
23
.ok_or_else(|| "geen Glow-context".to_string())
24
.and_then(|gl| unsafe {
25
paperdoll_renderer::preview_gl::depth_regression_probe(gl)
26
});
27
*result.lock().unwrap() = Some(test);
28
Ok(Box::new(Quit))
29
}),
30
)?;
31
outcome
32
.lock()
33
.unwrap()
34
.take()
35
.ok_or("GPU-test is niet uitgevoerd")??;
36
Ok(())
37
}
@@ -0,0 +1,37 @@1
//! Run with a real GL context: xvfb-run -a cargo run --example preview_depth_probe.2
use std::sync::{Arc, Mutex};3
struct Quit;4
impl eframe::App for Quit {5
fn update(&mut self, ctx: &eframe::egui::Context, _: &mut eframe::Frame) {6
ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);7
}8
}9
fn main() -> Result<(), Box<dyn std::error::Error>> {10
let outcome = Arc::new(Mutex::new(None));11
let result = outcome.clone();12
let options = eframe::NativeOptions {13
viewport: eframe::egui::ViewportBuilder::default().with_inner_size([64., 64.]),14
..paperdoll_renderer::preview_gl::native_options()15
};16
eframe::run_native(17
"Paperdoll depth regression",18
options,19
Box::new(move |creation| {20
let test = creation21
.gl22
.as_ref()23
.ok_or_else(|| "geen Glow-context".to_string())24
.and_then(|gl| unsafe {25
paperdoll_renderer::preview_gl::depth_regression_probe(gl)26
});27
*result.lock().unwrap() = Some(test);28
Ok(Box::new(Quit))29
}),30
)?;31
outcome32
.lock()33
.unwrap()34
.take()35
.ok_or("GPU-test is niet uitgevoerd")??;36
Ok(())37
}@@ -0,0 +1,17 @@
1
//! Start an actual still-render job and show its letterboxed result.
2
fn main() -> eframe::Result<()> {
3
let options = eframe::NativeOptions {
4
viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1100., 720.]),
5
..paperdoll_renderer::preview_gl::native_options()
6
};
7
eframe::run_native(
8
"Paperdoll render preview",
9
options,
10
Box::new(|_| {
11
let mut app = paperdoll_renderer::editor::EditorApp::default();
12
app.open_fixture_project();
13
app.render_current_pose();
14
Ok(Box::new(app))
15
}),
16
)
17
}
@@ -0,0 +1,17 @@1
//! Start an actual still-render job and show its letterboxed result.2
fn main() -> eframe::Result<()> {3
let options = eframe::NativeOptions {4
viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1100., 720.]),5
..paperdoll_renderer::preview_gl::native_options()6
};7
eframe::run_native(8
"Paperdoll render preview",9
options,10
Box::new(|_| {11
let mut app = paperdoll_renderer::editor::EditorApp::default();12
app.open_fixture_project();13
app.render_current_pose();14
Ok(Box::new(app))15
}),16
)17
}@@ -332,6 +332,11 @@ impl EditorApp {
332
332
self.session.set_render(width, height, fps);
333
333
}
334
334
335
/// Start the same worker used by the Render button (also used by the GUI smoke fixture).
336
pub fn render_current_pose(&mut self) {
337
self.start_render([480, 640]);
338
}
339
335
340
fn start_render(&mut self, preview: [u32; 2]) {
336
341
if let Some(opened) = &self.session.opened {
337
342
if let Err(error) = opened.document.validate_shape() {
@@ -677,8 +682,22 @@ impl EditorApp {
677
682
..Default::default()
678
683
},
679
684
);
680
let available = ui.available_size();
681
ui.image((texture.id(), available));
685
let (bounds, _) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());
686
let fit = crate::render_job::fit_preview(
687
[image.width, image.height],
688
[bounds.width(), bounds.height()],
689
);
690
let rect = egui::Rect::from_min_size(
691
bounds.min + egui::vec2(fit.offset[0], fit.offset[1]),
692
egui::vec2(fit.size[0], fit.size[1]),
693
);
694
ui.painter().rect_filled(bounds, 0.0, egui::Color32::BLACK);
695
ui.painter().image(
696
texture.id(),
697
rect,
698
egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),
699
egui::Color32::WHITE,
700
);
682
701
}
683
702
684
703
fn show_equipment_list(&mut self, ctx: &egui::Context) {
@@ -332,6 +332,11 @@ impl EditorApp {332
self.session.set_render(width, height, fps);332
self.session.set_render(width, height, fps);333
}333
}334
334
335
/// Start the same worker used by the Render button (also used by the GUI smoke fixture).336
pub fn render_current_pose(&mut self) {337
self.start_render([480, 640]);338
}339
335
fn start_render(&mut self, preview: [u32; 2]) {340
fn start_render(&mut self, preview: [u32; 2]) {336
if let Some(opened) = &self.session.opened {341
if let Some(opened) = &self.session.opened {337
if let Err(error) = opened.document.validate_shape() {342
if let Err(error) = opened.document.validate_shape() {@@ -677,8 +682,22 @@ impl EditorApp {677
..Default::default()682
..Default::default()678
},683
},679
);684
);680
let available = ui.available_size();685
let (bounds, _) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());681
ui.image((texture.id(), available));686
let fit = crate::render_job::fit_preview(687
[image.width, image.height],688
[bounds.width(), bounds.height()],689
);690
let rect = egui::Rect::from_min_size(691
bounds.min + egui::vec2(fit.offset[0], fit.offset[1]),692
egui::vec2(fit.size[0], fit.size[1]),693
);694
ui.painter().rect_filled(bounds, 0.0, egui::Color32::BLACK);695
ui.painter().image(696
texture.id(),697
rect,698
egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),699
egui::Color32::WHITE,700
);682
}701
}683
702
684
fn show_equipment_list(&mut self, ctx: &egui::Context) {703
fn show_equipment_list(&mut self, ctx: &egui::Context) {@@ -112,7 +112,7 @@ fn run_window(fixtures: bool, path: Option<std::path::PathBuf>) -> eframe::Resul
112
112
width,
113
113
height,
114
114
}),
115
..Default::default()
115
..paperdoll_renderer::preview_gl::native_options()
116
116
};
117
117
eframe::run_native(
118
118
"Paperdoll Renderer",
@@ -112,7 +112,7 @@ fn run_window(fixtures: bool, path: Option<std::path::PathBuf>) -> eframe::Resul112
width,112
width,113
height,113
height,114
}),114
}),115
..Default::default()115
..paperdoll_renderer::preview_gl::native_options()116
};116
};117
eframe::run_native(117
eframe::run_native(118
"Paperdoll Renderer",118
"Paperdoll Renderer",@@ -14,6 +14,16 @@ use crate::preview::{
14
14
PreviewCharacter, PreviewControls, PAPERDOLL_PITCHES_DEG, PAPERDOLL_YAW_COUNT,
15
15
};
16
16
17
/// egui itself requests no depth buffer. The custom 3D callback needs a real
18
/// 24-bit attachment in the native Glow render target.
19
pub fn native_options() -> eframe::NativeOptions {
20
eframe::NativeOptions {
21
depth_buffer: 24,
22
renderer: eframe::Renderer::Glow,
23
..Default::default()
24
}
25
}
26
17
27
const MAX_BONES: usize = crate::preview::MAX_JOINTS;
18
28
19
29
pub struct Viewport {
@@ -352,6 +362,8 @@ impl GpuCache {
352
362
viewport.height_px,
353
363
);
354
364
gl.enable(glow::DEPTH_TEST);
365
gl.depth_mask(true);
366
gl.clear_depth_f32(1.0);
355
367
gl.depth_func(glow::LESS);
356
368
gl.disable(glow::BLEND);
357
369
gl.clear_color(0.08, 0.09, 0.12, 1.0);
@@ -634,3 +646,158 @@ void main() {
634
646
frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);
635
647
}
636
648
"#;
649
650
/// Real GPU regression using the same cache/shader/paint path as the callback.
651
/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.
652
///
653
/// # Safety
654
/// `gl` must be a current native OpenGL context. Run in a disposable test window;
655
/// this probe changes GL state and restores the default framebuffer afterwards.
656
pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
657
let native_bits = gl.get_framebuffer_attachment_parameter_i32(
658
glow::FRAMEBUFFER,
659
glow::DEPTH,
660
glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,
661
);
662
if native_bits < 24 {
663
return Err(format!(
664
"native depth attachment ontbreekt: {native_bits} bits"
665
));
666
}
667
let framebuffer = gl.create_framebuffer()?;
668
let texture = gl.create_texture()?;
669
let depth = gl.create_renderbuffer()?;
670
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));
671
gl.bind_texture(glow::TEXTURE_2D, Some(texture));
672
gl.tex_parameter_i32(
673
glow::TEXTURE_2D,
674
glow::TEXTURE_MIN_FILTER,
675
glow::NEAREST as i32,
676
);
677
gl.tex_parameter_i32(
678
glow::TEXTURE_2D,
679
glow::TEXTURE_MAG_FILTER,
680
glow::NEAREST as i32,
681
);
682
gl.tex_image_2d(
683
glow::TEXTURE_2D,
684
0,
685
glow::RGBA8 as i32,
686
32,
687
32,
688
0,
689
glow::RGBA,
690
glow::UNSIGNED_BYTE,
691
None,
692
);
693
gl.framebuffer_texture_2d(
694
glow::FRAMEBUFFER,
695
glow::COLOR_ATTACHMENT0,
696
glow::TEXTURE_2D,
697
Some(texture),
698
0,
699
);
700
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));
701
gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);
702
gl.framebuffer_renderbuffer(
703
glow::FRAMEBUFFER,
704
glow::DEPTH_ATTACHMENT,
705
glow::RENDERBUFFER,
706
Some(depth),
707
);
708
gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);
709
gl.read_buffer(glow::COLOR_ATTACHMENT0);
710
if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
711
return Err("onvolledig offscreen depth-target".into());
712
}
713
let info = egui::PaintCallbackInfo {
714
viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),
715
clip_rect: egui::Rect::EVERYTHING,
716
pixels_per_point: 1.,
717
screen_size_px: [32, 32],
718
};
719
let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {
720
generation,
721
positions: vec![
722
[-0.9, -0.9, -0.5],
723
[0.9, -0.9, -0.5],
724
[0., 0.9, -0.5],
725
[-0.9, -0.9, 0.5],
726
[0.9, -0.9, 0.5],
727
[0., 0.9, 0.5],
728
],
729
normals: vec![
730
[0.35, 0.85, 0.4],
731
[0.35, 0.85, 0.4],
732
[0.35, 0.85, 0.4],
733
[-0.35, -0.85, -0.4],
734
[-0.35, -0.85, -0.4],
735
[-0.35, -0.85, -0.4],
736
],
737
joints: vec![[0; 4]; 6],
738
weights: vec![[1., 0., 0., 0.]; 6],
739
indices,
740
bones: vec![crate::preview::identity()],
741
view_proj: crate::preview::identity(),
742
color: [1.; 4],
743
};
744
let mut cache = GpuCache::default();
745
let read = || {
746
let mut rgba = [0u8; 4];
747
gl.read_pixels(
748
16,
749
16,
750
1,
751
1,
752
glow::RGBA,
753
glow::UNSIGNED_BYTE,
754
glow::PixelPackData::Slice(&mut rgba),
755
);
756
rgba
757
};
758
let result = (|| {
759
// Deliberately poison the incoming state: the callback owns its depth setup.
760
gl.depth_mask(false);
761
gl.clear_depth_f32(0.0);
762
gl.depth_func(glow::GREATER);
763
cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));
764
cache.paint(gl, &info);
765
let near = read();
766
if near[..3].iter().any(|c| *c < 250) {
767
return Err(format!("achterste vlak overschrijft voorste: {near:?}"));
768
}
769
cache.pending = Some(make_frame(vec![3, 4, 5], 2));
770
cache.paint(gl, &info);
771
let next = read();
772
if next[..3].iter().any(|c| !(69..=73).contains(c)) {
773
return Err(format!("depth niet gewist voor volgende frame: {next:?}"));
774
}
775
cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));
776
cache.paint(gl, &info);
777
let reverse = read();
778
if near != reverse {
779
return Err(format!(
780
"draw-order afhankelijke occlusie: {near:?} / {reverse:?}"
781
));
782
}
783
println!("GPU depth PASS: native {native_bits}-bit, front={near:?}, next-frame={next:?}, reversed={reverse:?}");
784
Ok(())
785
})();
786
if let Some(p) = cache.program {
787
gl.delete_program(p)
788
}
789
if let Some(v) = cache.vao {
790
gl.delete_vertex_array(v)
791
}
792
if let Some(v) = cache.vbo {
793
gl.delete_buffer(v)
794
}
795
if let Some(v) = cache.ebo {
796
gl.delete_buffer(v)
797
}
798
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
799
gl.delete_framebuffer(framebuffer);
800
gl.delete_texture(texture);
801
gl.delete_renderbuffer(depth);
802
result
803
}
@@ -14,6 +14,16 @@ use crate::preview::{14
PreviewCharacter, PreviewControls, PAPERDOLL_PITCHES_DEG, PAPERDOLL_YAW_COUNT,14
PreviewCharacter, PreviewControls, PAPERDOLL_PITCHES_DEG, PAPERDOLL_YAW_COUNT,15
};15
};16
16
17
/// egui itself requests no depth buffer. The custom 3D callback needs a real18
/// 24-bit attachment in the native Glow render target.19
pub fn native_options() -> eframe::NativeOptions {20
eframe::NativeOptions {21
depth_buffer: 24,22
renderer: eframe::Renderer::Glow,23
..Default::default()24
}25
}26
17
const MAX_BONES: usize = crate::preview::MAX_JOINTS;27
const MAX_BONES: usize = crate::preview::MAX_JOINTS;18
28
19
pub struct Viewport {29
pub struct Viewport {@@ -352,6 +362,8 @@ impl GpuCache {352
viewport.height_px,362
viewport.height_px,353
);363
);354
gl.enable(glow::DEPTH_TEST);364
gl.enable(glow::DEPTH_TEST);365
gl.depth_mask(true);366
gl.clear_depth_f32(1.0);355
gl.depth_func(glow::LESS);367
gl.depth_func(glow::LESS);356
gl.disable(glow::BLEND);368
gl.disable(glow::BLEND);357
gl.clear_color(0.08, 0.09, 0.12, 1.0);369
gl.clear_color(0.08, 0.09, 0.12, 1.0);@@ -634,3 +646,158 @@ void main() {634
frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);646
frag = vec4(clamp(color.rgb * shade + highlight,0.0,1.0), 1.0);635
}647
}636
"#;648
"#;649
650
/// Real GPU regression using the same cache/shader/paint path as the callback.651
/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.652
///653
/// # Safety654
/// `gl` must be a current native OpenGL context. Run in a disposable test window;655
/// this probe changes GL state and restores the default framebuffer afterwards.656
pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {657
let native_bits = gl.get_framebuffer_attachment_parameter_i32(658
glow::FRAMEBUFFER,659
glow::DEPTH,660
glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,661
);662
if native_bits < 24 {663
return Err(format!(664
"native depth attachment ontbreekt: {native_bits} bits"665
));666
}667
let framebuffer = gl.create_framebuffer()?;668
let texture = gl.create_texture()?;669
let depth = gl.create_renderbuffer()?;670
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));671
gl.bind_texture(glow::TEXTURE_2D, Some(texture));672
gl.tex_parameter_i32(673
glow::TEXTURE_2D,674
glow::TEXTURE_MIN_FILTER,675
glow::NEAREST as i32,676
);677
gl.tex_parameter_i32(678
glow::TEXTURE_2D,679
glow::TEXTURE_MAG_FILTER,680
glow::NEAREST as i32,681
);682
gl.tex_image_2d(683
glow::TEXTURE_2D,684
0,685
glow::RGBA8 as i32,686
32,687
32,688
0,689
glow::RGBA,690
glow::UNSIGNED_BYTE,691
None,692
);693
gl.framebuffer_texture_2d(694
glow::FRAMEBUFFER,695
glow::COLOR_ATTACHMENT0,696
glow::TEXTURE_2D,697
Some(texture),698
0,699
);700
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));701
gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);702
gl.framebuffer_renderbuffer(703
glow::FRAMEBUFFER,704
glow::DEPTH_ATTACHMENT,705
glow::RENDERBUFFER,706
Some(depth),707
);708
gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);709
gl.read_buffer(glow::COLOR_ATTACHMENT0);710
if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {711
return Err("onvolledig offscreen depth-target".into());712
}713
let info = egui::PaintCallbackInfo {714
viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),715
clip_rect: egui::Rect::EVERYTHING,716
pixels_per_point: 1.,717
screen_size_px: [32, 32],718
};719
let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {720
generation,721
positions: vec![722
[-0.9, -0.9, -0.5],723
[0.9, -0.9, -0.5],724
[0., 0.9, -0.5],725
[-0.9, -0.9, 0.5],726
[0.9, -0.9, 0.5],727
[0., 0.9, 0.5],728
],729
normals: vec![730
[0.35, 0.85, 0.4],731
[0.35, 0.85, 0.4],732
[0.35, 0.85, 0.4],733
[-0.35, -0.85, -0.4],734
[-0.35, -0.85, -0.4],735
[-0.35, -0.85, -0.4],736
],737
joints: vec![[0; 4]; 6],738
weights: vec![[1., 0., 0., 0.]; 6],739
indices,740
bones: vec![crate::preview::identity()],741
view_proj: crate::preview::identity(),742
color: [1.; 4],743
};744
let mut cache = GpuCache::default();745
let read = || {746
let mut rgba = [0u8; 4];747
gl.read_pixels(748
16,749
16,750
1,751
1,752
glow::RGBA,753
glow::UNSIGNED_BYTE,754
glow::PixelPackData::Slice(&mut rgba),755
);756
rgba757
};758
let result = (|| {759
// Deliberately poison the incoming state: the callback owns its depth setup.760
gl.depth_mask(false);761
gl.clear_depth_f32(0.0);762
gl.depth_func(glow::GREATER);763
cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));764
cache.paint(gl, &info);765
let near = read();766
if near[..3].iter().any(|c| *c < 250) {767
return Err(format!("achterste vlak overschrijft voorste: {near:?}"));768
}769
cache.pending = Some(make_frame(vec![3, 4, 5], 2));770
cache.paint(gl, &info);771
let next = read();772
if next[..3].iter().any(|c| !(69..=73).contains(c)) {773
return Err(format!("depth niet gewist voor volgende frame: {next:?}"));774
}775
cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));776
cache.paint(gl, &info);777
let reverse = read();778
if near != reverse {779
return Err(format!(780
"draw-order afhankelijke occlusie: {near:?} / {reverse:?}"781
));782
}783
println!("GPU depth PASS: native {native_bits}-bit, front={near:?}, next-frame={next:?}, reversed={reverse:?}");784
Ok(())785
})();786
if let Some(p) = cache.program {787
gl.delete_program(p)788
}789
if let Some(v) = cache.vao {790
gl.delete_vertex_array(v)791
}792
if let Some(v) = cache.vbo {793
gl.delete_buffer(v)794
}795
if let Some(v) = cache.ebo {796
gl.delete_buffer(v)797
}798
gl.bind_framebuffer(glow::FRAMEBUFFER, None);799
gl.delete_framebuffer(framebuffer);800
gl.delete_texture(texture);801
gl.delete_renderbuffer(depth);802
result803
}@@ -228,6 +228,27 @@ pub fn nearest_stretch(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8
228
228
out
229
229
}
230
230
231
#[derive(Debug, Clone, Copy, PartialEq)]
232
pub struct PreviewFit {
233
pub size: [f32; 2],
234
pub offset: [f32; 2],
235
}
236
237
pub fn fit_preview(image: [u32; 2], bounds: [f32; 2]) -> PreviewFit {
238
if image.contains(&0) || bounds.iter().any(|v| !v.is_finite() || *v <= 0.0) {
239
return PreviewFit {
240
size: [0.; 2],
241
offset: [0.; 2],
242
};
243
}
244
let scale = (bounds[0] / image[0] as f32).min(bounds[1] / image[1] as f32);
245
let size = [image[0] as f32 * scale, image[1] as f32 * scale];
246
PreviewFit {
247
size,
248
offset: [(bounds[0] - size[0]) * 0.5, (bounds[1] - size[1]) * 0.5],
249
}
250
}
251
231
252
pub struct RenderJob {
232
253
pub character: PreviewCharacter,
233
254
pub controls: PreviewControls,
@@ -337,12 +358,8 @@ pub fn execute(job: &RenderJob, emit: &mut dyn FnMut(RenderEvent)) {
337
358
return;
338
359
}
339
360
}
340
let (box_w, box_h) = (job.preview_width.max(1), job.preview_height.max(1));
341
Some(ImageBuf {
342
width: box_w,
343
height: box_h,
344
rgba: nearest_stretch(&target.rgba, target.width, target.height, box_w, box_h),
345
})
361
// Keep native dimensions; only the GUI scales for presentation.
362
Some(target)
346
363
}
347
364
_ => None,
348
365
};
@@ -419,6 +436,21 @@ mod tests {
419
436
assert_eq!(steps[8].kind, PassKind::HighresImage);
420
437
}
421
438
439
#[test]
440
fn preview_uniform_scale_centers_and_letterboxes_all_aspects() {
441
let square = fit_preview([128, 128], [480., 640.]);
442
assert_eq!(square.size, [480., 480.]);
443
assert_eq!(square.offset, [0., 80.]);
444
let landscape = fit_preview([640, 320], [300., 300.]);
445
assert_eq!(landscape.size, [300., 150.]);
446
assert_eq!(landscape.offset, [0., 75.]);
447
let portrait = fit_preview([64, 128], [400., 200.]);
448
assert_eq!(portrait.size, [100., 200.]);
449
assert_eq!(portrait.offset, [150., 0.]);
450
assert_eq!(fit_preview([0, 128], [400., 200.]).size, [0., 0.]);
451
assert_eq!(fit_preview([128, 128], [f32::NAN, 200.]).size, [0., 0.]);
452
}
453
422
454
#[test]
423
455
fn nearest_non_square_uses_source_height_for_rows() {
424
456
let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
@@ -476,9 +508,9 @@ mod tests {
476
508
assert_eq!(ui.progress.total, 2);
477
509
assert!(ui.error.is_none());
478
510
let preview = ui.preview.expect("a real pass image");
479
assert_eq!((preview.width, preview.height), (64, 80));
511
assert_eq!((preview.width, preview.height), (32, 40));
480
512
let filled = preview.rgba.chunks_exact(4).filter(|p| p[3] == 255).count();
481
assert_eq!(filled, 64 * 80);
513
assert_eq!(filled, 32 * 40);
482
514
assert!(output.is_file());
483
515
let _ = std::fs::remove_file(&output);
484
516
}
@@ -228,6 +228,27 @@ pub fn nearest_stretch(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8228
out228
out229
}229
}230
230
231
#[derive(Debug, Clone, Copy, PartialEq)]232
pub struct PreviewFit {233
pub size: [f32; 2],234
pub offset: [f32; 2],235
}236
237
pub fn fit_preview(image: [u32; 2], bounds: [f32; 2]) -> PreviewFit {238
if image.contains(&0) || bounds.iter().any(|v| !v.is_finite() || *v <= 0.0) {239
return PreviewFit {240
size: [0.; 2],241
offset: [0.; 2],242
};243
}244
let scale = (bounds[0] / image[0] as f32).min(bounds[1] / image[1] as f32);245
let size = [image[0] as f32 * scale, image[1] as f32 * scale];246
PreviewFit {247
size,248
offset: [(bounds[0] - size[0]) * 0.5, (bounds[1] - size[1]) * 0.5],249
}250
}251
231
pub struct RenderJob {252
pub struct RenderJob {232
pub character: PreviewCharacter,253
pub character: PreviewCharacter,233
pub controls: PreviewControls,254
pub controls: PreviewControls,@@ -337,12 +358,8 @@ pub fn execute(job: &RenderJob, emit: &mut dyn FnMut(RenderEvent)) {337
return;358
return;338
}359
}339
}360
}340
let (box_w, box_h) = (job.preview_width.max(1), job.preview_height.max(1));361
// Keep native dimensions; only the GUI scales for presentation.341
Some(ImageBuf {362
Some(target)342
width: box_w,343
height: box_h,344
rgba: nearest_stretch(&target.rgba, target.width, target.height, box_w, box_h),345
})346
}363
}347
_ => None,364
_ => None,348
};365
};@@ -419,6 +436,21 @@ mod tests {419
assert_eq!(steps[8].kind, PassKind::HighresImage);436
assert_eq!(steps[8].kind, PassKind::HighresImage);420
}437
}421
438
439
#[test]440
fn preview_uniform_scale_centers_and_letterboxes_all_aspects() {441
let square = fit_preview([128, 128], [480., 640.]);442
assert_eq!(square.size, [480., 480.]);443
assert_eq!(square.offset, [0., 80.]);444
let landscape = fit_preview([640, 320], [300., 300.]);445
assert_eq!(landscape.size, [300., 150.]);446
assert_eq!(landscape.offset, [0., 75.]);447
let portrait = fit_preview([64, 128], [400., 200.]);448
assert_eq!(portrait.size, [100., 200.]);449
assert_eq!(portrait.offset, [150., 0.]);450
assert_eq!(fit_preview([0, 128], [400., 200.]).size, [0., 0.]);451
assert_eq!(fit_preview([128, 128], [f32::NAN, 200.]).size, [0., 0.]);452
}453
422
#[test]454
#[test]423
fn nearest_non_square_uses_source_height_for_rows() {455
fn nearest_non_square_uses_source_height_for_rows() {424
let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];456
let src = [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];@@ -476,9 +508,9 @@ mod tests {476
assert_eq!(ui.progress.total, 2);508
assert_eq!(ui.progress.total, 2);477
assert!(ui.error.is_none());509
assert!(ui.error.is_none());478
let preview = ui.preview.expect("a real pass image");510
let preview = ui.preview.expect("a real pass image");479
assert_eq!((preview.width, preview.height), (64, 80));511
assert_eq!((preview.width, preview.height), (32, 40));480
let filled = preview.rgba.chunks_exact(4).filter(|p| p[3] == 255).count();512
let filled = preview.rgba.chunks_exact(4).filter(|p| p[3] == 255).count();481
assert_eq!(filled, 64 * 80);513
assert_eq!(filled, 32 * 40);482
assert!(output.is_file());514
assert!(output.is_file());483
let _ = std::fs::remove_file(&output);515
let _ = std::fs::remove_file(&output);484
}516
}@@ -68,7 +68,7 @@ pub fn raster_body(
68
68
}
69
69
let z = w0 * a.z + w1 * b.z + w2 * c.z;
70
70
let index = y * width + x;
71
if z < depth[index] {
71
if z.is_finite() && (-1.0..=1.0).contains(&z) && z < depth[index] {
72
72
depth[index] = z;
73
73
let pixel = &mut color[index * 4..index * 4 + 4];
74
74
pixel[0] = shade[0];
@@ -179,6 +179,38 @@ mod tests {
179
179
use crate::preview::load_gltf_file;
180
180
use std::path::PathBuf;
181
181
182
#[test]
183
fn front_surface_wins_independent_of_triangle_submission_order() {
184
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
185
.join("tests/fixtures/bodies/test-body-male.glb");
186
let mut character = load_gltf_file(&path).unwrap();
187
character.positions = vec![
188
[-0.5, 0.4, -0.25],
189
[0.5, 0.4, -0.25],
190
[0., 1.5, -0.25],
191
[-0.5, 0.4, 0.25],
192
[0.5, 0.4, 0.25],
193
[0., 1.5, 0.95],
194
];
195
character.joint_nodes.clear();
196
character.inverse_binds.clear();
197
character.clips.clear();
198
character.base_color = [1., 1., 1., 1.];
199
let controls = PreviewControls::default();
200
character.indices = vec![0, 1, 2];
201
let front = raster_body(&character, &controls, 96, 128).unwrap();
202
character.indices = vec![3, 4, 5];
203
let back = raster_body(&character, &controls, 96, 128).unwrap();
204
character.indices = vec![0, 1, 2, 3, 4, 5];
205
let first = raster_body(&character, &controls, 96, 128).unwrap();
206
character.indices = vec![3, 4, 5, 0, 1, 2];
207
let reversed = raster_body(&character, &controls, 96, 128).unwrap();
208
assert_eq!(first, reversed);
209
let pixel = (64 * 96 + 48) * 4;
210
assert_ne!(&front[pixel..pixel + 4], &back[pixel..pixel + 4]);
211
assert_eq!(&first[pixel..pixel + 4], &front[pixel..pixel + 4]);
212
}
213
182
214
#[test]
183
215
fn male_walk_still_has_a_visible_body() {
184
216
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -68,7 +68,7 @@ pub fn raster_body(68
}68
}69
let z = w0 * a.z + w1 * b.z + w2 * c.z;69
let z = w0 * a.z + w1 * b.z + w2 * c.z;70
let index = y * width + x;70
let index = y * width + x;71
if z < depth[index] {71
if z.is_finite() && (-1.0..=1.0).contains(&z) && z < depth[index] {72
depth[index] = z;72
depth[index] = z;73
let pixel = &mut color[index * 4..index * 4 + 4];73
let pixel = &mut color[index * 4..index * 4 + 4];74
pixel[0] = shade[0];74
pixel[0] = shade[0];@@ -179,6 +179,38 @@ mod tests {179
use crate::preview::load_gltf_file;179
use crate::preview::load_gltf_file;180
use std::path::PathBuf;180
use std::path::PathBuf;181
181
182
#[test]183
fn front_surface_wins_independent_of_triangle_submission_order() {184
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))185
.join("tests/fixtures/bodies/test-body-male.glb");186
let mut character = load_gltf_file(&path).unwrap();187
character.positions = vec![188
[-0.5, 0.4, -0.25],189
[0.5, 0.4, -0.25],190
[0., 1.5, -0.25],191
[-0.5, 0.4, 0.25],192
[0.5, 0.4, 0.25],193
[0., 1.5, 0.95],194
];195
character.joint_nodes.clear();196
character.inverse_binds.clear();197
character.clips.clear();198
character.base_color = [1., 1., 1., 1.];199
let controls = PreviewControls::default();200
character.indices = vec![0, 1, 2];201
let front = raster_body(&character, &controls, 96, 128).unwrap();202
character.indices = vec![3, 4, 5];203
let back = raster_body(&character, &controls, 96, 128).unwrap();204
character.indices = vec![0, 1, 2, 3, 4, 5];205
let first = raster_body(&character, &controls, 96, 128).unwrap();206
character.indices = vec![3, 4, 5, 0, 1, 2];207
let reversed = raster_body(&character, &controls, 96, 128).unwrap();208
assert_eq!(first, reversed);209
let pixel = (64 * 96 + 48) * 4;210
assert_ne!(&front[pixel..pixel + 4], &back[pixel..pixel + 4]);211
assert_eq!(&first[pixel..pixel + 4], &front[pixel..pixel + 4]);212
}213
182
#[test]214
#[test]183
fn male_walk_still_has_a_visible_body() {215
fn male_walk_still_has_a_visible_body() {184
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))216
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))