onedrop_renderer/
warp_mesh.rs

1//! Warp mesh — regular grid of vertices for the per-vertex warp pass.
2//!
3//! For each vertex we precompute:
4//! - `pos_clip` (clip space `[-1, 1]²`) — fed straight through the vertex shader.
5//! - `uv_orig` (`[0, 1]²`) — the unwarped texture coordinate. Used as `x`, `y`
6//!   inputs to per-vertex equations, and as the rasterizer-stable basis from
7//!   which the warped UV is derived each frame.
8//! - `rad`, `ang` — polar coordinates in MilkDrop's convention (`rad = 0` at
9//!   center, `rad = 1` at corners; `ang ∈ [0, 2π)`).
10//!
11//! The dynamic per-frame quantity (warped UV) lives in `warp_pipeline::WarpVertex`,
12//! produced by `onedrop-engine::warp_eval`.
13
14use std::f32::consts::{PI, SQRT_2};
15
16/// Static portion of one warp mesh vertex.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct WarpMeshVertex {
19    /// Clip-space position. `(-1, -1)` = bottom-left, `(1, 1)` = top-right.
20    pub pos_clip: [f32; 2],
21    /// Original (unwarped) texture coordinate, in `[0, 1]²`.
22    pub uv_orig: [f32; 2],
23    /// MilkDrop `rad` input: 0 at center, ≈ 1 at corners.
24    pub rad: f32,
25    /// MilkDrop `ang` input: radians in `[0, 2π)`. 0 = right, π/2 = up.
26    pub ang: f32,
27}
28
29/// Regular `cols × rows` warp grid.
30#[derive(Clone, Debug)]
31pub struct WarpMesh {
32    pub cols: u32,
33    pub rows: u32,
34    /// Viewport aspect ratio (`width / height`). Stored so consumers can
35    /// expose it to shaders, but does not affect mesh geometry directly.
36    pub aspect: f32,
37    /// Vertices in row-major order: `vertices[row * cols + col]`.
38    pub vertices: Vec<WarpMeshVertex>,
39    /// Triangle-list indices. Uint32 because `cols × rows` can blow past the
40    /// Uint16 ceiling for high-resolution meshes (192×96 = 18 432 verts).
41    pub indices: Vec<u32>,
42}
43
44impl WarpMesh {
45    /// Build a fresh `cols × rows` mesh.
46    ///
47    /// # Panics
48    /// Panics if `cols < 2` or `rows < 2`.
49    pub fn new(cols: u32, rows: u32, aspect: f32) -> Self {
50        assert!(cols >= 2, "WarpMesh: cols must be >= 2 (got {cols})");
51        assert!(rows >= 2, "WarpMesh: rows must be >= 2 (got {rows})");
52
53        let mut vertices = Vec::with_capacity((cols * rows) as usize);
54        let denom_x = (cols - 1) as f32;
55        let denom_y = (rows - 1) as f32;
56
57        for row in 0..rows {
58            for col in 0..cols {
59                let u = col as f32 / denom_x; // 0..=1
60                let v = row as f32 / denom_y; // 0..=1
61                let dx = u - 0.5;
62                let dy = v - 0.5;
63
64                // MilkDrop input rad: 0 at center, 1 at corners (when the
65                // viewport is unit-square). Multiplying by sqrt(2) makes the
66                // diagonal extreme exactly 1.0.
67                let rad = (dx * dx + dy * dy).sqrt() * SQRT_2;
68
69                let ang = if dx == 0.0 && dy == 0.0 {
70                    0.0
71                } else {
72                    let mut a = dy.atan2(dx);
73                    if a < 0.0 {
74                        a += 2.0 * PI;
75                    }
76                    a
77                };
78
79                vertices.push(WarpMeshVertex {
80                    pos_clip: [u * 2.0 - 1.0, v * 2.0 - 1.0],
81                    uv_orig: [u, v],
82                    rad,
83                    ang,
84                });
85            }
86        }
87
88        // Build TriList indices: 2 triangles per cell.
89        let cell_count = (cols - 1) * (rows - 1);
90        let mut indices = Vec::with_capacity((cell_count * 6) as usize);
91        for row in 0..(rows - 1) {
92            for col in 0..(cols - 1) {
93                let i00 = row * cols + col;
94                let i10 = row * cols + (col + 1);
95                let i01 = (row + 1) * cols + col;
96                let i11 = (row + 1) * cols + (col + 1);
97                indices.push(i00);
98                indices.push(i10);
99                indices.push(i01);
100                indices.push(i10);
101                indices.push(i11);
102                indices.push(i01);
103            }
104        }
105
106        Self {
107            cols,
108            rows,
109            aspect,
110            vertices,
111            indices,
112        }
113    }
114
115    pub fn vertex_count(&self) -> usize {
116        self.vertices.len()
117    }
118
119    pub fn index_count(&self) -> usize {
120        self.indices.len()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn dimensions_match_grid() {
130        let m = WarpMesh::new(32, 24, 16.0 / 9.0);
131        assert_eq!(m.vertex_count(), 32 * 24);
132        assert_eq!(m.index_count(), (32 - 1) * (24 - 1) * 6);
133    }
134
135    #[test]
136    fn corners_at_clip_extremes() {
137        let m = WarpMesh::new(3, 3, 1.0);
138        // Bottom-left corner is the first vertex.
139        assert_eq!(m.vertices[0].pos_clip, [-1.0, -1.0]);
140        assert_eq!(m.vertices[0].uv_orig, [0.0, 0.0]);
141        // Top-right corner is the last.
142        let last = m.vertices.len() - 1;
143        assert_eq!(m.vertices[last].pos_clip, [1.0, 1.0]);
144        assert_eq!(m.vertices[last].uv_orig, [1.0, 1.0]);
145    }
146
147    #[test]
148    fn corner_rad_is_one() {
149        let m = WarpMesh::new(3, 3, 1.0);
150        let last = m.vertices.len() - 1;
151        assert!((m.vertices[last].rad - 1.0).abs() < 1e-5);
152    }
153
154    #[test]
155    fn center_vertex_has_zero_rad() {
156        let m = WarpMesh::new(3, 3, 1.0);
157        let center = &m.vertices[3 + 1]; // row 1, col 1
158        assert!(center.rad.abs() < 1e-6);
159    }
160
161    #[test]
162    #[should_panic]
163    fn rejects_degenerate_grid() {
164        let _ = WarpMesh::new(1, 5, 1.0);
165    }
166}