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/// Per-object material properties for Blinn-Phong and PBR shading.
180///
181/// Materials carry all shading parameters that were previously global in `LightingSettings`.
182/// Each `SceneRenderItem` now has its own `Material`, enabling per-object visual distinction.
183///
184/// This struct is `#[non_exhaustive]`: construct via [`Material::default`],
185/// [`Material::from_colour`], or spread syntax (`..Default::default()`). This allows new
186/// fields to be added in future phases without breaking downstream code.
187#[non_exhaustive]
188#[derive(Debug, Clone, Copy, PartialEq)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190pub struct Material {
191    /// Base diffuse colour [r, g, b] in linear 0..1 range. Default [0.7, 0.7, 0.7].
192    pub base_colour: [f32; 3],
193    /// Ambient light coefficient. Default 0.15.
194    pub ambient: f32,
195    /// Diffuse light coefficient. Default 0.75.
196    pub diffuse: f32,
197    /// Specular highlight coefficient. Default 0.4.
198    pub specular: f32,
199    /// Specular shininess exponent. Default 32.0.
200    pub shininess: f32,
201    /// Metallic factor for PBR Cook-Torrance shading. 0=dielectric, 1=metal. Default 0.0.
202    pub metallic: f32,
203    /// Roughness factor for PBR microfacet distribution. 0=mirror, 1=fully rough. Default 0.5.
204    pub roughness: f32,
205    /// Optional albedo texture identifier. None = no texture applied. Default None.
206    pub texture_id: Option<u64>,
207    /// Optional normal map texture identifier. None = no normal mapping. Default None.
208    ///
209    /// The normal map must be in tangent-space with XY encoded as RG (0..1 -> -1..+1).
210    /// Requires UVs and tangents on the mesh for correct TBN construction.
211    pub normal_map_id: Option<u64>,
212    /// Optional ambient occlusion map texture identifier. None = no AO map. Default None.
213    ///
214    /// The AO map R channel encodes cavity factor (0=fully occluded, 1=fully lit).
215    /// Applied multiplicatively to ambient and diffuse terms.
216    pub ao_map_id: Option<u64>,
217    /// Use Cook-Torrance PBR shading instead of Blinn-Phong. Default false.
218    ///
219    /// When true, `metallic` and `roughness` drive the GGX BRDF.
220    /// PBR outputs linear HDR values; enable `post_process.enabled` for correct tone mapping.
221    pub use_pbr: bool,
222    /// Optional matcap texture identifier. When set, matcap shading replaces
223    /// Blinn-Phong/PBR. Default None.
224    ///
225    /// Obtain a `MatcapId` from [`ViewportGpuResources::builtin_matcap_id`] or
226    /// [`ViewportGpuResources::upload_matcap`].  Blendable matcaps (alpha-channel)
227    /// tint the result with `base_colour`; static matcaps override colour entirely.
228    pub matcap_id: Option<crate::resources::MatcapId>,
229    /// UV parameterization visualization. When set, replaces albedo/lighting with a
230    /// procedural pattern in UV space : useful for inspecting parameterization quality.
231    ///
232    /// Requires UV coordinates on the mesh. Default None (standard shading).
233    pub param_vis: Option<ParamVis>,
234    /// Back-face rendering policy. Default [`BackfacePolicy::Cull`] (back faces hidden).
235    ///
236    /// Use [`BackfacePolicy::Identical`] for single-sided geometry like planes and open
237    /// surfaces. Use [`BackfacePolicy::DifferentColour`] to highlight back faces in a
238    /// distinct colour : helpful for diagnosing mesh orientation errors.
239    pub backface_policy: BackfacePolicy,
240}
241
242impl Default for Material {
243    fn default() -> Self {
244        Self {
245            base_colour: [0.7, 0.7, 0.7],
246            ambient: 0.15,
247            diffuse: 0.75,
248            specular: 0.4,
249            shininess: 32.0,
250            metallic: 0.0,
251            roughness: 0.5,
252            texture_id: None,
253            normal_map_id: None,
254            ao_map_id: None,
255            use_pbr: false,
256            matcap_id: None,
257            param_vis: None,
258            backface_policy: BackfacePolicy::Cull,
259        }
260    }
261}
262
263impl Material {
264    /// Returns `true` if the backface policy makes back faces visible.
265    pub fn is_two_sided(&self) -> bool {
266        !matches!(self.backface_policy, BackfacePolicy::Cull)
267    }
268
269    /// Construct from a plain colour, all other parameters at their defaults.
270    pub fn from_colour(colour: [f32; 3]) -> Self {
271        Self {
272            base_colour: colour,
273            ..Default::default()
274        }
275    }
276
277    /// Construct a Cook-Torrance PBR material.
278    ///
279    /// - `metallic`: 0.0 = dielectric, 1.0 = full metal
280    /// - `roughness`: 0.0 = mirror, 1.0 = fully rough
281    ///
282    /// All other parameters take their defaults. Enable post-processing
283    /// (`PostProcessSettings::enabled = true`) for correct HDR tone mapping.
284    pub fn pbr(base_colour: [f32; 3], metallic: f32, roughness: f32) -> Self {
285        Self {
286            base_colour,
287            use_pbr: true,
288            metallic,
289            roughness,
290            ..Default::default()
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn appearance_defaults() {
301        let a = AppearanceSettings::default();
302        assert!(!a.hidden);
303        assert!(!a.unlit);
304        assert!((a.opacity - 1.0).abs() < 1e-6);
305        assert!(!a.wireframe);
306    }
307
308    #[test]
309    fn appearance_spread_syntax() {
310        let a = AppearanceSettings {
311            unlit: true,
312            ..Default::default()
313        };
314        assert!(a.unlit);
315        assert!(!a.hidden);
316        assert!((a.opacity - 1.0).abs() < 1e-6);
317    }
318
319    #[test]
320    fn default_values() {
321        let m = Material::default();
322        assert!((m.base_colour[0] - 0.7).abs() < 1e-6);
323        assert!((m.ambient - 0.15).abs() < 1e-6);
324        assert!((m.diffuse - 0.75).abs() < 1e-6);
325        assert!(!m.use_pbr);
326        assert!(m.texture_id.is_none());
327        assert!(m.normal_map_id.is_none());
328        assert!(m.ao_map_id.is_none());
329        assert!(m.matcap_id.is_none());
330        assert!(m.param_vis.is_none());
331    }
332
333    #[test]
334    fn from_colour_sets_base_colour() {
335        let m = Material::from_colour([1.0, 0.0, 0.5]);
336        assert!((m.base_colour[0] - 1.0).abs() < 1e-6);
337        assert!((m.base_colour[1]).abs() < 1e-6);
338        assert!((m.base_colour[2] - 0.5).abs() < 1e-6);
339        // Other fields should be defaults
340        assert!((m.ambient - 0.15).abs() < 1e-6);
341    }
342
343    #[test]
344    fn pbr_constructor() {
345        let m = Material::pbr([0.8, 0.2, 0.1], 0.9, 0.3);
346        assert!(m.use_pbr);
347        assert!((m.metallic - 0.9).abs() < 1e-6);
348        assert!((m.roughness - 0.3).abs() < 1e-6);
349        assert!((m.base_colour[0] - 0.8).abs() < 1e-6);
350    }
351
352    #[test]
353    fn is_two_sided_cull() {
354        let m = Material::default();
355        assert!(!m.is_two_sided());
356    }
357
358    #[test]
359    fn is_two_sided_identical() {
360        let m = Material {
361            backface_policy: BackfacePolicy::Identical,
362            ..Default::default()
363        };
364        assert!(m.is_two_sided());
365    }
366
367    #[test]
368    fn is_two_sided_different_colour() {
369        let m = Material {
370            backface_policy: BackfacePolicy::DifferentColour([1.0, 0.0, 0.0]),
371            ..Default::default()
372        };
373        assert!(m.is_two_sided());
374    }
375
376    #[test]
377    fn is_two_sided_tint() {
378        let m = Material {
379            backface_policy: BackfacePolicy::Tint(0.3),
380            ..Default::default()
381        };
382        assert!(m.is_two_sided());
383    }
384
385    #[test]
386    fn is_two_sided_pattern() {
387        let m = Material {
388            backface_policy: BackfacePolicy::Pattern(PatternConfig {
389                pattern: BackfacePattern::Hatching,
390                colour: [0.5, 0.5, 0.5],
391                ..Default::default()
392            }),
393            ..Default::default()
394        };
395        assert!(m.is_two_sided());
396    }
397
398    #[test]
399    fn param_vis_default() {
400        let pv = ParamVis::default();
401        assert_eq!(pv.mode, ParamVisMode::Checker);
402        assert!((pv.scale - 8.0).abs() < 1e-6);
403    }
404}