viewport_lib/scene/material.rs
1/// Per-item render settings: visibility, appearance overrides, pick identity, and selection state.
2///
3/// Replaces the former `AppearanceSettings` struct and absorbs the `pick_id` and `selected`
4/// fields that previously appeared at the top level of each item type.
5///
6/// Always use `Default::default()` as the base, then set individual fields:
7///
8/// ```rust
9/// # use viewport_lib::ItemSettings;
10/// let mut s = ItemSettings::default();
11/// s.hidden = true;
12/// ```
13#[non_exhaustive]
14#[derive(Debug, Clone, Copy, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct ItemSettings {
17 /// Hide the object entirely. Default `false`.
18 pub hidden: bool,
19 /// Skip all lighting calculations and output raw colour. Default `false`.
20 pub unlit: bool,
21 /// Global opacity multiplier. 1.0 = fully opaque, 0.0 = fully transparent. Default 1.0.
22 pub opacity: f32,
23 /// Render as wireframe regardless of the global wireframe flag. Default `false`. Extended conceptually to non-mesh
24 /// objects to render their sub-objects as meshes, e.g., a VolumeItem has its voxels as wireframe boxes.
25 pub wireframe: bool,
26 /// GPU pick identifier. `PickId::NONE` = not pickable. Default `PickId::NONE`.
27 pub pick_id: crate::renderer::PickId,
28 /// Whether the item is currently selected. Default `false`.
29 pub selected: bool,
30 /// Whether this item casts shadows in the shadow map pass. Default `true`.
31 ///
32 /// Set to `false` for items that should be visible but should not contribute
33 /// to the shadow map (HUD-style overlays, debug helpers, light source
34 /// indicators). Honoured by mesh-family items; non-mesh items currently do
35 /// not participate in the shadow pass and treat this flag as a no-op.
36 pub cast_shadows: bool,
37 /// Whether this item samples the shadow map when shaded. Default `true`.
38 ///
39 /// Set to `false` to render the item as if no shadow caster occludes it.
40 /// Useful for items that should remain fully lit regardless of the scene's
41 /// shadow casters (debug visualisations, glowing markers). Honoured by
42 /// mesh-family items; non-mesh items currently do not sample shadow maps and
43 /// treat this flag as a no-op.
44 pub receive_shadows: bool,
45}
46
47impl Default for ItemSettings {
48 fn default() -> Self {
49 Self {
50 hidden: false,
51 unlit: false,
52 opacity: 1.0,
53 wireframe: false,
54 pick_id: crate::renderer::PickId::NONE,
55 selected: false,
56 cast_shadows: true,
57 receive_shadows: true,
58 }
59 }
60}
61
62/// Lit shading model selected by a [`Material`].
63///
64/// The variants are mutually exclusive: a material picks one path. The `unlit`
65/// per-item bypass on [`ItemSettings`] is checked independently and takes
66/// precedence over the shading model.
67///
68/// New shading models (Toon, SSS, Gooch, ...) are added here in future phases
69/// of `docs/plans/shading-models-plan.md`. The glTF PBR extensions (clearcoat,
70/// sheen, anisotropy, iridescence) layer on top of `Pbr` as separate fields on
71/// `Material` rather than as variants here.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74pub enum ShadingModel {
75 /// Blinn-Phong lit shading using `ambient` / `diffuse` / `specular` / `shininess`.
76 Phong,
77 /// Cook-Torrance GGX physically based shading driven by `metallic` and `roughness`.
78 Pbr,
79 /// Matcap lookup. Replaces the lit path with a captured-lighting texture.
80 /// Blendable matcaps tint with `base_colour`; static matcaps override colour entirely.
81 Matcap(crate::resources::MatcapId),
82 /// Flat (faceted) shading: the per-vertex normal is discarded and a
83 /// geometric normal is recomputed per-fragment from screen-space
84 /// derivatives of world position. Produces visible polygon edges on
85 /// triangulated meshes without changing the lighting block; everything
86 /// else (hemisphere ambient, scene lights, shadows, AO, alpha mode)
87 /// runs as it does for [`Phong`].
88 ///
89 /// Edges appear at the triangulation level, not at logical-face
90 /// boundaries. A quad authored as two triangles shows both; a CAD
91 /// surface tessellated finely shows the tessellation.
92 Flat,
93}
94
95impl Default for ShadingModel {
96 fn default() -> Self {
97 Self::Phong
98 }
99}
100
101/// Procedural UV visualization mode for parameterization inspection.
102///
103/// When set on a [`Material`], the mesh fragment shader ignores the albedo texture and
104/// renders a procedural pattern driven by the mesh UV coordinates instead. Useful for
105/// inspecting UV distortion, seams, and parameterization quality without needing a texture.
106///
107/// Requires the mesh to have UV coordinates. Has no effect if the mesh lacks UVs.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum ParamVisMode {
111 /// Alternating black/white squares tiled in UV space.
112 Checker = 1,
113 /// Thin grid lines at UV integer boundaries.
114 Grid = 2,
115 /// Polar checkerboard centred at UV (0.5, 0.5) : reveals rotational consistency.
116 LocalChecker = 3,
117 /// Concentric rings centred at UV (0.5, 0.5) : reveals radial distortion.
118 LocalRadial = 4,
119}
120
121/// UV parameterization visualization settings.
122///
123/// Attach to [`Material::param_vis`] to enable procedural UV pattern rendering.
124/// The `scale` controls tile frequency : higher values produce more, smaller tiles.
125#[derive(Debug, Clone, Copy, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct ParamVis {
128 /// Which procedural pattern to render.
129 pub mode: ParamVisMode,
130 /// Tile frequency multiplier. Default 8.0 : produces 8 checker squares per UV unit.
131 pub scale: f32,
132}
133
134impl Default for ParamVis {
135 fn default() -> Self {
136 Self {
137 mode: ParamVisMode::Checker,
138 scale: 8.0,
139 }
140 }
141}
142
143/// Procedural pattern for back-face rendering.
144///
145/// Used with [`PatternConfig`] to select which procedural pattern is drawn on back faces.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub enum BackfacePattern {
149 /// Alternating squares in world XY.
150 Checker = 0,
151 /// Diagonal lines (45 degrees).
152 Hatching = 1,
153 /// Two sets of diagonal lines (45 and 135 degrees).
154 Crosshatch = 2,
155 /// Horizontal stripes.
156 Stripes = 3,
157}
158
159/// Configuration for procedural back-face patterns.
160///
161/// Used with [`BackfacePolicy::Pattern`]. The `scale` controls how many pattern cells
162/// fit across the object's longest bounding-box dimension, so the pattern always looks
163/// proportional regardless of the mesh's physical size.
164///
165/// Prefer using `..Default::default()` when constructing so that future fields
166/// added to this struct do not require changes at every call site:
167///
168/// ```rust
169/// # use viewport_lib::{PatternConfig, BackfacePattern};
170/// let cfg = PatternConfig {
171/// pattern: BackfacePattern::Hatching,
172/// colour: [1.0, 0.5, 0.0],
173/// ..Default::default()
174/// };
175/// ```
176#[derive(Debug, Clone, Copy, PartialEq)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub struct PatternConfig {
179 /// Which procedural pattern to draw on back faces.
180 pub pattern: BackfacePattern,
181 /// RGB foreground colour for the pattern (linear 0..1).
182 pub colour: [f32; 3],
183 /// Number of pattern cells across the object's longest bounding-box dimension.
184 ///
185 /// Default 20.0. Increase for finer detail, decrease for coarser.
186 pub scale: f32,
187}
188
189impl Default for PatternConfig {
190 fn default() -> Self {
191 Self {
192 pattern: BackfacePattern::Checker,
193 colour: [1.0, 0.5, 0.0],
194 scale: 20.0,
195 }
196 }
197}
198
199/// Controls how back faces of a mesh are rendered.
200///
201/// Use [`BackfacePolicy::Cull`] (the default) to hide back faces, [`BackfacePolicy::Identical`]
202/// to show them with the same shading as front faces, or [`BackfacePolicy::DifferentColour`]
203/// to shade back faces in a distinct colour : useful for spotting mesh orientation errors or
204/// highlighting the interior of open surfaces.
205#[derive(Debug, Clone, Copy, PartialEq)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
207pub enum BackfacePolicy {
208 /// Back faces are culled (invisible). Default.
209 Cull,
210 /// Back faces are visible and shaded identically to front faces.
211 Identical,
212 /// Back faces are visible and shaded in the given RGB colour (linear 0..1).
213 ///
214 /// Front faces receive normal shading; back faces receive Blinn-Phong shading
215 /// with the supplied colour and the same ambient/diffuse/specular coefficients.
216 /// The normal is flipped so lighting is computed from the back-face perspective.
217 DifferentColour([f32; 3]),
218 /// Back faces are visible and tinted darker by the given factor (0.0..1.0).
219 ///
220 /// The base colour is multiplied by `(1.0 - factor)`, so `Tint(0.3)` means
221 /// back faces are 30% darker. The normal is flipped for correct lighting.
222 Tint(f32),
223 /// Back faces are rendered with a procedural pattern scaled to the object's size.
224 ///
225 /// The pattern density is relative to the object's world-space bounding box, so
226 /// the appearance stays consistent across meshes of different physical sizes.
227 /// The normal is flipped for correct lighting.
228 Pattern(PatternConfig),
229}
230
231impl Default for BackfacePolicy {
232 fn default() -> Self {
233 BackfacePolicy::Cull
234 }
235}
236
237/// Alpha blending mode for a material, matching the glTF `alphaMode` field.
238///
239/// Controls how the fragment alpha value is interpreted by the renderer.
240#[derive(Debug, Clone, Copy, PartialEq)]
241#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
242pub enum AlphaMode {
243 /// Alpha is ignored; the surface is always fully opaque. Default.
244 Opaque,
245 /// Fragments with alpha below the cutoff are discarded; others are fully opaque.
246 /// The f32 value is the cutoff threshold in [0, 1].
247 Mask(f32),
248 /// Standard alpha blending through the OIT pass.
249 Blend,
250}
251
252impl Default for AlphaMode {
253 fn default() -> Self {
254 AlphaMode::Opaque
255 }
256}
257
258/// Per-object material properties for Blinn-Phong and PBR shading.
259///
260/// Materials carry all shading parameters that were previously global in `LightingSettings`.
261/// Each `SceneRenderItem` now has its own `Material`, enabling per-object visual distinction.
262///
263/// This struct is `#[non_exhaustive]`: construct via [`Material::default`],
264/// [`Material::from_colour`], or spread syntax (`..Default::default()`). This allows new
265/// fields to be added in future phases without breaking downstream code.
266#[non_exhaustive]
267#[derive(Debug, Clone, Copy, PartialEq)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269pub struct Material {
270 /// Base diffuse colour [r, g, b] in linear 0..1 range. Default [0.7, 0.7, 0.7].
271 pub base_colour: [f32; 3],
272 /// Ambient light coefficient. Default 0.15.
273 pub ambient: f32,
274 /// Diffuse light coefficient. Default 0.75.
275 pub diffuse: f32,
276 /// Specular highlight coefficient. Default 0.4.
277 pub specular: f32,
278 /// Specular shininess exponent. Default 32.0.
279 pub shininess: f32,
280 /// Metallic factor for PBR Cook-Torrance shading. 0=dielectric, 1=metal. Default 0.0.
281 pub metallic: f32,
282 /// Roughness factor for PBR microfacet distribution. 0=mirror, 1=fully rough. Default 0.5.
283 pub roughness: f32,
284 /// Optional albedo texture identifier. None = no texture applied. Default None.
285 pub texture_id: Option<u64>,
286 /// Optional normal map texture identifier. None = no normal mapping. Default None.
287 ///
288 /// The normal map must be in tangent-space with XY encoded as RG (0..1 -> -1..+1).
289 /// Requires UVs and tangents on the mesh for correct TBN construction.
290 pub normal_map_id: Option<u64>,
291 /// Optional ambient occlusion map texture identifier. None = no AO map. Default None.
292 ///
293 /// The AO map R channel encodes cavity factor (0=fully occluded, 1=fully lit).
294 /// Applied multiplicatively to ambient and diffuse terms.
295 pub ao_map_id: Option<u64>,
296 /// Optional combined metallic-roughness texture (ORM layout). Default None.
297 ///
298 /// Matches the glTF `metallicRoughnessTexture`: G channel encodes roughness,
299 /// B channel encodes metallic. Each channel is multiplied by the corresponding
300 /// scalar factor (`roughness`, `metallic`). Only sampled when the material's
301 /// `shading_model` is `ShadingModel::Pbr`.
302 pub metallic_roughness_texture_id: Option<u64>,
303 /// Self-illumination colour added after lighting. Default [0.0, 0.0, 0.0].
304 ///
305 /// Matches glTF `emissiveFactor`. Applied to both Blinn-Phong and PBR paths.
306 /// When `emissive_texture_id` is set, this value is multiplied by the texture sample.
307 pub emissive: [f32; 3],
308 /// Optional emissive texture identifier. Default None.
309 ///
310 /// Matches glTF `emissiveTexture`. Sampled and multiplied by `emissive`.
311 pub emissive_texture_id: Option<u64>,
312 /// Alpha handling mode. Default [`AlphaMode::Opaque`].
313 ///
314 /// `Mask(cutoff)` discards fragments below the cutoff using the alpha channel.
315 /// `Blend` routes the mesh through the OIT transparent pass.
316 pub alpha_mode: AlphaMode,
317 /// Which lit shading model evaluates this surface. Default [`ShadingModel::Phong`].
318 ///
319 /// `Phong` uses Blinn-Phong; `Pbr` uses Cook-Torrance GGX driven by `metallic`
320 /// and `roughness`; `Matcap(id)` replaces the lit path with a matcap texture
321 /// lookup. The `unlit` per-item bypass on [`ItemSettings`] is checked
322 /// independently and takes precedence over any value here.
323 pub shading_model: ShadingModel,
324 /// UV parameterization visualization. When set, replaces albedo/lighting with a
325 /// procedural pattern in UV space : useful for inspecting parameterization quality.
326 ///
327 /// Requires UV coordinates on the mesh. Default None (standard shading).
328 pub param_vis: Option<ParamVis>,
329 /// Back-face rendering policy. Default [`BackfacePolicy::Cull`] (back faces hidden).
330 ///
331 /// Use [`BackfacePolicy::Identical`] for single-sided geometry like planes and open
332 /// surfaces. Use [`BackfacePolicy::DifferentColour`] to highlight back faces in a
333 /// distinct colour : helpful for diagnosing mesh orientation errors.
334 pub backface_policy: BackfacePolicy,
335}
336
337impl Default for Material {
338 fn default() -> Self {
339 Self {
340 base_colour: [0.7, 0.7, 0.7],
341 ambient: 0.15,
342 diffuse: 0.75,
343 specular: 0.4,
344 shininess: 32.0,
345 metallic: 0.0,
346 roughness: 0.5,
347 texture_id: None,
348 normal_map_id: None,
349 ao_map_id: None,
350 metallic_roughness_texture_id: None,
351 emissive: [0.0, 0.0, 0.0],
352 emissive_texture_id: None,
353 alpha_mode: AlphaMode::Opaque,
354 shading_model: ShadingModel::Phong,
355 param_vis: None,
356 backface_policy: BackfacePolicy::Cull,
357 }
358 }
359}
360
361impl Material {
362 /// Returns `true` if the backface policy makes back faces visible.
363 pub fn is_two_sided(&self) -> bool {
364 !matches!(self.backface_policy, BackfacePolicy::Cull)
365 }
366
367 /// Returns `true` if this material uses alpha blending and should go through the OIT pass.
368 pub fn is_blend(&self) -> bool {
369 matches!(self.alpha_mode, AlphaMode::Blend)
370 }
371
372 /// Construct from a plain colour, all other parameters at their defaults.
373 pub fn from_colour(colour: [f32; 3]) -> Self {
374 Self {
375 base_colour: colour,
376 ..Default::default()
377 }
378 }
379
380 /// Construct a Cook-Torrance PBR material.
381 ///
382 /// - `metallic`: 0.0 = dielectric, 1.0 = full metal
383 /// - `roughness`: 0.0 = mirror, 1.0 = fully rough
384 ///
385 /// All other parameters take their defaults. Enable post-processing
386 /// (`PostProcessSettings::enabled = true`) for correct HDR tone mapping.
387 pub fn pbr(base_colour: [f32; 3], metallic: f32, roughness: f32) -> Self {
388 Self {
389 base_colour,
390 shading_model: ShadingModel::Pbr,
391 metallic,
392 roughness,
393 ..Default::default()
394 }
395 }
396
397 /// Returns `true` when the material's shading model is Cook-Torrance PBR.
398 pub fn is_pbr(&self) -> bool {
399 matches!(self.shading_model, ShadingModel::Pbr)
400 }
401
402 /// Returns `true` when the material's shading model is flat / faceted.
403 pub fn is_flat(&self) -> bool {
404 matches!(self.shading_model, ShadingModel::Flat)
405 }
406
407 /// Construct a flat (faceted) material with the given base colour. The
408 /// per-vertex normal is replaced at the fragment stage by a geometric
409 /// normal recovered from screen-space derivatives, so polygon edges
410 /// remain visible on otherwise smooth meshes. Everything else takes
411 /// material defaults.
412 pub fn flat(base_colour: [f32; 3]) -> Self {
413 Self {
414 base_colour,
415 shading_model: ShadingModel::Flat,
416 ..Default::default()
417 }
418 }
419
420 /// Returns the matcap id when the material uses a matcap shading model.
421 pub fn matcap_id(&self) -> Option<crate::resources::MatcapId> {
422 match self.shading_model {
423 ShadingModel::Matcap(id) => Some(id),
424 _ => None,
425 }
426 }
427
428 /// Alias for [`Material::from_colour`].
429 ///
430 /// Returns a material with the given base colour and all other fields at defaults.
431 /// "solid" is a more familiar name in some graphics contexts; both names are kept
432 /// so existing `from_colour` call sites do not need updating.
433 pub fn solid(colour: [f32; 3]) -> Self {
434 Self::from_colour(colour)
435 }
436
437 /// Returns a material with only `texture_id` set; all other fields take defaults.
438 ///
439 /// Useful when the mesh already carries a UV layout and the only requirement is
440 /// to apply a texture. Base colour, lighting model, and all other fields stay at
441 /// their default values.
442 pub fn textured(texture_id: u64) -> Self {
443 Self {
444 texture_id: Some(texture_id),
445 ..Default::default()
446 }
447 }
448
449 /// PBR constructor that also sets a baked ambient-occlusion texture.
450 ///
451 /// - `base_colour`: RGB albedo in linear 0..1
452 /// - `metallic`: 0.0 = dielectric, 1.0 = full metal
453 /// - `roughness`: 0.0 = mirror, 1.0 = fully rough
454 /// - `ao_map`: baked AO texture id, or `None` to disable AO
455 ///
456 /// Sets `shading_model = ShadingModel::Pbr`. All other fields take defaults.
457 /// Use [`Material::pbr`] when no AO map is needed.
458 pub fn pbr_with_ao(
459 base_colour: [f32; 3],
460 metallic: f32,
461 roughness: f32,
462 ao_map: Option<u64>,
463 ) -> Self {
464 Self {
465 base_colour,
466 shading_model: ShadingModel::Pbr,
467 metallic,
468 roughness,
469 ao_map_id: ao_map,
470 ..Default::default()
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn item_settings_defaults() {
481 let a = ItemSettings::default();
482 assert!(!a.hidden);
483 assert!(!a.unlit);
484 assert!((a.opacity - 1.0).abs() < 1e-6);
485 assert!(!a.wireframe);
486 assert!(!a.selected);
487 }
488
489 #[test]
490 fn item_settings_spread_syntax() {
491 let a = ItemSettings {
492 unlit: true,
493 ..Default::default()
494 };
495 assert!(a.unlit);
496 assert!(!a.hidden);
497 assert!((a.opacity - 1.0).abs() < 1e-6);
498 }
499
500 #[test]
501 fn flat_constructor_and_helpers() {
502 let m = Material::flat([0.4, 0.5, 0.6]);
503 assert!(m.is_flat());
504 assert!(!m.is_pbr());
505 assert!(m.matcap_id().is_none());
506 assert!(matches!(m.shading_model, ShadingModel::Flat));
507 assert_eq!(m.base_colour, [0.4, 0.5, 0.6]);
508
509 let p = Material::default();
510 assert!(!p.is_flat());
511 }
512
513 #[test]
514 fn default_values() {
515 let m = Material::default();
516 assert!((m.base_colour[0] - 0.7).abs() < 1e-6);
517 assert!((m.ambient - 0.15).abs() < 1e-6);
518 assert!((m.diffuse - 0.75).abs() < 1e-6);
519 assert!(!m.is_pbr());
520 assert!(m.texture_id.is_none());
521 assert!(m.normal_map_id.is_none());
522 assert!(m.ao_map_id.is_none());
523 assert!(m.matcap_id().is_none());
524 assert!(m.param_vis.is_none());
525 }
526
527 #[test]
528 fn from_colour_sets_base_colour() {
529 let m = Material::from_colour([1.0, 0.0, 0.5]);
530 assert!((m.base_colour[0] - 1.0).abs() < 1e-6);
531 assert!((m.base_colour[1]).abs() < 1e-6);
532 assert!((m.base_colour[2] - 0.5).abs() < 1e-6);
533 // Other fields should be defaults
534 assert!((m.ambient - 0.15).abs() < 1e-6);
535 }
536
537 #[test]
538 fn pbr_constructor() {
539 let m = Material::pbr([0.8, 0.2, 0.1], 0.9, 0.3);
540 assert!(m.is_pbr());
541 assert!((m.metallic - 0.9).abs() < 1e-6);
542 assert!((m.roughness - 0.3).abs() < 1e-6);
543 assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
544 }
545
546 #[test]
547 fn is_two_sided_cull() {
548 let m = Material::default();
549 assert!(!m.is_two_sided());
550 }
551
552 #[test]
553 fn is_two_sided_identical() {
554 let m = Material {
555 backface_policy: BackfacePolicy::Identical,
556 ..Default::default()
557 };
558 assert!(m.is_two_sided());
559 }
560
561 #[test]
562 fn is_two_sided_different_colour() {
563 let m = Material {
564 backface_policy: BackfacePolicy::DifferentColour([1.0, 0.0, 0.0]),
565 ..Default::default()
566 };
567 assert!(m.is_two_sided());
568 }
569
570 #[test]
571 fn is_two_sided_tint() {
572 let m = Material {
573 backface_policy: BackfacePolicy::Tint(0.3),
574 ..Default::default()
575 };
576 assert!(m.is_two_sided());
577 }
578
579 #[test]
580 fn is_two_sided_pattern() {
581 let m = Material {
582 backface_policy: BackfacePolicy::Pattern(PatternConfig {
583 pattern: BackfacePattern::Hatching,
584 colour: [0.5, 0.5, 0.5],
585 ..Default::default()
586 }),
587 ..Default::default()
588 };
589 assert!(m.is_two_sided());
590 }
591
592 #[test]
593 fn param_vis_default() {
594 let pv = ParamVis::default();
595 assert_eq!(pv.mode, ParamVisMode::Checker);
596 assert!((pv.scale - 8.0).abs() < 1e-6);
597 }
598
599 #[test]
600 fn solid_delegates_to_from_colour() {
601 let colour = [1.0_f32, 0.0, 0.5];
602 let a = Material::solid(colour);
603 let b = Material::from_colour(colour);
604 assert!((a.base_colour[0] - b.base_colour[0]).abs() < 1e-6);
605 assert!((a.base_colour[1] - b.base_colour[1]).abs() < 1e-6);
606 assert!((a.base_colour[2] - b.base_colour[2]).abs() < 1e-6);
607 assert_eq!(a.is_pbr(), b.is_pbr());
608 assert_eq!(a.texture_id, b.texture_id);
609 assert_eq!(a.ao_map_id, b.ao_map_id);
610 assert!((a.ambient - b.ambient).abs() < 1e-6);
611 }
612
613 #[test]
614 fn textured_sets_texture_id() {
615 let m = Material::textured(42);
616 assert_eq!(m.texture_id, Some(42));
617 let def = Material::default();
618 assert!((m.base_colour[0] - def.base_colour[0]).abs() < 1e-6);
619 assert!(!m.is_pbr());
620 }
621
622 #[test]
623 fn pbr_with_ao_sets_fields() {
624 let m = Material::pbr_with_ao([0.8, 0.2, 0.1], 0.9, 0.3, Some(7));
625 assert!(m.is_pbr());
626 assert!((m.metallic - 0.9).abs() < 1e-6);
627 assert!((m.roughness - 0.3).abs() < 1e-6);
628 assert_eq!(m.ao_map_id, Some(7));
629 assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
630 }
631
632 #[test]
633 fn pbr_with_ao_accepts_none() {
634 let m = Material::pbr_with_ao([0.5; 3], 0.0, 1.0, None);
635 assert_eq!(m.ao_map_id, None);
636 }
637}