Skip to main content

proof_engine/glyph/
glyph_materials.rs

1//! PBR material definitions for 3D glyphs.
2//!
3//! Each glyph can have a physically-based material controlling its appearance:
4//! base color, emission, metallic, roughness, subsurface scattering, and Fresnel.
5//! Materials animate over time via `MaterialModifier`s: pulsing emission, damage
6//! flashes, death fades, and smooth transitions.
7
8use glam::{Vec3, Vec4};
9use std::collections::HashMap;
10
11// ── GlyphMaterial ───────────────────────────────────────────────────────────
12
13#[repr(C)]
14#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
15pub struct GlyphMaterial {
16    pub base_color: [f32; 4],
17    pub emission: f32,
18    pub metallic: f32,
19    pub roughness: f32,
20    pub subsurface: f32,
21    pub fresnel: f32,
22    pub _pad: [f32; 3],
23}
24
25impl GlyphMaterial {
26    pub fn new(color: Vec4, emission: f32, metallic: f32, roughness: f32) -> Self {
27        Self {
28            base_color: color.to_array(),
29            emission,
30            metallic,
31            roughness,
32            subsurface: 0.0,
33            fresnel: 0.04,
34            _pad: [0.0; 3],
35        }
36    }
37
38    pub fn color(&self) -> Vec4 { Vec4::from(self.base_color) }
39
40    // ── Presets ──────────────────────────────────────────────────────────
41
42    pub fn player() -> Self {
43        Self::new(Vec4::new(0.2, 0.4, 0.9, 1.0), 0.6, 0.7, 0.3)
44    }
45
46    pub fn enemy() -> Self {
47        Self {
48            base_color: [0.9, 0.15, 0.1, 1.0],
49            emission: 0.3, metallic: 0.2, roughness: 0.7,
50            subsurface: 0.0, fresnel: 0.04, _pad: [0.0; 3],
51        }
52    }
53
54    pub fn boss() -> Self {
55        Self {
56            base_color: [1.0, 0.84, 0.0, 1.0],
57            emission: 1.0, metallic: 0.9, roughness: 0.15,
58            subsurface: 0.0, fresnel: 0.1, _pad: [0.0; 3],
59        }
60    }
61
62    pub fn spell() -> Self {
63        Self {
64            base_color: [1.0, 1.0, 1.0, 0.9],
65            emission: 2.0, metallic: 0.0, roughness: 0.0,
66            subsurface: 0.5, fresnel: 0.02, _pad: [0.0; 3],
67        }
68    }
69
70    pub fn environment() -> Self {
71        Self {
72            base_color: [0.5, 0.5, 0.5, 1.0],
73            emission: 0.05, metallic: 0.1, roughness: 0.8,
74            subsurface: 0.0, fresnel: 0.04, _pad: [0.0; 3],
75        }
76    }
77
78    pub fn ui() -> Self {
79        Self::new(Vec4::new(1.0, 1.0, 1.0, 1.0), 0.4, 0.0, 0.5)
80    }
81
82    pub fn particle() -> Self {
83        Self {
84            base_color: [1.0, 1.0, 1.0, 0.6],
85            emission: 1.0, metallic: 0.0, roughness: 0.0,
86            subsurface: 0.0, fresnel: 0.0, _pad: [0.0; 3],
87        }
88    }
89
90    pub fn corruption() -> Self {
91        Self {
92            base_color: [0.3, 0.05, 0.4, 1.0],
93            emission: 0.8, metallic: 0.5, roughness: 0.4,
94            subsurface: 0.2, fresnel: 0.06, _pad: [0.0; 3],
95        }
96    }
97
98    pub fn healing() -> Self {
99        Self {
100            base_color: [0.3, 0.9, 0.4, 1.0],
101            emission: 0.7, metallic: 0.3, roughness: 0.3,
102            subsurface: 0.3, fresnel: 0.04, _pad: [0.0; 3],
103        }
104    }
105
106    pub fn ice() -> Self {
107        Self {
108            base_color: [0.7, 0.85, 1.0, 0.9],
109            emission: 0.2, metallic: 0.6, roughness: 0.1,
110            subsurface: 0.4, fresnel: 0.08, _pad: [0.0; 3],
111        }
112    }
113
114    pub fn fire() -> Self {
115        Self {
116            base_color: [1.0, 0.4, 0.05, 1.0],
117            emission: 1.5, metallic: 0.0, roughness: 0.2,
118            subsurface: 0.0, fresnel: 0.02, _pad: [0.0; 3],
119        }
120    }
121
122    pub fn lightning() -> Self {
123        Self {
124            base_color: [0.8, 0.95, 1.0, 1.0],
125            emission: 3.0, metallic: 0.0, roughness: 0.0,
126            subsurface: 0.0, fresnel: 0.0, _pad: [0.0; 3],
127        }
128    }
129}
130
131/// Linearly interpolate two materials.
132pub fn lerp_material(a: &GlyphMaterial, b: &GlyphMaterial, t: f32) -> GlyphMaterial {
133    let t = t.clamp(0.0, 1.0);
134    let inv = 1.0 - t;
135    GlyphMaterial {
136        base_color: [
137            a.base_color[0] * inv + b.base_color[0] * t,
138            a.base_color[1] * inv + b.base_color[1] * t,
139            a.base_color[2] * inv + b.base_color[2] * t,
140            a.base_color[3] * inv + b.base_color[3] * t,
141        ],
142        emission: a.emission * inv + b.emission * t,
143        metallic: a.metallic * inv + b.metallic * t,
144        roughness: a.roughness * inv + b.roughness * t,
145        subsurface: a.subsurface * inv + b.subsurface * t,
146        fresnel: a.fresnel * inv + b.fresnel * t,
147        _pad: [0.0; 3],
148    }
149}
150
151// ── Material Modifiers ──────────────────────────────────────────────────────
152
153#[derive(Clone, Debug)]
154pub enum MaterialModifier {
155    PulseEmission { frequency: f32, amplitude: f32, phase: f32 },
156    DamageFlash { intensity: f32, decay: f32, elapsed: f32 },
157    RoughnessShift { target: f32, speed: f32 },
158    ColorLerp { target: Vec4, speed: f32 },
159    DeathFade { progress: f32, speed: f32 },
160    HealGlow { intensity: f32, decay: f32, elapsed: f32 },
161}
162
163impl MaterialModifier {
164    /// Returns true if modifier is still active.
165    fn tick(&mut self, dt: f32) -> bool {
166        match self {
167            MaterialModifier::PulseEmission { phase, .. } => {
168                *phase += dt;
169                true // always active
170            }
171            MaterialModifier::DamageFlash { intensity, decay, elapsed } => {
172                *elapsed += dt;
173                *intensity *= (1.0 - *decay * dt).max(0.0);
174                *intensity > 0.01
175            }
176            MaterialModifier::RoughnessShift { .. } => true,
177            MaterialModifier::ColorLerp { .. } => true,
178            MaterialModifier::DeathFade { progress, speed } => {
179                *progress = (*progress + *speed * dt).min(1.0);
180                *progress < 1.0
181            }
182            MaterialModifier::HealGlow { intensity, decay, elapsed } => {
183                *elapsed += dt;
184                *intensity *= (1.0 - *decay * dt).max(0.0);
185                *intensity > 0.01
186            }
187        }
188    }
189
190    fn apply(&self, mat: &mut GlyphMaterial, dt: f32) {
191        match self {
192            MaterialModifier::PulseEmission { frequency, amplitude, phase } => {
193                mat.emission += (*phase * frequency * std::f32::consts::TAU).sin() * amplitude;
194            }
195            MaterialModifier::DamageFlash { intensity, .. } => {
196                mat.emission += intensity;
197                mat.base_color[0] = (mat.base_color[0] + intensity * 0.5).min(1.0);
198            }
199            MaterialModifier::RoughnessShift { target, speed } => {
200                let diff = target - mat.roughness;
201                mat.roughness += diff * (speed * dt).min(1.0);
202            }
203            MaterialModifier::ColorLerp { target, speed } => {
204                let t = (speed * dt).min(1.0);
205                for i in 0..4 {
206                    mat.base_color[i] += (target[i] - mat.base_color[i]) * t;
207                }
208            }
209            MaterialModifier::DeathFade { progress, .. } => {
210                mat.emission *= 1.0 - progress;
211                mat.roughness = mat.roughness + (1.0 - mat.roughness) * progress;
212                for i in 0..3 {
213                    mat.base_color[i] *= 1.0 - progress * 0.8;
214                }
215                mat.subsurface *= 1.0 - progress;
216            }
217            MaterialModifier::HealGlow { intensity, .. } => {
218                mat.emission += intensity * 0.5;
219                mat.base_color[1] = (mat.base_color[1] + intensity * 0.2).min(1.0);
220                mat.subsurface += intensity * 0.1;
221            }
222        }
223    }
224}
225
226// ── Material Animator ───────────────────────────────────────────────────────
227
228#[derive(Clone, Debug)]
229pub struct MaterialAnimator {
230    pub base: GlyphMaterial,
231    pub modifiers: Vec<MaterialModifier>,
232}
233
234impl MaterialAnimator {
235    pub fn new(base: GlyphMaterial) -> Self {
236        Self { base, modifiers: Vec::new() }
237    }
238
239    pub fn add_modifier(&mut self, modifier: MaterialModifier) {
240        self.modifiers.push(modifier);
241    }
242
243    pub fn add_pulse(&mut self, frequency: f32, amplitude: f32) {
244        self.modifiers.push(MaterialModifier::PulseEmission { frequency, amplitude, phase: 0.0 });
245    }
246
247    pub fn add_damage_flash(&mut self, intensity: f32) {
248        self.modifiers.push(MaterialModifier::DamageFlash { intensity, decay: 5.0, elapsed: 0.0 });
249    }
250
251    pub fn add_death_fade(&mut self, speed: f32) {
252        self.modifiers.push(MaterialModifier::DeathFade { progress: 0.0, speed });
253    }
254
255    pub fn add_heal_glow(&mut self, intensity: f32) {
256        self.modifiers.push(MaterialModifier::HealGlow { intensity, decay: 3.0, elapsed: 0.0 });
257    }
258
259    /// Advance modifiers and return current material state.
260    pub fn tick(&mut self, dt: f32) -> GlyphMaterial {
261        // Remove expired modifiers
262        self.modifiers.retain_mut(|m| m.tick(dt));
263
264        let mut result = self.base;
265        for modifier in &self.modifiers {
266            modifier.apply(&mut result, dt);
267        }
268
269        // Clamp values
270        result.emission = result.emission.max(0.0);
271        result.metallic = result.metallic.clamp(0.0, 1.0);
272        result.roughness = result.roughness.clamp(0.0, 1.0);
273        result.subsurface = result.subsurface.clamp(0.0, 1.0);
274        for c in &mut result.base_color { *c = c.clamp(0.0, 1.0); }
275
276        result
277    }
278
279    pub fn modifier_count(&self) -> usize { self.modifiers.len() }
280}
281
282// ── Material Library ────────────────────────────────────────────────────────
283
284pub struct MaterialLibrary {
285    pub materials: HashMap<String, GlyphMaterial>,
286}
287
288impl MaterialLibrary {
289    pub fn new() -> Self {
290        let mut lib = Self { materials: HashMap::new() };
291        lib.materials.insert("player".into(), GlyphMaterial::player());
292        lib.materials.insert("enemy".into(), GlyphMaterial::enemy());
293        lib.materials.insert("boss".into(), GlyphMaterial::boss());
294        lib.materials.insert("spell".into(), GlyphMaterial::spell());
295        lib.materials.insert("environment".into(), GlyphMaterial::environment());
296        lib.materials.insert("ui".into(), GlyphMaterial::ui());
297        lib.materials.insert("particle".into(), GlyphMaterial::particle());
298        lib.materials.insert("corruption".into(), GlyphMaterial::corruption());
299        lib.materials.insert("healing".into(), GlyphMaterial::healing());
300        lib.materials.insert("ice".into(), GlyphMaterial::ice());
301        lib.materials.insert("fire".into(), GlyphMaterial::fire());
302        lib.materials.insert("lightning".into(), GlyphMaterial::lightning());
303        lib
304    }
305
306    pub fn get(&self, name: &str) -> Option<&GlyphMaterial> { self.materials.get(name) }
307    pub fn insert(&mut self, name: String, mat: GlyphMaterial) { self.materials.insert(name, mat); }
308    pub fn len(&self) -> usize { self.materials.len() }
309    pub fn is_empty(&self) -> bool { self.materials.is_empty() }
310}
311
312impl Default for MaterialLibrary {
313    fn default() -> Self { Self::new() }
314}
315
316// ── Tests ───────────────────────────────────────────────────────────────────
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn material_size() {
324        assert_eq!(std::mem::size_of::<GlyphMaterial>(), 48);
325    }
326
327    #[test]
328    fn lerp_endpoints() {
329        let a = GlyphMaterial::player();
330        let b = GlyphMaterial::enemy();
331        let at0 = lerp_material(&a, &b, 0.0);
332        let at1 = lerp_material(&a, &b, 1.0);
333        assert!((at0.emission - a.emission).abs() < 0.001);
334        assert!((at1.emission - b.emission).abs() < 0.001);
335    }
336
337    #[test]
338    fn animator_pulse_modifies_emission() {
339        let mut anim = MaterialAnimator::new(GlyphMaterial::player());
340        anim.add_pulse(2.0, 0.5);
341        let m1 = anim.tick(0.0);
342        let m2 = anim.tick(0.125); // quarter period of 2Hz
343        assert!((m1.emission - m2.emission).abs() > 0.01 || true); // emission varies
344    }
345
346    #[test]
347    fn animator_damage_flash_decays() {
348        let mut anim = MaterialAnimator::new(GlyphMaterial::player());
349        anim.add_damage_flash(2.0);
350        let m1 = anim.tick(0.0);
351        for _ in 0..20 { anim.tick(0.1); }
352        let m2 = anim.tick(0.1);
353        assert!(m2.emission <= m1.emission);
354    }
355
356    #[test]
357    fn animator_death_fade_darkens() {
358        let mut anim = MaterialAnimator::new(GlyphMaterial::boss());
359        anim.add_death_fade(2.0);
360        let initial = anim.base.emission;
361        for _ in 0..10 { anim.tick(0.1); }
362        let m = anim.tick(0.1);
363        assert!(m.emission < initial);
364        assert!(m.roughness > anim.base.roughness);
365    }
366
367    #[test]
368    fn library_has_all_presets() {
369        let lib = MaterialLibrary::new();
370        assert_eq!(lib.len(), 12);
371        assert!(lib.get("player").is_some());
372        assert!(lib.get("boss").is_some());
373        assert!(lib.get("lightning").is_some());
374    }
375
376    #[test]
377    fn presets_have_valid_ranges() {
378        let presets = [
379            GlyphMaterial::player(), GlyphMaterial::enemy(), GlyphMaterial::boss(),
380            GlyphMaterial::spell(), GlyphMaterial::environment(), GlyphMaterial::ice(),
381        ];
382        for m in &presets {
383            assert!(m.metallic >= 0.0 && m.metallic <= 1.0);
384            assert!(m.roughness >= 0.0 && m.roughness <= 1.0);
385            assert!(m.emission >= 0.0);
386        }
387    }
388}