BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / export_fixture.rs

export_fixture.rs

export_fixture.rs 9.2 KiB Download Raw
1//! Synthetic completed export runs for tests, examples and GUI smokes.
2//!
3//! Writes real loose target PNGs (uniform colour per cell), schema-4
4//! `render.json`, the per-map atlases + `.ppda` through the production
5//! [`crate::atlas::write_atlases`] path, and `complete.json`. This replaces
6//! hand-written partial manifests; it is not used by the renderer itself.
7use crate::{
8 atlas, digest,
9 export_set::{complete_identity_json, OUTPUT_META_VERSION},
10 preview::PRODUCTION_PITCHES,
11 production::{self, ClipPlan, Plan, PASSES},
12 project::CameraFraming,
13 render_job::ImageBuf,
14};
15use std::{collections::BTreeMap, path::Path, sync::atomic::AtomicBool};
16
17/// Uniform per-cell map values.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Pixel {
20 pub diffuse: [u8; 4],
21 pub normal: [u8; 4],
22 pub specular: [u8; 4],
23 pub depth: u16,
24}
25
26/// (clip, frame, pitch, yaw, x, y) → pixel has content.
27pub type PixelMask = fn(usize, u32, i32, u32, u32, u32) -> bool;
28
29#[derive(Debug, Clone)]
30pub struct SyntheticRun {
31 pub project_id: String,
32 pub sex: String,
33 pub set: String,
34 pub equipment_sha256: Option<String>,
35 pub body_bytes: Vec<u8>,
36 pub target: [u32; 2],
37 pub fps: u32,
38 pub framing: CameraFraming,
39 /// Depth PNG format (default 16-bit gray, like pre-t983u exports).
40 pub depth_format: crate::camera::DepthFormat,
41 /// (source name, frames, duration seconds)
42 pub clips: Vec<(String, u32, f32)>,
43 /// Trimmed atlases (t1030u); off by default in fixtures.
44 pub trim: bool,
45 /// Optional per-pixel content mask (clip, frame, pitch, yaw, x, y); pixels
46 /// outside the mask are all-zero in every map.
47 pub mask: Option<PixelMask>,
48}
49
50impl SyntheticRun {
51 pub fn body(project_id: &str, sex: &str, body_bytes: &[u8]) -> Self {
52 Self {
53 project_id: project_id.into(),
54 sex: sex.into(),
55 set: "body".into(),
56 equipment_sha256: None,
57 body_bytes: body_bytes.to_vec(),
58 target: [1, 1],
59 fps: crate::project::DEFAULT_FPS,
60 framing: CameraFraming {
61 height_milli: 1800,
62 center_y_milli: 900,
63 pivot_y_milli: Some(1500),
64 depth_range_milli: None,
65 },
66 clips: vec![("bind".into(), 1, 0.0)],
67 depth_format: crate::camera::DepthFormat::GRAY16,
68 trim: false,
69 mask: None,
70 }
71 }
72
73 pub fn plan(&self) -> Plan {
74 let clips: Vec<ClipPlan> = self
75 .clips
76 .iter()
77 .enumerate()
78 .map(|(i, (name, frames, _))| ClipPlan {
79 source_name: name.clone(),
80 token: production::clip_token(i, name),
81 frames: *frames,
82 })
83 .collect();
84 let frames: u32 = clips.iter().map(|c| c.frames).sum();
85 let images = frames * crate::preview::PRODUCTION_VIEWS;
86 Plan {
87 clips,
88 images,
89 passes: images * PASSES.len() as u32,
90 files: images * 4,
91 }
92 }
93
94 /// `render.json` with the same keys production writes.
95 pub fn render_doc(&self) -> serde_json::Value {
96 let plan = self.plan();
97 let clips: Vec<_> = plan
98 .clips
99 .iter()
100 .zip(&self.clips)
101 .map(|(c, (_, _, duration))| {
102 serde_json::json!({
103 "name": c.source_name,
104 "token": c.token,
105 "frames": c.frames,
106 "duration": duration,
107 })
108 })
109 .collect();
110 let equipment = self
111 .equipment_sha256
112 .as_ref()
113 .map(|sha| serde_json::json!({"name": self.set, "sha256": sha}));
114 serde_json::json!({
115 "format_version": OUTPUT_META_VERSION,
116 "status": "incomplete",
117 "project": {"id": self.project_id, "name": "Synthetic"},
118 "sex": self.sex,
119 "set": self.set,
120 "body_model": {"path": "assets/body/synthetic.glb", "sha256": digest::sha256_hex(&self.body_bytes)},
121 "equipment_preset": equipment,
122 "target": self.target,
123 "fps": self.fps,
124 "highres_scale": production::SCALE,
125 "framing": self.framing,
126 "pivot_source": {"kind": "manual"},
127 "near": crate::camera::depth_range(self.framing).0,
128 "far": crate::camera::depth_range(self.framing).1,
129 "depth_near": crate::camera::depth_range(self.framing).0,
130 "depth_far": crate::camera::depth_range(self.framing).1,
131 "depth_encoding": self.depth_format.encoding.token(),
132 "depth_bits": self.depth_format.bits,
133 "normal_space": "view (+Y up), PNG rows top-down",
134 "camera_distance": 10,
135 "yaw_zero": "camera on +Z",
136 "pitches": PRODUCTION_PITCHES,
137 "yaws": production::production_yaws(),
138 "yaw_step": 30,
139 "sampling": "synthetic fixture",
140 "alpha": "straight",
141 "diffuse": "unlit",
142 "depth": self.depth_format.description(),
143 "specular": "scalar",
144 "pass_order": PASSES,
145 "saved_maps": ["diffuse", "depth", "normal", "specular"],
146 "clips": clips,
147 "images": plan.images,
148 "passes": plan.passes,
149 "png_files": plan.files,
150 })
151 }
152
153 /// Write the complete run into `root` (created if needed).
154 pub fn write(
155 &self,
156 root: &Path,
157 pixel: impl Fn(usize, u32, i32, u32) -> Pixel,
158 ) -> Result<(), String> {
159 std::fs::create_dir_all(root).map_err(|e| e.to_string())?;
160 let plan = self.plan();
161 let render = self.render_doc();
162 let [w, h] = self.target;
163 let n = w as usize * h as usize;
164 let mut sums = BTreeMap::new();
165 for s in production::samples(&plan) {
166 let px = pixel(s.clip, s.frame, s.pitch, s.yaw);
167 let inside: Vec<bool> = (0..n as u32)
168 .map(|i| {
169 self.mask
170 .is_none_or(|m| m(s.clip, s.frame, s.pitch, s.yaw, i % w, i / w))
171 })
172 .collect();
173 for map in production::SAVED_ORDER {
174 let rel = production::output_path(
175 &self.sex,
176 &self.set,
177 &plan.clips[s.clip].token,
178 s.frame,
179 s.pitch,
180 s.yaw,
181 map,
182 )?;
183 let digest = if map == "depth" {
184 let pair = if px.diffuse[3] == 0 {
185 (0., 0.)
186 } else {
187 (f64::from(px.depth) / 65535., 1.)
188 };
189 let pixels: Vec<(f64, f64)> = inside
190 .iter()
191 .map(|&i| if i { pair } else { (0., 0.) })
192 .collect();
193 production::write_depth_fmt(root, &rel, w, h, &pixels, self.depth_format)?
194 } else {
195 let rgba = match map {
196 "diffuse" => px.diffuse,
197 "normal" => px.normal,
198 _ => px.specular,
199 };
200 production::write_rgba(
201 root,
202 &rel,
203 &ImageBuf {
204 width: w,
205 height: h,
206 rgba: inside
207 .iter()
208 .flat_map(|&i| if i { rgba } else { [0; 4] })
209 .collect(),
210 },
211 )?
212 };
213 sums.insert(rel.to_string_lossy().replace('\\', "/"), digest);
214 }
215 }
216 write_json(root, "render.json", &render)?;
217 let cancel = AtomicBool::new(false);
218 let atlas = atlas::write_atlases(
219 &atlas::AtlasJob {
220 root,
221 render_doc: &render,
222 plan: &plan,
223 sex: &self.sex,
224 set: &self.set,
225 cell_width: w,
226 cell_height: h,
227 png_sha256: &sums,
228 attempt: 0,
229 cancel: &cancel,
230 trim: self.trim,
231 cells: None,
232 },
233 &mut |_| {},
234 )?;
235 let identity = complete_identity_json(&render)?;
236 write_json(
237 root,
238 "complete.json",
239 &serde_json::json!({
240 "format_version": OUTPUT_META_VERSION,
241 "status": "complete",
242 "identity": identity,
243 "passes": plan.passes,
244 "png_files": plan.files,
245 "png_sha256": sums,
246 "atlas": atlas.to_json(),
247 }),
248 )?;
249 // Same final layout as production: only .ppda, atlases and `raw files/`.
250 crate::resume::strip_bookkeeping(root)
251 }
252}
253
254fn write_json(root: &Path, name: &str, value: &serde_json::Value) -> Result<(), String> {
255 let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
256 production::publish_bytes(root, Path::new(name), &bytes)
257}