BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / export_fixture.rs

export_fixture.rs

export_fixture.rs 9.5 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 /// When true (default), drop loose/checkpoint cells after atlases (Tim t1065u).
49 pub atlas_only: bool,
50}
51
52impl SyntheticRun {
53 pub fn body(project_id: &str, sex: &str, body_bytes: &[u8]) -> Self {
54 Self {
55 project_id: project_id.into(),
56 sex: sex.into(),
57 set: "body".into(),
58 equipment_sha256: None,
59 body_bytes: body_bytes.to_vec(),
60 target: [1, 1],
61 fps: crate::project::DEFAULT_FPS,
62 framing: CameraFraming {
63 height_milli: 1800,
64 center_y_milli: 900,
65 pivot_y_milli: Some(1500),
66 depth_range_milli: None,
67 },
68 clips: vec![("bind".into(), 1, 0.0)],
69 depth_format: crate::camera::DepthFormat::GRAY16,
70 trim: false,
71 mask: None,
72 atlas_only: true,
73 }
74 }
75
76 pub fn plan(&self) -> Plan {
77 let clips: Vec<ClipPlan> = self
78 .clips
79 .iter()
80 .enumerate()
81 .map(|(i, (name, frames, _))| ClipPlan {
82 source_name: name.clone(),
83 token: production::clip_token(i, name),
84 frames: *frames,
85 })
86 .collect();
87 let frames: u32 = clips.iter().map(|c| c.frames).sum();
88 let images = frames * crate::preview::PRODUCTION_VIEWS;
89 Plan {
90 clips,
91 images,
92 passes: images * PASSES.len() as u32,
93 files: images * 4,
94 }
95 }
96
97 /// `render.json` with the same keys production writes.
98 pub fn render_doc(&self) -> serde_json::Value {
99 let plan = self.plan();
100 let clips: Vec<_> = plan
101 .clips
102 .iter()
103 .zip(&self.clips)
104 .map(|(c, (_, _, duration))| {
105 serde_json::json!({
106 "name": c.source_name,
107 "token": c.token,
108 "frames": c.frames,
109 "duration": duration,
110 })
111 })
112 .collect();
113 let equipment = self
114 .equipment_sha256
115 .as_ref()
116 .map(|sha| serde_json::json!({"name": self.set, "sha256": sha}));
117 serde_json::json!({
118 "format_version": OUTPUT_META_VERSION,
119 "status": "incomplete",
120 "project": {"id": self.project_id, "name": "Synthetic"},
121 "sex": self.sex,
122 "set": self.set,
123 "body_model": {"path": "assets/body/synthetic.glb", "sha256": digest::sha256_hex(&self.body_bytes)},
124 "equipment_preset": equipment,
125 "target": self.target,
126 "fps": self.fps,
127 "highres_scale": production::SCALE,
128 "framing": self.framing,
129 "pivot_source": {"kind": "manual"},
130 "near": crate::camera::depth_range(self.framing).0,
131 "far": crate::camera::depth_range(self.framing).1,
132 "depth_near": crate::camera::depth_range(self.framing).0,
133 "depth_far": crate::camera::depth_range(self.framing).1,
134 "depth_encoding": self.depth_format.encoding.token(),
135 "depth_bits": self.depth_format.bits,
136 "normal_space": "view (+Y up), PNG rows top-down",
137 "camera_distance": 10,
138 "yaw_zero": "camera on +Z",
139 "pitches": PRODUCTION_PITCHES,
140 "yaws": production::production_yaws(),
141 "yaw_step": 30,
142 "sampling": "synthetic fixture",
143 "alpha": "straight",
144 "diffuse": "unlit",
145 "depth": self.depth_format.description(),
146 "specular": "scalar",
147 "pass_order": PASSES,
148 "saved_maps": ["diffuse", "depth", "normal", "specular"],
149 "clips": clips,
150 "images": plan.images,
151 "passes": plan.passes,
152 "png_files": plan.files,
153 })
154 }
155
156 /// Write the complete run into `root` (created if needed).
157 pub fn write(
158 &self,
159 root: &Path,
160 pixel: impl Fn(usize, u32, i32, u32) -> Pixel,
161 ) -> Result<(), String> {
162 std::fs::create_dir_all(root).map_err(|e| e.to_string())?;
163 let plan = self.plan();
164 let render = self.render_doc();
165 let [w, h] = self.target;
166 let n = w as usize * h as usize;
167 let mut sums = BTreeMap::new();
168 for s in production::samples(&plan) {
169 let px = pixel(s.clip, s.frame, s.pitch, s.yaw);
170 let inside: Vec<bool> = (0..n as u32)
171 .map(|i| {
172 self.mask
173 .is_none_or(|m| m(s.clip, s.frame, s.pitch, s.yaw, i % w, i / w))
174 })
175 .collect();
176 for map in production::SAVED_ORDER {
177 let rel = production::output_path(
178 &self.sex,
179 &self.set,
180 &plan.clips[s.clip].token,
181 s.frame,
182 s.pitch,
183 s.yaw,
184 map,
185 )?;
186 let digest = if map == "depth" {
187 let pair = if px.diffuse[3] == 0 {
188 (0., 0.)
189 } else {
190 (f64::from(px.depth) / 65535., 1.)
191 };
192 let pixels: Vec<(f64, f64)> = inside
193 .iter()
194 .map(|&i| if i { pair } else { (0., 0.) })
195 .collect();
196 production::write_depth_fmt(root, &rel, w, h, &pixels, self.depth_format)?
197 } else {
198 let rgba = match map {
199 "diffuse" => px.diffuse,
200 "normal" => px.normal,
201 _ => px.specular,
202 };
203 production::write_rgba(
204 root,
205 &rel,
206 &ImageBuf {
207 width: w,
208 height: h,
209 rgba: inside
210 .iter()
211 .flat_map(|&i| if i { rgba } else { [0; 4] })
212 .collect(),
213 },
214 )?
215 };
216 sums.insert(rel.to_string_lossy().replace('\\', "/"), digest);
217 }
218 }
219 write_json(root, "render.json", &render)?;
220 let cancel = AtomicBool::new(false);
221 let atlas = atlas::write_atlases(
222 &atlas::AtlasJob {
223 root,
224 render_doc: &render,
225 plan: &plan,
226 sex: &self.sex,
227 set: &self.set,
228 cell_width: w,
229 cell_height: h,
230 png_sha256: &sums,
231 attempt: 0,
232 cancel: &cancel,
233 trim: self.trim,
234 cells: None,
235 },
236 &mut |_| {},
237 )?;
238 let identity = complete_identity_json(&render)?;
239 write_json(
240 root,
241 "complete.json",
242 &serde_json::json!({
243 "format_version": OUTPUT_META_VERSION,
244 "status": "complete",
245 "identity": identity,
246 "passes": plan.passes,
247 "png_files": plan.files,
248 "png_sha256": sums,
249 "atlas": atlas.to_json(),
250 }),
251 )?;
252 // Same final layout as production: only .ppda + atlases (Tim t1065u).
253 if self.atlas_only {
254 crate::resume::remove_cell_checkpoints(root)?;
255 }
256 crate::resume::strip_bookkeeping(root)
257 }
258}
259
260fn write_json(root: &Path, name: &str, value: &serde_json::Value) -> Result<(), String> {
261 let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
262 production::publish_bytes(root, Path::new(name), &bytes)
263}