BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / preview2d_smoke.rs

preview2d_smoke.rs

preview2d_smoke.rs 7.0 KiB Download Raw
1//! Native GUI smoke for the rendered 2D preview tab: it loads a synthetic
2//! export through its `.ppda` + atlases (loose PNGs are deleted first).
3//! Usage: preview2d_smoke OUTPUT_DIR [EXISTING_PROJECT_DIR]
4
5use eframe::App;
6use paperdoll_renderer::editor::EditorApp;
7use paperdoll_renderer::export_fixture::{Pixel, SyntheticRun};
8use paperdoll_renderer::preview2d::discover_completed_runs;
9use paperdoll_renderer::render_job::ImageBuf;
10use paperdoll_renderer::session::Session;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::time::{Duration, Instant};
14
15struct Smoke {
16 editor: EditorApp,
17 out: PathBuf,
18 started: Instant,
19 frames: u64,
20 done: bool,
21 shot_requested: bool,
22 shot_saved: bool,
23}
24
25impl App for Smoke {
26 fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
27 self.frames += 1;
28 self.editor.update(ctx, frame);
29 if self.frames == 2 {
30 self.editor.open_preview2d();
31 }
32 if self.frames == 4 {
33 // Steepest pitch row (+60) and yaw 30 prove the five-row atlas layout.
34 let state = self.editor.preview2d_state_mut();
35 state.step_pitch(2);
36 state.step_yaw(1);
37 }
38 ctx.request_repaint();
39 if self.frames >= 30 && !self.shot_saved {
40 if !self.shot_requested {
41 self.shot_requested = true;
42 ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Screenshot);
43 } else if let Some(image) = ctx.input(|i| {
44 i.raw.events.iter().find_map(|e| match e {
45 eframe::egui::Event::Screenshot { image, .. } => Some(image.clone()),
46 _ => None,
47 })
48 }) {
49 let [w, h] = image.size;
50 let rgba: Vec<u8> = image.pixels.iter().flat_map(|p| p.to_array()).collect();
51 write_png(
52 &self.out.join("preview2d-gui.png"),
53 &ImageBuf {
54 width: w as u32,
55 height: h as u32,
56 rgba,
57 },
58 )
59 .expect("png");
60 self.shot_saved = true;
61 }
62 }
63 let ready = self.editor.preview2d_image().is_some()
64 || self.editor.preview2d_state().diagnostic.is_some();
65 let timeout = self.started.elapsed() > Duration::from_secs(20);
66 if self.done {
67 return;
68 }
69 if (self.shot_saved && ready) || timeout {
70 self.done = true;
71 let state = self.editor.preview2d_state();
72 let image_ok = if let Some(img) = self.editor.preview2d_image() {
73 write_png(&self.out.join("preview2d-composed.png"), img).expect("png");
74 true
75 } else {
76 false
77 };
78 let value = serde_json::json!({
79 "success": image_ok && self.shot_saved,
80 "screenshot": self.shot_saved,
81 "gui_updates": self.frames,
82 "sex": state.sex_token(),
83 "clip": state.clip_token,
84 "frame": state.frame_index(),
85 "yaw": state.yaw_deg(),
86 "pitch": state.pitch_deg(),
87 "playing": state.playing,
88 "diagnostic": state.diagnostic,
89 "elapsed_secs": state.elapsed_secs,
90 });
91 fs::write(
92 self.out.join("gui-smoke.json"),
93 serde_json::to_vec_pretty(&value).unwrap(),
94 )
95 .unwrap();
96 println!("RESULT {value}");
97 ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
98 }
99 }
100
101 fn on_exit(&mut self, gl: Option<&glow::Context>) {
102 self.editor.on_exit(gl);
103 }
104}
105
106fn write_png(path: &Path, img: &ImageBuf) -> Result<(), String> {
107 let file = fs::File::create(path).map_err(|e| e.to_string())?;
108 let mut encoder = png::Encoder::new(file, img.width, img.height);
109 encoder.set_color(png::ColorType::Rgba);
110 encoder.set_depth(png::BitDepth::Eight);
111 let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
112 writer
113 .write_image_data(&img.rgba)
114 .map_err(|e| e.to_string())
115}
116
117/// Body export: 2 frames, every view gets a distinct colour from its
118/// pitch/yaw, written with real loose PNGs + atlases + .ppda.
119fn install_body_export(project: &Path) -> Result<(), String> {
120 let mut spec = SyntheticRun::body("smoke-preview2d", "male", b"preview2d-smoke-body");
121 spec.target = [2, 2];
122 spec.clips = vec![("walk".into(), 2, 0.5)];
123 spec.write(
124 &project.join("renders/male-body-atlas"),
125 |_, frame, pitch, yaw| Pixel {
126 diffuse: [
127 40u8.wrapping_add(frame as u8 * 80),
128 (pitch + 90) as u8,
129 (yaw / 2) as u8,
130 255,
131 ],
132 normal: [128, 128, 255, 255],
133 specular: [0, 0, 0, 255],
134 depth: 25000 - frame as u16 * 1000,
135 },
136 )
137}
138
139fn main() -> Result<(), Box<dyn std::error::Error>> {
140 let out = PathBuf::from(
141 std::env::args()
142 .nth(1)
143 .ok_or("usage: preview2d_smoke OUTPUT_DIR")?,
144 );
145 fs::create_dir_all(&out)?;
146 // Optional second argument: an existing project with real production
147 // exports (its loose PNGs are left alone).
148 let project = match std::env::args().nth(2) {
149 Some(existing) => PathBuf::from(existing),
150 None => {
151 let project = out.join("project");
152 let mut session = Session::new();
153 session.install_new_project(&project)?;
154 session.save()?;
155 install_body_export(&project)?;
156 // Prove the preview reads the atlases: remove every loose PNG.
157 fs::remove_dir_all(project.join("renders/male-body-atlas/male"))?;
158 project
159 }
160 };
161 let catalog = discover_completed_runs(&project);
162 fs::write(
163 out.join("catalog.json"),
164 serde_json::to_vec_pretty(&serde_json::json!({
165 "count": catalog.len(),
166 "fps": catalog.iter().map(|e| e.fps).collect::<Vec<_>>(),
167 "sets": catalog.iter().map(|e| format!("{}:{}", e.sex, e.set)).collect::<Vec<_>>(),
168 }))?,
169 )?;
170
171 let mut editor = EditorApp::new(Box::new(paperdoll_renderer::dialogs::SystemDialogs::new()));
172 editor.open_path(project.join("character.ppd"));
173
174 eframe::run_native(
175 "Paperdoll Preview2d Smoke",
176 eframe::NativeOptions {
177 viewport: eframe::egui::ViewportBuilder::default()
178 .with_inner_size([1100.0, 720.0])
179 .with_visible(true),
180 ..Default::default()
181 },
182 Box::new(move |_cc| {
183 Ok(Box::new(Smoke {
184 editor,
185 out: out.clone(),
186 started: Instant::now(),
187 frames: 0,
188 done: false,
189 shot_requested: false,
190 shot_saved: false,
191 }) as Box<dyn App>)
192 }),
193 )
194 .map_err(|e| e.to_string())?;
195 Ok(())
196}