BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / production_fixture.rs

production_fixture.rs

production_fixture.rs 7.6 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|skinned-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|skinned-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 skinned = a.get(7).is_some_and(|v| v == "skinned-equipment");
79 let open_equipment = equipment || default_hair || skinned;
80 if let Some(value) = a.iter().find_map(|v| v.strip_prefix("--height-milli=")) {
81 s.opened.as_mut().unwrap().document.render.framing[usize::from(sex == Sex::Female)]
82 .height_milli = value.parse()?;
83 }
84 if let Some(value) = a.iter().find_map(|v| v.strip_prefix("--center-milli=")) {
85 s.opened.as_mut().unwrap().document.render.framing[usize::from(sex == Sex::Female)]
86 .center_y_milli = value.parse()?;
87 }
88
89 if equipment {
90 let assets = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/equipment");
91 let mut p = equipment::Preset::new("sword".into());
92 p.provenance = "CC0 generated fixture".into();
93 equipment::import_obj(&mut p, &assets.join("sword.obj"))?;
94 equipment::import_shiny(&mut p, 0, &assets.join("shiny.png"))?;
95 for (i, sex) in [(0, Sex::Male), (1, Sex::Female)] {
96 let b = preview::load_body_file(&root.join(s.body_path(sex).unwrap()))?;
97 let joint = b
98 .joint_names
99 .iter()
100 .position(|n| n == "RightHand")
101 .ok_or("RightHand absent")?;
102 p.bind(i, &b, 0, joint)?;
103 }
104 let i = s.add_equipment();
105 s.equipment[i].slug = "sword".into();
106 s.equipment[i].preset = Some(p);
107 }
108 if skinned {
109 // Tim t1027u: the male test body itself as a skinned equipment part,
110 // bones auto-linked by name, the Head link lifted by 15 cm.
111 let mut p = equipment::Preset::new("suit".into());
112 p.provenance = "CC0 test body used as skinned equipment".into();
113 let part = equipment::import_model(&mut p, &PathBuf::from(&a[1]))?;
114 for (i, sex) in [(0, Sex::Male), (1, Sex::Female)] {
115 let b = preview::load_body_file(&root.join(s.body_path(sex).unwrap()))?;
116 let n = paperdoll_renderer::equipment_rig::auto_link(&mut p, part, i, &b)?;
117 println!("LINKS {sex:?}: {n}");
118 let head = p.attachments[part]
119 .skeleton
120 .as_ref()
121 .unwrap()
122 .bones
123 .iter()
124 .position(|x| x.name == "Head")
125 .ok_or("Head absent")?;
126 if let Some(link) = p.attachments[part].bone_links[i]
127 .iter_mut()
128 .find(|l| l.bone == head)
129 {
130 link.translation[1] += 0.15;
131 }
132 }
133 let i = s.add_equipment();
134 s.equipment[i].slug = "suit".into();
135 s.equipment[i].preset = Some(p);
136 }
137 s.save()?;
138 for sex in [Sex::Male, Sex::Female] {
139 let b = preview::load_body_file(&root.join(s.body_path(sex).unwrap()))?;
140 println!(
141 "PLAN {sex:?}: {:?}",
142 paperdoll_renderer::production::plan(&b, a[6].parse()?)?
143 );
144 }
145 let root2 = root.clone();
146 eframe::run_native(
147 "Paperdoll Production Render",
148 eframe::NativeOptions {
149 viewport: eframe::egui::ViewportBuilder::default().with_inner_size([1150., 820.]),
150 ..paperdoll_renderer::preview_gl::native_options()
151 },
152 Box::new(move |_| {
153 let mut editor = EditorApp::default();
154 editor.open_path(root2.join("character.ppd"));
155 if open_equipment {
156 editor.open_equipment_configuration(0, sex);
157 } else {
158 editor.open_body_configuration(sex);
159 }
160 editor.render_all_angles();
161 Ok(Box::new(Smoke {
162 editor,
163 root: root2,
164 last: 0,
165 frames: 0,
166 done: false,
167 }))
168 }),
169 )?;
170 let report: serde_json::Value =
171 serde_json::from_slice(&std::fs::read(root.join("smoke-result.json"))?)?;
172 if report["success"] != true {
173 return Err("production smoke failed; see smoke-result.json".into());
174 }
175 // Newest run: loose maps and .ppda atlases must agree cell by cell.
176 let mut runs = std::fs::read_dir(root.join("renders"))?
177 .map(|e| e.map(|e| e.path()))
178 .collect::<Result<Vec<_>, _>>()?;
179 runs.retain(|p| p.is_dir());
180 runs.sort();
181 let run = runs.last().ok_or("geen run")?;
182 let atlas = paperdoll_renderer::atlas::cross_check_run(run)?;
183 println!("ATLAS {atlas}");
184 std::fs::write(
185 root.join("atlas-check.json"),
186 serde_json::to_vec_pretty(&atlas)?,
187 )?;
188 Ok(())
189}