1use bytemuck::{Pod, Zeroable};
7use glam::Vec3;
8
9use crate::visuals::{GlyphColorMode, GlyphConfig, GlyphMode};
10
11#[repr(C)]
13#[derive(Copy, Clone, Pod, Zeroable)]
14struct GlyphVertex {
15 position: [f32; 3],
16 color: [f32; 3],
17}
18
19pub struct GlyphRenderer {
21 vertex_buffer: wgpu::Buffer,
23 pipeline: wgpu::RenderPipeline,
25 bind_group: wgpu::BindGroup,
27 vertex_count: u32,
29 max_glyphs: u32,
31 config: GlyphConfig,
33}
34
35impl GlyphRenderer {
36 pub fn new(
38 device: &wgpu::Device,
39 uniform_buffer: &wgpu::Buffer,
40 surface_format: wgpu::TextureFormat,
41 max_glyphs: u32,
42 ) -> Self {
43 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 pub fn set_config(&mut self, config: GlyphConfig) {
67 self.config = config;
68 }
69
70 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 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 let color = match self.config.color_mode {
116 GlyphColorMode::Uniform => self.config.color,
117 GlyphColorMode::ByMagnitude => {
118 let t = (magnitude / 2.0).min(1.0);
120 Vec3::new(t, 1.0 - t, 0.0)
121 }
122 GlyphColorMode::ByDirection => {
123 Vec3::new(
125 dir.x.abs(),
126 dir.y.abs(),
127 dir.z.abs(),
128 )
129 }
130 };
131
132 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 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 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 let perp = get_perpendicular(dir);
211
212 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 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 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 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 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 pub fn is_enabled(&self) -> bool {
264 !matches!(self.config.mode, GlyphMode::None)
265 }
266
267 pub fn config(&self) -> &GlyphConfig {
269 &self.config
270 }
271}
272
273fn 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, 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"#;