Skip to main content

proof_engine/metaball/
field_eval.rs

1//! Field evaluation and sampling — evaluate the scalar field at any point, compute
2//! gradient for normals, color and emission via weighted average of source contributions.
3
4use glam::{Vec3, Vec4};
5use super::entity_field::{MetaballEntity, FieldSource};
6
7/// Complete sample of the entity's field at a point.
8#[derive(Debug, Clone)]
9pub struct FieldSample {
10    /// Total field strength at this point.
11    pub strength: f32,
12    /// Weighted-average color from contributing sources.
13    pub color: Vec4,
14    /// Weighted-average emission intensity.
15    pub emission: f32,
16    /// Field gradient (unnormalized — proportional to rate of change).
17    pub gradient: Vec3,
18    /// Whether this point is on or above the isosurface threshold.
19    pub above_threshold: bool,
20    /// How far above/below the threshold (signed distance estimate).
21    pub signed_distance_estimate: f32,
22}
23
24impl Default for FieldSample {
25    fn default() -> Self {
26        Self {
27            strength: 0.0,
28            color: Vec4::new(0.5, 0.5, 0.5, 1.0),
29            emission: 0.0,
30            gradient: Vec3::ZERO,
31            above_threshold: false,
32            signed_distance_estimate: 0.0,
33        }
34    }
35}
36
37/// Batch field evaluator — evaluates the entity's field with optimizations.
38pub struct FieldEvaluator {
39    /// Epsilon for gradient computation (central differences).
40    pub gradient_eps: f32,
41    /// Whether to compute gradient (can skip if only need strength).
42    pub compute_gradient: bool,
43    /// Whether to compute color/emission (can skip for pure MC classification).
44    pub compute_material: bool,
45    /// Minimum source contribution to consider (skip negligible sources).
46    pub min_contribution: f32,
47}
48
49impl Default for FieldEvaluator {
50    fn default() -> Self {
51        Self {
52            gradient_eps: 0.01,
53            compute_gradient: true,
54            compute_material: true,
55            min_contribution: 1e-6,
56        }
57    }
58}
59
60impl FieldEvaluator {
61    pub fn strength_only() -> Self {
62        Self { compute_gradient: false, compute_material: false, ..Default::default() }
63    }
64
65    pub fn with_gradient() -> Self {
66        Self { compute_gradient: true, compute_material: false, ..Default::default() }
67    }
68
69    pub fn full() -> Self { Self::default() }
70
71    /// Evaluate the field at a single point.
72    pub fn evaluate(&self, entity: &MetaballEntity, point: Vec3) -> FieldSample {
73        let mut total_strength = 0.0f32;
74        let mut weighted_color = Vec4::ZERO;
75        let mut weighted_emission = 0.0f32;
76
77        // Sum contributions from all active sources
78        for source in &entity.sources {
79            if !source.is_active() { continue; }
80
81            // Quick rejection: check if point is within source's influence sphere
82            let to_source = source.position - point;
83            let dist_sq = to_source.length_squared();
84            let max_dist = source.radius * 1.1; // small margin
85            if dist_sq > max_dist * max_dist { continue; }
86
87            let contrib = source.evaluate(point, entity.hp_ratio);
88            if contrib < self.min_contribution { continue; }
89
90            total_strength += contrib;
91
92            if self.compute_material {
93                weighted_color += source.color * contrib;
94                weighted_emission += source.emission * contrib;
95            }
96        }
97
98        // Normalize weighted averages
99        let color = if total_strength > 1e-8 && self.compute_material {
100            weighted_color / total_strength
101        } else {
102            Vec4::new(0.5, 0.5, 0.5, 1.0)
103        };
104
105        let emission = if total_strength > 1e-8 && self.compute_material {
106            weighted_emission / total_strength
107        } else {
108            0.0
109        };
110
111        // Gradient via central differences
112        let gradient = if self.compute_gradient {
113            let eps = self.gradient_eps;
114            Vec3::new(
115                evaluate_strength(entity, point + Vec3::X * eps)
116                    - evaluate_strength(entity, point - Vec3::X * eps),
117                evaluate_strength(entity, point + Vec3::Y * eps)
118                    - evaluate_strength(entity, point - Vec3::Y * eps),
119                evaluate_strength(entity, point + Vec3::Z * eps)
120                    - evaluate_strength(entity, point - Vec3::Z * eps),
121            ) / (2.0 * eps)
122        } else {
123            Vec3::ZERO
124        };
125
126        let above = total_strength >= entity.threshold;
127        let sde = (total_strength - entity.threshold) / gradient.length().max(1e-6);
128
129        FieldSample {
130            strength: total_strength,
131            color,
132            emission,
133            gradient,
134            above_threshold: above,
135            signed_distance_estimate: sde,
136        }
137    }
138
139    /// Evaluate field strength only (fast path for marching cubes classification).
140    pub fn evaluate_strength_at(&self, entity: &MetaballEntity, point: Vec3) -> f32 {
141        evaluate_strength(entity, point)
142    }
143
144    /// Sample the field on a regular 3D grid. Returns a flat array [z][y][x].
145    pub fn sample_grid(
146        &self,
147        entity: &MetaballEntity,
148        bounds_min: Vec3,
149        bounds_max: Vec3,
150        resolution: u32,
151    ) -> FieldGrid {
152        let res = resolution as usize;
153        let total = res * res * res;
154        let step = (bounds_max - bounds_min) / (resolution - 1) as f32;
155
156        let mut strengths = Vec::with_capacity(total);
157        let mut colors = if self.compute_material { Vec::with_capacity(total) } else { Vec::new() };
158        let mut emissions = if self.compute_material { Vec::with_capacity(total) } else { Vec::new() };
159
160        for z in 0..res {
161            for y in 0..res {
162                for x in 0..res {
163                    let point = bounds_min + Vec3::new(x as f32, y as f32, z as f32) * step;
164
165                    if self.compute_material {
166                        let (s, c, e) = entity.evaluate_full(point);
167                        strengths.push(s);
168                        colors.push(c);
169                        emissions.push(e);
170                    } else {
171                        strengths.push(evaluate_strength(entity, point));
172                    }
173                }
174            }
175        }
176
177        FieldGrid {
178            strengths,
179            colors,
180            emissions,
181            resolution,
182            bounds_min,
183            bounds_max,
184            step,
185        }
186    }
187}
188
189/// Sampled 3D field grid.
190#[derive(Debug, Clone)]
191pub struct FieldGrid {
192    pub strengths: Vec<f32>,
193    pub colors: Vec<Vec4>,
194    pub emissions: Vec<f32>,
195    pub resolution: u32,
196    pub bounds_min: Vec3,
197    pub bounds_max: Vec3,
198    pub step: Vec3,
199}
200
201impl FieldGrid {
202    pub fn index(&self, x: usize, y: usize, z: usize) -> usize {
203        let r = self.resolution as usize;
204        z * r * r + y * r + x
205    }
206
207    pub fn strength_at(&self, x: usize, y: usize, z: usize) -> f32 {
208        self.strengths[self.index(x, y, z)]
209    }
210
211    pub fn position_at(&self, x: usize, y: usize, z: usize) -> Vec3 {
212        self.bounds_min + Vec3::new(x as f32, y as f32, z as f32) * self.step
213    }
214
215    pub fn color_at(&self, x: usize, y: usize, z: usize) -> Vec4 {
216        if self.colors.is_empty() { Vec4::ONE } else { self.colors[self.index(x, y, z)] }
217    }
218
219    pub fn emission_at(&self, x: usize, y: usize, z: usize) -> f32 {
220        if self.emissions.is_empty() { 0.0 } else { self.emissions[self.index(x, y, z)] }
221    }
222}
223
224/// Fast strength-only evaluation (no color/gradient).
225fn evaluate_strength(entity: &MetaballEntity, point: Vec3) -> f32 {
226    let mut total = 0.0f32;
227    for source in &entity.sources {
228        if !source.is_active() { continue; }
229        total += source.evaluate(point, entity.hp_ratio);
230    }
231    total
232}
233
234// ── Damage zone modulation ──────────────────────────────────────────────────
235
236/// Temporary damage zone that reduces field strength in an area.
237#[derive(Debug, Clone)]
238pub struct DamageZone {
239    pub center: Vec3,
240    pub radius: f32,
241    pub intensity: f32,
242    pub time_remaining: f32,
243    pub total_duration: f32,
244}
245
246impl DamageZone {
247    pub fn new(center: Vec3, radius: f32, intensity: f32, duration: f32) -> Self {
248        Self { center, radius, intensity, time_remaining: duration, total_duration: duration }
249    }
250
251    /// Evaluate the strength reduction at a point (0.0 = no effect, 1.0 = full suppression).
252    pub fn evaluate(&self, point: Vec3) -> f32 {
253        let dist = (point - self.center).length();
254        if dist >= self.radius { return 0.0; }
255        let spatial = 1.0 - dist / self.radius;
256        let temporal = self.time_remaining / self.total_duration;
257        spatial * temporal * self.intensity
258    }
259
260    /// Update the zone (tick down timer). Returns false when expired.
261    pub fn update(&mut self, dt: f32) -> bool {
262        self.time_remaining -= dt;
263        self.time_remaining > 0.0
264    }
265}
266
267// ── Tests ───────────────────────────────────────────────────────────────────
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use super::super::entity_field::FieldSource;
273
274    fn test_entity() -> MetaballEntity {
275        let mut e = MetaballEntity::new(0.5, 8);
276        e.add_source(FieldSource::new(Vec3::ZERO, 1.0, 1.0));
277        e
278    }
279
280    #[test]
281    fn evaluator_strength_only() {
282        let e = test_entity();
283        let ev = FieldEvaluator::strength_only();
284        let sample = ev.evaluate(&e, Vec3::ZERO);
285        assert!(sample.strength > 0.0);
286        assert_eq!(sample.gradient, Vec3::ZERO); // not computed
287    }
288
289    #[test]
290    fn evaluator_with_gradient() {
291        let e = test_entity();
292        let ev = FieldEvaluator::with_gradient();
293        let sample = ev.evaluate(&e, Vec3::new(0.5, 0.0, 0.0));
294        assert!(sample.gradient.length() > 0.0, "Gradient should be non-zero off-center");
295    }
296
297    #[test]
298    fn evaluator_full() {
299        let mut e = MetaballEntity::new(0.5, 8);
300        e.add_source(FieldSource::new(Vec3::ZERO, 1.0, 1.0).with_color(Vec4::new(1.0, 0.0, 0.0, 1.0)));
301        let ev = FieldEvaluator::full();
302        let sample = ev.evaluate(&e, Vec3::ZERO);
303        assert!(sample.color.x > 0.5); // red
304    }
305
306    #[test]
307    fn sample_grid_correct_size() {
308        let e = test_entity();
309        let ev = FieldEvaluator::strength_only();
310        let grid = ev.sample_grid(&e, Vec3::splat(-2.0), Vec3::splat(2.0), 4);
311        assert_eq!(grid.strengths.len(), 4 * 4 * 4);
312    }
313
314    #[test]
315    fn damage_zone_decays() {
316        let mut zone = DamageZone::new(Vec3::ZERO, 1.0, 1.0, 1.0);
317        assert!(zone.evaluate(Vec3::ZERO) > 0.0);
318        zone.update(0.5);
319        let after = zone.evaluate(Vec3::ZERO);
320        assert!(after < 1.0);
321        zone.update(0.6);
322        assert!(!zone.update(0.0)); // expired
323    }
324
325    #[test]
326    fn grid_indexing() {
327        let e = test_entity();
328        let ev = FieldEvaluator::strength_only();
329        let grid = ev.sample_grid(&e, Vec3::splat(-1.0), Vec3::splat(1.0), 4);
330        let center_val = grid.strength_at(2, 2, 2);
331        let corner_val = grid.strength_at(0, 0, 0);
332        assert!(center_val > corner_val, "Center should be stronger than corner");
333    }
334}