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