BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / export_fixture.rs

export_fixture.rs

export_fixture.rs 7.6 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, FAR, NEAR, 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": NEAR,
114 "far": FAR,
115 "normal_space": "view (+Y up), PNG rows top-down",
116 "camera_distance": 10,
117 "yaw_zero": "camera on -Z",
118 "pitches": PRODUCTION_PITCHES,
119 "yaws": production::production_yaws(),
120 "yaw_step": 30,
121 "sampling": "synthetic fixture",
122 "alpha": "straight",
123 "diffuse": "unlit",
124 "depth": "16-bit gray+alpha; distance=near+(sample/65535)*(far-near)",
125 "specular": "scalar",
126 "pass_order": PASSES,
127 "saved_maps": ["diffuse", "depth", "normal", "specular"],
128 "clips": clips,
129 "images": plan.images,
130 "passes": plan.passes,
131 "png_files": plan.files,
132 })
133 }
134
135 /// Write the complete run into `root` (created if needed).
136 pub fn write(
137 &self,
138 root: &Path,
139 pixel: impl Fn(usize, u32, i32, u32) -> Pixel,
140 ) -> Result<(), String> {
141 std::fs::create_dir_all(root).map_err(|e| e.to_string())?;
142 let plan = self.plan();
143 let render = self.render_doc();
144 let [w, h] = self.target;
145 let n = w as usize * h as usize;
146 let mut sums = BTreeMap::new();
147 for s in production::samples(&plan) {
148 let px = pixel(s.clip, s.frame, s.pitch, s.yaw);
149 for map in production::SAVED_ORDER {
150 let rel = production::output_path(
151 &self.sex,
152 &self.set,
153 &plan.clips[s.clip].token,
154 s.frame,
155 s.pitch,
156 s.yaw,
157 map,
158 )?;
159 let digest = if map == "depth" {
160 let pair = if px.diffuse[3] == 0 {
161 [0, 0]
162 } else {
163 [px.depth, 65535]
164 };
165 let samples: Vec<u16> = pair.repeat(n);
166 production::write_depth(root, &rel, w, h, &samples)?
167 } else {
168 let rgba = match map {
169 "diffuse" => px.diffuse,
170 "normal" => px.normal,
171 _ => px.specular,
172 };
173 production::write_rgba(
174 root,
175 &rel,
176 &ImageBuf {
177 width: w,
178 height: h,
179 rgba: rgba.repeat(n),
180 },
181 )?
182 };
183 sums.insert(rel.to_string_lossy().replace('\\', "/"), digest);
184 }
185 }
186 write_json(root, "render.json", &render)?;
187 let cancel = AtomicBool::new(false);
188 let atlas = atlas::write_atlases(
189 &atlas::AtlasJob {
190 root,
191 render_doc: &render,
192 plan: &plan,
193 sex: &self.sex,
194 set: &self.set,
195 cell_width: w,
196 cell_height: h,
197 png_sha256: &sums,
198 attempt: 0,
199 cancel: &cancel,
200 },
201 &mut |_| {},
202 )?;
203 let identity = complete_identity_json(&render)?;
204 write_json(
205 root,
206 "complete.json",
207 &serde_json::json!({
208 "format_version": OUTPUT_META_VERSION,
209 "status": "complete",
210 "identity": identity,
211 "passes": plan.passes,
212 "png_files": plan.files,
213 "png_sha256": sums,
214 "atlas": atlas.to_json(),
215 }),
216 )
217 }
218}
219
220fn write_json(root: &Path, name: &str, value: &serde_json::Value) -> Result<(), String> {
221 let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
222 production::publish_bytes(root, Path::new(name), &bytes)
223}