Skip to main content

proof_engine/metaball/
surface_material.rs

1//! Surface material properties for the isosurface — iridescence, translucency,
2//! fresnel, procedural veining, and mathematical symbol projection.
3
4use glam::{Vec3, Vec4};
5use super::marching_cubes::MCVertex;
6
7/// Material properties for the metaball isosurface.
8#[derive(Debug, Clone)]
9pub struct SurfaceMaterial {
10    /// Base color multiplier.
11    pub base_color: Vec4,
12    /// Metallic factor (0 = dielectric, 1 = metallic).
13    pub metallic: f32,
14    /// Surface roughness (0 = mirror, 1 = matte).
15    pub roughness: f32,
16    /// Emission intensity multiplier.
17    pub emission_multiplier: f32,
18
19    // ── Iridescence ─────────────────────────────────────────────────────
20    /// Enable thin-film iridescence.
21    pub iridescence_enabled: bool,
22    /// Thin-film thickness (nm). Controls the base color shift.
23    pub film_thickness: f32,
24    /// Iridescence intensity (0 = none, 1 = full rainbow).
25    pub iridescence_strength: f32,
26
27    // ── Translucency ────────────────────────────────────────────────────
28    /// Enable translucency at thin surface areas.
29    pub translucency_enabled: bool,
30    /// Field strength below which translucency begins.
31    pub translucency_threshold: f32,
32    /// Maximum translucency amount (0 = opaque, 1 = fully translucent).
33    pub translucency_max: f32,
34    /// Translucency color (light bleeding through).
35    pub translucency_color: Vec3,
36
37    // ── Fresnel ─────────────────────────────────────────────────────────
38    /// F0 reflectance at normal incidence (Schlick approximation).
39    pub fresnel_f0: f32,
40    /// Fresnel power exponent (5.0 = Schlick standard).
41    pub fresnel_power: f32,
42
43    // ── Procedural veining ──────────────────────────────────────────────
44    /// Enable procedural veins from field gradient.
45    pub veining_enabled: bool,
46    /// Vein color.
47    pub vein_color: Vec4,
48    /// Vein frequency (how many veins per unit distance).
49    pub vein_frequency: f32,
50    /// Vein thickness (0 = thin, 1 = thick).
51    pub vein_thickness: f32,
52    /// Vein emission (glowing veins).
53    pub vein_emission: f32,
54
55    // ── Symbol projection ───────────────────────────────────────────────
56    /// Enable mathematical symbol projection onto surface.
57    pub symbols_enabled: bool,
58    /// Symbol density (symbols per unit area).
59    pub symbol_density: f32,
60    /// Symbol emission intensity.
61    pub symbol_emission: f32,
62}
63
64impl Default for SurfaceMaterial {
65    fn default() -> Self {
66        Self {
67            base_color: Vec4::ONE,
68            metallic: 0.0,
69            roughness: 0.4,
70            emission_multiplier: 1.0,
71            iridescence_enabled: false,
72            film_thickness: 500.0,
73            iridescence_strength: 0.5,
74            translucency_enabled: true,
75            translucency_threshold: 0.7,
76            translucency_max: 0.6,
77            translucency_color: Vec3::new(0.8, 0.3, 0.1),
78            fresnel_f0: 0.04,
79            fresnel_power: 5.0,
80            veining_enabled: false,
81            vein_color: Vec4::new(0.2, 0.8, 1.0, 1.0),
82            vein_frequency: 5.0,
83            vein_thickness: 0.1,
84            vein_emission: 0.5,
85            symbols_enabled: false,
86            symbol_density: 2.0,
87            symbol_emission: 0.3,
88        }
89    }
90}
91
92impl SurfaceMaterial {
93    /// Organic creature material (default + translucency).
94    pub fn organic() -> Self {
95        Self {
96            translucency_enabled: true,
97            roughness: 0.5,
98            ..Default::default()
99        }
100    }
101
102    /// Crystalline material with iridescence.
103    pub fn crystalline() -> Self {
104        Self {
105            iridescence_enabled: true,
106            iridescence_strength: 0.8,
107            film_thickness: 400.0,
108            roughness: 0.1,
109            metallic: 0.3,
110            ..Default::default()
111        }
112    }
113
114    /// Dark void material with glowing veins.
115    pub fn void_entity() -> Self {
116        Self {
117            base_color: Vec4::new(0.05, 0.02, 0.1, 1.0),
118            veining_enabled: true,
119            vein_color: Vec4::new(0.5, 0.0, 1.0, 1.0),
120            vein_emission: 1.5,
121            roughness: 0.8,
122            ..Default::default()
123        }
124    }
125
126    /// Boss material with symbols and intense emission.
127    pub fn boss() -> Self {
128        Self {
129            symbols_enabled: true,
130            symbol_emission: 1.0,
131            emission_multiplier: 2.0,
132            iridescence_enabled: true,
133            iridescence_strength: 0.4,
134            roughness: 0.3,
135            ..Default::default()
136        }
137    }
138
139    /// Mathematical entity with projected symbols.
140    pub fn mathematical() -> Self {
141        Self {
142            symbols_enabled: true,
143            symbol_density: 4.0,
144            symbol_emission: 0.8,
145            roughness: 0.2,
146            metallic: 0.5,
147            ..Default::default()
148        }
149    }
150}
151
152/// Computed material properties at a specific surface point.
153#[derive(Debug, Clone)]
154pub struct MaterialSample {
155    pub albedo: Vec4,
156    pub emission: f32,
157    pub roughness: f32,
158    pub metallic: f32,
159    pub opacity: f32,
160    pub fresnel: f32,
161    pub iridescence_shift: Vec3,
162}
163
164impl Default for MaterialSample {
165    fn default() -> Self {
166        Self {
167            albedo: Vec4::ONE, emission: 0.0, roughness: 0.4, metallic: 0.0,
168            opacity: 1.0, fresnel: 0.04, iridescence_shift: Vec3::ZERO,
169        }
170    }
171}
172
173/// Evaluate material at a surface point.
174pub fn evaluate_material(
175    material: &SurfaceMaterial,
176    vertex: &MCVertex,
177    view_dir: Vec3,
178    field_strength: f32,
179    threshold: f32,
180) -> MaterialSample {
181    let normal = vertex.normal.normalize_or_zero();
182    let n_dot_v = normal.dot(view_dir).abs();
183
184    // Base albedo
185    let mut albedo = vertex.color * material.base_color;
186    let mut emission = vertex.emission * material.emission_multiplier;
187
188    // Fresnel (Schlick approximation)
189    let fresnel = material.fresnel_f0
190        + (1.0 - material.fresnel_f0) * (1.0 - n_dot_v).powf(material.fresnel_power);
191
192    // Translucency
193    let mut opacity = 1.0f32;
194    if material.translucency_enabled {
195        let excess = field_strength - threshold;
196        let thin_range = material.translucency_threshold - threshold;
197        if excess < thin_range && thin_range > 0.0 {
198            let t = (1.0 - excess / thin_range).clamp(0.0, 1.0);
199            opacity = 1.0 - t * material.translucency_max;
200            // Blend in translucency color
201            let tc = material.translucency_color;
202            albedo = Vec4::new(
203                albedo.x * (1.0 - t) + tc.x * t,
204                albedo.y * (1.0 - t) + tc.y * t,
205                albedo.z * (1.0 - t) + tc.z * t,
206                albedo.w,
207            );
208            emission += t * 0.3; // thin areas glow slightly (internal glow visible)
209        }
210    }
211
212    // Iridescence
213    let iridescence_shift = if material.iridescence_enabled {
214        thin_film_iridescence(n_dot_v, material.film_thickness, material.iridescence_strength)
215    } else {
216        Vec3::ZERO
217    };
218
219    if material.iridescence_enabled {
220        albedo = Vec4::new(
221            (albedo.x + iridescence_shift.x).clamp(0.0, 1.0),
222            (albedo.y + iridescence_shift.y).clamp(0.0, 1.0),
223            (albedo.z + iridescence_shift.z).clamp(0.0, 1.0),
224            albedo.w,
225        );
226    }
227
228    // Procedural veining
229    if material.veining_enabled {
230        let vein_val = procedural_vein(vertex.position, material.vein_frequency);
231        if vein_val > 1.0 - material.vein_thickness {
232            let vein_blend = (vein_val - (1.0 - material.vein_thickness)) / material.vein_thickness;
233            albedo = albedo.lerp(material.vein_color, vein_blend.clamp(0.0, 1.0));
234            emission += material.vein_emission * vein_blend;
235        }
236    }
237
238    MaterialSample {
239        albedo,
240        emission,
241        roughness: material.roughness,
242        metallic: material.metallic,
243        opacity,
244        fresnel,
245        iridescence_shift,
246    }
247}
248
249/// Thin-film interference approximation.
250/// Returns an RGB color shift based on view angle and film thickness.
251fn thin_film_iridescence(n_dot_v: f32, thickness_nm: f32, strength: f32) -> Vec3 {
252    // Optical path difference depends on angle and film thickness
253    let opd = 2.0 * thickness_nm * (1.0 - n_dot_v * n_dot_v).sqrt().max(0.0);
254
255    // Convert OPD to wavelength-dependent phase shifts
256    // Red ≈ 650nm, Green ≈ 550nm, Blue ≈ 450nm
257    let phase_r = (opd / 650.0 * std::f32::consts::TAU).cos();
258    let phase_g = (opd / 550.0 * std::f32::consts::TAU).cos();
259    let phase_b = (opd / 450.0 * std::f32::consts::TAU).cos();
260
261    Vec3::new(phase_r, phase_g, phase_b) * strength * 0.5
262}
263
264/// Procedural veining pattern using sine waves.
265fn procedural_vein(position: Vec3, frequency: f32) -> f32 {
266    let v1 = (position.x * frequency + position.y * frequency * 0.7).sin();
267    let v2 = (position.y * frequency * 1.3 + position.z * frequency * 0.5).sin();
268    let v3 = (position.z * frequency * 0.9 + position.x * frequency * 1.1).sin();
269    ((v1 + v2 + v3) / 3.0 + 1.0) * 0.5 // normalize to [0, 1]
270}
271
272// ── GLSL fragment shader additions ──────────────────────────────────────────
273
274pub const METABALL_MATERIAL_FRAG: &str = r#"
275// Iridescence function for metaball fragment shader
276vec3 thin_film_iridescence(float NdotV, float thickness, float strength) {
277    float opd = 2.0 * thickness * sqrt(max(0.0, 1.0 - NdotV * NdotV));
278    float phase_r = cos(opd / 650.0 * 6.28318);
279    float phase_g = cos(opd / 550.0 * 6.28318);
280    float phase_b = cos(opd / 450.0 * 6.28318);
281    return vec3(phase_r, phase_g, phase_b) * strength * 0.5;
282}
283
284// Fresnel (Schlick)
285float fresnel_schlick(float NdotV, float f0) {
286    return f0 + (1.0 - f0) * pow(1.0 - NdotV, 5.0);
287}
288"#;
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn iridescence_varies_with_angle() {
296        let a = thin_film_iridescence(1.0, 500.0, 1.0); // head-on
297        let b = thin_film_iridescence(0.1, 500.0, 1.0); // grazing
298        assert!((a - b).length() > 0.01, "Iridescence should vary with angle");
299    }
300
301    #[test]
302    fn material_presets_differ() {
303        let organic = SurfaceMaterial::organic();
304        let crystal = SurfaceMaterial::crystalline();
305        assert_ne!(organic.iridescence_enabled, crystal.iridescence_enabled);
306    }
307
308    #[test]
309    fn translucency_at_thin_areas() {
310        let mat = SurfaceMaterial::organic();
311        let vertex = MCVertex {
312            position: Vec3::ZERO, normal: Vec3::Y,
313            color: Vec4::ONE, emission: 0.0,
314        };
315        // Thin area: field_strength barely above threshold
316        let sample = evaluate_material(&mat, &vertex, Vec3::Y, 0.51, 0.5);
317        assert!(sample.opacity < 1.0, "Thin areas should be translucent: {}", sample.opacity);
318    }
319
320    #[test]
321    fn thick_area_is_opaque() {
322        let mat = SurfaceMaterial::organic();
323        let vertex = MCVertex {
324            position: Vec3::ZERO, normal: Vec3::Y,
325            color: Vec4::ONE, emission: 0.0,
326        };
327        let sample = evaluate_material(&mat, &vertex, Vec3::Y, 2.0, 0.5);
328        assert!((sample.opacity - 1.0).abs() < 0.01, "Thick areas should be opaque");
329    }
330
331    #[test]
332    fn veining_produces_pattern() {
333        let v1 = procedural_vein(Vec3::ZERO, 5.0);
334        let v2 = procedural_vein(Vec3::new(0.3, 0.5, 0.7), 5.0);
335        assert!((v1 - v2).abs() > 0.01, "Veining should vary with position");
336    }
337}