BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / export_fixture.rs

export_fixture.rs

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