| 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 |
| 3 | use paperdoll_renderer::{camera, preview, production, project::CameraFraming}; |
| 4 | use std::{ |
| 5 | path::{Path, PathBuf}, |
| 6 | sync::{Arc, Mutex}, |
| 7 | }; |
| 8 | |
| 9 | struct Quit; |
| 10 | impl 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 | |
| 16 | fn 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 | |
| 28 | fn 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 | |
| 42 | fn 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 | character: body, |
| 83 | sex: "male".into(), |
| 84 | preset: None, |
| 85 | target_width: 64, |
| 86 | target_height: 96, |
| 87 | fps: 12, |
| 88 | framing: CameraFraming { |
| 89 | height_milli: 2600, |
| 90 | center_y_milli: 1000, |
| 91 | pivot_y_milli: Some(1000), |
| 92 | depth_range_milli: None, |
| 93 | }, |
| 94 | output: out.clone(), |
| 95 | project_id: "auto-pivot-probe".into(), |
| 96 | project_name: "Neck probe".into(), |
| 97 | body_model: production::SourceFingerprint { |
| 98 | path: "assets/body/test-body-male.glb".into(), |
| 99 | sha256: paperdoll_renderer::digest::sha256_hex(b"auto-pivot-probe-body"), |
| 100 | }, |
| 101 | preset_sha256: None, |
| 102 | }; |
| 103 | let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?; |
| 104 | let old = unsafe { renderer.render(gl, &old_scene) }?; |
| 105 | job.framing.pivot_y_milli = None; |
| 106 | let pivot_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?; |
| 107 | let pivoted = unsafe { renderer.render(gl, &pivot_scene) }?; |
| 108 | unsafe { renderer.destroy(gl) }; |
| 109 | assert_eq!((old.width, old.height), (pivoted.width, pivoted.height)); |
| 110 | let mut changed = 0usize; |
| 111 | for (a, b) in old |
| 112 | .diffuse |
| 113 | .chunks_exact(4) |
| 114 | .zip(pivoted.diffuse.chunks_exact(4)) |
| 115 | { |
| 116 | if a != b { |
| 117 | changed += 1; |
| 118 | } |
| 119 | } |
| 120 | if changed < 200 { |
| 121 | return Err(format!( |
| 122 | "te weinig pixelverschil tussen centrum-pivot en automatische pivot: {changed}" |
| 123 | )); |
| 124 | } |
| 125 | // Zero-pitch image center / feet framing must stay close after raising pivot. |
| 126 | job.framing.pivot_y_milli = Some(1000); |
| 127 | let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?; |
| 128 | job.framing.pivot_y_milli = None; |
| 129 | let pivot0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?; |
| 130 | let old0b = unsafe { |
| 131 | let mut r = production::GpuRenderer::default(); |
| 132 | let b = r.render(gl, &old0)?; |
| 133 | r.destroy(gl); |
| 134 | b |
| 135 | }; |
| 136 | let pivot0b = unsafe { |
| 137 | let mut r = production::GpuRenderer::default(); |
| 138 | let b = r.render(gl, &pivot0)?; |
| 139 | r.destroy(gl); |
| 140 | b |
| 141 | }; |
| 142 | let mid = ((old0b.height / 2) * old0b.width + old0b.width / 2) as usize; |
| 143 | let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize; |
| 144 | let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4] |
| 145 | .iter() |
| 146 | .zip(&pivot0b.diffuse[mid * 4..mid * 4 + 4]) |
| 147 | .map(|(a, b)| a.abs_diff(*b) as u32) |
| 148 | .sum::<u32>(); |
| 149 | let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4] |
| 150 | .iter() |
| 151 | .zip(&pivot0b.diffuse[feet * 4..feet * 4 + 4]) |
| 152 | .map(|(a, b)| a.abs_diff(*b) as u32) |
| 153 | .sum::<u32>(); |
| 154 | write_rgba( |
| 155 | &out.join("center-pivot-p30-y90.png"), |
| 156 | old.width, |
| 157 | old.height, |
| 158 | &old.diffuse, |
| 159 | )?; |
| 160 | write_rgba( |
| 161 | &out.join("auto-pivot-p30-y90.png"), |
| 162 | pivoted.width, |
| 163 | pivoted.height, |
| 164 | &pivoted.diffuse, |
| 165 | )?; |
| 166 | let contact_w = 256; |
| 167 | let contact_h = 192; |
| 168 | let left = downsample( |
| 169 | &old.diffuse, |
| 170 | old.width, |
| 171 | old.height, |
| 172 | contact_w / 2, |
| 173 | contact_h, |
| 174 | ); |
| 175 | let right = downsample( |
| 176 | &pivoted.diffuse, |
| 177 | pivoted.width, |
| 178 | pivoted.height, |
| 179 | contact_w / 2, |
| 180 | contact_h, |
| 181 | ); |
| 182 | let mut contact = vec![0u8; (contact_w * contact_h * 4) as usize]; |
| 183 | for y in 0..contact_h { |
| 184 | for x in 0..contact_w / 2 { |
| 185 | let si = ((y * (contact_w / 2) + x) * 4) as usize; |
| 186 | let li = ((y * contact_w + x) * 4) as usize; |
| 187 | let ri = ((y * contact_w + contact_w / 2 + x) * 4) as usize; |
| 188 | contact[li..li + 4].copy_from_slice(&left[si..si + 4]); |
| 189 | contact[ri..ri + 4].copy_from_slice(&right[si..si + 4]); |
| 190 | } |
| 191 | } |
| 192 | write_rgba( |
| 193 | &out.join("auto-vs-center-contact.png"), |
| 194 | contact_w, |
| 195 | contact_h, |
| 196 | &contact, |
| 197 | )?; |
| 198 | let report = serde_json::json!({ |
| 199 | "success": true, |
| 200 | "pitch": pitch, |
| 201 | "yaw": yaw, |
| 202 | "center_y_milli": 1000, |
| 203 | "pivot_y_milli": pivot_y, |
| 204 | "pivot_source": resolved.source, |
| 205 | "highres": [old.width, old.height], |
| 206 | "changed_pixels_pitch30": changed, |
| 207 | "zero_pitch_mid_channel_delta": mid_delta, |
| 208 | "zero_pitch_feet_channel_delta": feet_delta, |
| 209 | "artifacts": [ |
| 210 | "center-pivot-p30-y90.png", |
| 211 | "auto-pivot-p30-y90.png", |
| 212 | "auto-vs-center-contact.png" |
| 213 | ] |
| 214 | }); |
| 215 | std::fs::write( |
| 216 | out.join("auto-pivot-probe.json"), |
| 217 | serde_json::to_vec_pretty(&report).unwrap(), |
| 218 | ) |
| 219 | .map_err(|e| e.to_string())?; |
| 220 | println!("AUTO_PIVOT_PROBE {report}"); |
| 221 | Ok(report) |
| 222 | })(); |
| 223 | *result.lock().unwrap() = Some(test); |
| 224 | Ok(Box::new(Quit)) |
| 225 | }), |
| 226 | )?; |
| 227 | let value = outcome |
| 228 | .lock() |
| 229 | .unwrap() |
| 230 | .take() |
| 231 | .ok_or("GPU-probe is niet uitgevoerd")??; |
| 232 | if value["success"] != true { |
| 233 | return Err("probe failed".into()); |
| 234 | } |
| 235 | Ok(()) |
| 236 | } |