1use glam::{Vec3, Vec4};
5use super::marching_cubes::MCVertex;
6
7#[derive(Debug, Clone)]
9pub struct SurfaceMaterial {
10 pub base_color: Vec4,
12 pub metallic: f32,
14 pub roughness: f32,
16 pub emission_multiplier: f32,
18
19 pub iridescence_enabled: bool,
22 pub film_thickness: f32,
24 pub iridescence_strength: f32,
26
27 pub translucency_enabled: bool,
30 pub translucency_threshold: f32,
32 pub translucency_max: f32,
34 pub translucency_color: Vec3,
36
37 pub fresnel_f0: f32,
40 pub fresnel_power: f32,
42
43 pub veining_enabled: bool,
46 pub vein_color: Vec4,
48 pub vein_frequency: f32,
50 pub vein_thickness: f32,
52 pub vein_emission: f32,
54
55 pub symbols_enabled: bool,
58 pub symbol_density: f32,
60 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 pub fn organic() -> Self {
95 Self {
96 translucency_enabled: true,
97 roughness: 0.5,
98 ..Default::default()
99 }
100 }
101
102 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 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 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 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#[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
173pub 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 let mut albedo = vertex.color * material.base_color;
186 let mut emission = vertex.emission * material.emission_multiplier;
187
188 let fresnel = material.fresnel_f0
190 + (1.0 - material.fresnel_f0) * (1.0 - n_dot_v).powf(material.fresnel_power);
191
192 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 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; }
210 }
211
212 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 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
249fn thin_film_iridescence(n_dot_v: f32, thickness_nm: f32, strength: f32) -> Vec3 {
252 let opd = 2.0 * thickness_nm * (1.0 - n_dot_v * n_dot_v).sqrt().max(0.0);
254
255 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
264fn 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 }
271
272pub 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); let b = thin_film_iridescence(0.1, 500.0, 1.0); 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 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}