1use crate::{Dependencies, Error, Result};
2use clap::Parser;
3use std::path::{Path, PathBuf};
4
5#[derive(Clone, Debug, Parser)]
15pub struct CreateMse {
16 #[arg(value_name = "out-base")]
19 out_base: String,
20
21 #[arg(value_name = "prefix", long)]
25 prefix: Option<String>,
26
27 #[arg(value_name = "metal", long, conflicts_with = "combine_metal_rough")]
30 metal: Option<PathBuf>,
31
32 #[arg(value_name = "rough", long, conflicts_with = "combine_metal_rough")]
35 rough: Option<PathBuf>,
36
37 #[arg(value_name = "combine-metal-rough", long)]
40 combine_metal_rough: bool,
41
42 #[arg(value_name = "metal-rough", long, requires = "combine_metal_rough")]
45 metal_rough: Option<PathBuf>,
46
47 #[arg(value_name = "emissive", long)]
49 emissive: Option<PathBuf>,
50
51 #[arg(value_name = "albedo", long)]
53 albedo: Option<PathBuf>,
54
55 #[arg(value_name = "ignore-metal", long)]
57 ignore_metal: bool,
58
59 #[arg(value_name = "ignore-rough", long)]
61 ignore_rough: bool,
62
63 #[arg(value_name = "ignore-emissive", long)]
65 ignore_emissive: bool,
66
67 #[arg(value_name = "ignore-albedo", long)]
69 ignore_albedo: bool,
70
71 #[arg(value_name = "output-rough", long)]
74 output_rough: bool,
75}
76
77impl CreateMse {
78 pub fn execute(self, dependencies: impl Dependencies) -> Result<()> {
79 let CreateMse {
80 out_base,
81 prefix,
82 metal,
83 rough,
84 combine_metal_rough,
85 metal_rough,
86 emissive,
87 albedo,
88 ignore_metal,
89 ignore_rough,
90 ignore_emissive,
91 ignore_albedo,
92 output_rough,
93 } = self;
94
95 let metal_rough_path = if !combine_metal_rough || (ignore_metal && ignore_rough) {
99 None
100 } else {
101 Some(match metal_rough {
102 Some(p) => coerce_png(p),
103 None => match &prefix {
104 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-metalness.png"))?,
105 None => dependencies.glob_single_match("*metalness.png")?,
106 },
107 })
108 };
109
110 let metal_path = if combine_metal_rough || ignore_metal {
111 None
112 } else {
113 Some(match metal {
114 Some(p) => coerce_png(p),
115 None => match &prefix {
116 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-metalness.png"))?,
117 None => dependencies.glob_single_match("*metalness.png")?,
118 },
119 })
120 };
121
122 let rough_path = if combine_metal_rough || ignore_rough {
123 None
124 } else {
125 Some(match rough {
126 Some(p) => coerce_png(p),
127 None => match &prefix {
128 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-roughness.png"))?,
129 None => dependencies.glob_single_match("*roughness.png")?,
130 },
131 })
132 };
133
134 let emissive_path = if ignore_emissive {
135 None
136 } else {
137 Some(match emissive {
138 Some(p) => coerce_png(p),
139 None => match &prefix {
140 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-emission.png"))?,
141 None => dependencies.glob_single_match("*emission.png")?,
142 },
143 })
144 };
145
146 let albedo_path = if ignore_albedo {
147 None
148 } else {
149 Some(match albedo {
150 Some(p) => coerce_png(p),
151 None => match &prefix {
152 Some(pfx) => dependencies.glob_single_match(&format!("{pfx}-albedo.png"))?,
153 None => dependencies.glob_single_match("*albedo.png")?,
154 },
155 })
156 };
157
158 let base_img = metal_rough_path
162 .as_ref()
163 .or(metal_path.as_ref())
164 .or(rough_path.as_ref())
165 .or(emissive_path.as_ref())
166 .or(albedo_path.as_ref())
167 .ok_or_else(|| Error::Glob("all channels are ignored; nothing to do".into()))?;
168
169 let size_output = dependencies.exec_magick([
170 "identify".into(),
171 "-ping".into(),
172 "-format".into(),
173 "%wx%h".into(),
174 base_img.to_string_lossy().into_owned(),
175 ])?;
176 let size = String::from_utf8_lossy(&size_output).trim().to_string();
177 if size.is_empty() {
178 return Err(Error::Glob(format!(
179 "couldn't determine image size for: {}",
180 base_img.display()
181 )));
182 }
183
184 let tmpdir = dependencies.create_temp_dir()?;
188 let result = self::create_mse_inner(
189 &dependencies,
190 &metal_rough_path,
191 &metal_path,
192 &rough_path,
193 &emissive_path,
194 &albedo_path,
195 output_rough,
196 &out_base,
197 &size,
198 &tmpdir,
199 );
200 dependencies.remove_dir_all(&tmpdir)?;
201 result
202 }
203}
204
205#[allow(clippy::too_many_arguments)]
206fn create_mse_inner(
207 dependencies: &impl Dependencies,
208 metal_rough_path: &Option<PathBuf>,
209 metal_path: &Option<PathBuf>,
210 rough_path: &Option<PathBuf>,
211 emissive_path: &Option<PathBuf>,
212 albedo_path: &Option<PathBuf>,
213 output_rough: bool,
214 out_base: &str,
215 size: &str,
216 tmpdir: &Path,
217) -> Result<()> {
218 let r_img = tmpdir.join("r.png");
219 let g_img = tmpdir.join("g.png");
220 let b_img = tmpdir.join("b.png");
221
222 let r_str = r_img.to_string_lossy().into_owned();
223 let g_str = g_img.to_string_lossy().into_owned();
224 let b_str = b_img.to_string_lossy().into_owned();
225
226 match (metal_rough_path, metal_path) {
228 (Some(mr), _) => {
229 let mr_str = mr.to_string_lossy().into_owned();
230 dependencies.exec_magick([
231 &mr_str,
232 "-colorspace",
233 "sRGB",
234 "-alpha",
235 "on",
236 "-channel",
237 "R",
238 "-separate",
239 "+channel",
240 "-resize",
241 &format!("{size}!"),
242 &r_str,
243 ])?;
244 }
245 (None, Some(m)) => {
246 let metal_str = m.to_string_lossy().into_owned();
247 dependencies.exec_magick([
248 &metal_str,
249 "-colorspace",
250 "sRGB",
251 "-channel",
252 "R",
253 "-separate",
254 "+channel",
255 "-resize",
256 &format!("{size}!"),
257 &r_str,
258 ])?;
259 }
260 (None, None) => {
261 dependencies.exec_magick(["-size", size, "xc:black", &r_str])?;
262 }
263 }
264
265 match (metal_rough_path, rough_path) {
267 (Some(mr), _) => {
268 let mr_str = mr.to_string_lossy().into_owned();
269 if output_rough {
270 dependencies.exec_magick([
271 &mr_str,
272 "-colorspace",
273 "sRGB",
274 "-alpha",
275 "on",
276 "-alpha",
277 "extract",
278 "-resize",
279 &format!("{size}!"),
280 &g_str,
281 ])?;
282 } else {
283 dependencies.exec_magick([
284 &mr_str,
285 "-colorspace",
286 "sRGB",
287 "-alpha",
288 "on",
289 "-alpha",
290 "extract",
291 "-fx",
292 "u==0 ? 0 : 1-u",
293 "-resize",
294 &format!("{size}!"),
295 &g_str,
296 ])?;
297 }
298 }
299 (None, Some(r)) => {
300 let rough_str = r.to_string_lossy().into_owned();
301 if output_rough {
302 dependencies.exec_magick([
303 &rough_str,
304 "-colorspace",
305 "sRGB",
306 "-channel",
307 "R",
308 "-separate",
309 "+channel",
310 "-resize",
311 &format!("{size}!"),
312 &g_str,
313 ])?;
314 } else {
315 dependencies.exec_magick([
316 &rough_str,
317 "-colorspace",
318 "sRGB",
319 "-channel",
320 "R",
321 "-separate",
322 "+channel",
323 "-fx",
324 "1-u",
325 "-resize",
326 &format!("{size}!"),
327 &g_str,
328 ])?;
329 }
330 }
331 (None, None) => {
332 dependencies.exec_magick(["-size", size, "xc:black", &g_str])?;
333 }
334 }
335
336 match emissive_path {
338 None => {
339 dependencies.exec_magick(["-size", size, "xc:black", &b_str])?;
340 }
341 Some(em) => {
342 let em_str = em.to_string_lossy().into_owned();
343 dependencies.exec_magick([
344 &em_str,
345 "-colorspace",
346 "sRGB",
347 "-alpha",
348 "on",
349 "-alpha",
350 "extract",
351 "-resize",
352 &format!("{size}!"),
353 &b_str,
354 ])?;
355 }
356 }
357
358 let mse_out = format!("{out_base}-mse.png");
360 dependencies.exec_magick([
361 &r_str,
362 &g_str,
363 &b_str,
364 "-combine",
365 "-colorspace",
366 "sRGB",
367 &mse_out,
368 ])?;
369
370 dependencies.write_stdout(format!("Wrote: {mse_out}\n").as_bytes())?;
371
372 if let Some(albedo) = albedo_path {
374 let albedo_out = format!("{out_base}-albedo.png");
375 dependencies.copy_file(albedo, &albedo_out)?;
376 dependencies.write_stdout(format!("Wrote: {albedo_out}\n").as_bytes())?;
377 }
378
379 Ok(())
380}
381
382fn coerce_png(p: PathBuf) -> PathBuf {
384 if p.extension().is_none() {
385 p.with_extension("png")
386 } else {
387 p
388 }
389}