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
205fn create_mse_inner(
206 dependencies: &impl Dependencies,
207 metal_rough_path: &Option<PathBuf>,
208 metal_path: &Option<PathBuf>,
209 rough_path: &Option<PathBuf>,
210 emissive_path: &Option<PathBuf>,
211 albedo_path: &Option<PathBuf>,
212 output_rough: bool,
213 out_base: &str,
214 size: &str,
215 tmpdir: &Path,
216) -> Result<()> {
217 let r_img = tmpdir.join("r.png");
218 let g_img = tmpdir.join("g.png");
219 let b_img = tmpdir.join("b.png");
220
221 let r_str = r_img.to_string_lossy().into_owned();
222 let g_str = g_img.to_string_lossy().into_owned();
223 let b_str = b_img.to_string_lossy().into_owned();
224
225 match (metal_rough_path, metal_path) {
227 (Some(mr), _) => {
228 let mr_str = mr.to_string_lossy().into_owned();
229 dependencies.exec_magick([
230 &mr_str,
231 "-colorspace",
232 "sRGB",
233 "-alpha",
234 "on",
235 "-channel",
236 "R",
237 "-separate",
238 "+channel",
239 "-resize",
240 &format!("{size}!"),
241 &r_str,
242 ])?;
243 }
244 (None, Some(m)) => {
245 let metal_str = m.to_string_lossy().into_owned();
246 dependencies.exec_magick([
247 &metal_str,
248 "-colorspace",
249 "sRGB",
250 "-channel",
251 "R",
252 "-separate",
253 "+channel",
254 "-resize",
255 &format!("{size}!"),
256 &r_str,
257 ])?;
258 }
259 (None, None) => {
260 dependencies.exec_magick(["-size", size, "xc:black", &r_str])?;
261 }
262 }
263
264 match (metal_rough_path, rough_path) {
266 (Some(mr), _) => {
267 let mr_str = mr.to_string_lossy().into_owned();
268 if output_rough {
269 dependencies.exec_magick([
270 &mr_str,
271 "-colorspace",
272 "sRGB",
273 "-alpha",
274 "on",
275 "-alpha",
276 "extract",
277 "-resize",
278 &format!("{size}!"),
279 &g_str,
280 ])?;
281 } else {
282 dependencies.exec_magick([
283 &mr_str,
284 "-colorspace",
285 "sRGB",
286 "-alpha",
287 "on",
288 "-alpha",
289 "extract",
290 "-fx",
291 "u==0 ? 0 : 1-u",
292 "-resize",
293 &format!("{size}!"),
294 &g_str,
295 ])?;
296 }
297 }
298 (None, Some(r)) => {
299 let rough_str = r.to_string_lossy().into_owned();
300 if output_rough {
301 dependencies.exec_magick([
302 &rough_str,
303 "-colorspace",
304 "sRGB",
305 "-channel",
306 "R",
307 "-separate",
308 "+channel",
309 "-resize",
310 &format!("{size}!"),
311 &g_str,
312 ])?;
313 } else {
314 dependencies.exec_magick([
315 &rough_str,
316 "-colorspace",
317 "sRGB",
318 "-channel",
319 "R",
320 "-separate",
321 "+channel",
322 "-fx",
323 "1-u",
324 "-resize",
325 &format!("{size}!"),
326 &g_str,
327 ])?;
328 }
329 }
330 (None, None) => {
331 dependencies.exec_magick(["-size", size, "xc:black", &g_str])?;
332 }
333 }
334
335 match emissive_path {
337 None => {
338 dependencies.exec_magick(["-size", size, "xc:black", &b_str])?;
339 }
340 Some(em) => {
341 let em_str = em.to_string_lossy().into_owned();
342 dependencies.exec_magick([
343 &em_str,
344 "-colorspace",
345 "sRGB",
346 "-alpha",
347 "on",
348 "-alpha",
349 "extract",
350 "-resize",
351 &format!("{size}!"),
352 &b_str,
353 ])?;
354 }
355 }
356
357 let mse_out = format!("{out_base}-mse.png");
359 dependencies.exec_magick([
360 &r_str,
361 &g_str,
362 &b_str,
363 "-combine",
364 "-colorspace",
365 "sRGB",
366 &mse_out,
367 ])?;
368
369 dependencies.write_stdout(format!("Wrote: {mse_out}\n").as_bytes())?;
370
371 if let Some(albedo) = albedo_path {
373 let albedo_out = format!("{out_base}-albedo.png");
374 dependencies.copy_file(albedo, &albedo_out)?;
375 dependencies.write_stdout(format!("Wrote: {albedo_out}\n").as_bytes())?;
376 }
377
378 Ok(())
379}
380
381fn coerce_png(p: PathBuf) -> PathBuf {
383 if p.extension().is_none() {
384 p.with_extension("png")
385 } else {
386 p
387 }
388}