BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / export_fixture.rs

export_fixture.rs

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