BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / preview2d_smoke.rs

preview2d_smoke.rs

preview2d_smoke.rs 9.6 KiB Download Raw
1//! Native GUI smoke for the rendered 2D preview tab (schema-3 export fixtures).
2//! Usage: preview2d_smoke OUTPUT_DIR
3
4use eframe::App;
5use paperdoll_renderer::digest;
6use paperdoll_renderer::editor::EditorApp;
7use paperdoll_renderer::export_set::complete_identity_json;
8use paperdoll_renderer::preview2d::discover_completed_runs;
9use paperdoll_renderer::production::{write_depth, write_rgba, FAR, NEAR, PASSES};
10use paperdoll_renderer::project::CameraFraming;
11use paperdoll_renderer::render_job::ImageBuf;
12use paperdoll_renderer::session::Session;
13use std::collections::BTreeMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::time::{Duration, Instant};
17
18struct Smoke {
19 editor: EditorApp,
20 out: PathBuf,
21 started: Instant,
22 frames: u64,
23 done: bool,
24}
25
26impl App for Smoke {
27 fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
28 self.frames += 1;
29 self.editor.update(ctx, frame);
30 if self.frames == 2 {
31 self.editor.open_preview2d();
32 }
33 let ready = self.editor.preview2d_image().is_some()
34 || self.editor.preview2d_state().diagnostic.is_some();
35 let timeout = self.started.elapsed() > Duration::from_secs(8);
36 if self.done {
37 return;
38 }
39 if (self.frames >= 12 && ready) || timeout {
40 self.done = true;
41 let state = self.editor.preview2d_state();
42 let image_ok = if let Some(img) = self.editor.preview2d_image() {
43 write_png(&self.out.join("preview2d-composed.png"), img).expect("png");
44 true
45 } else {
46 false
47 };
48 let value = serde_json::json!({
49 "success": image_ok,
50 "gui_updates": self.frames,
51 "sex": state.sex_token(),
52 "clip": state.clip_token,
53 "frame": state.frame_index(),
54 "yaw": state.yaw_deg(),
55 "pitch": state.pitch_deg(),
56 "playing": state.playing,
57 "diagnostic": state.diagnostic,
58 "elapsed_secs": state.elapsed_secs,
59 });
60 fs::write(
61 self.out.join("gui-smoke.json"),
62 serde_json::to_vec_pretty(&value).unwrap(),
63 )
64 .unwrap();
65 println!("RESULT {value}");
66 ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
67 }
68 }
69
70 fn on_exit(&mut self, gl: Option<&glow::Context>) {
71 self.editor.on_exit(gl);
72 }
73}
74
75fn write_png(path: &Path, img: &ImageBuf) -> Result<(), String> {
76 let file = fs::File::create(path).map_err(|e| e.to_string())?;
77 let mut encoder = png::Encoder::new(file, img.width, img.height);
78 encoder.set_color(png::ColorType::Rgba);
79 encoder.set_depth(png::BitDepth::Eight);
80 let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
81 writer
82 .write_image_data(&img.rgba)
83 .map_err(|e| e.to_string())
84}
85
86fn write_atomic_json(root: &Path, name: &str, value: &serde_json::Value) -> Result<(), String> {
87 let data = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
88 let path = root.join(name);
89 let temp = path.with_extension(format!("part-{}", std::process::id()));
90 fs::write(&temp, &data).map_err(|e| e.to_string())?;
91 fs::hard_link(&temp, &path).map_err(|e| e.to_string())?;
92 let _ = fs::remove_file(temp);
93 Ok(())
94}
95
96#[allow(clippy::too_many_arguments)]
97fn publish_pose(
98 root: &Path,
99 sex: &str,
100 set: &str,
101 clip: &str,
102 frame: u32,
103 pitch: i32,
104 yaw: u32,
105 diffuse: [u8; 4],
106 depth_gray: u16,
107) -> Result<BTreeMap<String, String>, String> {
108 let mut map = BTreeMap::new();
109 let base = format!("{sex}/{set}/{clip}/f{frame:06}/p{pitch:+03}_y{yaw:03}");
110 let normal = [128u8, 128, 255, 255];
111 let specular = [0u8, 0, 0, 255];
112 for (rel, kind) in [
113 (format!("{base}_diffuse.png"), "diffuse"),
114 (format!("{base}_depth.png"), "depth"),
115 (format!("{base}_normal.png"), "normal"),
116 (format!("{base}_specular.png"), "specular"),
117 ] {
118 let path = Path::new(&rel);
119 match kind {
120 "depth" => {
121 write_depth(
122 root,
123 path,
124 2,
125 2,
126 &[
127 depth_gray, 65535, depth_gray, 65535, depth_gray, 65535, depth_gray, 65535,
128 ],
129 )?;
130 }
131 "normal" => {
132 write_rgba(
133 root,
134 path,
135 &ImageBuf {
136 width: 2,
137 height: 2,
138 rgba: normal.repeat(4),
139 },
140 )?;
141 }
142 "specular" => {
143 write_rgba(
144 root,
145 path,
146 &ImageBuf {
147 width: 2,
148 height: 2,
149 rgba: specular.repeat(4),
150 },
151 )?;
152 }
153 _ => {
154 write_rgba(
155 root,
156 path,
157 &ImageBuf {
158 width: 2,
159 height: 2,
160 rgba: diffuse.repeat(4),
161 },
162 )?;
163 }
164 }
165 let bytes = fs::read(root.join(&rel)).map_err(|e| e.to_string())?;
166 map.insert(rel, digest::sha256_hex(&bytes));
167 }
168 Ok(map)
169}
170
171fn install_body_export(project: &Path) -> Result<(), String> {
172 let run = project.join("renders/run-preview2d-body");
173 fs::create_dir_all(&run).map_err(|e| e.to_string())?;
174 let body_bytes = b"preview2d-smoke-body";
175 let body_hash = digest::sha256_hex(body_bytes);
176 let clips = vec![serde_json::json!({
177 "name": "walk",
178 "token": "clip000-walk",
179 "frames": 2,
180 "duration": 0.1667
181 })];
182 let mut checksums = BTreeMap::new();
183 for frame in [0u32, 1] {
184 for pitch in [-30i32, 0, 30] {
185 for yaw in [0u32, 30, 90] {
186 let tint = [40u8.wrapping_add(frame as u8 * 80), 90, 140, 255];
187 checksums.extend(publish_pose(
188 &run,
189 "male",
190 "body",
191 "clip000-walk",
192 frame,
193 pitch,
194 yaw,
195 tint,
196 25000 - frame as u16 * 1000,
197 )?);
198 }
199 }
200 }
201 let png_files = checksums.len() as u32;
202 let render = serde_json::json!({
203 "format_version": 3,
204 "status": "incomplete",
205 "project": {"id": "smoke-preview2d", "name": "Preview2d Smoke"},
206 "sex": "male",
207 "set": "body",
208 "body_model": {"path": "assets/body/test-body-male.glb", "sha256": body_hash},
209 "equipment_preset": null,
210 "target": [2, 2],
211 "fps": 12,
212 "highres_scale": 8,
213 "framing": CameraFraming {
214 height_milli: 1800,
215 center_y_milli: 900,
216 pivot_y_milli: Some(1500),
217 },
218 "pivot_source": {"kind": "manual"},
219 "near": NEAR,
220 "far": FAR,
221 "camera_distance": 10.0,
222 "yaw_zero": "camera on -Z",
223 "normal_space": "view (+Y up), PNG rows top-down",
224 "pitches": [-30, 0, 30],
225 "yaw_step": 30,
226 "sampling": "smoke",
227 "alpha": "straight",
228 "diffuse": "unlit",
229 "depth": "16-bit",
230 "specular": "scalar",
231 "pass_order": PASSES,
232 "saved_maps": ["diffuse", "depth", "normal", "specular"],
233 "clips": clips,
234 "images": 2 * 36,
235 "passes": 2 * 36 * 6,
236 "png_files": png_files
237 });
238 write_atomic_json(&run, "render.json", &render)?;
239 let identity = complete_identity_json(&render)?;
240 write_atomic_json(
241 &run,
242 "complete.json",
243 &serde_json::json!({
244 "format_version": 3,
245 "status": "complete",
246 "identity": identity,
247 "passes": 2 * 36 * 6,
248 "png_files": png_files,
249 "png_sha256": checksums
250 }),
251 )?;
252 Ok(())
253}
254
255fn main() -> Result<(), Box<dyn std::error::Error>> {
256 let out = PathBuf::from(
257 std::env::args()
258 .nth(1)
259 .ok_or("usage: preview2d_smoke OUTPUT_DIR")?,
260 );
261 fs::create_dir_all(&out)?;
262 let project = out.join("project");
263 let mut session = Session::new();
264 session.install_new_project(&project)?;
265 session.save()?;
266 install_body_export(&project)?;
267 let catalog = discover_completed_runs(&project);
268 fs::write(
269 out.join("catalog.json"),
270 serde_json::to_vec_pretty(&serde_json::json!({
271 "count": catalog.len(),
272 "sets": catalog.iter().map(|e| format!("{}:{}", e.sex, e.set)).collect::<Vec<_>>(),
273 }))?,
274 )?;
275
276 let mut editor = EditorApp::new(Box::new(paperdoll_renderer::dialogs::SystemDialogs::new()));
277 editor.open_path(project.join("character.ppd"));
278
279 eframe::run_native(
280 "Paperdoll Preview2d Smoke",
281 eframe::NativeOptions {
282 viewport: eframe::egui::ViewportBuilder::default()
283 .with_inner_size([1100.0, 720.0])
284 .with_visible(true),
285 ..Default::default()
286 },
287 Box::new(move |_cc| {
288 Ok(Box::new(Smoke {
289 editor,
290 out: out.clone(),
291 started: Instant::now(),
292 frames: 0,
293 done: false,
294 }) as Box<dyn App>)
295 }),
296 )
297 .map_err(|e| e.to_string())?;
298 Ok(())
299}