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}