BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / auto_pivot_probe.rs

auto_pivot_probe.rs

auto_pivot_probe.rs 9.9 KiB Download Raw
1//! Real Mesa/Xvfb proof that the automatic (rest-pose centre) pivot differs from a center-only orbit at pitched yaw.
2//! Usage: OUTDIR=... xvfb-run -a cargo run --example auto_pivot_probe --locked
3use paperdoll_renderer::{camera, preview, production, project::CameraFraming};
4use std::{
5 path::{Path, PathBuf},
6 sync::{Arc, Mutex},
7};
8
9struct Quit;
10impl eframe::App for Quit {
11 fn update(&mut self, ctx: &eframe::egui::Context, _: &mut eframe::Frame) {
12 ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
13 }
14}
15
16fn write_rgba(path: &Path, width: u32, height: u32, rgba: &[u8]) -> Result<(), String> {
17 let file = std::fs::File::create(path).map_err(|e| e.to_string())?;
18 let mut encoder = png::Encoder::new(file, width, height);
19 encoder.set_color(png::ColorType::Rgba);
20 encoder.set_depth(png::BitDepth::Eight);
21 encoder
22 .write_header()
23 .map_err(|e| e.to_string())?
24 .write_image_data(rgba)
25 .map_err(|e| e.to_string())
26}
27
28fn downsample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
29 let mut out = vec![0u8; (dw * dh * 4) as usize];
30 for y in 0..dh {
31 for x in 0..dw {
32 let sx = x * sw / dw;
33 let sy = y * sh / dh;
34 let si = ((sy * sw + sx) * 4) as usize;
35 let di = ((y * dw + x) * 4) as usize;
36 out[di..di + 4].copy_from_slice(&src[si..si + 4]);
37 }
38 }
39 out
40}
41
42fn main() -> Result<(), Box<dyn std::error::Error>> {
43 let out = PathBuf::from(
44 std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-pivot-artifacts".into()),
45 );
46 std::fs::create_dir_all(&out)?;
47 let outcome = Arc::new(Mutex::new(None));
48 let result = outcome.clone();
49 let options = eframe::NativeOptions {
50 viewport: eframe::egui::ViewportBuilder::default().with_inner_size([64., 64.]),
51 ..paperdoll_renderer::preview_gl::native_options()
52 };
53 eframe::run_native(
54 "Paperdoll auto pivot probe",
55 options,
56 Box::new(move |creation| {
57 let test = (|| -> Result<serde_json::Value, String> {
58 let gl = creation
59 .gl
60 .as_ref()
61 .ok_or_else(|| "geen Glow-context".to_string())?;
62 let body = preview::load_gltf_file(
63 &Path::new(env!("CARGO_MANIFEST_DIR"))
64 .join("tests/fixtures/bodies/test-body-male.glb"),
65 )?;
66 let resolved = camera::resolve(&body, CameraFraming::default());
67 let pivot_y = resolved
68 .framing
69 .pivot_y_milli
70 .ok_or("automatische pivot ontbreekt")?;
71 if pivot_y == CameraFraming::default().center_y_milli {
72 return Err(
73 "automatische pivot mag niet gelijk zijn aan default centrum".into(),
74 );
75 }
76 let mut renderer = production::GpuRenderer::default();
77 let mut cache = paperdoll_renderer::equipment::TextureCache::default();
78 let pitch = 30.;
79 let yaw = 90.;
80 let mut job = production::RenderJob {
81 depth_format: paperdoll_renderer::camera::DepthFormat::GRAY16,
82 atlas_trim: false,
83 semi_transparency: true,
84 character: body,
85 sex: "male".into(),
86 preset: None,
87 target_width: 64,
88 target_height: 96,
89 fps: 12,
90 framing: CameraFraming {
91 height_milli: 2600,
92 center_y_milli: 1000,
93 pivot_y_milli: Some(1000),
94 depth_range_milli: None,
95 },
96 output: out.clone(),
97 project_id: "auto-pivot-probe".into(),
98 project_name: "Neck probe".into(),
99 body_model: production::SourceFingerprint {
100 path: "assets/body/test-body-male.glb".into(),
101 sha256: paperdoll_renderer::digest::sha256_hex(b"auto-pivot-probe-body"),
102 },
103 preset_sha256: None,
104 };
105 let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
106 let old = unsafe { renderer.render(gl, &old_scene) }?;
107 job.framing.pivot_y_milli = None;
108 let pivot_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
109 let pivoted = unsafe { renderer.render(gl, &pivot_scene) }?;
110 unsafe { renderer.destroy(gl) };
111 assert_eq!((old.width, old.height), (pivoted.width, pivoted.height));
112 let mut changed = 0usize;
113 for (a, b) in old
114 .diffuse
115 .chunks_exact(4)
116 .zip(pivoted.diffuse.chunks_exact(4))
117 {
118 if a != b {
119 changed += 1;
120 }
121 }
122 if changed < 200 {
123 return Err(format!(
124 "te weinig pixelverschil tussen centrum-pivot en automatische pivot: {changed}"
125 ));
126 }
127 // Zero-pitch image center / feet framing must stay close after raising pivot.
128 job.framing.pivot_y_milli = Some(1000);
129 let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
130 job.framing.pivot_y_milli = None;
131 let pivot0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
132 let old0b = unsafe {
133 let mut r = production::GpuRenderer::default();
134 let b = r.render(gl, &old0)?;
135 r.destroy(gl);
136 b
137 };
138 let pivot0b = unsafe {
139 let mut r = production::GpuRenderer::default();
140 let b = r.render(gl, &pivot0)?;
141 r.destroy(gl);
142 b
143 };
144 let mid = ((old0b.height / 2) * old0b.width + old0b.width / 2) as usize;
145 let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize;
146 let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4]
147 .iter()
148 .zip(&pivot0b.diffuse[mid * 4..mid * 4 + 4])
149 .map(|(a, b)| a.abs_diff(*b) as u32)
150 .sum::<u32>();
151 let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4]
152 .iter()
153 .zip(&pivot0b.diffuse[feet * 4..feet * 4 + 4])
154 .map(|(a, b)| a.abs_diff(*b) as u32)
155 .sum::<u32>();
156 write_rgba(
157 &out.join("center-pivot-p30-y90.png"),
158 old.width,
159 old.height,
160 &old.diffuse,
161 )?;
162 write_rgba(
163 &out.join("auto-pivot-p30-y90.png"),
164 pivoted.width,
165 pivoted.height,
166 &pivoted.diffuse,
167 )?;
168 let contact_w = 256;
169 let contact_h = 192;
170 let left = downsample(
171 &old.diffuse,
172 old.width,
173 old.height,
174 contact_w / 2,
175 contact_h,
176 );
177 let right = downsample(
178 &pivoted.diffuse,
179 pivoted.width,
180 pivoted.height,
181 contact_w / 2,
182 contact_h,
183 );
184 let mut contact = vec![0u8; (contact_w * contact_h * 4) as usize];
185 for y in 0..contact_h {
186 for x in 0..contact_w / 2 {
187 let si = ((y * (contact_w / 2) + x) * 4) as usize;
188 let li = ((y * contact_w + x) * 4) as usize;
189 let ri = ((y * contact_w + contact_w / 2 + x) * 4) as usize;
190 contact[li..li + 4].copy_from_slice(&left[si..si + 4]);
191 contact[ri..ri + 4].copy_from_slice(&right[si..si + 4]);
192 }
193 }
194 write_rgba(
195 &out.join("auto-vs-center-contact.png"),
196 contact_w,
197 contact_h,
198 &contact,
199 )?;
200 let report = serde_json::json!({
201 "success": true,
202 "pitch": pitch,
203 "yaw": yaw,
204 "center_y_milli": 1000,
205 "pivot_y_milli": pivot_y,
206 "pivot_source": resolved.source,
207 "highres": [old.width, old.height],
208 "changed_pixels_pitch30": changed,
209 "zero_pitch_mid_channel_delta": mid_delta,
210 "zero_pitch_feet_channel_delta": feet_delta,
211 "artifacts": [
212 "center-pivot-p30-y90.png",
213 "auto-pivot-p30-y90.png",
214 "auto-vs-center-contact.png"
215 ]
216 });
217 std::fs::write(
218 out.join("auto-pivot-probe.json"),
219 serde_json::to_vec_pretty(&report).unwrap(),
220 )
221 .map_err(|e| e.to_string())?;
222 println!("AUTO_PIVOT_PROBE {report}");
223 Ok(report)
224 })();
225 *result.lock().unwrap() = Some(test);
226 Ok(Box::new(Quit))
227 }),
228 )?;
229 let value = outcome
230 .lock()
231 .unwrap()
232 .take()
233 .ok_or("GPU-probe is niet uitgevoerd")??;
234 if value["success"] != true {
235 return Err("probe failed".into());
236 }
237 Ok(())
238}