BlueGit

golden-age paperdoll-renderer

Merge feature/rename-pivot-probe

timfa Grok Bot · pushed by timfa

Authored 2026-11-07 · 6 days ago

Hash
c3df4bd72c3ebd14d5e3027d8bf06d4a6567b6f5
Pipeline
Passed
1 file changed +37 −32
examples/neck_pivot_probe.rs examples/auto_pivot_probe.rs renamed +37 −32
@@ -1,5 +1,5 @@
1 //! Real Mesa/Xvfb proof that neck pivot differs from center-only orbit at pitched yaw.
2 //! Usage: OUTDIR=... xvfb-run -a cargo run --example neck_pivot_probe --locked
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 3 use paperdoll_renderer::{camera, preview, production, project::CameraFraming};
4 4 use std::{
5 5 path::{Path, PathBuf},
@@ -41,7 +41,7 @@ fn downsample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
41 41
42 42 fn main() -> Result<(), Box<dyn std::error::Error>> {
43 43 let out = PathBuf::from(
44 std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-neck-artifacts".into()),
44 std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-pivot-artifacts".into()),
45 45 );
46 46 std::fs::create_dir_all(&out)?;
47 47 let outcome = Arc::new(Mutex::new(None));
@@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
51 51 ..paperdoll_renderer::preview_gl::native_options()
52 52 };
53 53 eframe::run_native(
54 "Paperdoll neck pivot probe",
54 "Paperdoll auto pivot probe",
55 55 options,
56 56 Box::new(move |creation| {
57 57 let test = (|| -> Result<serde_json::Value, String> {
@@ -64,9 +64,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
64 64 .join("tests/fixtures/bodies/test-body-male.glb"),
65 65 )?;
66 66 let resolved = camera::resolve(&body, CameraFraming::default());
67 let neck_y = resolved.framing.pivot_y_milli.ok_or("nekpivot ontbreekt")?;
68 if neck_y == CameraFraming::default().center_y_milli {
69 return Err("nekpivot mag niet gelijk zijn aan default centrum".into());
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 );
70 75 }
71 76 let mut renderer = production::GpuRenderer::default();
72 77 let mut cache = paperdoll_renderer::equipment::TextureCache::default();
@@ -87,26 +92,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
87 92 depth_range_milli: None,
88 93 },
89 94 output: out.clone(),
90 project_id: "neck-probe".into(),
95 project_id: "auto-pivot-probe".into(),
91 96 project_name: "Neck probe".into(),
92 97 body_model: production::SourceFingerprint {
93 98 path: "assets/body/test-body-male.glb".into(),
94 sha256: paperdoll_renderer::digest::sha256_hex(b"neck-probe-body"),
99 sha256: paperdoll_renderer::digest::sha256_hex(b"auto-pivot-probe-body"),
95 100 },
96 101 preset_sha256: None,
97 102 };
98 103 let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
99 104 let old = unsafe { renderer.render(gl, &old_scene) }?;
100 105 job.framing.pivot_y_milli = None;
101 let neck_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
102 let neck = unsafe { renderer.render(gl, &neck_scene) }?;
106 let pivot_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
107 let pivoted = unsafe { renderer.render(gl, &pivot_scene) }?;
103 108 unsafe { renderer.destroy(gl) };
104 assert_eq!((old.width, old.height), (neck.width, neck.height));
109 assert_eq!((old.width, old.height), (pivoted.width, pivoted.height));
105 110 let mut changed = 0usize;
106 111 for (a, b) in old
107 112 .diffuse
108 113 .chunks_exact(4)
109 .zip(neck.diffuse.chunks_exact(4))
114 .zip(pivoted.diffuse.chunks_exact(4))
110 115 {
111 116 if a != b {
112 117 changed += 1;
@@ -114,23 +119,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
114 119 }
115 120 if changed < 200 {
116 121 return Err(format!(
117 "te weinig pixelverschil tussen centrum-pivot en nekpivot: {changed}"
122 "te weinig pixelverschil tussen centrum-pivot en automatische pivot: {changed}"
118 123 ));
119 124 }
120 125 // Zero-pitch image center / feet framing must stay close after raising pivot.
121 126 job.framing.pivot_y_milli = Some(1000);
122 127 let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
123 128 job.framing.pivot_y_milli = None;
124 let neck0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
129 let pivot0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
125 130 let old0b = unsafe {
126 131 let mut r = production::GpuRenderer::default();
127 132 let b = r.render(gl, &old0)?;
128 133 r.destroy(gl);
129 134 b
130 135 };
131 let neck0b = unsafe {
136 let pivot0b = unsafe {
132 137 let mut r = production::GpuRenderer::default();
133 let b = r.render(gl, &neck0)?;
138 let b = r.render(gl, &pivot0)?;
134 139 r.destroy(gl);
135 140 b
136 141 };
@@ -138,12 +143,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
138 143 let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize;
139 144 let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4]
140 145 .iter()
141 .zip(&neck0b.diffuse[mid * 4..mid * 4 + 4])
146 .zip(&pivot0b.diffuse[mid * 4..mid * 4 + 4])
142 147 .map(|(a, b)| a.abs_diff(*b) as u32)
143 148 .sum::<u32>();
144 149 let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4]
145 150 .iter()
146 .zip(&neck0b.diffuse[feet * 4..feet * 4 + 4])
151 .zip(&pivot0b.diffuse[feet * 4..feet * 4 + 4])
147 152 .map(|(a, b)| a.abs_diff(*b) as u32)
148 153 .sum::<u32>();
149 154 write_rgba(
@@ -153,10 +158,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
153 158 &old.diffuse,
154 159 )?;
155 160 write_rgba(
156 &out.join("neck-pivot-p30-y90.png"),
157 neck.width,
158 neck.height,
159 &neck.diffuse,
161 &out.join("auto-pivot-p30-y90.png"),
162 pivoted.width,
163 pivoted.height,
164 &pivoted.diffuse,
160 165 )?;
161 166 let contact_w = 256;
162 167 let contact_h = 192;
@@ -168,9 +173,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
168 173 contact_h,
169 174 );
170 175 let right = downsample(
171 &neck.diffuse,
172 neck.width,
173 neck.height,
176 &pivoted.diffuse,
177 pivoted.width,
178 pivoted.height,
174 179 contact_w / 2,
175 180 contact_h,
176 181 );
@@ -185,7 +190,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
185 190 }
186 191 }
187 192 write_rgba(
188 &out.join("neck-vs-center-contact.png"),
193 &out.join("auto-vs-center-contact.png"),
189 194 contact_w,
190 195 contact_h,
191 196 &contact,
@@ -195,7 +200,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
195 200 "pitch": pitch,
196 201 "yaw": yaw,
197 202 "center_y_milli": 1000,
198 "neck_y_milli": neck_y,
203 "pivot_y_milli": pivot_y,
199 204 "pivot_source": resolved.source,
200 205 "highres": [old.width, old.height],
201 206 "changed_pixels_pitch30": changed,
@@ -203,16 +208,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
203 208 "zero_pitch_feet_channel_delta": feet_delta,
204 209 "artifacts": [
205 210 "center-pivot-p30-y90.png",
206 "neck-pivot-p30-y90.png",
207 "neck-vs-center-contact.png"
211 "auto-pivot-p30-y90.png",
212 "auto-vs-center-contact.png"
208 213 ]
209 214 });
210 215 std::fs::write(
211 out.join("neck-pivot-probe.json"),
216 out.join("auto-pivot-probe.json"),
212 217 serde_json::to_vec_pretty(&report).unwrap(),
213 218 )
214 219 .map_err(|e| e.to_string())?;
215 println!("NECK_PIVOT_PROBE {report}");
220 println!("AUTO_PIVOT_PROBE {report}");
216 221 Ok(report)
217 222 })();
218 223 *result.lock().unwrap() = Some(test);
@@ -1,5 +1,5 @@
1//! Real Mesa/Xvfb proof that neck pivot differs from center-only orbit at pitched yaw.
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 neck_pivot_probe --locked
2//! Usage: OUTDIR=... xvfb-run -a cargo run --example auto_pivot_probe --locked
3use paperdoll_renderer::{camera, preview, production, project::CameraFraming};
3use paperdoll_renderer::{camera, preview, production, project::CameraFraming};
4use std::{
4use std::{
5 path::{Path, PathBuf},
5 path::{Path, PathBuf},
@@ -41,7 +41,7 @@ fn downsample(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
41
41
42fn main() -> Result<(), Box<dyn std::error::Error>> {
42fn main() -> Result<(), Box<dyn std::error::Error>> {
43 let out = PathBuf::from(
43 let out = PathBuf::from(
44 std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-neck-artifacts".into()),
44 std::env::var("OUTDIR").unwrap_or_else(|_| "/workspace/paperdoll-pivot-artifacts".into()),
45 );
45 );
46 std::fs::create_dir_all(&out)?;
46 std::fs::create_dir_all(&out)?;
47 let outcome = Arc::new(Mutex::new(None));
47 let outcome = Arc::new(Mutex::new(None));
@@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
51 ..paperdoll_renderer::preview_gl::native_options()
51 ..paperdoll_renderer::preview_gl::native_options()
52 };
52 };
53 eframe::run_native(
53 eframe::run_native(
54 "Paperdoll neck pivot probe",
54 "Paperdoll auto pivot probe",
55 options,
55 options,
56 Box::new(move |creation| {
56 Box::new(move |creation| {
57 let test = (|| -> Result<serde_json::Value, String> {
57 let test = (|| -> Result<serde_json::Value, String> {
@@ -64,9 +64,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
64 .join("tests/fixtures/bodies/test-body-male.glb"),
64 .join("tests/fixtures/bodies/test-body-male.glb"),
65 )?;
65 )?;
66 let resolved = camera::resolve(&body, CameraFraming::default());
66 let resolved = camera::resolve(&body, CameraFraming::default());
67 let neck_y = resolved.framing.pivot_y_milli.ok_or("nekpivot ontbreekt")?;
67 let pivot_y = resolved
68 if neck_y == CameraFraming::default().center_y_milli {
68 .framing
69 return Err("nekpivot mag niet gelijk zijn aan default centrum".into());
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 );
70 }
75 }
71 let mut renderer = production::GpuRenderer::default();
76 let mut renderer = production::GpuRenderer::default();
72 let mut cache = paperdoll_renderer::equipment::TextureCache::default();
77 let mut cache = paperdoll_renderer::equipment::TextureCache::default();
@@ -87,26 +92,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
87 depth_range_milli: None,
92 depth_range_milli: None,
88 },
93 },
89 output: out.clone(),
94 output: out.clone(),
90 project_id: "neck-probe".into(),
95 project_id: "auto-pivot-probe".into(),
91 project_name: "Neck probe".into(),
96 project_name: "Neck probe".into(),
92 body_model: production::SourceFingerprint {
97 body_model: production::SourceFingerprint {
93 path: "assets/body/test-body-male.glb".into(),
98 path: "assets/body/test-body-male.glb".into(),
94 sha256: paperdoll_renderer::digest::sha256_hex(b"neck-probe-body"),
99 sha256: paperdoll_renderer::digest::sha256_hex(b"auto-pivot-probe-body"),
95 },
100 },
96 preset_sha256: None,
101 preset_sha256: None,
97 };
102 };
98 let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
103 let old_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
99 let old = unsafe { renderer.render(gl, &old_scene) }?;
104 let old = unsafe { renderer.render(gl, &old_scene) }?;
100 job.framing.pivot_y_milli = None;
105 job.framing.pivot_y_milli = None;
101 let neck_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
106 let pivot_scene = production::scene(&job, "walk", 0.0, pitch, yaw, &mut cache)?;
102 let neck = unsafe { renderer.render(gl, &neck_scene) }?;
107 let pivoted = unsafe { renderer.render(gl, &pivot_scene) }?;
103 unsafe { renderer.destroy(gl) };
108 unsafe { renderer.destroy(gl) };
104 assert_eq!((old.width, old.height), (neck.width, neck.height));
109 assert_eq!((old.width, old.height), (pivoted.width, pivoted.height));
105 let mut changed = 0usize;
110 let mut changed = 0usize;
106 for (a, b) in old
111 for (a, b) in old
107 .diffuse
112 .diffuse
108 .chunks_exact(4)
113 .chunks_exact(4)
109 .zip(neck.diffuse.chunks_exact(4))
114 .zip(pivoted.diffuse.chunks_exact(4))
110 {
115 {
111 if a != b {
116 if a != b {
112 changed += 1;
117 changed += 1;
@@ -114,23 +119,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
114 }
119 }
115 if changed < 200 {
120 if changed < 200 {
116 return Err(format!(
121 return Err(format!(
117 "te weinig pixelverschil tussen centrum-pivot en nekpivot: {changed}"
122 "te weinig pixelverschil tussen centrum-pivot en automatische pivot: {changed}"
118 ));
123 ));
119 }
124 }
120 // Zero-pitch image center / feet framing must stay close after raising pivot.
125 // Zero-pitch image center / feet framing must stay close after raising pivot.
121 job.framing.pivot_y_milli = Some(1000);
126 job.framing.pivot_y_milli = Some(1000);
122 let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
127 let old0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
123 job.framing.pivot_y_milli = None;
128 job.framing.pivot_y_milli = None;
124 let neck0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
129 let pivot0 = production::scene(&job, "walk", 0.0, 0., 0., &mut cache)?;
125 let old0b = unsafe {
130 let old0b = unsafe {
126 let mut r = production::GpuRenderer::default();
131 let mut r = production::GpuRenderer::default();
127 let b = r.render(gl, &old0)?;
132 let b = r.render(gl, &old0)?;
128 r.destroy(gl);
133 r.destroy(gl);
129 b
134 b
130 };
135 };
131 let neck0b = unsafe {
136 let pivot0b = unsafe {
132 let mut r = production::GpuRenderer::default();
137 let mut r = production::GpuRenderer::default();
133 let b = r.render(gl, &neck0)?;
138 let b = r.render(gl, &pivot0)?;
134 r.destroy(gl);
139 r.destroy(gl);
135 b
140 b
136 };
141 };
@@ -138,12 +143,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
138 let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize;
143 let feet = ((old0b.height * 9 / 10) * old0b.width + old0b.width / 2) as usize;
139 let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4]
144 let mid_delta = old0b.diffuse[mid * 4..mid * 4 + 4]
140 .iter()
145 .iter()
141 .zip(&neck0b.diffuse[mid * 4..mid * 4 + 4])
146 .zip(&pivot0b.diffuse[mid * 4..mid * 4 + 4])
142 .map(|(a, b)| a.abs_diff(*b) as u32)
147 .map(|(a, b)| a.abs_diff(*b) as u32)
143 .sum::<u32>();
148 .sum::<u32>();
144 let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4]
149 let feet_delta = old0b.diffuse[feet * 4..feet * 4 + 4]
145 .iter()
150 .iter()
146 .zip(&neck0b.diffuse[feet * 4..feet * 4 + 4])
151 .zip(&pivot0b.diffuse[feet * 4..feet * 4 + 4])
147 .map(|(a, b)| a.abs_diff(*b) as u32)
152 .map(|(a, b)| a.abs_diff(*b) as u32)
148 .sum::<u32>();
153 .sum::<u32>();
149 write_rgba(
154 write_rgba(
@@ -153,10 +158,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
153 &old.diffuse,
158 &old.diffuse,
154 )?;
159 )?;
155 write_rgba(
160 write_rgba(
156 &out.join("neck-pivot-p30-y90.png"),
161 &out.join("auto-pivot-p30-y90.png"),
157 neck.width,
162 pivoted.width,
158 neck.height,
163 pivoted.height,
159 &neck.diffuse,
164 &pivoted.diffuse,
160 )?;
165 )?;
161 let contact_w = 256;
166 let contact_w = 256;
162 let contact_h = 192;
167 let contact_h = 192;
@@ -168,9 +173,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
168 contact_h,
173 contact_h,
169 );
174 );
170 let right = downsample(
175 let right = downsample(
171 &neck.diffuse,
176 &pivoted.diffuse,
172 neck.width,
177 pivoted.width,
173 neck.height,
178 pivoted.height,
174 contact_w / 2,
179 contact_w / 2,
175 contact_h,
180 contact_h,
176 );
181 );
@@ -185,7 +190,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
185 }
190 }
186 }
191 }
187 write_rgba(
192 write_rgba(
188 &out.join("neck-vs-center-contact.png"),
193 &out.join("auto-vs-center-contact.png"),
189 contact_w,
194 contact_w,
190 contact_h,
195 contact_h,
191 &contact,
196 &contact,
@@ -195,7 +200,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
195 "pitch": pitch,
200 "pitch": pitch,
196 "yaw": yaw,
201 "yaw": yaw,
197 "center_y_milli": 1000,
202 "center_y_milli": 1000,
198 "neck_y_milli": neck_y,
203 "pivot_y_milli": pivot_y,
199 "pivot_source": resolved.source,
204 "pivot_source": resolved.source,
200 "highres": [old.width, old.height],
205 "highres": [old.width, old.height],
201 "changed_pixels_pitch30": changed,
206 "changed_pixels_pitch30": changed,
@@ -203,16 +208,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
203 "zero_pitch_feet_channel_delta": feet_delta,
208 "zero_pitch_feet_channel_delta": feet_delta,
204 "artifacts": [
209 "artifacts": [
205 "center-pivot-p30-y90.png",
210 "center-pivot-p30-y90.png",
206 "neck-pivot-p30-y90.png",
211 "auto-pivot-p30-y90.png",
207 "neck-vs-center-contact.png"
212 "auto-vs-center-contact.png"
208 ]
213 ]
209 });
214 });
210 std::fs::write(
215 std::fs::write(
211 out.join("neck-pivot-probe.json"),
216 out.join("auto-pivot-probe.json"),
212 serde_json::to_vec_pretty(&report).unwrap(),
217 serde_json::to_vec_pretty(&report).unwrap(),
213 )
218 )
214 .map_err(|e| e.to_string())?;
219 .map_err(|e| e.to_string())?;
215 println!("NECK_PIVOT_PROBE {report}");
220 println!("AUTO_PIVOT_PROBE {report}");
216 Ok(report)
221 Ok(report)
217 })();
222 })();
218 *result.lock().unwrap() = Some(test);
223 *result.lock().unwrap() = Some(test);