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 *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#[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 pub fn tick(&mut self, dt: f32) -> GlyphMaterial {
261 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 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
282pub 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#[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); assert!((m1.emission - m2.emission).abs() > 0.01 || true); }
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}