1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Typed, finite surface-response state shared by all representations.
#[cfg(test)]
#[path = "material_tests.rs"]
mod tests;
/// Surface response of a drawn representation.
///
/// The restrained molecular response is the physical default. Other tagged
/// models are explicit art direction and never inferred from an element name.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Material {
/// Overall opacity in [0, 1]; below 1 the representation draws in the
/// translucent pass.
pub opacity: f32,
/// Perceptual micro-surface roughness in [0, 1].
pub roughness: f32,
/// Specular strength in [0, 1]; kept low, molecules are not chrome.
pub specular: f32,
/// Lighting response model. This is presentation state, not chemistry.
pub model: MaterialModel,
}
/// Tagged lighting response selected per representation.
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum MaterialModel {
/// Restrained dielectric response for physical inspection.
#[default]
Molecular,
/// Energy-conserving metalness workflow for explicit art direction.
Principled {
/// Metalness in `[0, 1]`; pure surfaces normally use an endpoint.
metallic: f32,
},
/// Tangent-aligned dielectric response for polymer ribbons.
AnisotropicRibbon {
/// Lobe elongation in `[0, 1]`; zero resolves to isotropic dielectric.
strength: f32,
},
/// Bounded screen-space diffusion cue for explicit art direction.
Diffusion {
/// Scattering strength in `[0, 1]`; this is not a measured radius.
strength: f32,
},
}
impl Default for Material {
fn default() -> Self {
Self {
opacity: 1.0,
// A polished dielectric rather than a matte one. A tight highlight
// is what lets a curved impostor read as a solid body instead of a
// flat disc, so the default sits where a wet biological surface
// does, not where dry plastic does.
roughness: 0.34,
specular: 0.5,
model: MaterialModel::Molecular,
}
}
}
impl Material {
/// Finite roughness consumed by lighting; malformed input resolves neutral.
#[must_use]
pub fn perceptual_roughness(self) -> f32 {
if self.roughness.is_finite() {
self.roughness.clamp(0.05, 0.92)
} else {
Self::default().roughness
}
}
/// Finite dielectric highlight strength consumed by the lighting model.
#[must_use]
pub fn specular_strength(self) -> f32 {
if self.specular.is_finite() {
self.specular.clamp(0.0, 1.0)
} else {
Self::default().specular
}
}
/// Finite principled metalness, or zero for another model.
#[must_use]
pub fn metallic(self) -> f32 {
match self.model {
MaterialModel::Principled { metallic } if metallic.is_finite() => {
metallic.clamp(0.0, 1.0)
}
MaterialModel::Molecular
| MaterialModel::Principled { .. }
| MaterialModel::AnisotropicRibbon { .. }
| MaterialModel::Diffusion { .. } => 0.0,
}
}
/// Finite tangent-aligned response strength, or zero for another model.
#[must_use]
pub fn anisotropy(self) -> f32 {
match self.model {
MaterialModel::AnisotropicRibbon { strength } if strength.is_finite() => {
strength.clamp(0.0, 1.0)
}
MaterialModel::Molecular
| MaterialModel::Principled { .. }
| MaterialModel::AnisotropicRibbon { .. }
| MaterialModel::Diffusion { .. } => 0.0,
}
}
/// Finite bounded diffusion response, or zero for another model.
#[must_use]
pub fn diffusion_strength(self) -> f32 {
match self.model {
MaterialModel::Diffusion { strength } if strength.is_finite() => {
strength.clamp(0.0, 1.0)
}
MaterialModel::Molecular
| MaterialModel::Principled { .. }
| MaterialModel::AnisotropicRibbon { .. }
| MaterialModel::Diffusion { .. } => 0.0,
}
}
/// Compact shader tag and model parameter.
#[must_use]
pub fn model_lanes(self) -> [f32; 2] {
match self.model {
MaterialModel::Molecular => [0.0, 0.0],
MaterialModel::Principled { .. } => [1.0, self.metallic()],
MaterialModel::AnisotropicRibbon { .. } => [2.0, self.anisotropy()],
MaterialModel::Diffusion { .. } => [3.0, self.diffusion_strength()],
}
}
/// Creates an explicit art-directed principled material.
#[must_use]
pub fn principled(metallic: f32) -> Self {
Self {
model: MaterialModel::Principled { metallic },
..Self::default()
}
}
/// Creates an explicit tangent-aligned ribbon material.
#[must_use]
pub fn anisotropic_ribbon(strength: f32) -> Self {
Self {
model: MaterialModel::AnisotropicRibbon { strength },
..Self::default()
}
}
/// Creates an explicit, bounded diffusion presentation.
#[must_use]
pub fn diffusion(strength: f32) -> Self {
Self {
model: MaterialModel::Diffusion { strength },
..Self::default()
}
}
/// True when this material belongs in the order-independent translucent pass.
#[must_use]
pub fn is_translucent(self) -> bool {
self.opacity.is_finite() && self.opacity.clamp(0.0, 1.0) < 1.0
}
/// Opacity quantized to unorm8 with deterministic round-to-nearest conversion.
#[must_use]
pub fn opacity_unorm8(self) -> u8 {
if !self.opacity.is_finite() {
return u8::MAX;
}
molgfx_math::unorm8(self.opacity)
}
}