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