Skip to main content

viewport_lib/scene/
material.rs

1/// Per-item appearance overrides that apply uniformly across all renderable types.
2///
3/// Every field defaults to the "do nothing" state so that `AppearanceSettings::default()`
4/// is always safe to use without changing existing behaviour.
5///
6/// This struct is `#[non_exhaustive]`: always construct with `..Default::default()` so
7/// that new fields added in future versions do not break your code:
8///
9/// ```rust
10/// # use viewport_lib::AppearanceSettings;
11/// let mut app = AppearanceSettings::default();
12/// app.unlit = true;
13/// ```
14#[non_exhaustive]
15#[derive(Debug, Clone, Copy, PartialEq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct AppearanceSettings {
18    /// Hide the object entirely. Default `false`.
19    pub hidden: bool,
20    /// Skip all lighting calculations and output raw colour. Default `false`.
21    ///
22    /// For types that are already flat-shaded (polylines, point clouds, image slices)
23    /// this flag is accepted but has no visible effect.
24    pub unlit: bool,
25    /// Global opacity multiplier applied on top of the item's own opacity mechanism.
26    /// 1.0 = fully opaque, 0.0 = fully transparent. Default 1.0.
27    pub opacity: f32,
28    /// Render as wireframe regardless of the global wireframe mode flag. Default `false`.
29    pub wireframe: bool,
30}
31
32impl Default for AppearanceSettings {
33    fn default() -> Self {
34        Self {
35            hidden: false,
36            unlit: false,
37            opacity: 1.0,
38            wireframe: false,
39        }
40    }
41}
42
43/// Procedural UV visualization mode for parameterization inspection.
44///
45/// When set on a [`Material`], the mesh fragment shader ignores the albedo texture and
46/// renders a procedural pattern driven by the mesh UV coordinates instead. Useful for
47/// inspecting UV distortion, seams, and parameterization quality without needing a texture.
48///
49/// Requires the mesh to have UV coordinates. Has no effect if the mesh lacks UVs.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum ParamVisMode {
53    /// Alternating black/white squares tiled in UV space.
54    Checker = 1,
55    /// Thin grid lines at UV integer boundaries.
56    Grid = 2,
57    /// Polar checkerboard centred at UV (0.5, 0.5) : reveals rotational consistency.
58    LocalChecker = 3,
59    /// Concentric rings centred at UV (0.5, 0.5) : reveals radial distortion.
60    LocalRadial = 4,
61}
62
63/// UV parameterization visualization settings.
64///
65/// Attach to [`Material::param_vis`] to enable procedural UV pattern rendering.
66/// The `scale` controls tile frequency : higher values produce more, smaller tiles.
67#[derive(Debug, Clone, Copy, PartialEq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69pub struct ParamVis {
70    /// Which procedural pattern to render.
71    pub mode: ParamVisMode,
72    /// Tile frequency multiplier. Default 8.0 : produces 8 checker squares per UV unit.
73    pub scale: f32,
74}
75
76impl Default for ParamVis {
77    fn default() -> Self {
78        Self {
79            mode: ParamVisMode::Checker,
80            scale: 8.0,
81        }
82    }
83}
84
85/// Procedural pattern for back-face rendering.
86///
87/// Used with [`PatternConfig`] to select which procedural pattern is drawn on back faces.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub enum BackfacePattern {
91    /// Alternating squares in world XY.
92    Checker = 0,
93    /// Diagonal lines (45 degrees).
94    Hatching = 1,
95    /// Two sets of diagonal lines (45 and 135 degrees).
96    Crosshatch = 2,
97    /// Horizontal stripes.
98    Stripes = 3,
99}
100
101/// Configuration for procedural back-face patterns.
102///
103/// Used with [`BackfacePolicy::Pattern`]. The `scale` controls how many pattern cells
104/// fit across the object's longest bounding-box dimension, so the pattern always looks
105/// proportional regardless of the mesh's physical size.
106///
107/// Prefer using `..Default::default()` when constructing so that future fields
108/// added to this struct do not require changes at every call site:
109///
110/// ```rust
111/// # use viewport_lib::{PatternConfig, BackfacePattern};
112/// let cfg = PatternConfig {
113///     pattern: BackfacePattern::Hatching,
114///     colour: [1.0, 0.5, 0.0],
115///     ..Default::default()
116/// };
117/// ```
118#[derive(Debug, Clone, Copy, PartialEq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub struct PatternConfig {
121    /// Which procedural pattern to draw on back faces.
122    pub pattern: BackfacePattern,
123    /// RGB foreground colour for the pattern (linear 0..1).
124    pub colour: [f32; 3],
125    /// Number of pattern cells across the object's longest bounding-box dimension.
126    ///
127    /// Default 20.0. Increase for finer detail, decrease for coarser.
128    pub scale: f32,
129}
130
131impl Default for PatternConfig {
132    fn default() -> Self {
133        Self {
134            pattern: BackfacePattern::Checker,
135            colour: [1.0, 0.5, 0.0],
136            scale: 20.0,
137        }
138    }
139}
140
141/// Controls how back faces of a mesh are rendered.
142///
143/// Use [`BackfacePolicy::Cull`] (the default) to hide back faces, [`BackfacePolicy::Identical`]
144/// to show them with the same shading as front faces, or [`BackfacePolicy::DifferentColour`]
145/// to shade back faces in a distinct colour : useful for spotting mesh orientation errors or
146/// highlighting the interior of open surfaces.
147#[derive(Debug, Clone, Copy, PartialEq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub enum BackfacePolicy {
150    /// Back faces are culled (invisible). Default.
151    Cull,
152    /// Back faces are visible and shaded identically to front faces.
153    Identical,
154    /// Back faces are visible and shaded in the given RGB colour (linear 0..1).
155    ///
156    /// Front faces receive normal shading; back faces receive Blinn-Phong shading
157    /// with the supplied colour and the same ambient/diffuse/specular coefficients.
158    /// The normal is flipped so lighting is computed from the back-face perspective.
159    DifferentColour([f32; 3]),
160    /// Back faces are visible and tinted darker by the given factor (0.0..1.0).
161    ///
162    /// The base colour is multiplied by `(1.0 - factor)`, so `Tint(0.3)` means
163    /// back faces are 30% darker. The normal is flipped for correct lighting.
164    Tint(f32),
165    /// Back faces are rendered with a procedural pattern scaled to the object's size.
166    ///
167    /// The pattern density is relative to the object's world-space bounding box, so
168    /// the appearance stays consistent across meshes of different physical sizes.
169    /// The normal is flipped for correct lighting.
170    Pattern(PatternConfig),
171}
172
173impl Default for BackfacePolicy {
174    fn default() -> Self {
175        BackfacePolicy::Cull
176    }
177}
178
179/// Alpha blending mode for a material, matching the glTF `alphaMode` field.
180///
181/// Controls how the fragment alpha value is interpreted by the renderer.
182#[derive(Debug, Clone, Copy, PartialEq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184pub enum AlphaMode {
185    /// Alpha is ignored; the surface is always fully opaque. Default.
186    Opaque,
187    /// Fragments with alpha below the cutoff are discarded; others are fully opaque.
188    /// The f32 value is the cutoff threshold in [0, 1].
189    Mask(f32),
190    /// Standard alpha blending through the OIT pass.
191    Blend,
192}
193
194impl Default for AlphaMode {
195    fn default() -> Self {
196        AlphaMode::Opaque
197    }
198}
199
200/// Per-object material properties for Blinn-Phong and PBR shading.
201///
202/// Materials carry all shading parameters that were previously global in `LightingSettings`.
203/// Each `SceneRenderItem` now has its own `Material`, enabling per-object visual distinction.
204///
205/// This struct is `#[non_exhaustive]`: construct via [`Material::default`],
206/// [`Material::from_colour`], or spread syntax (`..Default::default()`). This allows new
207/// fields to be added in future phases without breaking downstream code.
208#[non_exhaustive]
209#[derive(Debug, Clone, Copy, PartialEq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211pub struct Material {
212    /// Base diffuse colour [r, g, b] in linear 0..1 range. Default [0.7, 0.7, 0.7].
213    pub base_colour: [f32; 3],
214    /// Ambient light coefficient. Default 0.15.
215    pub ambient: f32,
216    /// Diffuse light coefficient. Default 0.75.
217    pub diffuse: f32,
218    /// Specular highlight coefficient. Default 0.4.
219    pub specular: f32,
220    /// Specular shininess exponent. Default 32.0.
221    pub shininess: f32,
222    /// Metallic factor for PBR Cook-Torrance shading. 0=dielectric, 1=metal. Default 0.0.
223    pub metallic: f32,
224    /// Roughness factor for PBR microfacet distribution. 0=mirror, 1=fully rough. Default 0.5.
225    pub roughness: f32,
226    /// Optional albedo texture identifier. None = no texture applied. Default None.
227    pub texture_id: Option<u64>,
228    /// Optional normal map texture identifier. None = no normal mapping. Default None.
229    ///
230    /// The normal map must be in tangent-space with XY encoded as RG (0..1 -> -1..+1).
231    /// Requires UVs and tangents on the mesh for correct TBN construction.
232    pub normal_map_id: Option<u64>,
233    /// Optional ambient occlusion map texture identifier. None = no AO map. Default None.
234    ///
235    /// The AO map R channel encodes cavity factor (0=fully occluded, 1=fully lit).
236    /// Applied multiplicatively to ambient and diffuse terms.
237    pub ao_map_id: Option<u64>,
238    /// Optional combined metallic-roughness texture (ORM layout). Default None.
239    ///
240    /// Matches the glTF `metallicRoughnessTexture`: G channel encodes roughness,
241    /// B channel encodes metallic. Each channel is multiplied by the corresponding
242    /// scalar factor (`roughness`, `metallic`). Only sampled when `use_pbr` is true.
243    pub metallic_roughness_texture_id: Option<u64>,
244    /// Self-illumination colour added after lighting. Default [0.0, 0.0, 0.0].
245    ///
246    /// Matches glTF `emissiveFactor`. Applied to both Blinn-Phong and PBR paths.
247    /// When `emissive_texture_id` is set, this value is multiplied by the texture sample.
248    pub emissive: [f32; 3],
249    /// Optional emissive texture identifier. Default None.
250    ///
251    /// Matches glTF `emissiveTexture`. Sampled and multiplied by `emissive`.
252    pub emissive_texture_id: Option<u64>,
253    /// Alpha handling mode. Default [`AlphaMode::Opaque`].
254    ///
255    /// `Mask(cutoff)` discards fragments below the cutoff using the alpha channel.
256    /// `Blend` routes the mesh through the OIT transparent pass.
257    pub alpha_mode: AlphaMode,
258    /// Use Cook-Torrance PBR shading instead of Blinn-Phong. Default false.
259    ///
260    /// When true, `metallic` and `roughness` drive the GGX BRDF.
261    /// PBR outputs linear HDR values; enable `post_process.enabled` for correct tone mapping.
262    pub use_pbr: bool,
263    /// Optional matcap texture identifier. When set, matcap shading replaces
264    /// Blinn-Phong/PBR. Default None.
265    ///
266    /// Obtain a `MatcapId` from [`ViewportGpuResources::builtin_matcap_id`] or
267    /// [`ViewportGpuResources::upload_matcap`].  Blendable matcaps (alpha-channel)
268    /// tint the result with `base_colour`; static matcaps override colour entirely.
269    pub matcap_id: Option<crate::resources::MatcapId>,
270    /// UV parameterization visualization. When set, replaces albedo/lighting with a
271    /// procedural pattern in UV space : useful for inspecting parameterization quality.
272    ///
273    /// Requires UV coordinates on the mesh. Default None (standard shading).
274    pub param_vis: Option<ParamVis>,
275    /// Back-face rendering policy. Default [`BackfacePolicy::Cull`] (back faces hidden).
276    ///
277    /// Use [`BackfacePolicy::Identical`] for single-sided geometry like planes and open
278    /// surfaces. Use [`BackfacePolicy::DifferentColour`] to highlight back faces in a
279    /// distinct colour : helpful for diagnosing mesh orientation errors.
280    pub backface_policy: BackfacePolicy,
281}
282
283impl Default for Material {
284    fn default() -> Self {
285        Self {
286            base_colour: [0.7, 0.7, 0.7],
287            ambient: 0.15,
288            diffuse: 0.75,
289            specular: 0.4,
290            shininess: 32.0,
291            metallic: 0.0,
292            roughness: 0.5,
293            texture_id: None,
294            normal_map_id: None,
295            ao_map_id: None,
296            metallic_roughness_texture_id: None,
297            emissive: [0.0, 0.0, 0.0],
298            emissive_texture_id: None,
299            alpha_mode: AlphaMode::Opaque,
300            use_pbr: false,
301            matcap_id: None,
302            param_vis: None,
303            backface_policy: BackfacePolicy::Cull,
304        }
305    }
306}
307
308impl Material {
309    /// Returns `true` if the backface policy makes back faces visible.
310    pub fn is_two_sided(&self) -> bool {
311        !matches!(self.backface_policy, BackfacePolicy::Cull)
312    }
313
314    /// Returns `true` if this material uses alpha blending and should go through the OIT pass.
315    pub fn is_blend(&self) -> bool {
316        matches!(self.alpha_mode, AlphaMode::Blend)
317    }
318
319    /// Construct from a plain colour, all other parameters at their defaults.
320    pub fn from_colour(colour: [f32; 3]) -> Self {
321        Self {
322            base_colour: colour,
323            ..Default::default()
324        }
325    }
326
327    /// Construct a Cook-Torrance PBR material.
328    ///
329    /// - `metallic`: 0.0 = dielectric, 1.0 = full metal
330    /// - `roughness`: 0.0 = mirror, 1.0 = fully rough
331    ///
332    /// All other parameters take their defaults. Enable post-processing
333    /// (`PostProcessSettings::enabled = true`) for correct HDR tone mapping.
334    pub fn pbr(base_colour: [f32; 3], metallic: f32, roughness: f32) -> Self {
335        Self {
336            base_colour,
337            use_pbr: true,
338            metallic,
339            roughness,
340            ..Default::default()
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn appearance_defaults() {
351        let a = AppearanceSettings::default();
352        assert!(!a.hidden);
353        assert!(!a.unlit);
354        assert!((a.opacity - 1.0).abs() < 1e-6);
355        assert!(!a.wireframe);
356    }
357
358    #[test]
359    fn appearance_spread_syntax() {
360        let a = AppearanceSettings {
361            unlit: true,
362            ..Default::default()
363        };
364        assert!(a.unlit);
365        assert!(!a.hidden);
366        assert!((a.opacity - 1.0).abs() < 1e-6);
367    }
368
369    #[test]
370    fn default_values() {
371        let m = Material::default();
372        assert!((m.base_colour[0] - 0.7).abs() < 1e-6);
373        assert!((m.ambient - 0.15).abs() < 1e-6);
374        assert!((m.diffuse - 0.75).abs() < 1e-6);
375        assert!(!m.use_pbr);
376        assert!(m.texture_id.is_none());
377        assert!(m.normal_map_id.is_none());
378        assert!(m.ao_map_id.is_none());
379        assert!(m.matcap_id.is_none());
380        assert!(m.param_vis.is_none());
381    }
382
383    #[test]
384    fn from_colour_sets_base_colour() {
385        let m = Material::from_colour([1.0, 0.0, 0.5]);
386        assert!((m.base_colour[0] - 1.0).abs() < 1e-6);
387        assert!((m.base_colour[1]).abs() < 1e-6);
388        assert!((m.base_colour[2] - 0.5).abs() < 1e-6);
389        // Other fields should be defaults
390        assert!((m.ambient - 0.15).abs() < 1e-6);
391    }
392
393    #[test]
394    fn pbr_constructor() {
395        let m = Material::pbr([0.8, 0.2, 0.1], 0.9, 0.3);
396        assert!(m.use_pbr);
397        assert!((m.metallic - 0.9).abs() < 1e-6);
398        assert!((m.roughness - 0.3).abs() < 1e-6);
399        assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
400    }
401
402    #[test]
403    fn is_two_sided_cull() {
404        let m = Material::default();
405        assert!(!m.is_two_sided());
406    }
407
408    #[test]
409    fn is_two_sided_identical() {
410        let m = Material {
411            backface_policy: BackfacePolicy::Identical,
412            ..Default::default()
413        };
414        assert!(m.is_two_sided());
415    }
416
417    #[test]
418    fn is_two_sided_different_colour() {
419        let m = Material {
420            backface_policy: BackfacePolicy::DifferentColour([1.0, 0.0, 0.0]),
421            ..Default::default()
422        };
423        assert!(m.is_two_sided());
424    }
425
426    #[test]
427    fn is_two_sided_tint() {
428        let m = Material {
429            backface_policy: BackfacePolicy::Tint(0.3),
430            ..Default::default()
431        };
432        assert!(m.is_two_sided());
433    }
434
435    #[test]
436    fn is_two_sided_pattern() {
437        let m = Material {
438            backface_policy: BackfacePolicy::Pattern(PatternConfig {
439                pattern: BackfacePattern::Hatching,
440                colour: [0.5, 0.5, 0.5],
441                ..Default::default()
442            }),
443            ..Default::default()
444        };
445        assert!(m.is_two_sided());
446    }
447
448    #[test]
449    fn param_vis_default() {
450        let pv = ParamVis::default();
451        assert_eq!(pv.mode, ParamVisMode::Checker);
452        assert!((pv.scale - 8.0).abs() < 1e-6);
453    }
454}