BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / production_fixture.rs

production_fixture.rs

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