fix: restore native preview depth and preserve pass aspect ratio
@@ -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
}@@ -265,6 +265,11 @@ impl EditorApp {
265
265
self.session.set_render(width, height, fps);
266
266
}
267
267
268
/// Start the same worker used by the Render button (also used by the GUI smoke fixture).
269
pub fn render_current_pose(&mut self) {
270
self.start_render([480, 640]);
271
}
272
268
273
fn start_render(&mut self, preview: [u32; 2]) {
269
274
if let Some(opened) = &self.session.opened {
270
275
if let Err(error) = opened.document.validate_shape() {
@@ -605,8 +610,22 @@ impl EditorApp {
605
610
..Default::default()
606
611
},
607
612
);
608
let available = ui.available_size();
609
ui.image((texture.id(), available));
613
let (bounds, _) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());
614
let fit = crate::render_job::fit_preview(
615
[image.width, image.height],
616
[bounds.width(), bounds.height()],
617
);
618
let rect = egui::Rect::from_min_size(
619
bounds.min + egui::vec2(fit.offset[0], fit.offset[1]),
620
egui::vec2(fit.size[0], fit.size[1]),
621
);
622
ui.painter().rect_filled(bounds, 0.0, egui::Color32::BLACK);
623
ui.painter().image(
624
texture.id(),
625
rect,
626
egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),
627
egui::Color32::WHITE,
628
);
610
629
}
611
630
612
631
fn show_equipment_list(&mut self, ctx: &egui::Context) {
@@ -265,6 +265,11 @@ impl EditorApp {265
self.session.set_render(width, height, fps);265
self.session.set_render(width, height, fps);266
}266
}267
267
268
/// Start the same worker used by the Render button (also used by the GUI smoke fixture).269
pub fn render_current_pose(&mut self) {270
self.start_render([480, 640]);271
}272
268
fn start_render(&mut self, preview: [u32; 2]) {273
fn start_render(&mut self, preview: [u32; 2]) {269
if let Some(opened) = &self.session.opened {274
if let Some(opened) = &self.session.opened {270
if let Err(error) = opened.document.validate_shape() {275
if let Err(error) = opened.document.validate_shape() {@@ -605,8 +610,22 @@ impl EditorApp {605
..Default::default()610
..Default::default()606
},611
},607
);612
);608
let available = ui.available_size();613
let (bounds, _) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());609
ui.image((texture.id(), available));614
let fit = crate::render_job::fit_preview(615
[image.width, image.height],616
[bounds.width(), bounds.height()],617
);618
let rect = egui::Rect::from_min_size(619
bounds.min + egui::vec2(fit.offset[0], fit.offset[1]),620
egui::vec2(fit.size[0], fit.size[1]),621
);622
ui.painter().rect_filled(bounds, 0.0, egui::Color32::BLACK);623
ui.painter().image(624
texture.id(),625
rect,626
egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)),627
egui::Color32::WHITE,628
);610
}629
}611
630
612
fn show_equipment_list(&mut self, ctx: &egui::Context) {631
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 {
@@ -234,6 +244,8 @@ impl GpuCache {
234
244
viewport.height_px,
235
245
);
236
246
gl.enable(glow::DEPTH_TEST);
247
gl.depth_mask(true);
248
gl.clear_depth_f32(1.0);
237
249
gl.depth_func(glow::LESS);
238
250
gl.disable(glow::BLEND);
239
251
gl.clear_color(0.08, 0.09, 0.12, 1.0);
@@ -396,3 +408,158 @@ void main() {
396
408
frag = vec4(u_color.rgb * shade, 1.0);
397
409
}
398
410
"#;
411
412
/// Real GPU regression using the same cache/shader/paint path as the callback.
413
/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.
414
///
415
/// # Safety
416
/// `gl` must be a current native OpenGL context. Run in a disposable test window;
417
/// this probe changes GL state and restores the default framebuffer afterwards.
418
pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
419
let native_bits = gl.get_framebuffer_attachment_parameter_i32(
420
glow::FRAMEBUFFER,
421
glow::DEPTH,
422
glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,
423
);
424
if native_bits < 24 {
425
return Err(format!(
426
"native depth attachment ontbreekt: {native_bits} bits"
427
));
428
}
429
let framebuffer = gl.create_framebuffer()?;
430
let texture = gl.create_texture()?;
431
let depth = gl.create_renderbuffer()?;
432
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));
433
gl.bind_texture(glow::TEXTURE_2D, Some(texture));
434
gl.tex_parameter_i32(
435
glow::TEXTURE_2D,
436
glow::TEXTURE_MIN_FILTER,
437
glow::NEAREST as i32,
438
);
439
gl.tex_parameter_i32(
440
glow::TEXTURE_2D,
441
glow::TEXTURE_MAG_FILTER,
442
glow::NEAREST as i32,
443
);
444
gl.tex_image_2d(
445
glow::TEXTURE_2D,
446
0,
447
glow::RGBA8 as i32,
448
32,
449
32,
450
0,
451
glow::RGBA,
452
glow::UNSIGNED_BYTE,
453
None,
454
);
455
gl.framebuffer_texture_2d(
456
glow::FRAMEBUFFER,
457
glow::COLOR_ATTACHMENT0,
458
glow::TEXTURE_2D,
459
Some(texture),
460
0,
461
);
462
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));
463
gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);
464
gl.framebuffer_renderbuffer(
465
glow::FRAMEBUFFER,
466
glow::DEPTH_ATTACHMENT,
467
glow::RENDERBUFFER,
468
Some(depth),
469
);
470
gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);
471
gl.read_buffer(glow::COLOR_ATTACHMENT0);
472
if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
473
return Err("onvolledig offscreen depth-target".into());
474
}
475
let info = egui::PaintCallbackInfo {
476
viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),
477
clip_rect: egui::Rect::EVERYTHING,
478
pixels_per_point: 1.,
479
screen_size_px: [32, 32],
480
};
481
let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {
482
generation,
483
positions: vec![
484
[-0.9, -0.9, -0.5],
485
[0.9, -0.9, -0.5],
486
[0., 0.9, -0.5],
487
[-0.9, -0.9, 0.5],
488
[0.9, -0.9, 0.5],
489
[0., 0.9, 0.5],
490
],
491
normals: vec![
492
[0.35, 0.85, 0.4],
493
[0.35, 0.85, 0.4],
494
[0.35, 0.85, 0.4],
495
[-0.35, -0.85, -0.4],
496
[-0.35, -0.85, -0.4],
497
[-0.35, -0.85, -0.4],
498
],
499
joints: vec![[0; 4]; 6],
500
weights: vec![[1., 0., 0., 0.]; 6],
501
indices,
502
bones: vec![crate::preview::identity()],
503
view_proj: crate::preview::identity(),
504
color: [1.; 4],
505
};
506
let mut cache = GpuCache::default();
507
let read = || {
508
let mut rgba = [0u8; 4];
509
gl.read_pixels(
510
16,
511
16,
512
1,
513
1,
514
glow::RGBA,
515
glow::UNSIGNED_BYTE,
516
glow::PixelPackData::Slice(&mut rgba),
517
);
518
rgba
519
};
520
let result = (|| {
521
// Deliberately poison the incoming state: the callback owns its depth setup.
522
gl.depth_mask(false);
523
gl.clear_depth_f32(0.0);
524
gl.depth_func(glow::GREATER);
525
cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));
526
cache.paint(gl, &info);
527
let near = read();
528
if near[..3].iter().any(|c| *c < 250) {
529
return Err(format!("achterste vlak overschrijft voorste: {near:?}"));
530
}
531
cache.pending = Some(make_frame(vec![3, 4, 5], 2));
532
cache.paint(gl, &info);
533
let next = read();
534
if next[..3].iter().any(|c| !(69..=73).contains(c)) {
535
return Err(format!("depth niet gewist voor volgende frame: {next:?}"));
536
}
537
cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));
538
cache.paint(gl, &info);
539
let reverse = read();
540
if near != reverse {
541
return Err(format!(
542
"draw-order afhankelijke occlusie: {near:?} / {reverse:?}"
543
));
544
}
545
println!("GPU depth PASS: native {native_bits}-bit, front={near:?}, next-frame={next:?}, reversed={reverse:?}");
546
Ok(())
547
})();
548
if let Some(p) = cache.program {
549
gl.delete_program(p)
550
}
551
if let Some(v) = cache.vao {
552
gl.delete_vertex_array(v)
553
}
554
if let Some(v) = cache.vbo {
555
gl.delete_buffer(v)
556
}
557
if let Some(v) = cache.ebo {
558
gl.delete_buffer(v)
559
}
560
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
561
gl.delete_framebuffer(framebuffer);
562
gl.delete_texture(texture);
563
gl.delete_renderbuffer(depth);
564
result
565
}
@@ -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 {@@ -234,6 +244,8 @@ impl GpuCache {234
viewport.height_px,244
viewport.height_px,235
);245
);236
gl.enable(glow::DEPTH_TEST);246
gl.enable(glow::DEPTH_TEST);247
gl.depth_mask(true);248
gl.clear_depth_f32(1.0);237
gl.depth_func(glow::LESS);249
gl.depth_func(glow::LESS);238
gl.disable(glow::BLEND);250
gl.disable(glow::BLEND);239
gl.clear_color(0.08, 0.09, 0.12, 1.0);251
gl.clear_color(0.08, 0.09, 0.12, 1.0);@@ -396,3 +408,158 @@ void main() {396
frag = vec4(u_color.rgb * shade, 1.0);408
frag = vec4(u_color.rgb * shade, 1.0);397
}409
}398
"#;410
"#;411
412
/// Real GPU regression using the same cache/shader/paint path as the callback.413
/// Tests front-first/back-first overlap, depth writes, and the next-frame clear.414
///415
/// # Safety416
/// `gl` must be a current native OpenGL context. Run in a disposable test window;417
/// this probe changes GL state and restores the default framebuffer afterwards.418
pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {419
let native_bits = gl.get_framebuffer_attachment_parameter_i32(420
glow::FRAMEBUFFER,421
glow::DEPTH,422
glow::FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,423
);424
if native_bits < 24 {425
return Err(format!(426
"native depth attachment ontbreekt: {native_bits} bits"427
));428
}429
let framebuffer = gl.create_framebuffer()?;430
let texture = gl.create_texture()?;431
let depth = gl.create_renderbuffer()?;432
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(framebuffer));433
gl.bind_texture(glow::TEXTURE_2D, Some(texture));434
gl.tex_parameter_i32(435
glow::TEXTURE_2D,436
glow::TEXTURE_MIN_FILTER,437
glow::NEAREST as i32,438
);439
gl.tex_parameter_i32(440
glow::TEXTURE_2D,441
glow::TEXTURE_MAG_FILTER,442
glow::NEAREST as i32,443
);444
gl.tex_image_2d(445
glow::TEXTURE_2D,446
0,447
glow::RGBA8 as i32,448
32,449
32,450
0,451
glow::RGBA,452
glow::UNSIGNED_BYTE,453
None,454
);455
gl.framebuffer_texture_2d(456
glow::FRAMEBUFFER,457
glow::COLOR_ATTACHMENT0,458
glow::TEXTURE_2D,459
Some(texture),460
0,461
);462
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth));463
gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT24, 32, 32);464
gl.framebuffer_renderbuffer(465
glow::FRAMEBUFFER,466
glow::DEPTH_ATTACHMENT,467
glow::RENDERBUFFER,468
Some(depth),469
);470
gl.draw_buffers(&[glow::COLOR_ATTACHMENT0]);471
gl.read_buffer(glow::COLOR_ATTACHMENT0);472
if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {473
return Err("onvolledig offscreen depth-target".into());474
}475
let info = egui::PaintCallbackInfo {476
viewport: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(32., 32.)),477
clip_rect: egui::Rect::EVERYTHING,478
pixels_per_point: 1.,479
screen_size_px: [32, 32],480
};481
let make_frame = |indices: Vec<u32>, generation: u64| PendingFrame {482
generation,483
positions: vec![484
[-0.9, -0.9, -0.5],485
[0.9, -0.9, -0.5],486
[0., 0.9, -0.5],487
[-0.9, -0.9, 0.5],488
[0.9, -0.9, 0.5],489
[0., 0.9, 0.5],490
],491
normals: vec![492
[0.35, 0.85, 0.4],493
[0.35, 0.85, 0.4],494
[0.35, 0.85, 0.4],495
[-0.35, -0.85, -0.4],496
[-0.35, -0.85, -0.4],497
[-0.35, -0.85, -0.4],498
],499
joints: vec![[0; 4]; 6],500
weights: vec![[1., 0., 0., 0.]; 6],501
indices,502
bones: vec![crate::preview::identity()],503
view_proj: crate::preview::identity(),504
color: [1.; 4],505
};506
let mut cache = GpuCache::default();507
let read = || {508
let mut rgba = [0u8; 4];509
gl.read_pixels(510
16,511
16,512
1,513
1,514
glow::RGBA,515
glow::UNSIGNED_BYTE,516
glow::PixelPackData::Slice(&mut rgba),517
);518
rgba519
};520
let result = (|| {521
// Deliberately poison the incoming state: the callback owns its depth setup.522
gl.depth_mask(false);523
gl.clear_depth_f32(0.0);524
gl.depth_func(glow::GREATER);525
cache.pending = Some(make_frame(vec![0, 1, 2, 3, 4, 5], 1));526
cache.paint(gl, &info);527
let near = read();528
if near[..3].iter().any(|c| *c < 250) {529
return Err(format!("achterste vlak overschrijft voorste: {near:?}"));530
}531
cache.pending = Some(make_frame(vec![3, 4, 5], 2));532
cache.paint(gl, &info);533
let next = read();534
if next[..3].iter().any(|c| !(69..=73).contains(c)) {535
return Err(format!("depth niet gewist voor volgende frame: {next:?}"));536
}537
cache.pending = Some(make_frame(vec![3, 4, 5, 0, 1, 2], 3));538
cache.paint(gl, &info);539
let reverse = read();540
if near != reverse {541
return Err(format!(542
"draw-order afhankelijke occlusie: {near:?} / {reverse:?}"543
));544
}545
println!("GPU depth PASS: native {native_bits}-bit, front={near:?}, next-frame={next:?}, reversed={reverse:?}");546
Ok(())547
})();548
if let Some(p) = cache.program {549
gl.delete_program(p)550
}551
if let Some(v) = cache.vao {552
gl.delete_vertex_array(v)553
}554
if let Some(v) = cache.vbo {555
gl.delete_buffer(v)556
}557
if let Some(v) = cache.ebo {558
gl.delete_buffer(v)559
}560
gl.bind_framebuffer(glow::FRAMEBUFFER, None);561
gl.delete_framebuffer(framebuffer);562
gl.delete_texture(texture);563
gl.delete_renderbuffer(depth);564
result565
}@@ -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"))