Skip to main content

ringkernel_wavesim3d/visualization/
mod.rs

1//! 3D visualization module for acoustic wave simulation.
2//!
3//! Provides:
4//! - 3D slice rendering (XY, XZ, YZ planes)
5//! - Volume rendering with ray marching
6//! - Interactive camera controls
7//! - Source and listener visualization
8
9pub mod camera;
10pub mod renderer;
11pub mod slice;
12pub mod volume;
13
14pub use camera::{Camera3D, CameraController, MouseButton};
15pub use renderer::{RenderConfig, Renderer3D, VisualizationMode};
16pub use slice::{SliceAxis, SliceConfig, SliceRenderer};
17pub use volume::{VolumeParams, VolumeRenderer};
18
19use crate::simulation::physics::Position3D;
20use glam::Mat4;
21
22/// Color mapping for pressure values.
23#[derive(Debug, Clone, Copy)]
24pub enum ColorMap {
25    /// Blue (negative) - White - Red (positive)
26    BlueWhiteRed,
27    /// Black - Hot (fire colors)
28    Hot,
29    /// Purple - White - Green
30    Diverging,
31    /// Grayscale
32    Grayscale,
33    /// Cool to warm (blue - red through white)
34    CoolWarm,
35}
36
37impl ColorMap {
38    /// Map a pressure value (-1 to 1) to RGBA color.
39    pub fn map(&self, value: f32, max_abs: f32) -> [f32; 4] {
40        let normalized = (value / max_abs.max(0.001)).clamp(-1.0, 1.0);
41
42        match self {
43            ColorMap::BlueWhiteRed => {
44                if normalized >= 0.0 {
45                    // White to Red
46                    [1.0, 1.0 - normalized, 1.0 - normalized, 1.0]
47                } else {
48                    // Blue to White
49                    [1.0 + normalized, 1.0 + normalized, 1.0, 1.0]
50                }
51            }
52            ColorMap::Hot => {
53                let abs_val = normalized.abs();
54                if abs_val < 0.33 {
55                    [abs_val * 3.0, 0.0, 0.0, 1.0]
56                } else if abs_val < 0.66 {
57                    [1.0, (abs_val - 0.33) * 3.0, 0.0, 1.0]
58                } else {
59                    [1.0, 1.0, (abs_val - 0.66) * 3.0, 1.0]
60                }
61            }
62            ColorMap::Diverging => {
63                if normalized >= 0.0 {
64                    [1.0 - normalized * 0.5, 1.0, 1.0 - normalized, 1.0]
65                } else {
66                    [1.0, 1.0 + normalized * 0.5, 1.0 + normalized, 1.0]
67                }
68            }
69            ColorMap::Grayscale => {
70                let gray = (normalized + 1.0) * 0.5;
71                [gray, gray, gray, 1.0]
72            }
73            ColorMap::CoolWarm => {
74                // Moreland's CoolWarm colormap
75                let t = (normalized + 1.0) * 0.5;
76                let r = 0.230 + t * (0.706 - 0.230);
77                let g = 0.299 + t * (0.016 - 0.299);
78                let b = 0.754 + t * (0.150 - 0.754);
79                [r, g, b, 1.0]
80            }
81        }
82    }
83
84    /// Map with alpha based on magnitude.
85    pub fn map_with_alpha(&self, value: f32, max_abs: f32, alpha_scale: f32) -> [f32; 4] {
86        let mut color = self.map(value, max_abs);
87        color[3] = (value.abs() / max_abs.max(0.001)).clamp(0.0, 1.0) * alpha_scale;
88        color
89    }
90}
91
92/// Vertex for 3D rendering.
93#[repr(C)]
94#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
95pub struct Vertex3D {
96    pub position: [f32; 3],
97    pub color: [f32; 4],
98    pub normal: [f32; 3],
99    pub tex_coord: [f32; 2],
100}
101
102impl Vertex3D {
103    pub fn new(position: [f32; 3], color: [f32; 4]) -> Self {
104        Self {
105            position,
106            color,
107            normal: [0.0, 1.0, 0.0],
108            tex_coord: [0.0, 0.0],
109        }
110    }
111
112    pub fn with_normal(mut self, normal: [f32; 3]) -> Self {
113        self.normal = normal;
114        self
115    }
116
117    pub fn with_tex_coord(mut self, tex_coord: [f32; 2]) -> Self {
118        self.tex_coord = tex_coord;
119        self
120    }
121
122    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
123        wgpu::VertexBufferLayout {
124            array_stride: std::mem::size_of::<Vertex3D>() as wgpu::BufferAddress,
125            step_mode: wgpu::VertexStepMode::Vertex,
126            attributes: &[
127                wgpu::VertexAttribute {
128                    offset: 0,
129                    shader_location: 0,
130                    format: wgpu::VertexFormat::Float32x3,
131                },
132                wgpu::VertexAttribute {
133                    offset: 12,
134                    shader_location: 1,
135                    format: wgpu::VertexFormat::Float32x4,
136                },
137                wgpu::VertexAttribute {
138                    offset: 28,
139                    shader_location: 2,
140                    format: wgpu::VertexFormat::Float32x3,
141                },
142                wgpu::VertexAttribute {
143                    offset: 40,
144                    shader_location: 3,
145                    format: wgpu::VertexFormat::Float32x2,
146                },
147            ],
148        }
149    }
150}
151
152/// Uniform buffer for camera/projection data.
153#[repr(C)]
154#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
155pub struct CameraUniform {
156    pub view_proj: [[f32; 4]; 4],
157    pub view: [[f32; 4]; 4],
158    pub camera_pos: [f32; 4],
159    pub grid_size: [f32; 4],
160}
161
162impl CameraUniform {
163    pub fn new() -> Self {
164        Self {
165            view_proj: Mat4::IDENTITY.to_cols_array_2d(),
166            view: Mat4::IDENTITY.to_cols_array_2d(),
167            camera_pos: [0.0, 0.0, 0.0, 1.0],
168            grid_size: [1.0, 1.0, 1.0, 1.0],
169        }
170    }
171
172    pub fn update(&mut self, camera: &Camera3D, grid_size: (f32, f32, f32)) {
173        self.view_proj = camera.view_projection_matrix().to_cols_array_2d();
174        self.view = camera.view_matrix().to_cols_array_2d();
175        let pos = camera.position();
176        self.camera_pos = [pos.x, pos.y, pos.z, 1.0];
177        self.grid_size = [grid_size.0, grid_size.1, grid_size.2, 1.0];
178    }
179}
180
181impl Default for CameraUniform {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187/// Marker sphere for sources and listeners.
188pub struct MarkerSphere {
189    pub position: Position3D,
190    pub radius: f32,
191    pub color: [f32; 4],
192}
193
194impl MarkerSphere {
195    pub fn new(position: Position3D, radius: f32, color: [f32; 4]) -> Self {
196        Self {
197            position,
198            radius,
199            color,
200        }
201    }
202
203    /// Generate sphere vertices.
204    pub fn generate_vertices(&self, segments: u32) -> Vec<Vertex3D> {
205        let mut vertices = Vec::new();
206
207        for i in 0..=segments {
208            let lat = std::f32::consts::PI * i as f32 / segments as f32;
209            let sin_lat = lat.sin();
210            let cos_lat = lat.cos();
211
212            for j in 0..=segments {
213                let lon = 2.0 * std::f32::consts::PI * j as f32 / segments as f32;
214                let sin_lon = lon.sin();
215                let cos_lon = lon.cos();
216
217                let x = self.position.x + self.radius * sin_lat * cos_lon;
218                let y = self.position.y + self.radius * cos_lat;
219                let z = self.position.z + self.radius * sin_lat * sin_lon;
220
221                let nx = sin_lat * cos_lon;
222                let ny = cos_lat;
223                let nz = sin_lat * sin_lon;
224
225                vertices.push(
226                    Vertex3D::new([x, y, z], self.color)
227                        .with_normal([nx, ny, nz])
228                        .with_tex_coord([j as f32 / segments as f32, i as f32 / segments as f32]),
229                );
230            }
231        }
232
233        vertices
234    }
235
236    /// Generate sphere indices.
237    pub fn generate_indices(&self, segments: u32) -> Vec<u32> {
238        let mut indices = Vec::new();
239        let row_length = segments + 1;
240
241        for i in 0..segments {
242            for j in 0..segments {
243                let a = i * row_length + j;
244                let b = a + row_length;
245
246                indices.push(a);
247                indices.push(b);
248                indices.push(a + 1);
249
250                indices.push(a + 1);
251                indices.push(b);
252                indices.push(b + 1);
253            }
254        }
255
256        indices
257    }
258}
259
260/// Grid lines for visualization.
261pub struct GridLines {
262    pub size: (f32, f32, f32),
263    pub divisions: (u32, u32, u32),
264    pub color: [f32; 4],
265}
266
267impl GridLines {
268    pub fn new(size: (f32, f32, f32)) -> Self {
269        Self {
270            size,
271            divisions: (10, 10, 10),
272            color: [0.3, 0.3, 0.3, 1.0],
273        }
274    }
275
276    /// Generate grid line vertices (XY plane at z=0).
277    pub fn generate_floor_grid(&self) -> Vec<Vertex3D> {
278        let mut vertices = Vec::new();
279        let (sx, _, sz) = self.size;
280        let (dx, _, dz) = self.divisions;
281
282        // X-direction lines
283        for i in 0..=dz {
284            let z = i as f32 * sz / dz as f32;
285            vertices.push(Vertex3D::new([0.0, 0.0, z], self.color));
286            vertices.push(Vertex3D::new([sx, 0.0, z], self.color));
287        }
288
289        // Z-direction lines
290        for i in 0..=dx {
291            let x = i as f32 * sx / dx as f32;
292            vertices.push(Vertex3D::new([x, 0.0, 0.0], self.color));
293            vertices.push(Vertex3D::new([x, 0.0, sz], self.color));
294        }
295
296        vertices
297    }
298
299    /// Generate a bounding box wireframe.
300    pub fn generate_box(&self) -> Vec<Vertex3D> {
301        let mut vertices = Vec::new();
302        let (sx, sy, sz) = self.size;
303
304        // Bottom face
305        vertices.extend([
306            Vertex3D::new([0.0, 0.0, 0.0], self.color),
307            Vertex3D::new([sx, 0.0, 0.0], self.color),
308            Vertex3D::new([sx, 0.0, 0.0], self.color),
309            Vertex3D::new([sx, 0.0, sz], self.color),
310            Vertex3D::new([sx, 0.0, sz], self.color),
311            Vertex3D::new([0.0, 0.0, sz], self.color),
312            Vertex3D::new([0.0, 0.0, sz], self.color),
313            Vertex3D::new([0.0, 0.0, 0.0], self.color),
314        ]);
315
316        // Top face
317        vertices.extend([
318            Vertex3D::new([0.0, sy, 0.0], self.color),
319            Vertex3D::new([sx, sy, 0.0], self.color),
320            Vertex3D::new([sx, sy, 0.0], self.color),
321            Vertex3D::new([sx, sy, sz], self.color),
322            Vertex3D::new([sx, sy, sz], self.color),
323            Vertex3D::new([0.0, sy, sz], self.color),
324            Vertex3D::new([0.0, sy, sz], self.color),
325            Vertex3D::new([0.0, sy, 0.0], self.color),
326        ]);
327
328        // Vertical edges
329        vertices.extend([
330            Vertex3D::new([0.0, 0.0, 0.0], self.color),
331            Vertex3D::new([0.0, sy, 0.0], self.color),
332            Vertex3D::new([sx, 0.0, 0.0], self.color),
333            Vertex3D::new([sx, sy, 0.0], self.color),
334            Vertex3D::new([sx, 0.0, sz], self.color),
335            Vertex3D::new([sx, sy, sz], self.color),
336            Vertex3D::new([0.0, 0.0, sz], self.color),
337            Vertex3D::new([0.0, sy, sz], self.color),
338        ]);
339
340        vertices
341    }
342}
343
344/// Head wireframe for visualization.
345pub struct HeadWireframe {
346    pub position: Position3D,
347    pub scale: f32,
348    pub yaw: f32,
349}
350
351impl HeadWireframe {
352    pub fn new(position: Position3D, scale: f32) -> Self {
353        Self {
354            position,
355            scale,
356            yaw: 0.0,
357        }
358    }
359
360    /// Generate simplified head shape vertices (ellipsoid + ears).
361    pub fn generate_vertices(&self) -> Vec<Vertex3D> {
362        let mut vertices = Vec::new();
363        let head_color = [0.2, 0.8, 0.2, 1.0];
364        let ear_color = [0.8, 0.2, 0.2, 1.0];
365
366        // Generate ellipsoid for head
367        let segments = 16u32;
368        let head_width = 0.15 * self.scale;
369        let head_height = 0.22 * self.scale;
370        let head_depth = 0.18 * self.scale;
371
372        for i in 0..=segments {
373            let lat = std::f32::consts::PI * i as f32 / segments as f32;
374
375            for j in 0..segments {
376                let lon1 = 2.0 * std::f32::consts::PI * j as f32 / segments as f32;
377                let lon2 = 2.0 * std::f32::consts::PI * (j + 1) as f32 / segments as f32;
378
379                let y1 = self.position.y + head_height * lat.cos();
380                let x1 = self.position.x + head_width * lat.sin() * lon1.cos();
381                let z1 = self.position.z + head_depth * lat.sin() * lon1.sin();
382
383                let x2 = self.position.x + head_width * lat.sin() * lon2.cos();
384                let z2 = self.position.z + head_depth * lat.sin() * lon2.sin();
385
386                vertices.push(Vertex3D::new([x1, y1, z1], head_color));
387                vertices.push(Vertex3D::new([x2, y1, z2], head_color));
388            }
389        }
390
391        // Ear markers
392        let ear_offset = 0.085 * self.scale;
393        let ear_radius = 0.03 * self.scale;
394
395        // Left ear
396        for i in 0..=8 {
397            let angle = 2.0 * std::f32::consts::PI * i as f32 / 8.0;
398            let x = self.position.x - ear_offset;
399            let y = self.position.y + ear_radius * angle.cos();
400            let z = self.position.z + ear_radius * angle.sin();
401            vertices.push(Vertex3D::new([x, y, z], ear_color));
402        }
403
404        // Right ear
405        for i in 0..=8 {
406            let angle = 2.0 * std::f32::consts::PI * i as f32 / 8.0;
407            let x = self.position.x + ear_offset;
408            let y = self.position.y + ear_radius * angle.cos();
409            let z = self.position.z + ear_radius * angle.sin();
410            vertices.push(Vertex3D::new([x, y, z], ear_color));
411        }
412
413        // Nose direction indicator
414        let nose_length = 0.15 * self.scale;
415        vertices.push(Vertex3D::new(
416            [self.position.x, self.position.y, self.position.z],
417            [1.0, 1.0, 0.0, 1.0],
418        ));
419        vertices.push(Vertex3D::new(
420            [
421                self.position.x,
422                self.position.y,
423                self.position.z + nose_length,
424            ],
425            [1.0, 1.0, 0.0, 1.0],
426        ));
427
428        vertices
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_color_map() {
438        let map = ColorMap::BlueWhiteRed;
439
440        // Zero should be white
441        let white = map.map(0.0, 1.0);
442        assert!((white[0] - 1.0).abs() < 0.01);
443        assert!((white[1] - 1.0).abs() < 0.01);
444        assert!((white[2] - 1.0).abs() < 0.01);
445
446        // Positive should be reddish
447        let red = map.map(1.0, 1.0);
448        assert!(red[0] > red[1]);
449        assert!(red[0] > red[2]);
450
451        // Negative should be bluish
452        let blue = map.map(-1.0, 1.0);
453        assert!(blue[2] > blue[0]);
454        assert!(blue[2] > blue[1]);
455    }
456
457    #[test]
458    fn test_sphere_generation() {
459        let sphere = MarkerSphere::new(Position3D::origin(), 1.0, [1.0, 0.0, 0.0, 1.0]);
460
461        let vertices = sphere.generate_vertices(8);
462        let indices = sphere.generate_indices(8);
463
464        assert!(!vertices.is_empty());
465        assert!(!indices.is_empty());
466    }
467
468    #[test]
469    fn test_grid_lines() {
470        let grid = GridLines::new((10.0, 5.0, 10.0));
471
472        let floor = grid.generate_floor_grid();
473        let bbox = grid.generate_box();
474
475        assert!(!floor.is_empty());
476        assert!(!bbox.is_empty());
477        // Bounding box should have 24 vertices (12 edges * 2 endpoints)
478        assert_eq!(bbox.len(), 24);
479    }
480}