BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / production_fixture.rs

production_fixture.rs

production_fixture.rs 6.3 KiB Download Raw
1//! Actual native end-to-end batch smoke; no GUI input automation or preview delays.
2//! Usage: production_fixture ROOT MALE.(glb|fbx) FEMALE.(glb|fbx) male|female WIDTH HEIGHT FPS [equipment|default-hair|default-body] [--height-milli=N] [--center-milli=N]
3use eframe::App;
4use paperdoll_renderer::{
5 editor::EditorApp,
6 equipment, preview,
7 session::{adopt_file, Session, Sex},
8};
9use std::path::PathBuf;
10struct Smoke {
11 editor: EditorApp,
12 root: PathBuf,
13 last: u32,
14 frames: u64,
15 done: bool,
16}
17impl App for Smoke {
18 fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
19 self.frames += 1;
20 self.editor.update(ctx, frame);
21 let (completed, total, label) = self.editor.render_progress();
22 if completed != self.last {
23 println!("PASS {completed}/{total}: {label}");
24 self.last = completed;
25 }
26 if !self.editor.render_is_running() && !self.done {
27 self.done = true;
28 let error = self.editor.render_error();
29 let success = error.is_none() && total > 0 && completed == total;
30 let value = serde_json::json!({"success":success,"completed":completed,"total":total,"last_pass":label,"gui_updates":self.frames,"error":error});
31 std::fs::write(
32 self.root.join("smoke-result.json"),
33 serde_json::to_vec_pretty(&value).unwrap(),
34 )
35 .unwrap();
36 for warning in self.editor.render_warnings() {
37 println!("WARNING {warning}");
38 }
39 println!("RESULT {value}");
40 ctx.send_viewport_cmd(eframe::egui::ViewportCommand::Close);
41 }
42 }
43 fn on_exit(&mut self, gl: Option<&glow::Context>) {
44 self.editor.on_exit(gl);
45 }
46}
47fn main() -> Result<(), Box<dyn std::error::Error>> {
48 let a = std::env::args().skip(1).collect::<Vec<_>>();
49 if a.len() < 7 {
50 return Err(
51 "ROOT MALE FEMALE male|female WIDTH HEIGHT FPS [equipment|default-hair|default-body] [--height-milli=N] [--center-milli=N]".into(),
52 );
53 }
54 let mut root = PathBuf::from(&a[0]);
55 let mut s = Session::new();
56 // `default-hair`: a real "Nieuw project" (bundled bodies + female-hair
57 // preset, t1019u) in ROOT/Starter; MALE/FEMALE are ignored.
58 let default_hair = a.get(7).is_some_and(|v| v == "default-hair");
59 let default_project = default_hair || a.get(7).is_some_and(|v| v == "default-body");
60 if default_project {
61 std::fs::create_dir_all(&root)?;
62 s.install_named_project(&root, "Starter")?;
63 root = root.join("Starter");
64 } else {
65 s.install_new_project(&root)?;
66 for (sex, index) in [(Sex::Male, 1), (Sex::Female, 2)] {
67 let path = adopt_file(&root, &PathBuf::from(&a[index]), "assets/body")?;
68 s.set_body_path(sex, path);
69 }
70 }
71 s.set_render(a[4].parse()?, a[5].parse()?, a[6].parse()?);
72 let sex = match a[3].as_str() {
73 "male" => Sex::Male,
74 "female" => Sex::Female,
75 _ => return Err("invalid sex".into()),
76 };
77 let equipment = a.get(7).is_some_and(|v| v == "equipment");
78 let open_equipment = equipment || default_hair;
79 if let Some(value) = a.iter().find_map(|v| v.strip_prefix("--height-milli=")) {
80 s.opened.as_mut().unwrap().document.render.framing[usize::from(sex == Sex::Female)]
81 .height_milli = value.parse()?;
82 }
83 if let Some(value) = a.iter().find_map(|v| v.strip_prefix("--center-milli=")) {
84 s.opened.as_mut().unwrap().document.render.framing[usize::from(sex == Sex::Female)]
85 .center_y_milli = value.parse()?;
86 }
87
88 if equipment {
89 let assets = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/equipment");
90 let mut p = equipment::Preset::new("sword".into());
91 p.provenance = "CC0 generated fixture".into();
92 equipment::import_obj(&mut p, &assets.join("sword.obj"))?;
93 equipment::import_shiny(&mut p, 0, &assets.join("shiny.png"))?;
94 for (i, sex) in [(0, Sex::Male), (1, Sex::Female)] {
95 let b = preview::load_body_file(&root.join(s.body_path(sex).unwrap()))?;
96 let joint = b
97 .joint_names
98 .iter()
99 .position(|n| n == "RightHand")
100 .ok_or("RightHand absent")?;
101 p.bind(i, &b, 0, joint)?;
102 }
103 let i = s.add_equipment();
104 s.equipment[i].slug = "sword".into();
105 s.equipment[i].preset = Some(p);
106 }
107 s.save()?;
108 for sex in [Sex::Male, Sex::Female] {
109 let b = preview::load_body_file(&root.join(s.body_path(sex).unwrap()))?;
110 println!(
111 "PLAN {sex:?}: {:?}",
112 paperdoll_renderer::production::plan(&b, a[6].parse()?)?
113 );
114 }
115 let root2 = root.clone();
116 eframe::run_native(
117 "Paperdoll Production Render",
118 eframe::NativeOptions {
119 viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1150., 820.]),
120 ..paperdoll_renderer::preview_gl::native_options()
121 },
122 Box::new(move |_| {
123 let mut editor = EditorApp::default();
124 editor.open_path(root2.join("character.ppd"));
125 if open_equipment {
126 editor.open_equipment_configuration(0, sex);
127 } else {
128 editor.open_body_configuration(sex);
129 }
130 editor.render_all_angles();
131 Ok(Box::new(Smoke {
132 editor,
133 root: root2,
134 last: 0,
135 frames: 0,
136 done: false,
137 }))
138 }),
139 )?;
140 let report: serde_json::Value =
141 serde_json::from_slice(&std::fs::read(root.join("smoke-result.json"))?)?;
142 if report["success"] != true {
143 return Err("production smoke failed; see smoke-result.json".into());
144 }
145 // Newest run: loose maps and .ppda atlases must agree cell by cell.
146 let mut runs = std::fs::read_dir(root.join("renders"))?
147 .map(|e| e.map(|e| e.path()))
148 .collect::<Result<Vec<_>, _>>()?;
149 runs.retain(|p| p.is_dir());
150 runs.sort();
151 let run = runs.last().ok_or("geen run")?;
152 let atlas = paperdoll_renderer::atlas::cross_check_run(run)?;
153 println!("ATLAS {atlas}");
154 std::fs::write(
155 root.join("atlas-check.json"),
156 serde_json::to_vec_pretty(&atlas)?,
157 )?;
158 Ok(())
159}