Skip to main content

proof_engine/surfaces/
uvanimation.rs

1//! # UV Animation
2//!
3//! Animation and manipulation of UV texture coordinates on surfaces.
4//!
5//! ## Features
6//!
7//! - [`UVAnimator`] — scroll, rotate, scale, and warp UV coordinates over time
8//! - [`FlowMap`] — distort UVs along a 2D vector field
9//! - [`ParallaxLayer`] — multi-layer parallax scrolling
10//! - [`SpriteSheetAnimator`] — step through texture atlas frames
11//! - [`TriplanarProjector`] — seamless texturing of arbitrary geometry
12//! - [`UVUnwrap`] — UV unwrapping utilities (cylindrical, spherical, box projection)
13
14use glam::{Vec2, Vec3};
15use std::f32::consts::{PI, TAU};
16
17// ─────────────────────────────────────────────────────────────────────────────
18// UV animation modes
19// ─────────────────────────────────────────────────────────────────────────────
20
21/// UV animation mode.
22#[derive(Debug, Clone)]
23pub enum UVMode {
24    /// Constant scrolling in a direction.
25    Scroll {
26        velocity: Vec2,
27    },
28    /// Rotation around a pivot point.
29    Rotate {
30        pivot: Vec2,
31        speed: f32,
32    },
33    /// Oscillating scale.
34    Scale {
35        center: Vec2,
36        amplitude: Vec2,
37        frequency: f32,
38    },
39    /// Sinusoidal warp distortion.
40    SineWarp {
41        amplitude: Vec2,
42        frequency: Vec2,
43        speed: f32,
44    },
45    /// Radial warp from a center point.
46    RadialWarp {
47        center: Vec2,
48        amplitude: f32,
49        frequency: f32,
50        speed: f32,
51    },
52    /// Turbulence-based warp.
53    Turbulence {
54        amplitude: f32,
55        frequency: f32,
56        speed: f32,
57        octaves: u32,
58    },
59    /// Custom warp function.
60    Custom {
61        label: String,
62    },
63}
64
65/// Animates UV coordinates over time.
66pub struct UVAnimator {
67    pub mode: UVMode,
68    pub time: f32,
69    /// Optional custom warp function.
70    custom_fn: Option<Box<dyn Fn(Vec2, f32) -> Vec2 + Send + Sync>>,
71}
72
73impl UVAnimator {
74    /// Create a new UV animator with the given mode.
75    pub fn new(mode: UVMode) -> Self {
76        Self {
77            mode,
78            time: 0.0,
79            custom_fn: None,
80        }
81    }
82
83    /// Create a scrolling UV animator.
84    pub fn scroll(velocity: Vec2) -> Self {
85        Self::new(UVMode::Scroll { velocity })
86    }
87
88    /// Create a rotating UV animator.
89    pub fn rotate(pivot: Vec2, speed: f32) -> Self {
90        Self::new(UVMode::Rotate { pivot, speed })
91    }
92
93    /// Create a scaling UV animator.
94    pub fn scale(center: Vec2, amplitude: Vec2, frequency: f32) -> Self {
95        Self::new(UVMode::Scale { center, amplitude, frequency })
96    }
97
98    /// Create a sine-warp UV animator.
99    pub fn sine_warp(amplitude: Vec2, frequency: Vec2, speed: f32) -> Self {
100        Self::new(UVMode::SineWarp { amplitude, frequency, speed })
101    }
102
103    /// Create a custom UV animator.
104    pub fn custom<F>(label: &str, func: F) -> Self
105    where
106        F: Fn(Vec2, f32) -> Vec2 + Send + Sync + 'static,
107    {
108        Self {
109            mode: UVMode::Custom { label: label.to_string() },
110            time: 0.0,
111            custom_fn: Some(Box::new(func)),
112        }
113    }
114
115    /// Advance the animation time.
116    pub fn tick(&mut self, dt: f32) {
117        self.time += dt;
118    }
119
120    /// Reset the animation time.
121    pub fn reset(&mut self) {
122        self.time = 0.0;
123    }
124
125    /// Transform a single UV coordinate.
126    pub fn transform(&self, uv: Vec2) -> Vec2 {
127        self.transform_at(uv, self.time)
128    }
129
130    /// Transform a UV coordinate at a specific time.
131    pub fn transform_at(&self, uv: Vec2, time: f32) -> Vec2 {
132        match &self.mode {
133            UVMode::Scroll { velocity } => {
134                let offset = *velocity * time;
135                fract_vec2(uv + offset)
136            }
137            UVMode::Rotate { pivot, speed } => {
138                let angle = time * speed;
139                let cos_a = angle.cos();
140                let sin_a = angle.sin();
141                let centered = uv - *pivot;
142                let rotated = Vec2::new(
143                    centered.x * cos_a - centered.y * sin_a,
144                    centered.x * sin_a + centered.y * cos_a,
145                );
146                rotated + *pivot
147            }
148            UVMode::Scale { center, amplitude, frequency } => {
149                let scale = Vec2::ONE + *amplitude * (time * frequency * TAU).sin();
150                let centered = uv - *center;
151                centered * scale + *center
152            }
153            UVMode::SineWarp { amplitude, frequency, speed } => {
154                let offset_x = (uv.y * frequency.y * TAU + time * speed).sin() * amplitude.x;
155                let offset_y = (uv.x * frequency.x * TAU + time * speed).sin() * amplitude.y;
156                Vec2::new(uv.x + offset_x, uv.y + offset_y)
157            }
158            UVMode::RadialWarp { center, amplitude, frequency, speed } => {
159                let dir = uv - *center;
160                let dist = dir.length();
161                if dist < 1e-6 {
162                    return uv;
163                }
164                let wave = (dist * *frequency * TAU - time * speed).sin() * amplitude;
165                let offset = dir.normalize() * wave;
166                uv + offset
167            }
168            UVMode::Turbulence { amplitude, frequency, speed, octaves } => {
169                let mut dx = 0.0_f32;
170                let mut dy = 0.0_f32;
171                let mut freq = *frequency;
172                let mut amp = *amplitude;
173                for _ in 0..*octaves {
174                    dx += ((uv.x * freq + time * speed) * TAU).sin() * amp;
175                    dy += ((uv.y * freq + time * speed * 1.3) * TAU).sin() * amp;
176                    freq *= 2.0;
177                    amp *= 0.5;
178                }
179                Vec2::new(uv.x + dx, uv.y + dy)
180            }
181            UVMode::Custom { .. } => {
182                if let Some(ref func) = self.custom_fn {
183                    func(uv, time)
184                } else {
185                    uv
186                }
187            }
188        }
189    }
190
191    /// Transform an array of UV coordinates in place.
192    pub fn transform_array(&self, uvs: &mut [[f32; 2]]) {
193        for uv_pair in uvs.iter_mut() {
194            let uv = Vec2::new(uv_pair[0], uv_pair[1]);
195            let result = self.transform(uv);
196            uv_pair[0] = result.x;
197            uv_pair[1] = result.y;
198        }
199    }
200
201    /// Transform an array and return new UVs (non-mutating).
202    pub fn transform_array_new(&self, uvs: &[[f32; 2]]) -> Vec<[f32; 2]> {
203        uvs.iter().map(|&uv_pair| {
204            let uv = Vec2::new(uv_pair[0], uv_pair[1]);
205            let result = self.transform(uv);
206            [result.x, result.y]
207        }).collect()
208    }
209}
210
211// ─────────────────────────────────────────────────────────────────────────────
212// Multi-layer UV animation (chaining)
213// ─────────────────────────────────────────────────────────────────────────────
214
215/// Chain of UV animators applied sequentially.
216pub struct UVAnimatorChain {
217    pub animators: Vec<UVAnimator>,
218}
219
220impl UVAnimatorChain {
221    pub fn new() -> Self {
222        Self { animators: Vec::new() }
223    }
224
225    pub fn push(&mut self, animator: UVAnimator) {
226        self.animators.push(animator);
227    }
228
229    pub fn tick(&mut self, dt: f32) {
230        for anim in &mut self.animators {
231            anim.tick(dt);
232        }
233    }
234
235    pub fn transform(&self, mut uv: Vec2) -> Vec2 {
236        for anim in &self.animators {
237            uv = anim.transform(uv);
238        }
239        uv
240    }
241}
242
243impl Default for UVAnimatorChain {
244    fn default() -> Self { Self::new() }
245}
246
247// ─────────────────────────────────────────────────────────────────────────────
248// Flow map
249// ─────────────────────────────────────────────────────────────────────────────
250
251/// A flow map for UV distortion.
252///
253/// A flow map stores 2D velocity vectors at each texel. At render time, UVs are
254/// offset by the flow direction, creating the illusion of flowing liquid, lava, etc.
255pub struct FlowMap {
256    /// Flow vectors stored row-major. Each entry is a 2D direction.
257    pub vectors: Vec<Vec2>,
258    /// Width of the flow map.
259    pub width: usize,
260    /// Height of the flow map.
261    pub height: usize,
262    /// Flow speed multiplier.
263    pub speed: f32,
264    /// Flow strength multiplier.
265    pub strength: f32,
266    /// Current cycle time.
267    pub time: f32,
268    /// Duration of one flow cycle (UV offset resets to avoid extreme distortion).
269    pub cycle_duration: f32,
270}
271
272impl FlowMap {
273    /// Create a new flow map with the given dimensions.
274    pub fn new(width: usize, height: usize) -> Self {
275        Self {
276            vectors: vec![Vec2::ZERO; width * height],
277            width,
278            height,
279            speed: 1.0,
280            strength: 0.1,
281            time: 0.0,
282            cycle_duration: 2.0,
283        }
284    }
285
286    /// Create a flow map with a uniform direction.
287    pub fn uniform(width: usize, height: usize, direction: Vec2) -> Self {
288        let mut fm = Self::new(width, height);
289        fm.vectors.fill(direction);
290        fm
291    }
292
293    /// Create a circular flow map (vortex).
294    pub fn vortex(width: usize, height: usize, strength: f32) -> Self {
295        let mut fm = Self::new(width, height);
296        let cx = width as f32 * 0.5;
297        let cy = height as f32 * 0.5;
298        for y in 0..height {
299            for x in 0..width {
300                let dx = x as f32 - cx;
301                let dy = y as f32 - cy;
302                let dist = (dx * dx + dy * dy).sqrt().max(0.01);
303                let falloff = 1.0 / (1.0 + dist * 0.1);
304                fm.vectors[y * width + x] = Vec2::new(-dy, dx).normalize_or_zero() * strength * falloff;
305            }
306        }
307        fm
308    }
309
310    /// Create a divergent flow map (expanding from center).
311    pub fn divergent(width: usize, height: usize, strength: f32) -> Self {
312        let mut fm = Self::new(width, height);
313        let cx = width as f32 * 0.5;
314        let cy = height as f32 * 0.5;
315        for y in 0..height {
316            for x in 0..width {
317                let dx = x as f32 - cx;
318                let dy = y as f32 - cy;
319                fm.vectors[y * width + x] = Vec2::new(dx, dy).normalize_or_zero() * strength;
320            }
321        }
322        fm
323    }
324
325    /// Set flow speed.
326    pub fn with_speed(mut self, speed: f32) -> Self {
327        self.speed = speed;
328        self
329    }
330
331    /// Set flow strength.
332    pub fn with_strength(mut self, strength: f32) -> Self {
333        self.strength = strength;
334        self
335    }
336
337    /// Set cycle duration.
338    pub fn with_cycle(mut self, duration: f32) -> Self {
339        self.cycle_duration = duration;
340        self
341    }
342
343    /// Advance time.
344    pub fn tick(&mut self, dt: f32) {
345        self.time += dt * self.speed;
346    }
347
348    /// Sample the flow vector at a UV coordinate using bilinear interpolation.
349    pub fn sample(&self, uv: Vec2) -> Vec2 {
350        let fx = (uv.x.fract() + 1.0).fract() * (self.width - 1) as f32;
351        let fy = (uv.y.fract() + 1.0).fract() * (self.height - 1) as f32;
352
353        let ix = fx as usize;
354        let iy = fy as usize;
355        let sx = fx - ix as f32;
356        let sy = fy - iy as f32;
357
358        let ix1 = (ix + 1) % self.width;
359        let iy1 = (iy + 1) % self.height;
360
361        let v00 = self.vectors[iy * self.width + ix];
362        let v10 = self.vectors[iy * self.width + ix1];
363        let v01 = self.vectors[iy1 * self.width + ix];
364        let v11 = self.vectors[iy1 * self.width + ix1];
365
366        let top = v00 * (1.0 - sx) + v10 * sx;
367        let bottom = v01 * (1.0 - sx) + v11 * sx;
368        top * (1.0 - sy) + bottom * sy
369    }
370
371    /// Apply flow distortion to a UV coordinate.
372    ///
373    /// Uses dual-phase approach to avoid visual discontinuities when the
374    /// flow cycle resets.
375    pub fn distort(&self, uv: Vec2) -> Vec2 {
376        let flow = self.sample(uv) * self.strength;
377        let cycle = self.cycle_duration;
378        if cycle < 1e-6 {
379            return uv + flow * self.time;
380        }
381
382        let phase0 = (self.time / cycle).fract();
383        let phase1 = ((self.time / cycle) + 0.5).fract();
384        let blend = (phase0 * 2.0 - 1.0).abs(); // triangle wave 0->1->0
385
386        let uv0 = uv - flow * phase0;
387        let uv1 = uv - flow * phase1;
388
389        uv0 * (1.0 - blend) + uv1 * blend
390    }
391
392    /// Distort an array of UVs.
393    pub fn distort_array(&self, uvs: &mut [[f32; 2]]) {
394        for uv_pair in uvs.iter_mut() {
395            let uv = Vec2::new(uv_pair[0], uv_pair[1]);
396            let result = self.distort(uv);
397            uv_pair[0] = result.x;
398            uv_pair[1] = result.y;
399        }
400    }
401}
402
403// ─────────────────────────────────────────────────────────────────────────────
404// Parallax scrolling layers
405// ─────────────────────────────────────────────────────────────────────────────
406
407/// A single parallax scrolling layer.
408#[derive(Debug, Clone)]
409pub struct ParallaxLayer {
410    /// Scroll speed relative to camera (1.0 = same speed, 0.5 = half speed).
411    pub speed_factor: f32,
412    /// UV offset of this layer.
413    pub offset: Vec2,
414    /// UV scale of this layer.
415    pub scale: Vec2,
416    /// Opacity of this layer (0.0 = transparent, 1.0 = opaque).
417    pub opacity: f32,
418    /// Depth (for sorting; higher = further back).
419    pub depth: f32,
420}
421
422impl ParallaxLayer {
423    pub fn new(speed_factor: f32, depth: f32) -> Self {
424        Self {
425            speed_factor,
426            offset: Vec2::ZERO,
427            scale: Vec2::ONE,
428            opacity: 1.0,
429            depth,
430        }
431    }
432
433    pub fn with_scale(mut self, scale: Vec2) -> Self {
434        self.scale = scale;
435        self
436    }
437
438    pub fn with_opacity(mut self, opacity: f32) -> Self {
439        self.opacity = opacity;
440        self
441    }
442}
443
444/// Multi-layer parallax scrolling system.
445pub struct ParallaxScroller {
446    pub layers: Vec<ParallaxLayer>,
447    pub scroll_position: Vec2,
448}
449
450impl ParallaxScroller {
451    pub fn new() -> Self {
452        Self {
453            layers: Vec::new(),
454            scroll_position: Vec2::ZERO,
455        }
456    }
457
458    /// Add a layer. Returns the layer index.
459    pub fn add_layer(&mut self, layer: ParallaxLayer) -> usize {
460        let idx = self.layers.len();
461        self.layers.push(layer);
462        // Sort by depth (furthest first)
463        self.layers.sort_by(|a, b| b.depth.partial_cmp(&a.depth).unwrap_or(std::cmp::Ordering::Equal));
464        idx
465    }
466
467    /// Create a standard multi-layer parallax setup.
468    pub fn standard_layers(num_layers: usize) -> Self {
469        let mut scroller = Self::new();
470        for i in 0..num_layers {
471            let depth = (i + 1) as f32;
472            let speed = 1.0 / depth;
473            let opacity = 1.0 - (i as f32 * 0.15).min(0.8);
474            scroller.add_layer(
475                ParallaxLayer::new(speed, depth)
476                    .with_opacity(opacity),
477            );
478        }
479        scroller
480    }
481
482    /// Update scroll position.
483    pub fn scroll(&mut self, delta: Vec2) {
484        self.scroll_position += delta;
485    }
486
487    /// Set absolute scroll position.
488    pub fn set_position(&mut self, position: Vec2) {
489        self.scroll_position = position;
490    }
491
492    /// Get the UV offset for a specific layer.
493    pub fn layer_uv(&self, layer_index: usize, base_uv: Vec2) -> Vec2 {
494        if let Some(layer) = self.layers.get(layer_index) {
495            let offset = self.scroll_position * layer.speed_factor + layer.offset;
496            fract_vec2((base_uv + offset) * layer.scale)
497        } else {
498            base_uv
499        }
500    }
501
502    /// Get all layer UVs for a given base UV.
503    pub fn all_layer_uvs(&self, base_uv: Vec2) -> Vec<(Vec2, f32)> {
504        self.layers.iter().map(|layer| {
505            let offset = self.scroll_position * layer.speed_factor + layer.offset;
506            let uv = fract_vec2((base_uv + offset) * layer.scale);
507            (uv, layer.opacity)
508        }).collect()
509    }
510}
511
512impl Default for ParallaxScroller {
513    fn default() -> Self { Self::new() }
514}
515
516// ─────────────────────────────────────────────────────────────────────────────
517// Sprite sheet animation
518// ─────────────────────────────────────────────────────────────────────────────
519
520/// Sprite sheet (texture atlas) animator.
521///
522/// Steps through frames of a regular grid of sprites in a texture atlas.
523pub struct SpriteSheetAnimator {
524    /// Number of columns in the sprite sheet.
525    pub columns: usize,
526    /// Number of rows in the sprite sheet.
527    pub rows: usize,
528    /// Total number of frames (may be less than columns * rows if last row is partial).
529    pub total_frames: usize,
530    /// Current frame index.
531    pub current_frame: usize,
532    /// Frames per second.
533    pub fps: f32,
534    /// Whether to loop.
535    pub looping: bool,
536    /// Accumulated time.
537    time_accumulator: f32,
538    /// Whether the animation is playing.
539    pub playing: bool,
540}
541
542impl SpriteSheetAnimator {
543    /// Create a new sprite sheet animator.
544    pub fn new(columns: usize, rows: usize, fps: f32) -> Self {
545        Self {
546            columns: columns.max(1),
547            rows: rows.max(1),
548            total_frames: columns * rows,
549            current_frame: 0,
550            fps,
551            looping: true,
552            time_accumulator: 0.0,
553            playing: true,
554        }
555    }
556
557    /// Set the total frame count (for partial last rows).
558    pub fn with_total_frames(mut self, total: usize) -> Self {
559        self.total_frames = total.min(self.columns * self.rows);
560        self
561    }
562
563    /// Set looping.
564    pub fn with_looping(mut self, looping: bool) -> Self {
565        self.looping = looping;
566        self
567    }
568
569    /// Advance the animation.
570    pub fn tick(&mut self, dt: f32) {
571        if !self.playing || self.total_frames == 0 {
572            return;
573        }
574
575        self.time_accumulator += dt;
576        let frame_duration = 1.0 / self.fps;
577
578        while self.time_accumulator >= frame_duration {
579            self.time_accumulator -= frame_duration;
580            self.current_frame += 1;
581
582            if self.current_frame >= self.total_frames {
583                if self.looping {
584                    self.current_frame = 0;
585                } else {
586                    self.current_frame = self.total_frames - 1;
587                    self.playing = false;
588                    return;
589                }
590            }
591        }
592    }
593
594    /// Set the current frame.
595    pub fn set_frame(&mut self, frame: usize) {
596        self.current_frame = frame.min(self.total_frames.saturating_sub(1));
597    }
598
599    /// Get the UV rect for the current frame: (uv_min, uv_max).
600    pub fn current_uv_rect(&self) -> (Vec2, Vec2) {
601        self.frame_uv_rect(self.current_frame)
602    }
603
604    /// Get the UV rect for a specific frame.
605    pub fn frame_uv_rect(&self, frame: usize) -> (Vec2, Vec2) {
606        let col = frame % self.columns;
607        let row = frame / self.columns;
608        let cell_w = 1.0 / self.columns as f32;
609        let cell_h = 1.0 / self.rows as f32;
610        let uv_min = Vec2::new(col as f32 * cell_w, row as f32 * cell_h);
611        let uv_max = Vec2::new((col + 1) as f32 * cell_w, (row + 1) as f32 * cell_h);
612        (uv_min, uv_max)
613    }
614
615    /// Transform a normalized UV (0-1) to the atlas UV for the current frame.
616    pub fn map_uv(&self, local_uv: Vec2) -> Vec2 {
617        let (uv_min, uv_max) = self.current_uv_rect();
618        Vec2::new(
619            uv_min.x + local_uv.x * (uv_max.x - uv_min.x),
620            uv_min.y + local_uv.y * (uv_max.y - uv_min.y),
621        )
622    }
623
624    /// Get the current frame row and column.
625    pub fn current_cell(&self) -> (usize, usize) {
626        (self.current_frame / self.columns, self.current_frame % self.columns)
627    }
628
629    /// Play the animation.
630    pub fn play(&mut self) {
631        self.playing = true;
632    }
633
634    /// Pause the animation.
635    pub fn pause(&mut self) {
636        self.playing = false;
637    }
638
639    /// Reset to the first frame.
640    pub fn reset(&mut self) {
641        self.current_frame = 0;
642        self.time_accumulator = 0.0;
643        self.playing = true;
644    }
645
646    /// Is the animation finished (non-looping only)?
647    pub fn is_finished(&self) -> bool {
648        !self.looping && self.current_frame >= self.total_frames.saturating_sub(1)
649    }
650}
651
652// ─────────────────────────────────────────────────────────────────────────────
653// Animated normal maps
654// ─────────────────────────────────────────────────────────────────────────────
655
656/// Animated normal map generator.
657///
658/// Generates procedural animated normal maps for water, organic surfaces, etc.
659pub struct AnimatedNormalMap {
660    /// Width of the normal map.
661    pub width: usize,
662    /// Height of the normal map.
663    pub height: usize,
664    /// Generated normals in RGB format (x, y, z mapped to [0, 1]).
665    pub normals: Vec<[f32; 3]>,
666    /// Animation time.
667    pub time: f32,
668    /// Wave parameters.
669    pub waves: Vec<NormalMapWave>,
670}
671
672/// A single wave contribution to the normal map.
673#[derive(Debug, Clone, Copy)]
674pub struct NormalMapWave {
675    pub direction: Vec2,
676    pub frequency: f32,
677    pub amplitude: f32,
678    pub speed: f32,
679}
680
681impl NormalMapWave {
682    pub fn new(direction: Vec2, frequency: f32, amplitude: f32, speed: f32) -> Self {
683        Self { direction: direction.normalize_or_zero(), frequency, amplitude, speed }
684    }
685}
686
687impl AnimatedNormalMap {
688    /// Create a new animated normal map.
689    pub fn new(width: usize, height: usize) -> Self {
690        Self {
691            width,
692            height,
693            normals: vec![[0.5, 0.5, 1.0]; width * height],
694            time: 0.0,
695            waves: Vec::new(),
696        }
697    }
698
699    /// Add a wave pattern.
700    pub fn add_wave(&mut self, wave: NormalMapWave) {
701        self.waves.push(wave);
702    }
703
704    /// Create a water-like normal map with default waves.
705    pub fn water(width: usize, height: usize) -> Self {
706        let mut nm = Self::new(width, height);
707        nm.add_wave(NormalMapWave::new(Vec2::new(1.0, 0.3), 4.0, 0.3, 0.5));
708        nm.add_wave(NormalMapWave::new(Vec2::new(-0.5, 1.0), 6.0, 0.2, 0.7));
709        nm.add_wave(NormalMapWave::new(Vec2::new(0.7, -0.7), 10.0, 0.1, 1.2));
710        nm
711    }
712
713    /// Update the normal map for the current time.
714    pub fn tick(&mut self, dt: f32) {
715        self.time += dt;
716        self.regenerate();
717    }
718
719    /// Regenerate the normal map at the current time.
720    pub fn regenerate(&mut self) {
721        for y in 0..self.height {
722            for x in 0..self.width {
723                let uv = Vec2::new(
724                    x as f32 / self.width as f32,
725                    y as f32 / self.height as f32,
726                );
727
728                let mut dx = 0.0_f32;
729                let mut dy = 0.0_f32;
730
731                for wave in &self.waves {
732                    let phase = uv.dot(wave.direction) * wave.frequency - self.time * wave.speed;
733                    let deriv = (phase * TAU).cos() * wave.amplitude * wave.frequency * TAU;
734                    dx += wave.direction.x * deriv;
735                    dy += wave.direction.y * deriv;
736                }
737
738                // Construct normal from derivatives
739                let normal = Vec3::new(-dx, -dy, 1.0).normalize();
740                // Map from [-1,1] to [0,1]
741                let idx = y * self.width + x;
742                self.normals[idx] = [
743                    normal.x * 0.5 + 0.5,
744                    normal.y * 0.5 + 0.5,
745                    normal.z * 0.5 + 0.5,
746                ];
747            }
748        }
749    }
750
751    /// Sample the normal at a UV coordinate.
752    pub fn sample(&self, uv: Vec2) -> Vec3 {
753        let fx = (uv.x.fract() + 1.0).fract() * (self.width - 1) as f32;
754        let fy = (uv.y.fract() + 1.0).fract() * (self.height - 1) as f32;
755
756        let ix = (fx as usize).min(self.width - 1);
757        let iy = (fy as usize).min(self.height - 1);
758
759        let n = self.normals[iy * self.width + ix];
760        Vec3::new(n[0] * 2.0 - 1.0, n[1] * 2.0 - 1.0, n[2] * 2.0 - 1.0).normalize()
761    }
762}
763
764// ─────────────────────────────────────────────────────────────────────────────
765// Triplanar projection
766// ─────────────────────────────────────────────────────────────────────────────
767
768/// Triplanar projection for seamless texturing of arbitrary surfaces.
769///
770/// Projects texture coordinates from three orthogonal planes (XY, XZ, YZ)
771/// and blends based on the surface normal direction.
772pub struct TriplanarProjector {
773    /// Scale factor for the texture coordinates.
774    pub scale: f32,
775    /// Blending sharpness (higher = sharper transitions between planes).
776    pub sharpness: f32,
777    /// Per-axis scale adjustment.
778    pub axis_scale: Vec3,
779    /// Per-axis offset.
780    pub axis_offset: Vec3,
781}
782
783impl TriplanarProjector {
784    pub fn new(scale: f32) -> Self {
785        Self {
786            scale,
787            sharpness: 1.0,
788            axis_scale: Vec3::ONE,
789            axis_offset: Vec3::ZERO,
790        }
791    }
792
793    pub fn with_sharpness(mut self, sharpness: f32) -> Self {
794        self.sharpness = sharpness;
795        self
796    }
797
798    pub fn with_axis_scale(mut self, axis_scale: Vec3) -> Self {
799        self.axis_scale = axis_scale;
800        self
801    }
802
803    pub fn with_offset(mut self, offset: Vec3) -> Self {
804        self.axis_offset = offset;
805        self
806    }
807
808    /// Compute the blending weights for a given normal.
809    pub fn blend_weights(&self, normal: Vec3) -> Vec3 {
810        let n = normal.abs();
811        // Raise to power for sharpness
812        let mut w = Vec3::new(
813            n.x.powf(self.sharpness),
814            n.y.powf(self.sharpness),
815            n.z.powf(self.sharpness),
816        );
817        let sum = w.x + w.y + w.z;
818        if sum > 1e-6 {
819            w /= sum;
820        }
821        w
822    }
823
824    /// Compute triplanar UVs for a vertex.
825    ///
826    /// Returns three UV pairs (for XY, XZ, YZ planes) and their blend weights.
827    pub fn project(&self, position: Vec3, normal: Vec3) -> TriplanarUVs {
828        let p = (position + self.axis_offset) * self.scale;
829        let weights = self.blend_weights(normal);
830
831        TriplanarUVs {
832            uv_xy: Vec2::new(p.x * self.axis_scale.x, p.y * self.axis_scale.y),
833            uv_xz: Vec2::new(p.x * self.axis_scale.x, p.z * self.axis_scale.z),
834            uv_yz: Vec2::new(p.y * self.axis_scale.y, p.z * self.axis_scale.z),
835            weight_xy: weights.z, // Z-facing surfaces use XY plane
836            weight_xz: weights.y, // Y-facing surfaces use XZ plane
837            weight_yz: weights.x, // X-facing surfaces use YZ plane
838        }
839    }
840
841    /// Compute a single blended UV for a vertex (simplified, loses some quality).
842    pub fn project_single(&self, position: Vec3, normal: Vec3) -> Vec2 {
843        let uvs = self.project(position, normal);
844        uvs.uv_xy * uvs.weight_xy
845            + uvs.uv_xz * uvs.weight_xz
846            + uvs.uv_yz * uvs.weight_yz
847    }
848}
849
850/// Result of a triplanar projection.
851#[derive(Debug, Clone, Copy)]
852pub struct TriplanarUVs {
853    /// UV coordinates for the XY plane.
854    pub uv_xy: Vec2,
855    /// UV coordinates for the XZ plane.
856    pub uv_xz: Vec2,
857    /// UV coordinates for the YZ plane.
858    pub uv_yz: Vec2,
859    /// Blend weight for XY plane.
860    pub weight_xy: f32,
861    /// Blend weight for XZ plane.
862    pub weight_xz: f32,
863    /// Blend weight for YZ plane.
864    pub weight_yz: f32,
865}
866
867impl TriplanarUVs {
868    /// Blend three sampled values (e.g., colors) using the triplanar weights.
869    pub fn blend_f32(&self, val_xy: f32, val_xz: f32, val_yz: f32) -> f32 {
870        val_xy * self.weight_xy + val_xz * self.weight_xz + val_yz * self.weight_yz
871    }
872
873    /// Blend three Vec3 values.
874    pub fn blend_vec3(&self, val_xy: Vec3, val_xz: Vec3, val_yz: Vec3) -> Vec3 {
875        val_xy * self.weight_xy + val_xz * self.weight_xz + val_yz * self.weight_yz
876    }
877}
878
879// ─────────────────────────────────────────────────────────────────────────────
880// UV unwrapping utilities
881// ─────────────────────────────────────────────────────────────────────────────
882
883/// UV unwrapping projection modes.
884pub struct UVUnwrap;
885
886impl UVUnwrap {
887    /// Cylindrical UV projection around the Y axis.
888    ///
889    /// Maps position to UV: u = atan2(z, x) / TAU, v = y / height.
890    pub fn cylindrical(position: Vec3, height: f32) -> [f32; 2] {
891        let u = (position.z.atan2(position.x) / TAU + 0.5).fract();
892        let v = (position.y / height + 0.5).clamp(0.0, 1.0);
893        [u, v]
894    }
895
896    /// Spherical UV projection.
897    ///
898    /// Maps position to UV using spherical coordinates.
899    pub fn spherical(position: Vec3) -> [f32; 2] {
900        let len = position.length();
901        if len < 1e-6 {
902            return [0.5, 0.5];
903        }
904        let normalized = position / len;
905        let u = (normalized.z.atan2(normalized.x) / TAU + 0.5).fract();
906        let v = (normalized.y.asin() / PI + 0.5).clamp(0.0, 1.0);
907        [u, v]
908    }
909
910    /// Box (cube) projection — project from the most-facing axis.
911    pub fn box_projection(position: Vec3, normal: Vec3) -> [f32; 2] {
912        let abs_n = normal.abs();
913        if abs_n.x >= abs_n.y && abs_n.x >= abs_n.z {
914            // Project from X
915            [position.z.fract().abs(), position.y.fract().abs()]
916        } else if abs_n.y >= abs_n.z {
917            // Project from Y
918            [position.x.fract().abs(), position.z.fract().abs()]
919        } else {
920            // Project from Z
921            [position.x.fract().abs(), position.y.fract().abs()]
922        }
923    }
924
925    /// Planar projection onto a plane defined by origin, u-axis, and v-axis.
926    pub fn planar(
927        position: Vec3,
928        origin: Vec3,
929        u_axis: Vec3,
930        v_axis: Vec3,
931    ) -> [f32; 2] {
932        let relative = position - origin;
933        let u = relative.dot(u_axis) / u_axis.length_squared();
934        let v = relative.dot(v_axis) / v_axis.length_squared();
935        [u, v]
936    }
937
938    /// Camera (view-space) projection.
939    pub fn camera_projection(
940        position: Vec3,
941        camera_pos: Vec3,
942        camera_forward: Vec3,
943        camera_up: Vec3,
944        fov: f32,
945    ) -> [f32; 2] {
946        let right = camera_forward.cross(camera_up).normalize_or_zero();
947        let up = right.cross(camera_forward).normalize_or_zero();
948        let to_point = position - camera_pos;
949        let depth = to_point.dot(camera_forward);
950        if depth < 1e-6 {
951            return [0.5, 0.5];
952        }
953        let scale = 1.0 / (fov * 0.5).tan() / depth;
954        let u = to_point.dot(right) * scale * 0.5 + 0.5;
955        let v = to_point.dot(up) * scale * 0.5 + 0.5;
956        [u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)]
957    }
958
959    /// Unwrap an array of positions using cylindrical projection.
960    pub fn cylindrical_array(positions: &[Vec3], height: f32) -> Vec<[f32; 2]> {
961        positions.iter().map(|&p| Self::cylindrical(p, height)).collect()
962    }
963
964    /// Unwrap an array of positions using spherical projection.
965    pub fn spherical_array(positions: &[Vec3]) -> Vec<[f32; 2]> {
966        positions.iter().map(|&p| Self::spherical(p)).collect()
967    }
968
969    /// Unwrap an array using box projection.
970    pub fn box_projection_array(positions: &[Vec3], normals: &[Vec3]) -> Vec<[f32; 2]> {
971        positions.iter().zip(normals.iter()).map(|(&p, &n)| {
972            Self::box_projection(p, n)
973        }).collect()
974    }
975
976    /// Fix UV seams for cylindrical/spherical projections.
977    ///
978    /// When a triangle spans the 0/1 boundary in U, this adjusts UVs to
979    /// prevent stretching.
980    pub fn fix_seams(uvs: &mut [[f32; 2]], indices: &[[u32; 3]]) {
981        for tri in indices {
982            let u0 = uvs[tri[0] as usize][0];
983            let u1 = uvs[tri[1] as usize][0];
984            let u2 = uvs[tri[2] as usize][0];
985
986            // Check if triangle spans the 0/1 boundary
987            let max_u = u0.max(u1).max(u2);
988            let min_u = u0.min(u1).min(u2);
989            if max_u - min_u > 0.5 {
990                // Adjust UVs less than 0.5 by adding 1.0
991                for &idx in tri {
992                    if uvs[idx as usize][0] < 0.5 {
993                        uvs[idx as usize][0] += 1.0;
994                    }
995                }
996            }
997        }
998    }
999}
1000
1001// ─────────────────────────────────────────────────────────────────────────────
1002// Utility
1003// ─────────────────────────────────────────────────────────────────────────────
1004
1005/// Fractional part of a Vec2 (wrap to [0, 1)).
1006fn fract_vec2(v: Vec2) -> Vec2 {
1007    Vec2::new(
1008        (v.x.fract() + 1.0).fract(),
1009        (v.y.fract() + 1.0).fract(),
1010    )
1011}
1012
1013// ─────────────────────────────────────────────────────────────────────────────
1014// Tests
1015// ─────────────────────────────────────────────────────────────────────────────
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020
1021    #[test]
1022    fn uv_scroll() {
1023        let mut anim = UVAnimator::scroll(Vec2::new(1.0, 0.0));
1024        anim.tick(0.5);
1025        let uv = anim.transform(Vec2::new(0.0, 0.0));
1026        assert!((uv.x - 0.5).abs() < 1e-5);
1027        assert!(uv.y.abs() < 1e-5);
1028    }
1029
1030    #[test]
1031    fn uv_rotate() {
1032        let anim = UVAnimator::rotate(Vec2::new(0.5, 0.5), PI);
1033        let uv = anim.transform_at(Vec2::new(1.0, 0.5), 0.0);
1034        // At time 0, no rotation
1035        assert!((uv.x - 1.0).abs() < 1e-4);
1036    }
1037
1038    #[test]
1039    fn uv_sine_warp() {
1040        let anim = UVAnimator::sine_warp(Vec2::new(0.1, 0.1), Vec2::new(2.0, 2.0), 1.0);
1041        let uv = anim.transform_at(Vec2::new(0.5, 0.5), 0.0);
1042        // Just verify it produces reasonable output
1043        assert!(uv.x.is_finite());
1044        assert!(uv.y.is_finite());
1045    }
1046
1047    #[test]
1048    fn flow_map_uniform() {
1049        let mut fm = FlowMap::uniform(8, 8, Vec2::new(1.0, 0.0))
1050            .with_strength(0.1);
1051        fm.tick(0.5);
1052        let uv = fm.distort(Vec2::new(0.5, 0.5));
1053        assert!(uv.x.is_finite());
1054    }
1055
1056    #[test]
1057    fn flow_map_vortex() {
1058        let fm = FlowMap::vortex(16, 16, 1.0);
1059        let center = fm.sample(Vec2::new(0.5, 0.5));
1060        // At center, flow should be near zero
1061        assert!(center.length() < 5.0);
1062    }
1063
1064    #[test]
1065    fn sprite_sheet() {
1066        let mut ss = SpriteSheetAnimator::new(4, 4, 10.0);
1067        assert_eq!(ss.total_frames, 16);
1068        assert_eq!(ss.current_frame, 0);
1069
1070        ss.tick(0.1); // Should advance 1 frame
1071        assert_eq!(ss.current_frame, 1);
1072
1073        let (uv_min, uv_max) = ss.current_uv_rect();
1074        assert!((uv_min.x - 0.25).abs() < 1e-5);
1075        assert!((uv_max.x - 0.5).abs() < 1e-5);
1076    }
1077
1078    #[test]
1079    fn sprite_sheet_looping() {
1080        let mut ss = SpriteSheetAnimator::new(2, 2, 10.0);
1081        // 4 frames at 10fps, advance 0.5s = 5 frames, should wrap to frame 1
1082        ss.tick(0.5);
1083        assert!(ss.current_frame < 4);
1084    }
1085
1086    #[test]
1087    fn triplanar_projection() {
1088        let proj = TriplanarProjector::new(1.0).with_sharpness(2.0);
1089
1090        // Y-facing surface should use XZ plane primarily
1091        let uvs = proj.project(Vec3::new(1.0, 2.0, 3.0), Vec3::Y);
1092        assert!(uvs.weight_xz > 0.9);
1093    }
1094
1095    #[test]
1096    fn triplanar_blend_weights() {
1097        let proj = TriplanarProjector::new(1.0);
1098        let weights = proj.blend_weights(Vec3::new(0.0, 1.0, 0.0));
1099        assert!((weights.x + weights.y + weights.z - 1.0).abs() < 1e-5);
1100    }
1101
1102    #[test]
1103    fn cylindrical_unwrap() {
1104        let uv = UVUnwrap::cylindrical(Vec3::new(1.0, 0.0, 0.0), 2.0);
1105        assert!(uv[0] >= 0.0 && uv[0] <= 1.0);
1106        assert!(uv[1] >= 0.0 && uv[1] <= 1.0);
1107    }
1108
1109    #[test]
1110    fn spherical_unwrap() {
1111        let uv = UVUnwrap::spherical(Vec3::new(0.0, 1.0, 0.0));
1112        assert!((uv[1] - 1.0).abs() < 1e-5); // North pole
1113    }
1114
1115    #[test]
1116    fn box_projection_unwrap() {
1117        let uv = UVUnwrap::box_projection(
1118            Vec3::new(0.5, 0.3, 0.7),
1119            Vec3::new(0.0, 1.0, 0.0),
1120        );
1121        // Y-facing: should use X and Z
1122        assert!(uv[0].is_finite());
1123        assert!(uv[1].is_finite());
1124    }
1125
1126    #[test]
1127    fn parallax_layers() {
1128        let mut scroller = ParallaxScroller::standard_layers(4);
1129        assert_eq!(scroller.layers.len(), 4);
1130
1131        scroller.scroll(Vec2::new(1.0, 0.0));
1132        let uvs = scroller.all_layer_uvs(Vec2::new(0.5, 0.5));
1133        assert_eq!(uvs.len(), 4);
1134        // Each layer should have different UVs due to different speed factors
1135    }
1136
1137    #[test]
1138    fn animated_normal_map() {
1139        let mut nm = AnimatedNormalMap::water(32, 32);
1140        nm.tick(0.016);
1141        let normal = nm.sample(Vec2::new(0.5, 0.5));
1142        assert!(normal.z > 0.0); // Normal should point generally upward
1143    }
1144
1145    #[test]
1146    fn uv_chain() {
1147        let mut chain = UVAnimatorChain::new();
1148        chain.push(UVAnimator::scroll(Vec2::new(0.1, 0.0)));
1149        chain.push(UVAnimator::scroll(Vec2::new(0.0, 0.1)));
1150        chain.tick(1.0);
1151        let uv = chain.transform(Vec2::ZERO);
1152        assert!((uv.x - 0.1).abs() < 1e-4);
1153        assert!((uv.y - 0.1).abs() < 1e-4);
1154    }
1155
1156    #[test]
1157    fn planar_unwrap() {
1158        let uv = UVUnwrap::planar(
1159            Vec3::new(1.0, 2.0, 3.0),
1160            Vec3::ZERO,
1161            Vec3::X,
1162            Vec3::Y,
1163        );
1164        assert!((uv[0] - 1.0).abs() < 1e-5);
1165        assert!((uv[1] - 2.0).abs() < 1e-5);
1166    }
1167}