1use crate::json::JsonError;
9use crate::world::{Component, Entity};
10use iridia_interface::Filters;
11use serde::Serialize;
12use serde_json::Value;
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)]
15pub enum Wrap {
16 #[default]
17 Repeat,
18 Clamp,
20 Mirror,
21}
22#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
26pub struct UvTransform {
27 pub offset: [f32; 2],
28 pub scale: [f32; 2],
29 pub rotation: f32,
31}
32impl UvTransform {
33 pub const IDENTITY: UvTransform = UvTransform {
35 offset: [0.0; 2],
36 scale: [1.0; 2],
37 rotation: 0.0,
38 };
39
40 pub fn apply(&self, [u, v]: [f32; 2]) -> [f32; 2] {
42 let (u, v) = (u * self.scale[0], v * self.scale[1]);
43 let (u, v) = if self.rotation == 0.0 {
44 (u, v)
45 } else {
46 let (s, c) = self.rotation.sin_cos();
47 (u * c - v * s, u * s + v * c)
48 };
49 [u + self.offset[0], v + self.offset[1]]
50 }
51
52 pub fn from_bottom_left(scale: [f32; 2], offset: [f32; 2]) -> UvTransform {
60 UvTransform {
61 offset: [offset[0], 1.0 - scale[1] - offset[1]],
62 scale,
63 rotation: 0.0,
64 }
65 }
66}
67#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
69pub struct TextureRef {
70 pub texture: Entity,
74 pub texcoord: u8,
76 pub wrap: [Wrap; 2],
78 pub transform: Option<UvTransform>,
80 pub filters: Filters,
82}
83impl TextureRef {
84 pub fn new(texture: Entity) -> Self {
86 TextureRef {
87 texture,
88 texcoord: 0,
89 wrap: [Wrap::Repeat; 2],
90 transform: None,
91 filters: Filters::default(),
92 }
93 }
94}
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize)]
97pub enum Shading {
98 #[default]
100 Lit,
101 Unlit,
105}
106#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize)]
108pub enum AlphaMode {
109 #[default]
111 Opaque,
112 Mask { cutoff: f32 },
114 Blend,
116}
117#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
121pub struct Specular {
122 pub factor: f32,
124 pub texture: Option<TextureRef>,
126 pub color: [f32; 3],
128 pub color_texture: Option<TextureRef>,
130}
131impl Default for Specular {
132 fn default() -> Self {
134 Specular {
135 factor: 1.0,
136 texture: None,
137 color: [1.0; 3],
138 color_texture: None,
139 }
140 }
141}
142#[derive(Clone, Debug, PartialEq, Serialize)]
144pub struct PbrMaterial {
145 pub base_color: [f32; 4],
149 pub base_color_texture: Option<TextureRef>,
151 pub metallic: f32,
153 pub roughness: f32,
155 pub metallic_roughness_texture: Option<TextureRef>,
157 pub occlusion_texture: Option<TextureRef>,
159 pub occlusion_strength: f32,
161 pub normal_texture: Option<TextureRef>,
163 pub normal_scale: f32,
165 pub emissive: [f32; 3],
167 pub emissive_texture: Option<TextureRef>,
169 pub emissive_strength: f32,
173 pub alpha_mode: AlphaMode,
174 pub double_sided: bool,
176 pub specular: Option<Specular>,
178 pub shading: Shading,
181}
182impl Component for PbrMaterial {
183 const NAME: &'static str = "pbr.material";
184 const GEOMETRY: crate::Geometry = crate::Geometry::None;
185 const VERSION: u32 = 5;
188 fn to_json(&self) -> Result<Option<Value>, JsonError> {
189 crate::json::to_value(self).map(Some)
190 }
191 fn verify(&self, _e: Entity, _w: &crate::World) -> Result<(), String> {
194 let unit = |x: f32| (0.0..=1.0).contains(&x);
195 let [r, g, b, a] = self.base_color;
196 if ![r, g, b].iter().all(|&c| c.is_finite() && c >= 0.0) || !unit(a) {
197 return Err(format!(
198 "base colour {:?} is not finite and >= 0 with alpha within 0-1",
199 self.base_color
200 ));
201 }
202 for (name, x) in [
203 ("metallic", self.metallic),
204 ("roughness", self.roughness),
205 ("occlusion strength", self.occlusion_strength),
206 ] {
207 if !unit(x) {
208 return Err(format!("{name} {x} is not within 0-1"));
209 }
210 }
211 if !self.normal_scale.is_finite() {
212 return Err(format!("normal scale {} is not finite", self.normal_scale));
213 }
214 if let AlphaMode::Mask { cutoff } = self.alpha_mode
215 && !(cutoff.is_finite() && cutoff >= 0.0)
216 {
217 return Err(format!("mask cutoff {cutoff} is not finite and >= 0"));
218 }
219 for (slot, r) in self.slots() {
220 if let Some(t) = r.transform
221 && !t
222 .offset
223 .iter()
224 .chain(&t.scale)
225 .chain([&t.rotation])
226 .all(|x| x.is_finite())
227 {
228 return Err(format!("{slot:?} texture transform {t:?} is not finite"));
229 }
230 }
231 if !self.emissive.iter().all(|c| (0.0..=1.0).contains(c)) {
232 return Err(format!(
233 "emissive {:?} is not within 0-1 (brighter emission is carried by emissive_strength)",
234 self.emissive
235 ));
236 }
237 if !self.emissive_strength.is_finite() || self.emissive_strength < 0.0 {
238 return Err(format!(
239 "emissive strength {} is not finite and >= 0",
240 self.emissive_strength
241 ));
242 }
243 if let Some(s) = &self.specular {
244 if !(s.factor.is_finite() && (0.0..=1.0).contains(&s.factor)) {
245 return Err(format!("specular factor {} is not in 0-1", s.factor));
246 }
247 if s.color.iter().any(|c| !c.is_finite() || *c < 0.0) {
248 return Err(format!("specular colour {:?} is not finite and >= 0", s.color));
249 }
250 }
251 Ok(())
252 }
253 fn references(&self, out: &mut Vec<Entity>) {
254 out.extend(self.textures().map(|r| r.texture));
255 }
256}
257impl Default for PbrMaterial {
258 fn default() -> Self {
260 PbrMaterial {
261 base_color: [1.0; 4],
262 base_color_texture: None,
263 metallic: 1.0,
264 roughness: 1.0,
265 metallic_roughness_texture: None,
266 occlusion_texture: None,
267 occlusion_strength: 1.0,
268 normal_texture: None,
269 normal_scale: 1.0,
270 emissive: [0.0; 3],
271 emissive_texture: None,
272 emissive_strength: 1.0,
273 alpha_mode: AlphaMode::Opaque,
274 double_sided: false,
275 specular: None,
276 shading: Shading::Lit,
277 }
278 }
279}
280#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
282pub enum TextureSlot {
283 BaseColor,
284 MetallicRoughness,
285 Normal,
286 Occlusion,
287 Emissive,
288 Specular,
290 SpecularColor,
292}
293impl TextureSlot {
294 pub fn name(self) -> &'static str {
296 match self {
297 TextureSlot::BaseColor => "base colour",
298 TextureSlot::MetallicRoughness => "metallic-roughness",
299 TextureSlot::Normal => "normal",
300 TextureSlot::Occlusion => "occlusion",
301 TextureSlot::Emissive => "emissive",
302 TextureSlot::Specular => "specular",
303 TextureSlot::SpecularColor => "specular colour",
304 }
305 }
306}
307macro_rules! texture_slots {
310 ($m:expr, $($b:tt)*) => {{
311 let m = $m;
312 let (specular, color) = match $($b)* m.specular {
313 Some(s) => (Some($($b)* s.texture), Some($($b)* s.color_texture)),
314 None => (None, None),
315 };
316 [
317 (TextureSlot::BaseColor, Some($($b)* m.base_color_texture)),
318 (TextureSlot::MetallicRoughness, Some($($b)* m.metallic_roughness_texture)),
319 (TextureSlot::Normal, Some($($b)* m.normal_texture)),
320 (TextureSlot::Occlusion, Some($($b)* m.occlusion_texture)),
321 (TextureSlot::Emissive, Some($($b)* m.emissive_texture)),
322 (TextureSlot::Specular, specular),
323 (TextureSlot::SpecularColor, color),
324 ]
325 }};
326}
327impl PbrMaterial {
328 pub fn slots(&self) -> impl Iterator<Item = (TextureSlot, TextureRef)> {
331 texture_slots!(self,)
332 .into_iter()
333 .filter_map(|(slot, r)| r.flatten().map(|r| (slot, r)))
334 }
335 pub fn slots_mut(&mut self) -> impl Iterator<Item = (TextureSlot, &mut TextureRef)> {
337 texture_slots!(self, &mut)
338 .into_iter()
339 .filter_map(|(slot, r)| r.and_then(Option::as_mut).map(|r| (slot, r)))
340 }
341 pub fn textures(&self) -> impl Iterator<Item = TextureRef> {
343 self.slots().map(|(_, r)| r)
344 }
345 pub fn textures_mut(&mut self) -> impl Iterator<Item = &mut TextureRef> {
347 self.slots_mut().map(|(_, r)| r)
348 }
349}
350#[cfg(test)]
351mod tests {
352 use super::{AlphaMode, PbrMaterial, TextureRef, UvTransform};
353 use crate::{Component, World};
354
355 #[test]
357 fn verify_covers_every_documented_range() {
358 let mut w = World::new();
359 let e = w.spawn();
360 let texture = w.spawn();
361 let ok = PbrMaterial::default();
362 assert_eq!(ok.verify(e, &w), Ok(()));
363 let refused = |m: PbrMaterial, what: &str| {
364 let err = m.verify(e, &w).unwrap_err();
365 assert!(err.contains(what), "{what}: {err}");
366 };
367 let bright = PbrMaterial {
369 base_color: [26.5, 0.5, 0.0, 1.0],
370 ..ok.clone()
371 };
372 assert_eq!(bright.verify(e, &w), Ok(()));
373 for base_color in [
374 [-0.1, 0.0, 0.0, 1.0],
375 [f32::INFINITY, 0.0, 0.0, 1.0],
376 [0.0, f32::NAN, 0.0, 1.0],
377 [1.0, 1.0, 1.0, 1.5],
378 ] {
379 refused(
380 PbrMaterial {
381 base_color,
382 ..ok.clone()
383 },
384 "base colour",
385 );
386 }
387 refused(
388 PbrMaterial {
389 metallic: -0.1,
390 ..ok.clone()
391 },
392 "metallic",
393 );
394 refused(
395 PbrMaterial {
396 roughness: f32::NAN,
397 ..ok.clone()
398 },
399 "roughness",
400 );
401 refused(
402 PbrMaterial {
403 occlusion_strength: 2.0,
404 ..ok.clone()
405 },
406 "occlusion strength",
407 );
408 refused(
409 PbrMaterial {
410 normal_scale: f32::INFINITY,
411 ..ok.clone()
412 },
413 "normal scale",
414 );
415 refused(
416 PbrMaterial {
417 alpha_mode: AlphaMode::Mask { cutoff: -0.5 },
418 ..ok.clone()
419 },
420 "mask cutoff",
421 );
422 let mut transformed = TextureRef::new(texture);
423 transformed.transform = Some(UvTransform {
424 rotation: f32::NAN,
425 ..UvTransform::IDENTITY
426 });
427 refused(
428 PbrMaterial {
429 normal_texture: Some(transformed),
430 ..ok.clone()
431 },
432 "texture transform",
433 );
434 assert_eq!(
436 PbrMaterial {
437 normal_scale: -1.0,
438 ..ok.clone()
439 }
440 .verify(e, &w),
441 Ok(())
442 );
443 assert_eq!(
444 PbrMaterial {
445 alpha_mode: AlphaMode::Mask { cutoff: 1.5 },
446 ..ok
447 }
448 .verify(e, &w),
449 Ok(())
450 );
451 }
452
453 #[test]
455 fn every_slot_is_listed_by_value_and_mutably_in_gltf_order() {
456 use super::{Entity, Specular, TextureRef, TextureSlot};
457 let r = |i: u32| TextureRef::new(Entity::from_index(i));
458 let mut m = PbrMaterial {
459 base_color_texture: Some(r(1)),
460 metallic_roughness_texture: Some(r(2)),
461 normal_texture: Some(r(3)),
462 occlusion_texture: Some(r(4)),
463 emissive_texture: Some(r(5)),
464 specular: Some(Specular {
465 factor: 1.0,
466 texture: Some(r(6)),
467 color: [1.0; 3],
468 color_texture: Some(r(7)),
469 }),
470 ..PbrMaterial::default()
471 };
472 let slots: Vec<TextureSlot> = m.slots().map(|(s, _)| s).collect();
473 assert_eq!(
474 slots,
475 [
476 TextureSlot::BaseColor,
477 TextureSlot::MetallicRoughness,
478 TextureSlot::Normal,
479 TextureSlot::Occlusion,
480 TextureSlot::Emissive,
481 TextureSlot::Specular,
482 TextureSlot::SpecularColor
483 ]
484 );
485 assert_eq!(
486 m.textures().map(|t| t.texture.index()).collect::<Vec<_>>(),
487 [1, 2, 3, 4, 5, 6, 7]
488 );
489 for t in m.textures_mut() {
490 t.texcoord = 1;
491 }
492 assert!(m.textures().all(|t| t.texcoord == 1));
493 m.specular = None;
495 m.normal_texture = None;
496 assert_eq!(m.textures_mut().count(), 4);
497 }
498
499 #[test]
500 fn bottom_left_tiling_samples_the_same_texels() {
501 let (scale, offset) = ([2.0f32, 0.5], [0.25f32, 0.125]);
503 let t = UvTransform::from_bottom_left(scale, offset);
504 assert_eq!(t.offset, [0.25, 0.375]);
505 for (u, v) in [(0.0f32, 0.0f32), (0.5, 0.25), (0.75, 1.0)] {
506 let source = [u * scale[0] + offset[0], v * scale[1] + offset[1]];
509 let world = t.apply([u, 1.0 - v]);
510 assert_eq!(world, [source[0], 1.0 - source[1]], "({u}, {v})");
511 }
512 }
513
514 #[test]
515 fn rotation_is_counter_clockwise_after_the_scale() {
516 let t = UvTransform {
517 offset: [0.0, 1.0],
518 scale: [0.5, 0.5],
519 rotation: std::f32::consts::FRAC_PI_2,
520 };
521 let [u, v] = t.apply([1.0, 0.0]);
522 assert!(u.abs() < 1e-7 && (v - 1.5).abs() < 1e-7, "{u} {v}");
523 assert_eq!(UvTransform::IDENTITY.apply([0.3, 0.7]), [0.3, 0.7]);
524 }
525
526 #[test]
529 fn emissive_strength_is_finite_and_non_negative() {
530 use crate::World;
531
532 let failures = |strength: f32| {
533 let mut w = World::new();
534 let e = w.spawn();
535 w.insert(
536 e,
537 PbrMaterial {
538 emissive_strength: strength,
539 ..PbrMaterial::default()
540 },
541 );
542 w.verify_components()
543 .into_iter()
544 .map(|(name, _, detail)| (name.to_owned(), detail))
545 .collect::<Vec<_>>()
546 };
547 for strength in [-1.0, -0.001, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
548 let failures = failures(strength);
549 assert_eq!(failures.len(), 1, "{strength}");
550 assert_eq!(failures[0].0, "pbr.material", "{strength}");
551 assert!(failures[0].1.contains("emissive strength"), "{:?}", failures[0]);
552 }
553 assert!(failures(0.0).is_empty());
554 assert!(failures(1.0).is_empty());
555 }
556
557 #[test]
560 fn emissive_is_within_0_to_1() {
561 use crate::World;
562
563 let failures = |emissive: [f32; 3]| {
564 let mut w = World::new();
565 let e = w.spawn();
566 w.insert(
567 e,
568 PbrMaterial {
569 emissive,
570 emissive_strength: 4.0,
571 ..PbrMaterial::default()
572 },
573 );
574 w.verify_components()
575 .into_iter()
576 .map(|(_, _, detail)| detail)
577 .collect::<Vec<_>>()
578 };
579 for emissive in [
580 [1.5, 0.0, 0.0],
581 [0.0, -0.25, 0.0],
582 [0.0, 0.0, f32::NAN],
583 [f32::INFINITY, 1.0, 1.0],
584 ] {
585 let failures = failures(emissive);
586 assert_eq!(failures.len(), 1, "{emissive:?}");
587 assert!(failures[0].contains("emissive"), "{failures:?}");
588 }
589 assert!(failures([0.0; 3]).is_empty());
590 assert!(failures([1.0, 0.5, 0.0]).is_empty());
591 }
592}