//! Real Mesa/Xvfb proof that the automatic (rest-pose centre) pivot differs from a center-only orbit at pitched yaw. //! Usage: OUTDIR=... xvfb-run -a cargo run --example auto_pivot_probe --locked use paperdoll_renderer::{camera, preview, production, project::CameraFraming}; use std::{ path::{Path, PathBuf}, sync::{Arc, Mutex}, }; struct Quit; impl eframe::App for Quit { fn update(&mut self, ctx: &eframe::egui::Context, _: &mut eframe::Frame) { ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close); } } fn write_rgba(path: &Path, width: u32, height: u32, rgba: &[u8]) -> Result<(), String> { let file = std::fs::File::create(path).map_err(|e| e.to_string())?; let mut encoder = png::Encoder::new(file, width, height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); encoder .write_header() .map_err(|e| e.to_string())? .write_image_data(rgba) .map_err(|e| e.to_string()) } fn downsample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec { let mut out = vec![0u8; (dw * dh * 4) as usize]; for y in 0..dh { for x in 0..dw { let sx = x * sw / dw; let sy = y * sh / dh; let si = ((sy * sw + sx) * 4) as usize; let di = ((y * dw + x) * 4) as usize; out[di..di + 4].copy_from_slice(&src[si..si + 4]); } } out } fn main() -> Result<(), Box> { let out = PathBuf::from( std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-pivot-artifacts".into()), ); std::fs::create_dir_all(&out)?; let outcome = Arc::new(Mutex::new(None)); let result = outcome.clone(); let options = eframe::NativeOptions { viewport: eframe::egui::ViewportBuilder::default().with_inner_size([64., 64.]), ..paperdoll_renderer::preview_gl::native_options() }; eframe::run_native( "Paperdoll auto pivot probe", options, Box::new(move |creation| { let test = (|| -> Result { let gl = creation .gl .as_ref() .ok_or_else(|| "geen Glow-context".to_string())?; let body = preview::load_gltf_file( &Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/bodies/test-body-male.glb"), )?; let resolved = camera::resolve(&body, CameraFraming::default()); let pivot_y = resolved .framing .pivot_y_milli .ok_or("automatische pivot ontbreekt")?; if pivot_y == CameraFraming::default().center_y_milli { return Err( "automatische pivot mag niet gelijk zijn aan default centrum".into(), ); } let mut renderer = production::GpuRenderer::default(); let mut cache = paperdoll_renderer::equipment::TextureCache::default(); let pitch = 30.; let yaw = 90.; let mut job = production::RenderJob { depth_format: paperdoll_renderer::camera::DepthFormat::GRAY16, atlas_trim: false, character: body, sex: "male".into(), preset: None, target_width: 64, target_height: 96, fps: 12, framing: CameraFraming { height_milli: 2600, center_y_milli: 1000, pivot_y_milli: Some(1000), depth_range_milli: None, }, output: out.clone(), project_id: "auto-pivot-probe".into(), project_name: "Neck probe".into(), body_model: production::SourceFingerprint { path: "assets/body/test-body-male.glb".into(), sha256: paperdoll_renderer::digest::sha256_hex(b"auto-pivot-probe-body"), }, preset_sha256: None, }; let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?; let old = unsafe { renderer.render(gl, &old_scene) }?; job.framing.pivot_y_milli = None; let pivot_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?; let pivoted = unsafe { renderer.render(gl, &pivot_scene) }?; unsafe { renderer.destroy(gl) }; assert_eq!((old.width, old.height), (pivoted.width, pivoted.height)); let mut changed = 0usize; for (a, b) in old .diffuse .chunks_exact(4) .zip(pivoted.diffuse.chunks_exact(4)) { if a != b { changed += 1; } } if changed < 200 { return Err(format!( "te weinig pixelverschil tussen centrum-pivot en automatische pivot: {changed}" )); } // Zero-pitch image center / feet framing must stay close after raising pivot. job.framing.pivot_y_milli = Some(1000); let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?; job.framing.pivot_y_milli = None; let pivot0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?; let old0b = unsafe { let mut r = production::GpuRenderer::default(); let b = r.render(gl, &old0)?; r.destroy(gl); b }; let pivot0b = unsafe { let mut r = production::GpuRenderer::default(); let b = r.render(gl, &pivot0)?; r.destroy(gl); b }; let mid = ((old0b.height / 2) * old0b.width + old0b.width / 2) as usize; let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize; let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4] .iter() .zip(&pivot0b.diffuse[mid * 4..mid * 4 + 4]) .map(|(a, b)| a.abs_diff(*b) as u32) .sum::(); let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4] .iter() .zip(&pivot0b.diffuse[feet * 4..feet * 4 + 4]) .map(|(a, b)| a.abs_diff(*b) as u32) .sum::(); write_rgba( &out.join("center-pivot-p30-y90.png"), old.width, old.height, &old.diffuse, )?; write_rgba( &out.join("auto-pivot-p30-y90.png"), pivoted.width, pivoted.height, &pivoted.diffuse, )?; let contact_w = 256; let contact_h = 192; let left = downsample( &old.diffuse, old.width, old.height, contact_w / 2, contact_h, ); let right = downsample( &pivoted.diffuse, pivoted.width, pivoted.height, contact_w / 2, contact_h, ); let mut contact = vec![0u8; (contact_w * contact_h * 4) as usize]; for y in 0..contact_h { for x in 0..contact_w / 2 { let si = ((y * (contact_w / 2) + x) * 4) as usize; let li = ((y * contact_w + x) * 4) as usize; let ri = ((y * contact_w + contact_w / 2 + x) * 4) as usize; contact[li..li + 4].copy_from_slice(&left[si..si + 4]); contact[ri..ri + 4].copy_from_slice(&right[si..si + 4]); } } write_rgba( &out.join("auto-vs-center-contact.png"), contact_w, contact_h, &contact, )?; let report = serde_json::json!({ "success": true, "pitch": pitch, "yaw": yaw, "center_y_milli": 1000, "pivot_y_milli": pivot_y, "pivot_source": resolved.source, "highres": [old.width, old.height], "changed_pixels_pitch30": changed, "zero_pitch_mid_channel_delta": mid_delta, "zero_pitch_feet_channel_delta": feet_delta, "artifacts": [ "center-pivot-p30-y90.png", "auto-pivot-p30-y90.png", "auto-vs-center-contact.png" ] }); std::fs::write( out.join("auto-pivot-probe.json"), serde_json::to_vec_pretty(&report).unwrap(), ) .map_err(|e| e.to_string())?; println!("AUTO_PIVOT_PROBE {report}"); Ok(report) })(); *result.lock().unwrap() = Some(test); Ok(Box::new(Quit)) }), )?; let value = outcome .lock() .unwrap() .take() .ok_or("GPU-probe is niet uitgevoerd")??; if value["success"] != true { return Err("probe failed".into()); } Ok(()) }