rdpe/gpu/
glyphs.rs

1//! Vector glyph rendering for field and velocity visualization.
2//!
3//! Renders arrows/glyphs showing vector data in 3D space.
4//! Can visualize vector fields at sample points or particle velocities.
5
6use bytemuck::{Pod, Zeroable};
7use glam::Vec3;
8
9use crate::visuals::{GlyphColorMode, GlyphConfig, GlyphMode};
10
11/// Vertex data for a single glyph line segment.
12#[repr(C)]
13#[derive(Copy, Clone, Pod, Zeroable)]
14struct GlyphVertex {
15    position: [f32; 3],
16    color: [f32; 3],
17}
18
19/// GPU resources for glyph rendering.
20pub struct GlyphRenderer {
21    /// Vertex buffer for glyph geometry.
22    vertex_buffer: wgpu::Buffer,
23    /// Render pipeline.
24    pipeline: wgpu::RenderPipeline,
25    /// Bind group for uniforms.
26    bind_group: wgpu::BindGroup,
27    /// Number of vertices to render.
28    vertex_count: u32,
29    /// Maximum number of glyphs.
30    max_glyphs: u32,
31    /// Current configuration.
32    config: GlyphConfig,
33}
34
35impl GlyphRenderer {
36    /// Create a new glyph renderer.
37    pub fn new(
38        device: &wgpu::Device,
39        uniform_buffer: &wgpu::Buffer,
40        surface_format: wgpu::TextureFormat,
41        max_glyphs: u32,
42    ) -> Self {
43        // 6 vertices per glyph (3 line segments * 2 endpoints)
44        let max_vertices = max_glyphs * 6;
45        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
46            label: Some("Glyph Vertex Buffer"),
47            size: (max_vertices as usize * std::mem::size_of::<GlyphVertex>()) as u64,
48            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
49            mapped_at_creation: false,
50        });
51
52        let (pipeline, bind_group) =
53            create_render_pipeline(device, uniform_buffer, surface_format);
54
55        Self {
56            vertex_buffer,
57            pipeline,
58            bind_group,
59            vertex_count: 0,
60            max_glyphs,
61            config: GlyphConfig::default(),
62        }
63    }
64
65    /// Update glyph configuration.
66    pub fn set_config(&mut self, config: GlyphConfig) {
67        self.config = config;
68    }
69
70    /// Update glyphs from vector field data.
71    ///
72    /// `sample_field` is called with (x, y, z) position and should return the vector at that point.
73    pub fn update_from_field(
74        &mut self,
75        queue: &wgpu::Queue,
76        bounds: f32,
77        sample_field: impl Fn(Vec3) -> Vec3,
78    ) {
79        if matches!(self.config.mode, GlyphMode::None) {
80            self.vertex_count = 0;
81            return;
82        }
83
84        let res = self.config.grid_resolution;
85        let mut vertices = Vec::new();
86
87        // Sample field at grid points
88        let step = (bounds * 2.0) / res as f32;
89        let start = -bounds + step * 0.5;
90
91        for ix in 0..res {
92            for iy in 0..res {
93                for iz in 0..res {
94                    if vertices.len() / 6 >= self.max_glyphs as usize {
95                        break;
96                    }
97
98                    let pos = Vec3::new(
99                        start + ix as f32 * step,
100                        start + iy as f32 * step,
101                        start + iz as f32 * step,
102                    );
103
104                    let vec = sample_field(pos);
105                    let magnitude = vec.length();
106
107                    if magnitude < 0.0001 {
108                        continue;
109                    }
110
111                    let dir = vec / magnitude;
112                    let arrow_len = self.config.scale * magnitude.min(1.0);
113
114                    // Determine color
115                    let color = match self.config.color_mode {
116                        GlyphColorMode::Uniform => self.config.color,
117                        GlyphColorMode::ByMagnitude => {
118                            // Map magnitude to color (green to red)
119                            let t = (magnitude / 2.0).min(1.0);
120                            Vec3::new(t, 1.0 - t, 0.0)
121                        }
122                        GlyphColorMode::ByDirection => {
123                            // Map direction to RGB
124                            Vec3::new(
125                                dir.x.abs(),
126                                dir.y.abs(),
127                                dir.z.abs(),
128                            )
129                        }
130                    };
131
132                    // Generate arrow vertices
133                    self.add_arrow_vertices(&mut vertices, pos, dir, arrow_len, color);
134                }
135            }
136        }
137
138        self.vertex_count = vertices.len() as u32;
139
140        if !vertices.is_empty() {
141            queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
142        }
143    }
144
145    /// Update glyphs from particle velocities.
146    pub fn update_from_particles(
147        &mut self,
148        queue: &wgpu::Queue,
149        positions: &[Vec3],
150        velocities: &[Vec3],
151        sample_rate: u32,
152    ) {
153        if matches!(self.config.mode, GlyphMode::None) || !matches!(self.config.mode, GlyphMode::ParticleVelocity) {
154            self.vertex_count = 0;
155            return;
156        }
157
158        let mut vertices = Vec::new();
159
160        for (i, (pos, vel)) in positions.iter().zip(velocities.iter()).enumerate() {
161            if i % sample_rate as usize != 0 {
162                continue;
163            }
164            if vertices.len() / 6 >= self.max_glyphs as usize {
165                break;
166            }
167
168            let magnitude = vel.length();
169            if magnitude < 0.0001 {
170                continue;
171            }
172
173            let dir = *vel / magnitude;
174            let arrow_len = self.config.scale * magnitude.min(1.0);
175
176            let color = match self.config.color_mode {
177                GlyphColorMode::Uniform => self.config.color,
178                GlyphColorMode::ByMagnitude => {
179                    let t = (magnitude / 2.0).min(1.0);
180                    Vec3::new(t, 1.0 - t, 0.0)
181                }
182                GlyphColorMode::ByDirection => {
183                    Vec3::new(dir.x.abs(), dir.y.abs(), dir.z.abs())
184                }
185            };
186
187            self.add_arrow_vertices(&mut vertices, *pos, dir, arrow_len, color);
188        }
189
190        self.vertex_count = vertices.len() as u32;
191
192        if !vertices.is_empty() {
193            queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
194        }
195    }
196
197    /// Add arrow vertices for a single glyph.
198    fn add_arrow_vertices(
199        &self,
200        vertices: &mut Vec<GlyphVertex>,
201        base: Vec3,
202        dir: Vec3,
203        length: f32,
204        color: Vec3,
205    ) {
206        let tip = base + dir * length;
207        let head_size = length * 0.25;
208
209        // Get perpendicular vector for arrowhead
210        let perp = get_perpendicular(dir);
211
212        // Arrowhead points
213        let head_back = tip - dir * head_size;
214        let head_left = head_back + perp * head_size * 0.4;
215        let head_right = head_back - perp * head_size * 0.4;
216
217        let color_arr = color.to_array();
218
219        // Shaft (line segment)
220        vertices.push(GlyphVertex {
221            position: base.to_array(),
222            color: color_arr,
223        });
224        vertices.push(GlyphVertex {
225            position: tip.to_array(),
226            color: color_arr,
227        });
228
229        // Arrowhead left
230        vertices.push(GlyphVertex {
231            position: tip.to_array(),
232            color: color_arr,
233        });
234        vertices.push(GlyphVertex {
235            position: head_left.to_array(),
236            color: color_arr,
237        });
238
239        // Arrowhead right
240        vertices.push(GlyphVertex {
241            position: tip.to_array(),
242            color: color_arr,
243        });
244        vertices.push(GlyphVertex {
245            position: head_right.to_array(),
246            color: color_arr,
247        });
248    }
249
250    /// Render the glyphs.
251    pub fn render(&self, render_pass: &mut wgpu::RenderPass<'_>) {
252        if self.vertex_count == 0 || matches!(self.config.mode, GlyphMode::None) {
253            return;
254        }
255
256        render_pass.set_pipeline(&self.pipeline);
257        render_pass.set_bind_group(0, &self.bind_group, &[]);
258        render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
259        render_pass.draw(0..self.vertex_count, 0..1);
260    }
261
262    /// Check if glyphs are enabled.
263    pub fn is_enabled(&self) -> bool {
264        !matches!(self.config.mode, GlyphMode::None)
265    }
266
267    /// Get current config.
268    pub fn config(&self) -> &GlyphConfig {
269        &self.config
270    }
271}
272
273/// Get a vector perpendicular to the given direction.
274fn get_perpendicular(dir: Vec3) -> Vec3 {
275    let up = if dir.y.abs() < 0.9 {
276        Vec3::Y
277    } else {
278        Vec3::X
279    };
280    dir.cross(up).normalize()
281}
282
283fn create_render_pipeline(
284    device: &wgpu::Device,
285    uniform_buffer: &wgpu::Buffer,
286    surface_format: wgpu::TextureFormat,
287) -> (wgpu::RenderPipeline, wgpu::BindGroup) {
288    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
289        label: Some("Glyph Shader"),
290        source: wgpu::ShaderSource::Wgsl(SHADER.into()),
291    });
292
293    let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
294        label: Some("Glyph Bind Group Layout"),
295        entries: &[wgpu::BindGroupLayoutEntry {
296            binding: 0,
297            visibility: wgpu::ShaderStages::VERTEX,
298            ty: wgpu::BindingType::Buffer {
299                ty: wgpu::BufferBindingType::Uniform,
300                has_dynamic_offset: false,
301                min_binding_size: None,
302            },
303            count: None,
304        }],
305    });
306
307    let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
308        label: Some("Glyph Bind Group"),
309        layout: &bind_group_layout,
310        entries: &[wgpu::BindGroupEntry {
311            binding: 0,
312            resource: uniform_buffer.as_entire_binding(),
313        }],
314    });
315
316    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
317        label: Some("Glyph Pipeline Layout"),
318        bind_group_layouts: &[&bind_group_layout],
319        push_constant_ranges: &[],
320    });
321
322    let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
323        label: Some("Glyph Render Pipeline"),
324        layout: Some(&pipeline_layout),
325        vertex: wgpu::VertexState {
326            module: &shader,
327            entry_point: Some("vs_main"),
328            buffers: &[wgpu::VertexBufferLayout {
329                array_stride: std::mem::size_of::<GlyphVertex>() as u64,
330                step_mode: wgpu::VertexStepMode::Vertex,
331                attributes: &[
332                    wgpu::VertexAttribute {
333                        offset: 0,
334                        shader_location: 0,
335                        format: wgpu::VertexFormat::Float32x3,
336                    },
337                    wgpu::VertexAttribute {
338                        offset: 12,
339                        shader_location: 1,
340                        format: wgpu::VertexFormat::Float32x3,
341                    },
342                ],
343            }],
344            compilation_options: Default::default(),
345        },
346        fragment: Some(wgpu::FragmentState {
347            module: &shader,
348            entry_point: Some("fs_main"),
349            targets: &[Some(wgpu::ColorTargetState {
350                format: surface_format,
351                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
352                write_mask: wgpu::ColorWrites::ALL,
353            })],
354            compilation_options: Default::default(),
355        }),
356        primitive: wgpu::PrimitiveState {
357            topology: wgpu::PrimitiveTopology::LineList,
358            strip_index_format: None,
359            front_face: wgpu::FrontFace::Ccw,
360            cull_mode: None,
361            polygon_mode: wgpu::PolygonMode::Fill,
362            unclipped_depth: false,
363            conservative: false,
364        },
365        depth_stencil: None, // No depth - glyphs render on top of everything
366        multisample: wgpu::MultisampleState::default(),
367        multiview: None,
368        cache: None,
369    });
370
371    (pipeline, bind_group)
372}
373
374const SHADER: &str = r#"
375struct Uniforms {
376    view_proj: mat4x4<f32>,
377    time: f32,
378    delta_time: f32,
379};
380
381@group(0) @binding(0) var<uniform> uniforms: Uniforms;
382
383struct VertexInput {
384    @location(0) position: vec3<f32>,
385    @location(1) color: vec3<f32>,
386};
387
388struct VertexOutput {
389    @builtin(position) clip_position: vec4<f32>,
390    @location(0) color: vec3<f32>,
391};
392
393@vertex
394fn vs_main(in: VertexInput) -> VertexOutput {
395    var out: VertexOutput;
396    out.clip_position = uniforms.view_proj * vec4<f32>(in.position, 1.0);
397    out.color = in.color;
398    return out;
399}
400
401@fragment
402fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
403    return vec4<f32>(in.color, 1.0);
404}
405"#;