1use crate::simulation::SimulationGrid3D;
6use half::f16;
7use wgpu::util::DeviceExt;
8
9const VOLUME_SHADER: &str = r#"
12struct CameraUniform {
13 view_proj: mat4x4<f32>,
14 view: mat4x4<f32>,
15 camera_pos: vec4<f32>,
16 grid_size: vec4<f32>,
17};
18
19struct VolumeParams {
20 grid_dims: vec4<u32>, // x, y, z dimensions, padding
21 density_scale: f32,
22 step_size: f32,
23 max_steps: u32,
24 threshold: f32,
25};
26
27// Group 0: Camera bind group (shared with other render passes)
28@group(0) @binding(0) var<uniform> camera: CameraUniform;
29// Group 1: Volume-specific bind group
30@group(1) @binding(0) var<uniform> params: VolumeParams;
31@group(1) @binding(1) var volume_texture: texture_3d<f32>;
32@group(1) @binding(2) var volume_sampler: sampler;
33
34struct VertexOutput {
35 @builtin(position) clip_position: vec4<f32>,
36 @location(0) world_pos: vec3<f32>,
37};
38
39// Cube vertices for proxy geometry (36 vertices for 12 triangles)
40@vertex
41fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
42 // Unit cube vertices (will be scaled by grid_size)
43 var cube_vertices = array<vec3<f32>, 36>(
44 // Front face
45 vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 1.0), vec3<f32>(1.0, 1.0, 1.0),
46 vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.0, 1.0, 1.0),
47 // Back face
48 vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(0.0, 0.0, 0.0), vec3<f32>(0.0, 1.0, 0.0),
49 vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 1.0, 0.0),
50 // Top face
51 vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(0.0, 1.0, 1.0), vec3<f32>(1.0, 1.0, 1.0),
52 vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(1.0, 1.0, 0.0),
53 // Bottom face
54 vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(0.0, 0.0, 0.0), vec3<f32>(1.0, 0.0, 0.0),
55 vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(1.0, 0.0, 1.0),
56 // Right face
57 vec3<f32>(1.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(1.0, 1.0, 0.0),
58 vec3<f32>(1.0, 0.0, 1.0), vec3<f32>(1.0, 1.0, 0.0), vec3<f32>(1.0, 1.0, 1.0),
59 // Left face
60 vec3<f32>(0.0, 0.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(0.0, 1.0, 1.0),
61 vec3<f32>(0.0, 0.0, 0.0), vec3<f32>(0.0, 1.0, 1.0), vec3<f32>(0.0, 1.0, 0.0),
62 );
63
64 // Scale cube to grid size
65 let local_pos = cube_vertices[vertex_index];
66 let world_pos = local_pos * camera.grid_size.xyz;
67
68 var output: VertexOutput;
69 output.clip_position = camera.view_proj * vec4<f32>(world_pos, 1.0);
70 output.world_pos = world_pos;
71
72 return output;
73}
74
75// Ray-box intersection
76fn intersect_box(ray_origin: vec3<f32>, ray_dir: vec3<f32>, box_min: vec3<f32>, box_max: vec3<f32>) -> vec2<f32> {
77 let inv_dir = 1.0 / ray_dir;
78 let t1 = (box_min - ray_origin) * inv_dir;
79 let t2 = (box_max - ray_origin) * inv_dir;
80
81 let tmin = min(t1, t2);
82 let tmax = max(t1, t2);
83
84 let t_near = max(max(tmin.x, tmin.y), tmin.z);
85 let t_far = min(min(tmax.x, tmax.y), tmax.z);
86
87 return vec2<f32>(t_near, t_far);
88}
89
90// Color mapping: blue (negative) - white (zero) - red (positive)
91fn pressure_to_color(pressure: f32) -> vec4<f32> {
92 let p = clamp(pressure, -1.0, 1.0);
93
94 if (p >= 0.0) {
95 // White to Red
96 return vec4<f32>(1.0, 1.0 - p, 1.0 - p, abs(p) * params.density_scale);
97 } else {
98 // Blue to White
99 return vec4<f32>(1.0 + p, 1.0 + p, 1.0, abs(p) * params.density_scale);
100 }
101}
102
103@fragment
104fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
105 let ray_origin = camera.camera_pos.xyz;
106 // Ray direction from camera through the cube surface point
107 let ray_dir = normalize(in.world_pos - ray_origin);
108
109 // Box bounds (0 to grid_size)
110 let box_min = vec3<f32>(0.0, 0.0, 0.0);
111 let box_max = camera.grid_size.xyz;
112
113 // Find intersection with volume box
114 let t = intersect_box(ray_origin, ray_dir, box_min, box_max);
115
116 if (t.x > t.y || t.y < 0.0) {
117 // No intersection - transparent
118 discard;
119 }
120
121 // Clamp to positive t values (start from where ray enters the volume)
122 let t_start = max(t.x, 0.0);
123 let t_end = t.y;
124
125 // Ray marching through the volume
126 var accumulated_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
127 var current_t = t_start;
128 let step = params.step_size;
129 var steps: u32 = 0u;
130
131 loop {
132 if (current_t >= t_end || steps >= params.max_steps || accumulated_color.a >= 0.95) {
133 break;
134 }
135
136 let pos = ray_origin + ray_dir * current_t;
137
138 // Convert to normalized texture coordinates [0, 1]
139 let uvw = pos / box_max;
140
141 // Sample pressure from 3D texture
142 let pressure = textureSample(volume_texture, volume_sampler, uvw).r;
143
144 // Only render if above threshold
145 if (abs(pressure) > params.threshold) {
146 let sample_color = pressure_to_color(pressure);
147
148 // Front-to-back compositing
149 let alpha = sample_color.a * (1.0 - accumulated_color.a);
150 accumulated_color = vec4<f32>(
151 accumulated_color.rgb + sample_color.rgb * alpha,
152 accumulated_color.a + alpha
153 );
154 }
155
156 current_t = current_t + step;
157 steps = steps + 1u;
158 }
159
160 // Blend with background
161 if (accumulated_color.a < 0.01) {
162 discard;
163 }
164
165 return accumulated_color;
166}
167"#;
168
169#[repr(C)]
171#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
172pub struct VolumeParams {
173 pub grid_dims: [u32; 4],
175 pub density_scale: f32,
177 pub step_size: f32,
179 pub max_steps: u32,
181 pub threshold: f32,
183}
184
185impl Default for VolumeParams {
186 fn default() -> Self {
187 Self {
188 grid_dims: [64, 32, 64, 0],
189 density_scale: 2.0,
190 step_size: 0.05,
191 max_steps: 256,
192 threshold: 0.01,
193 }
194 }
195}
196
197pub struct VolumeRenderer {
199 pipeline: wgpu::RenderPipeline,
201 volume_texture: wgpu::Texture,
203 #[allow(dead_code)]
205 volume_view: wgpu::TextureView,
206 #[allow(dead_code)]
208 sampler: wgpu::Sampler,
209 params_buffer: wgpu::Buffer,
211 bind_group: wgpu::BindGroup,
213 #[allow(dead_code)]
215 bind_group_layout: wgpu::BindGroupLayout,
216 pub params: VolumeParams,
218 dimensions: (usize, usize, usize),
220 max_pressure: f32,
222}
223
224impl VolumeRenderer {
225 pub fn new(
227 device: &wgpu::Device,
228 surface_format: wgpu::TextureFormat,
229 camera_bind_group_layout: &wgpu::BindGroupLayout,
230 dimensions: (usize, usize, usize),
231 ) -> Self {
232 let (width, height, depth) = dimensions;
233
234 let volume_texture = device.create_texture(&wgpu::TextureDescriptor {
237 label: Some("volume_texture"),
238 size: wgpu::Extent3d {
239 width: width as u32,
240 height: height as u32,
241 depth_or_array_layers: depth as u32,
242 },
243 mip_level_count: 1,
244 sample_count: 1,
245 dimension: wgpu::TextureDimension::D3,
246 format: wgpu::TextureFormat::R16Float,
247 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
248 view_formats: &[],
249 });
250
251 let volume_view = volume_texture.create_view(&wgpu::TextureViewDescriptor::default());
252
253 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
254 label: Some("volume_sampler"),
255 address_mode_u: wgpu::AddressMode::ClampToEdge,
256 address_mode_v: wgpu::AddressMode::ClampToEdge,
257 address_mode_w: wgpu::AddressMode::ClampToEdge,
258 mag_filter: wgpu::FilterMode::Linear,
259 min_filter: wgpu::FilterMode::Linear,
260 mipmap_filter: wgpu::FilterMode::Nearest,
261 ..Default::default()
262 });
263
264 let params = VolumeParams {
265 grid_dims: [width as u32, height as u32, depth as u32, 0],
266 ..Default::default()
267 };
268
269 let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
270 label: Some("volume_params_buffer"),
271 contents: bytemuck::cast_slice(&[params]),
272 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
273 });
274
275 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
277 label: Some("volume_bind_group_layout"),
278 entries: &[
279 wgpu::BindGroupLayoutEntry {
281 binding: 0,
282 visibility: wgpu::ShaderStages::FRAGMENT,
283 ty: wgpu::BindingType::Buffer {
284 ty: wgpu::BufferBindingType::Uniform,
285 has_dynamic_offset: false,
286 min_binding_size: None,
287 },
288 count: None,
289 },
290 wgpu::BindGroupLayoutEntry {
292 binding: 1,
293 visibility: wgpu::ShaderStages::FRAGMENT,
294 ty: wgpu::BindingType::Texture {
295 sample_type: wgpu::TextureSampleType::Float { filterable: true },
296 view_dimension: wgpu::TextureViewDimension::D3,
297 multisampled: false,
298 },
299 count: None,
300 },
301 wgpu::BindGroupLayoutEntry {
303 binding: 2,
304 visibility: wgpu::ShaderStages::FRAGMENT,
305 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
306 count: None,
307 },
308 ],
309 });
310
311 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
312 label: Some("volume_bind_group"),
313 layout: &bind_group_layout,
314 entries: &[
315 wgpu::BindGroupEntry {
316 binding: 0,
317 resource: params_buffer.as_entire_binding(),
318 },
319 wgpu::BindGroupEntry {
320 binding: 1,
321 resource: wgpu::BindingResource::TextureView(&volume_view),
322 },
323 wgpu::BindGroupEntry {
324 binding: 2,
325 resource: wgpu::BindingResource::Sampler(&sampler),
326 },
327 ],
328 });
329
330 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
332 label: Some("volume_pipeline_layout"),
333 bind_group_layouts: &[camera_bind_group_layout, &bind_group_layout],
334 push_constant_ranges: &[],
335 });
336
337 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
339 label: Some("volume_shader"),
340 source: wgpu::ShaderSource::Wgsl(VOLUME_SHADER.into()),
341 });
342
343 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
345 label: Some("volume_pipeline"),
346 layout: Some(&pipeline_layout),
347 vertex: wgpu::VertexState {
348 module: &shader,
349 entry_point: Some("vs_main"),
350 compilation_options: Default::default(),
351 buffers: &[],
352 },
353 fragment: Some(wgpu::FragmentState {
354 module: &shader,
355 entry_point: Some("fs_main"),
356 compilation_options: Default::default(),
357 targets: &[Some(wgpu::ColorTargetState {
358 format: surface_format,
359 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
360 write_mask: wgpu::ColorWrites::ALL,
361 })],
362 }),
363 primitive: wgpu::PrimitiveState {
364 topology: wgpu::PrimitiveTopology::TriangleList,
365 strip_index_format: None,
366 front_face: wgpu::FrontFace::Ccw,
367 cull_mode: None,
368 polygon_mode: wgpu::PolygonMode::Fill,
369 unclipped_depth: false,
370 conservative: false,
371 },
372 depth_stencil: Some(wgpu::DepthStencilState {
373 format: wgpu::TextureFormat::Depth32Float,
374 depth_write_enabled: false,
375 depth_compare: wgpu::CompareFunction::Always,
376 stencil: wgpu::StencilState::default(),
377 bias: wgpu::DepthBiasState::default(),
378 }),
379 multisample: wgpu::MultisampleState::default(),
380 multiview: None,
381 cache: None,
382 });
383
384 Self {
385 pipeline,
386 volume_texture,
387 volume_view,
388 sampler,
389 params_buffer,
390 bind_group,
391 bind_group_layout,
392 params,
393 dimensions,
394 max_pressure: 1.0,
395 }
396 }
397
398 pub fn update_volume(&mut self, queue: &wgpu::Queue, grid: &SimulationGrid3D) {
400 let (width, height, depth) = self.dimensions;
401
402 self.max_pressure = grid.max_pressure().max(0.001);
404
405 let mut data: Vec<u8> = Vec::with_capacity(width * height * depth * 2);
407 for z in 0..depth {
408 for y in 0..height {
409 for x in 0..width {
410 let idx = z * (width * height) + y * width + x;
411 let pressure = grid.pressure[idx] / self.max_pressure;
412 let f16_val = f16::from_f32(pressure);
413 data.extend_from_slice(&f16_val.to_le_bytes());
414 }
415 }
416 }
417
418 queue.write_texture(
420 wgpu::TexelCopyTextureInfo {
421 texture: &self.volume_texture,
422 mip_level: 0,
423 origin: wgpu::Origin3d::ZERO,
424 aspect: wgpu::TextureAspect::All,
425 },
426 &data,
427 wgpu::TexelCopyBufferLayout {
428 offset: 0,
429 bytes_per_row: Some((width * 2) as u32), rows_per_image: Some(height as u32),
431 },
432 wgpu::Extent3d {
433 width: width as u32,
434 height: height as u32,
435 depth_or_array_layers: depth as u32,
436 },
437 );
438 }
439
440 pub fn update_params(&self, queue: &wgpu::Queue) {
442 queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[self.params]));
443 }
444
445 pub fn render<'a>(
447 &'a self,
448 render_pass: &mut wgpu::RenderPass<'a>,
449 camera_bind_group: &'a wgpu::BindGroup,
450 ) {
451 render_pass.set_pipeline(&self.pipeline);
452 render_pass.set_bind_group(0, camera_bind_group, &[]);
453 render_pass.set_bind_group(1, &self.bind_group, &[]);
454 render_pass.draw(0..36, 0..1); }
456
457 pub fn set_density_scale(&mut self, scale: f32) {
459 self.params.density_scale = scale;
460 }
461
462 pub fn set_step_size(&mut self, step: f32) {
464 self.params.step_size = step;
465 }
466
467 pub fn set_threshold(&mut self, threshold: f32) {
469 self.params.threshold = threshold;
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_volume_params_default() {
479 let params = VolumeParams::default();
480 assert_eq!(params.max_steps, 256);
481 assert!(params.density_scale > 0.0);
482 }
483}