Skip to main content

viewport_lib/scene/
material.rs

1/// Procedural UV visualization mode for parameterization inspection.
2///
3/// When set on a [`Material`], the mesh fragment shader ignores the albedo texture and
4/// renders a procedural pattern driven by the mesh UV coordinates instead. Useful for
5/// inspecting UV distortion, seams, and parameterization quality without needing a texture.
6///
7/// Requires the mesh to have UV coordinates. Has no effect if the mesh lacks UVs.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum ParamVisMode {
11    /// Alternating black/white squares tiled in UV space.
12    Checker = 1,
13    /// Thin grid lines at UV integer boundaries.
14    Grid = 2,
15    /// Polar checkerboard centred at UV (0.5, 0.5) : reveals rotational consistency.
16    LocalChecker = 3,
17    /// Concentric rings centred at UV (0.5, 0.5) : reveals radial distortion.
18    LocalRadial = 4,
19}
20
21/// UV parameterization visualization settings.
22///
23/// Attach to [`Material::param_vis`] to enable procedural UV pattern rendering.
24/// The `scale` controls tile frequency : higher values produce more, smaller tiles.
25#[derive(Debug, Clone, Copy, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct ParamVis {
28    /// Which procedural pattern to render.
29    pub mode: ParamVisMode,
30    /// Tile frequency multiplier. Default 8.0 : produces 8 checker squares per UV unit.
31    pub scale: f32,
32}
33
34impl Default for ParamVis {
35    fn default() -> Self {
36        Self {
37            mode: ParamVisMode::Checker,
38            scale: 8.0,
39        }
40    }
41}
42
43/// Procedural pattern for back-face rendering.
44///
45/// Used with [`PatternConfig`] to select which procedural pattern is drawn on back faces.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub enum BackfacePattern {
49    /// Alternating squares in world XY.
50    Checker = 0,
51    /// Diagonal lines (45 degrees).
52    Hatching = 1,
53    /// Two sets of diagonal lines (45 and 135 degrees).
54    Crosshatch = 2,
55    /// Horizontal stripes.
56    Stripes = 3,
57}
58
59/// Configuration for procedural back-face patterns.
60///
61/// Used with [`BackfacePolicy::Pattern`]. The `scale` controls how many pattern cells
62/// fit across the object's longest bounding-box dimension, so the pattern always looks
63/// proportional regardless of the mesh's physical size.
64///
65/// Prefer using `..Default::default()` when constructing so that future fields
66/// added to this struct do not require changes at every call site:
67///
68/// ```rust
69/// # use viewport_lib::{PatternConfig, BackfacePattern};
70/// let cfg = PatternConfig {
71///     pattern: BackfacePattern::Hatching,
72///     color: [1.0, 0.5, 0.0],
73///     ..Default::default()
74/// };
75/// ```
76#[derive(Debug, Clone, Copy, PartialEq)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct PatternConfig {
79    /// Which procedural pattern to draw on back faces.
80    pub pattern: BackfacePattern,
81    /// RGB foreground color for the pattern (linear 0..1).
82    pub color: [f32; 3],
83    /// Number of pattern cells across the object's longest bounding-box dimension.
84    ///
85    /// Default 20.0. Increase for finer detail, decrease for coarser.
86    pub scale: f32,
87}
88
89impl Default for PatternConfig {
90    fn default() -> Self {
91        Self {
92            pattern: BackfacePattern::Checker,
93            color: [1.0, 0.5, 0.0],
94            scale: 20.0,
95        }
96    }
97}
98
99/// Controls how back faces of a mesh are rendered.
100///
101/// Use [`BackfacePolicy::Cull`] (the default) to hide back faces, [`BackfacePolicy::Identical`]
102/// to show them with the same shading as front faces, or [`BackfacePolicy::DifferentColor`]
103/// to shade back faces in a distinct color : useful for spotting mesh orientation errors or
104/// highlighting the interior of open surfaces.
105#[derive(Debug, Clone, Copy, PartialEq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub enum BackfacePolicy {
108    /// Back faces are culled (invisible). Default.
109    Cull,
110    /// Back faces are visible and shaded identically to front faces.
111    Identical,
112    /// Back faces are visible and shaded in the given RGB color (linear 0..1).
113    ///
114    /// Front faces receive normal shading; back faces receive Blinn-Phong shading
115    /// with the supplied color and the same ambient/diffuse/specular coefficients.
116    /// The normal is flipped so lighting is computed from the back-face perspective.
117    DifferentColor([f32; 3]),
118    /// Back faces are visible and tinted darker by the given factor (0.0..1.0).
119    ///
120    /// The base color is multiplied by `(1.0 - factor)`, so `Tint(0.3)` means
121    /// back faces are 30% darker. The normal is flipped for correct lighting.
122    Tint(f32),
123    /// Back faces are rendered with a procedural pattern scaled to the object's size.
124    ///
125    /// The pattern density is relative to the object's world-space bounding box, so
126    /// the appearance stays consistent across meshes of different physical sizes.
127    /// The normal is flipped for correct lighting.
128    Pattern(PatternConfig),
129}
130
131impl Default for BackfacePolicy {
132    fn default() -> Self {
133        BackfacePolicy::Cull
134    }
135}
136
137/// Per-object material properties for Blinn-Phong and PBR shading.
138///
139/// Materials carry all shading parameters that were previously global in `LightingSettings`.
140/// Each `SceneRenderItem` now has its own `Material`, enabling per-object visual distinction.
141///
142/// This struct is `#[non_exhaustive]`: construct via [`Material::default`],
143/// [`Material::from_color`], or spread syntax (`..Default::default()`). This allows new
144/// fields to be added in future phases without breaking downstream code.
145#[non_exhaustive]
146#[derive(Debug, Clone, Copy, PartialEq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct Material {
149    /// Base diffuse color [r, g, b] in linear 0..1 range. Default [0.7, 0.7, 0.7].
150    pub base_color: [f32; 3],
151    /// Ambient light coefficient. Default 0.15.
152    pub ambient: f32,
153    /// Diffuse light coefficient. Default 0.75.
154    pub diffuse: f32,
155    /// Specular highlight coefficient. Default 0.4.
156    pub specular: f32,
157    /// Specular shininess exponent. Default 32.0.
158    pub shininess: f32,
159    /// Metallic factor for PBR Cook-Torrance shading. 0=dielectric, 1=metal. Default 0.0.
160    pub metallic: f32,
161    /// Roughness factor for PBR microfacet distribution. 0=mirror, 1=fully rough. Default 0.5.
162    pub roughness: f32,
163    /// Opacity (1.0 = fully opaque, 0.0 = fully transparent). Default 1.0.
164    pub opacity: f32,
165    /// Optional albedo texture identifier. None = no texture applied. Default None.
166    pub texture_id: Option<u64>,
167    /// Optional normal map texture identifier. None = no normal mapping. Default None.
168    ///
169    /// The normal map must be in tangent-space with XY encoded as RG (0..1 -> -1..+1).
170    /// Requires UVs and tangents on the mesh for correct TBN construction.
171    pub normal_map_id: Option<u64>,
172    /// Optional ambient occlusion map texture identifier. None = no AO map. Default None.
173    ///
174    /// The AO map R channel encodes cavity factor (0=fully occluded, 1=fully lit).
175    /// Applied multiplicatively to ambient and diffuse terms.
176    pub ao_map_id: Option<u64>,
177    /// Use Cook-Torrance PBR shading instead of Blinn-Phong. Default false.
178    ///
179    /// When true, `metallic` and `roughness` drive the GGX BRDF.
180    /// PBR outputs linear HDR values; enable `post_process.enabled` for correct tone mapping.
181    pub use_pbr: bool,
182    /// Optional matcap texture identifier. When set, matcap shading replaces
183    /// Blinn-Phong/PBR. Default None.
184    ///
185    /// Obtain a `MatcapId` from [`ViewportGpuResources::builtin_matcap_id`] or
186    /// [`ViewportGpuResources::upload_matcap`].  Blendable matcaps (alpha-channel)
187    /// tint the result with `base_color`; static matcaps override color entirely.
188    pub matcap_id: Option<crate::resources::MatcapId>,
189    /// UV parameterization visualization. When set, replaces albedo/lighting with a
190    /// procedural pattern in UV space : useful for inspecting parameterization quality.
191    ///
192    /// Requires UV coordinates on the mesh. Default None (standard shading).
193    pub param_vis: Option<ParamVis>,
194    /// Back-face rendering policy. Default [`BackfacePolicy::Cull`] (back faces hidden).
195    ///
196    /// Use [`BackfacePolicy::Identical`] for single-sided geometry like planes and open
197    /// surfaces. Use [`BackfacePolicy::DifferentColor`] to highlight back faces in a
198    /// distinct color : helpful for diagnosing mesh orientation errors.
199    pub backface_policy: BackfacePolicy,
200    /// Skip all lighting and output the raw surface color directly. Default `false`.
201    ///
202    /// When `true`, the fragment shader bypasses Blinn-Phong, PBR, and matcap shading
203    /// and returns the base color (or colormap value) unchanged. Useful for 2D chart
204    /// overlays, pre-lit data, and flat UI geometry.
205    pub unlit: bool,
206}
207
208impl Default for Material {
209    fn default() -> Self {
210        Self {
211            base_color: [0.7, 0.7, 0.7],
212            ambient: 0.15,
213            diffuse: 0.75,
214            specular: 0.4,
215            shininess: 32.0,
216            metallic: 0.0,
217            roughness: 0.5,
218            opacity: 1.0,
219            texture_id: None,
220            normal_map_id: None,
221            ao_map_id: None,
222            use_pbr: false,
223            matcap_id: None,
224            param_vis: None,
225            backface_policy: BackfacePolicy::Cull,
226            unlit: false,
227        }
228    }
229}
230
231impl Material {
232    /// Returns `true` if the backface policy makes back faces visible.
233    pub fn is_two_sided(&self) -> bool {
234        !matches!(self.backface_policy, BackfacePolicy::Cull)
235    }
236
237    /// Construct from a plain color, all other parameters at their defaults.
238    pub fn from_color(color: [f32; 3]) -> Self {
239        Self {
240            base_color: color,
241            ..Default::default()
242        }
243    }
244
245    /// Construct a Cook-Torrance PBR material.
246    ///
247    /// - `metallic`: 0.0 = dielectric, 1.0 = full metal
248    /// - `roughness`: 0.0 = mirror, 1.0 = fully rough
249    ///
250    /// All other parameters take their defaults. Enable post-processing
251    /// (`PostProcessSettings::enabled = true`) for correct HDR tone mapping.
252    pub fn pbr(base_color: [f32; 3], metallic: f32, roughness: f32) -> Self {
253        Self {
254            base_color,
255            use_pbr: true,
256            metallic,
257            roughness,
258            ..Default::default()
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn default_values() {
269        let m = Material::default();
270        assert!((m.base_color[0] - 0.7).abs() < 1e-6);
271        assert!((m.ambient - 0.15).abs() < 1e-6);
272        assert!((m.diffuse - 0.75).abs() < 1e-6);
273        assert!((m.opacity - 1.0).abs() < 1e-6);
274        assert!(!m.use_pbr);
275        assert!(m.texture_id.is_none());
276        assert!(m.normal_map_id.is_none());
277        assert!(m.ao_map_id.is_none());
278        assert!(m.matcap_id.is_none());
279        assert!(m.param_vis.is_none());
280    }
281
282    #[test]
283    fn from_color_sets_base_color() {
284        let m = Material::from_color([1.0, 0.0, 0.5]);
285        assert!((m.base_color[0] - 1.0).abs() < 1e-6);
286        assert!((m.base_color[1]).abs() < 1e-6);
287        assert!((m.base_color[2] - 0.5).abs() < 1e-6);
288        // Other fields should be defaults
289        assert!((m.ambient - 0.15).abs() < 1e-6);
290    }
291
292    #[test]
293    fn pbr_constructor() {
294        let m = Material::pbr([0.8, 0.2, 0.1], 0.9, 0.3);
295        assert!(m.use_pbr);
296        assert!((m.metallic - 0.9).abs() < 1e-6);
297        assert!((m.roughness - 0.3).abs() < 1e-6);
298        assert!((m.base_color[0] - 0.8).abs() < 1e-6);
299    }
300
301    #[test]
302    fn is_two_sided_cull() {
303        let m = Material::default();
304        assert!(!m.is_two_sided());
305    }
306
307    #[test]
308    fn is_two_sided_identical() {
309        let m = Material {
310            backface_policy: BackfacePolicy::Identical,
311            ..Default::default()
312        };
313        assert!(m.is_two_sided());
314    }
315
316    #[test]
317    fn is_two_sided_different_color() {
318        let m = Material {
319            backface_policy: BackfacePolicy::DifferentColor([1.0, 0.0, 0.0]),
320            ..Default::default()
321        };
322        assert!(m.is_two_sided());
323    }
324
325    #[test]
326    fn is_two_sided_tint() {
327        let m = Material {
328            backface_policy: BackfacePolicy::Tint(0.3),
329            ..Default::default()
330        };
331        assert!(m.is_two_sided());
332    }
333
334    #[test]
335    fn is_two_sided_pattern() {
336        let m = Material {
337            backface_policy: BackfacePolicy::Pattern(PatternConfig {
338                pattern: BackfacePattern::Hatching,
339                color: [0.5, 0.5, 0.5],
340                ..Default::default()
341            }),
342            ..Default::default()
343        };
344        assert!(m.is_two_sided());
345    }
346
347    #[test]
348    fn param_vis_default() {
349        let pv = ParamVis::default();
350        assert_eq!(pv.mode, ParamVisMode::Checker);
351        assert!((pv.scale - 8.0).abs() < 1e-6);
352    }
353}