use std::sync::atomic::{AtomicU32, Ordering};
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct SmokeParams {
bounds_min: [f32; 4], bounds_max: [f32; 4], p0: [f32; 4], color: [f32; 4], grid: [f32; 4], source: [f32; 4], sim: [f32; 4], }
pub struct SmokeVolume {
grid_n: u32,
#[allow(dead_code)]
density: [wgpu::Buffer; 2],
params_buffer: wgpu::Buffer,
compute_pipeline: wgpu::ComputePipeline,
compute_bg: [wgpu::BindGroup; 2],
raymarch_pipeline: wgpu::RenderPipeline,
depth_layout: wgpu::BindGroupLayout,
params_bind_group: wgpu::BindGroup,
density_bg: [wgpu::BindGroup; 2],
parity: AtomicU32,
pub bounds_min: [f32; 3],
pub bounds_max: [f32; 3],
pub absorption: f32,
pub density_scale: f32,
pub steps: u32,
pub color: [f32; 3],
pub ambient: f32,
pub source: [f32; 3],
pub source_radius: f32,
pub inject: f32,
pub dissipation: f32,
pub buoyancy: f32,
pub curl_strength: f32,
pub curl_scale: f32,
}
impl SmokeVolume {
pub fn new(
device: &wgpu::Device,
scene_global_layout: &wgpu::BindGroupLayout,
output_format: wgpu::TextureFormat,
) -> Self {
let grid_n: u32 = 64;
let cells = (grid_n * grid_n * grid_n) as usize;
let zero = vec![0.0f32; cells];
let mk_density = |label: &str| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(&zero),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
})
};
let density = [mk_density("smoke_density_0"), mk_density("smoke_density_1")];
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("smoke_params"),
contents: bytemuck::cast_slice(&[SmokeParams {
bounds_min: [0.0; 4],
bounds_max: [0.0; 4],
p0: [0.0; 4],
color: [0.0; 4],
grid: [0.0; 4],
source: [0.0; 4],
sim: [0.0; 4],
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let compute_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("smoke_compute_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let mk_compute_bg = |src: &wgpu::Buffer, dst: &wgpu::Buffer, label: &str| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &compute_layout,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: src.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: dst.as_entire_binding() },
],
})
};
let compute_bg = [
mk_compute_bg(&density[0], &density[1], "smoke_advect_0to1"),
mk_compute_bg(&density[1], &density[0], "smoke_advect_1to0"),
];
let advect_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Smoke Advect Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/smoke_advect.wgsl").into()),
});
let compute_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Smoke Advect Layout"),
bind_group_layouts: &[Some(&compute_layout)],
immediate_size: 0,
});
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Smoke Advect Pipeline"),
layout: Some(&compute_pl_layout),
module: &advect_shader,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let depth_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("smoke_depth_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let params_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("smoke_params_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let density_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("smoke_density_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let params_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("smoke_params_bg"),
layout: ¶ms_layout,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: params_buffer.as_entire_binding() }],
});
let mk_density_bg = |buf: &wgpu::Buffer, label: &str| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &density_layout,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding() }],
})
};
let density_bg = [
mk_density_bg(&density[0], "smoke_rm_density_0"),
mk_density_bg(&density[1], "smoke_rm_density_1"),
];
let rm_shader = crate::pipeline::load_shader_composed(
device,
"crates/gizmo-renderer/src/shaders/smoke_raymarch.wgsl",
include_str!("shaders/smoke_raymarch.wgsl"),
"Smoke Raymarch Shader",
);
let rm_pl_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Smoke Raymarch Layout"),
bind_group_layouts: &[
Some(scene_global_layout),
Some(&depth_layout),
Some(¶ms_layout),
Some(&density_layout),
],
immediate_size: 0,
});
let raymarch_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Smoke Raymarch Pipeline"),
layout: Some(&rm_pl_layout),
vertex: wgpu::VertexState {
module: &rm_shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &rm_shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: output_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Self {
grid_n,
density,
params_buffer,
compute_pipeline,
compute_bg,
raymarch_pipeline,
depth_layout,
params_bind_group,
density_bg,
parity: AtomicU32::new(0),
bounds_min: [-1.6, 0.05, -1.6],
bounds_max: [1.6, 5.5, 1.6],
absorption: 2.0,
density_scale: 1.0,
steps: 56,
color: [0.95, 0.96, 1.0],
ambient: 0.4,
source: [0.0, 0.4, 0.0],
source_radius: 0.6,
inject: 3.0,
dissipation: 0.985,
buoyancy: 1.2,
curl_strength: 1.6,
curl_scale: 0.7,
}
}
fn write_params(&self, queue: &wgpu::Queue, time: f32, dt: f32) {
let p = SmokeParams {
bounds_min: [self.bounds_min[0], self.bounds_min[1], self.bounds_min[2], time],
bounds_max: [self.bounds_max[0], self.bounds_max[1], self.bounds_max[2], self.absorption],
p0: [self.density_scale, 0.0, self.steps as f32, dt],
color: [self.color[0], self.color[1], self.color[2], self.ambient],
grid: [self.grid_n as f32, 0.0, self.source_radius, self.inject],
source: [self.source[0], self.source[1], self.source[2], self.dissipation],
sim: [self.buoyancy, self.curl_strength, self.curl_scale, 0.0],
};
queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[p]));
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
device: &wgpu::Device,
queue: &wgpu::Queue,
scene_bg: &wgpu::BindGroup,
hdr_view: &wgpu::TextureView,
depth_view: &wgpu::TextureView,
time: f32,
dt: f32,
) {
let sim_dt = dt.clamp(1.0 / 240.0, 1.0 / 30.0);
self.write_params(queue, time, sim_dt);
let cur = self.parity.load(Ordering::Relaxed) as usize;
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Smoke Advect"),
timestamp_writes: None,
});
cpass.set_pipeline(&self.compute_pipeline);
cpass.set_bind_group(0, &self.compute_bg[cur], &[]);
let wg = self.grid_n.div_ceil(4);
cpass.dispatch_workgroups(wg, wg, wg);
}
let new_cur = 1 - cur;
self.parity.store(new_cur as u32, Ordering::Relaxed);
let depth_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("smoke_depth_bg"),
layout: &self.depth_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(depth_view),
}],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Smoke Raymarch Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: hdr_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.raymarch_pipeline);
pass.set_bind_group(0, scene_bg, &[]);
pass.set_bind_group(1, &depth_bg, &[]);
pass.set_bind_group(2, &self.params_bind_group, &[]);
pass.set_bind_group(3, &self.density_bg[new_cur], &[]);
pass.draw(0..3, 0..1);
}
}