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}