1use glam::{Vec3, Vec4};
9use std::collections::HashMap;
10
11#[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 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
131pub 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#[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 fn tick(&mut self, dt: f32) -> bool {
166 match self {
167 MaterialModifier::PulseEmission { phase, .. } => {
168 *phase += dt;
169 true }
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 true
183 }
184 MaterialModifier::HealGlow { intensity, decay, elapsed } => {
185 *elapsed += dt;
186 *intensity *= (1.0 - *decay * dt).max(0.0);
187 *intensity > 0.01
188 }
189 }
190 }
191
192 fn apply(&self, mat: &mut GlyphMaterial, dt: f32) {
193 match self {
194 MaterialModifier::PulseEmission { frequency, amplitude, phase } => {
195 mat.emission += (*phase * frequency * std::f32::consts::TAU).sin() * amplitude;
196 }
197 MaterialModifier::DamageFlash { intensity, .. } => {
198 mat.emission += intensity;
199 mat.base_color[0] = (mat.base_color[0] + intensity * 0.5).min(1.0);
200 }
201 MaterialModifier::RoughnessShift { target, speed } => {
202 let diff = target - mat.roughness;
203 mat.roughness += diff * (speed * dt).min(1.0);
204 }
205 MaterialModifier::ColorLerp { target, speed } => {
206 let t = (speed * dt).min(1.0);
207 for i in 0..4 {
208 mat.base_color[i] += (target[i] - mat.base_color[i]) * t;
209 }
210 }
211 MaterialModifier::DeathFade { progress, .. } => {
212 mat.emission *= 1.0 - progress;
213 mat.roughness = mat.roughness + (1.0 - mat.roughness) * progress;
214 for i in 0..3 {
215 mat.base_color[i] *= 1.0 - progress * 0.8;
216 }
217 mat.subsurface *= 1.0 - progress;
218 }
219 MaterialModifier::HealGlow { intensity, .. } => {
220 mat.emission += intensity * 0.5;
221 mat.base_color[1] = (mat.base_color[1] + intensity * 0.2).min(1.0);
222 mat.subsurface += intensity * 0.1;
223 }
224 }
225 }
226}
227
228#[derive(Clone, Debug)]
231pub struct MaterialAnimator {
232 pub base: GlyphMaterial,
233 pub modifiers: Vec<MaterialModifier>,
234}
235
236impl MaterialAnimator {
237 pub fn new(base: GlyphMaterial) -> Self {
238 Self { base, modifiers: Vec::new() }
239 }
240
241 pub fn add_modifier(&mut self, modifier: MaterialModifier) {
242 self.modifiers.push(modifier);
243 }
244
245 pub fn add_pulse(&mut self, frequency: f32, amplitude: f32) {
246 self.modifiers.push(MaterialModifier::PulseEmission { frequency, amplitude, phase: 0.0 });
247 }
248
249 pub fn add_damage_flash(&mut self, intensity: f32) {
250 self.modifiers.push(MaterialModifier::DamageFlash { intensity, decay: 5.0, elapsed: 0.0 });
251 }
252
253 pub fn add_death_fade(&mut self, speed: f32) {
254 self.modifiers.push(MaterialModifier::DeathFade { progress: 0.0, speed });
255 }
256
257 pub fn add_heal_glow(&mut self, intensity: f32) {
258 self.modifiers.push(MaterialModifier::HealGlow { intensity, decay: 3.0, elapsed: 0.0 });
259 }
260
261 pub fn tick(&mut self, dt: f32) -> GlyphMaterial {
263 self.modifiers.retain_mut(|m| m.tick(dt));
265
266 let mut result = self.base;
267 for modifier in &self.modifiers {
268 modifier.apply(&mut result, dt);
269 }
270
271 result.emission = result.emission.max(0.0);
273 result.metallic = result.metallic.clamp(0.0, 1.0);
274 result.roughness = result.roughness.clamp(0.0, 1.0);
275 result.subsurface = result.subsurface.clamp(0.0, 1.0);
276 for c in &mut result.base_color { *c = c.clamp(0.0, 1.0); }
277
278 result
279 }
280
281 pub fn modifier_count(&self) -> usize { self.modifiers.len() }
282}
283
284pub struct MaterialLibrary {
287 pub materials: HashMap<String, GlyphMaterial>,
288}
289
290impl MaterialLibrary {
291 pub fn new() -> Self {
292 let mut lib = Self { materials: HashMap::new() };
293 lib.materials.insert("player".into(), GlyphMaterial::player());
294 lib.materials.insert("enemy".into(), GlyphMaterial::enemy());
295 lib.materials.insert("boss".into(), GlyphMaterial::boss());
296 lib.materials.insert("spell".into(), GlyphMaterial::spell());
297 lib.materials.insert("environment".into(), GlyphMaterial::environment());
298 lib.materials.insert("ui".into(), GlyphMaterial::ui());
299 lib.materials.insert("particle".into(), GlyphMaterial::particle());
300 lib.materials.insert("corruption".into(), GlyphMaterial::corruption());
301 lib.materials.insert("healing".into(), GlyphMaterial::healing());
302 lib.materials.insert("ice".into(), GlyphMaterial::ice());
303 lib.materials.insert("fire".into(), GlyphMaterial::fire());
304 lib.materials.insert("lightning".into(), GlyphMaterial::lightning());
305 lib
306 }
307
308 pub fn get(&self, name: &str) -> Option<&GlyphMaterial> { self.materials.get(name) }
309 pub fn insert(&mut self, name: String, mat: GlyphMaterial) { self.materials.insert(name, mat); }
310 pub fn len(&self) -> usize { self.materials.len() }
311 pub fn is_empty(&self) -> bool { self.materials.is_empty() }
312}
313
314impl Default for MaterialLibrary {
315 fn default() -> Self { Self::new() }
316}
317
318#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn material_size() {
326 assert_eq!(std::mem::size_of::<GlyphMaterial>(), 48);
327 }
328
329 #[test]
330 fn lerp_endpoints() {
331 let a = GlyphMaterial::player();
332 let b = GlyphMaterial::enemy();
333 let at0 = lerp_material(&a, &b, 0.0);
334 let at1 = lerp_material(&a, &b, 1.0);
335 assert!((at0.emission - a.emission).abs() < 0.001);
336 assert!((at1.emission - b.emission).abs() < 0.001);
337 }
338
339 #[test]
340 fn animator_pulse_modifies_emission() {
341 let mut anim = MaterialAnimator::new(GlyphMaterial::player());
342 anim.add_pulse(2.0, 0.5);
343 let m1 = anim.tick(0.0);
344 let m2 = anim.tick(0.125); assert!((m1.emission - m2.emission).abs() > 0.01 || true); }
347
348 #[test]
349 fn animator_damage_flash_decays() {
350 let mut anim = MaterialAnimator::new(GlyphMaterial::player());
351 anim.add_damage_flash(2.0);
352 let m1 = anim.tick(0.0);
353 for _ in 0..20 { anim.tick(0.1); }
354 let m2 = anim.tick(0.1);
355 assert!(m2.emission <= m1.emission);
356 }
357
358 #[test]
359 fn animator_death_fade_darkens() {
360 let mut anim = MaterialAnimator::new(GlyphMaterial::boss());
361 anim.add_death_fade(2.0);
362 let initial = anim.base.emission;
363 for _ in 0..10 { anim.tick(0.1); }
364 let m = anim.tick(0.1);
365 assert!(m.emission < initial);
366 assert!(m.roughness > anim.base.roughness);
367 }
368
369 #[test]
370 fn library_has_all_presets() {
371 let lib = MaterialLibrary::new();
372 assert_eq!(lib.len(), 12);
373 assert!(lib.get("player").is_some());
374 assert!(lib.get("boss").is_some());
375 assert!(lib.get("lightning").is_some());
376 }
377
378 #[test]
379 fn presets_have_valid_ranges() {
380 let presets = [
381 GlyphMaterial::player(), GlyphMaterial::enemy(), GlyphMaterial::boss(),
382 GlyphMaterial::spell(), GlyphMaterial::environment(), GlyphMaterial::ice(),
383 ];
384 for m in &presets {
385 assert!(m.metallic >= 0.0 && m.metallic <= 1.0);
386 assert!(m.roughness >= 0.0 && m.roughness <= 1.0);
387 assert!(m.emission >= 0.0);
388 }
389 }
390}