use crate::ecs::particles::components::{ColorGradient, EmitterShape, EmitterType};
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use freecs::Entity;
use nalgebra_glm::{Mat4, Vec3};
use std::collections::HashMap;
use wgpu::util::DeviceExt;
const MAX_PARTICLES: u32 = 1_000_000;
const MAX_EMITTERS: u32 = 512;
const WORKGROUP_SIZE: u32 = 256;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GpuParticle {
position: [f32; 4],
velocity: [f32; 4],
color: [f32; 4],
size_lifetime: [f32; 4],
emitter_data: [f32; 4],
physics: [f32; 4],
color_start: [f32; 4],
color_end: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GpuEmitterData {
position: [f32; 4],
direction: [f32; 4],
velocity_range: [f32; 4],
lifetime_range: [f32; 4],
size_range: [f32; 4],
gravity: [f32; 4],
color_gradient: [[f32; 4]; 16],
gradient_count: u32,
spawn_count: u32,
emitter_id: u32,
shape_type: u32,
shape_params: [f32; 4],
turbulence: [f32; 4],
emissive_strength: f32,
drag: f32,
emitter_type: u32,
texture_index: u32,
}
impl Default for GpuEmitterData {
fn default() -> Self {
Self {
position: [0.0; 4],
direction: [0.0, 1.0, 0.0, 0.0],
velocity_range: [1.0, 3.0, 0.3, 0.0],
lifetime_range: [1.0, 2.0, 0.0, 0.0],
size_range: [0.1, 0.0, 0.0, 0.0],
gravity: [0.0, -9.81, 0.0, 0.0],
color_gradient: [[0.0; 4]; 16],
gradient_count: 0,
spawn_count: 0,
emitter_id: 0,
shape_type: 0,
shape_params: [0.0; 4],
turbulence: [0.0; 4],
emissive_strength: 1.0,
drag: 0.1,
emitter_type: 0,
texture_index: 0,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SimParams {
delta_time: f32,
time: f32,
max_particles: u32,
_padding: u32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct DrawIndirect {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct CameraUniforms {
view: [[f32; 4]; 4],
projection: [[f32; 4]; 4],
view_projection: [[f32; 4]; 4],
camera_position: [f32; 4],
camera_right: [f32; 4],
camera_up: [f32; 4],
}
const PARTICLE_TEXTURE_SIZE: u32 = 512;
const MAX_PARTICLE_TEXTURES: u32 = 64;
pub struct ParticlePass {
_particle_buffer: wgpu::Buffer,
emitter_buffer: wgpu::Buffer,
params_buffer: wgpu::Buffer,
_free_indices_buffer: wgpu::Buffer,
_free_count_buffer: wgpu::Buffer,
_alive_indices_buffer: wgpu::Buffer,
_alive_count_buffer: wgpu::Buffer,
draw_indirect_buffer: wgpu::Buffer,
camera_buffer: wgpu::Buffer,
update_pipeline: wgpu::ComputePipeline,
spawn_pipeline: wgpu::ComputePipeline,
reset_pipeline: wgpu::ComputePipeline,
render_pipeline: wgpu::RenderPipeline,
render_pipeline_additive: wgpu::RenderPipeline,
_compute_bind_group_layout: wgpu::BindGroupLayout,
compute_bind_group: wgpu::BindGroup,
_render_bind_group_layout: wgpu::BindGroupLayout,
render_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
texture_bind_group: wgpu::BindGroup,
texture_array: wgpu::Texture,
texture_array_view: wgpu::TextureView,
texture_sampler: wgpu::Sampler,
max_particles: u32,
_initialized: bool,
emitter_data: Vec<GpuEmitterData>,
time: f32,
use_additive_blending: bool,
emitter_spawn_offsets: HashMap<Entity, f32>,
loaded_texture_count: usize,
}
impl ParticlePass {
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let max_particles = MAX_PARTICLES;
let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Particle Buffer"),
size: (std::mem::size_of::<GpuParticle>() * max_particles as usize) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let emitter_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Emitter Buffer"),
size: (std::mem::size_of::<GpuEmitterData>() * MAX_EMITTERS as usize) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Particle Sim Params Buffer"),
size: std::mem::size_of::<SimParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut free_indices: Vec<u32> = (0..max_particles).collect();
free_indices.reverse();
let free_indices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Free Indices Buffer"),
contents: bytemuck::cast_slice(&free_indices),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let free_count_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Free Count Buffer"),
contents: bytemuck::cast_slice(&[max_particles]),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let alive_indices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Alive Indices Buffer"),
size: (std::mem::size_of::<u32>() * max_particles as usize) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let alive_count_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Alive Count Buffer"),
contents: bytemuck::cast_slice(&[0u32]),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let draw_indirect_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Draw Indirect Buffer"),
contents: bytemuck::cast_slice(&[DrawIndirect {
vertex_count: 6,
instance_count: 0,
first_vertex: 0,
first_instance: 0,
}]),
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::COPY_DST,
});
let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Particle Camera Buffer"),
size: std::mem::size_of::<CameraUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let compute_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Particle Compute Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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,
},
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::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
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,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
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,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
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,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
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,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
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 compute_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Compute Bind Group"),
layout: &compute_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: particle_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: emitter_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: free_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: free_count_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: alive_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: alive_count_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: draw_indirect_buffer.as_entire_binding(),
},
],
});
let render_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Particle Render Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
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::VERTEX,
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::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let render_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Render Bind Group"),
layout: &render_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: particle_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: alive_indices_buffer.as_entire_binding(),
},
],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Particle Texture Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2Array,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let texture_array = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Particle Texture Array"),
size: wgpu::Extent3d {
width: PARTICLE_TEXTURE_SIZE,
height: PARTICLE_TEXTURE_SIZE,
depth_or_array_layers: MAX_PARTICLE_TEXTURES,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let texture_array_view = texture_array.create_view(&wgpu::TextureViewDescriptor::default());
let texture_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Texture Bind Group"),
layout: &texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_array_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&texture_sampler),
},
],
});
let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Particle Compute Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/particle_update.wgsl").into(),
),
});
let render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Particle Render Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/particle_render.wgsl").into(),
),
});
let compute_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Particle Compute Pipeline Layout"),
bind_group_layouts: &[Some(&compute_bind_group_layout)],
immediate_size: 0,
});
let update_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Particle Update Pipeline"),
layout: Some(&compute_pipeline_layout),
module: &compute_shader,
entry_point: Some("update"),
compilation_options: Default::default(),
cache: None,
});
let spawn_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Particle Spawn Pipeline"),
layout: Some(&compute_pipeline_layout),
module: &compute_shader,
entry_point: Some("spawn"),
compilation_options: Default::default(),
cache: None,
});
let reset_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Particle Reset Pipeline"),
layout: Some(&compute_pipeline_layout),
module: &compute_shader,
entry_point: Some("reset_counters"),
compilation_options: Default::default(),
cache: None,
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Particle Render Pipeline Layout"),
bind_group_layouts: &[
Some(&render_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Particle Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let render_pipeline_additive =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Particle Render Pipeline Additive"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_main_additive"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Self {
_particle_buffer: particle_buffer,
emitter_buffer,
params_buffer,
_free_indices_buffer: free_indices_buffer,
_free_count_buffer: free_count_buffer,
_alive_indices_buffer: alive_indices_buffer,
_alive_count_buffer: alive_count_buffer,
draw_indirect_buffer,
camera_buffer,
update_pipeline,
spawn_pipeline,
reset_pipeline,
render_pipeline,
render_pipeline_additive,
_compute_bind_group_layout: compute_bind_group_layout,
compute_bind_group,
_render_bind_group_layout: render_bind_group_layout,
render_bind_group,
texture_bind_group_layout,
texture_bind_group,
texture_array,
texture_array_view,
texture_sampler,
max_particles,
_initialized: true,
emitter_data: Vec::new(),
time: 0.0,
use_additive_blending: true,
emitter_spawn_offsets: HashMap::new(),
loaded_texture_count: 0,
}
}
pub fn set_additive_blending(&mut self, enabled: bool) {
self.use_additive_blending = enabled;
}
fn convert_gradient_to_gpu(gradient: &ColorGradient) -> [[f32; 4]; 16] {
let mut gpu_gradient = [[0.0f32; 4]; 16];
for (index, (time, color)) in gradient.colors.iter().take(8).enumerate() {
gpu_gradient[index * 2] = [*time, color.x, color.y, color.z];
gpu_gradient[index * 2 + 1] = [color.w, 0.0, 0.0, 0.0];
}
gpu_gradient
}
fn update_emitters(&mut self, world: &World) {
self.emitter_data.clear();
let emitter_entities: Vec<_> = world
.core
.query_entities(crate::ecs::PARTICLE_EMITTER)
.collect();
for (emitter_index, entity) in emitter_entities.iter().enumerate() {
if self.emitter_data.len() >= MAX_EMITTERS as usize {
break;
}
if let Some(emitter) = world.core.get_particle_emitter(*entity) {
if !emitter.enabled {
self.emitter_spawn_offsets.remove(entity);
continue;
}
if emitter.one_shot && emitter.has_fired {
let offset = self
.emitter_spawn_offsets
.get(entity)
.copied()
.unwrap_or(0.0);
if offset >= emitter.accumulated_spawn {
continue;
}
}
let offset = self
.emitter_spawn_offsets
.get(entity)
.copied()
.unwrap_or(0.0);
let spawn_count = ((emitter.accumulated_spawn - offset).max(0.0)) as u32;
if spawn_count > 0 {
self.emitter_spawn_offsets
.insert(*entity, offset + spawn_count as f32);
}
if spawn_count == 0 {
continue;
}
let (shape_type, shape_params) = match emitter.shape {
EmitterShape::Point => (0u32, [0.0f32; 4]),
EmitterShape::Sphere { radius } => (1u32, [radius, 0.0, 0.0, 0.0]),
EmitterShape::Cone { angle, height } => (2u32, [angle, height, 0.0, 0.0]),
EmitterShape::Box { half_extents } => {
(3u32, [half_extents.x, half_extents.y, half_extents.z, 0.0])
}
};
let emitter_type_id = match emitter.emitter_type {
EmitterType::Firework => 0u32,
EmitterType::Fire => 1u32,
EmitterType::Smoke => 2u32,
EmitterType::Sparks => 3u32,
EmitterType::Trail => 4u32,
};
let gpu_emitter = GpuEmitterData {
position: [
emitter.position.x,
emitter.position.y,
emitter.position.z,
1.0,
],
direction: [
emitter.direction.x,
emitter.direction.y,
emitter.direction.z,
0.0,
],
velocity_range: [
emitter.initial_velocity_min,
emitter.initial_velocity_max,
emitter.velocity_spread,
0.0,
],
lifetime_range: [
emitter.particle_lifetime_min,
emitter.particle_lifetime_max,
0.0,
0.0,
],
size_range: [emitter.size_start, emitter.size_end, 0.0, 0.0],
gravity: [emitter.gravity.x, emitter.gravity.y, emitter.gravity.z, 0.0],
color_gradient: Self::convert_gradient_to_gpu(&emitter.color_gradient),
gradient_count: emitter.color_gradient.color_count(),
spawn_count,
emitter_id: emitter_index as u32,
shape_type,
shape_params,
turbulence: [
emitter.turbulence_strength,
emitter.turbulence_frequency,
0.0,
0.0,
],
emissive_strength: emitter.emissive_strength,
drag: emitter.drag,
emitter_type: emitter_type_id,
texture_index: emitter.texture_index,
};
self.emitter_data.push(gpu_emitter);
}
}
}
}
impl PassNode<World> for ParticlePass {
fn name(&self) -> &str {
"particle_pass"
}
fn reads(&self) -> Vec<&str> {
vec![]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color", "depth"]
}
fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
let pending = &world.resources.pending_particle_textures;
if pending.len() > self.loaded_texture_count {
for upload in &pending[self.loaded_texture_count..] {
if upload.slot == 0 || upload.slot >= MAX_PARTICLE_TEXTURES {
continue;
}
let layer = upload.slot - 1;
let expected_size = (PARTICLE_TEXTURE_SIZE * PARTICLE_TEXTURE_SIZE * 4) as usize;
if upload.rgba_data.len() == expected_size
&& upload.width == PARTICLE_TEXTURE_SIZE
&& upload.height == PARTICLE_TEXTURE_SIZE
{
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture_array,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: layer,
},
aspect: wgpu::TextureAspect::All,
},
&upload.rgba_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(PARTICLE_TEXTURE_SIZE * 4),
rows_per_image: Some(PARTICLE_TEXTURE_SIZE),
},
wgpu::Extent3d {
width: PARTICLE_TEXTURE_SIZE,
height: PARTICLE_TEXTURE_SIZE,
depth_or_array_layers: 1,
},
);
}
}
self.loaded_texture_count = pending.len();
self.texture_array_view = self
.texture_array
.create_view(&wgpu::TextureViewDescriptor::default());
self.texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Texture Bind Group"),
layout: &self.texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&self.texture_array_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.texture_sampler),
},
],
});
}
let delta_time = world.resources.window.timing.delta_time;
self.time += delta_time;
self.update_emitters(world);
if !self.emitter_data.is_empty() {
queue.write_buffer(
&self.emitter_buffer,
0,
bytemuck::cast_slice(&self.emitter_data),
);
}
let params = SimParams {
delta_time,
time: self.time,
max_particles: self.max_particles,
_padding: 0,
};
queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[params]));
if let Some(camera_matrices) =
crate::ecs::camera::queries::query_active_camera_matrices(world)
{
let view: Mat4 = camera_matrices.view;
let projection: Mat4 = camera_matrices.projection;
let view_projection = projection * view;
let view_inverse = nalgebra_glm::inverse(&view);
let camera_right = Vec3::new(
view_inverse[(0, 0)],
view_inverse[(1, 0)],
view_inverse[(2, 0)],
);
let camera_up = Vec3::new(
view_inverse[(0, 1)],
view_inverse[(1, 1)],
view_inverse[(2, 1)],
);
let camera_position = Vec3::new(
view_inverse[(0, 3)],
view_inverse[(1, 3)],
view_inverse[(2, 3)],
);
let camera_uniforms = CameraUniforms {
view: view.into(),
projection: projection.into(),
view_projection: view_projection.into(),
camera_position: [camera_position.x, camera_position.y, camera_position.z, 1.0],
camera_right: [camera_right.x, camera_right.y, camera_right.z, 0.0],
camera_up: [camera_up.x, camera_up.y, camera_up.z, 0.0],
};
queue.write_buffer(
&self.camera_buffer,
0,
bytemuck::cast_slice(&[camera_uniforms]),
);
}
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> crate::render::wgpu::rendergraph::Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
> {
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Particle Reset Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.reset_pipeline);
compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
compute_pass.dispatch_workgroups(1, 1, 1);
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Particle Update Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.update_pipeline);
compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
let workgroups = self.max_particles.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
if !self.emitter_data.is_empty() {
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Particle Spawn Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.spawn_pipeline);
compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
compute_pass.dispatch_workgroups(self.emitter_data.len() as u32, 1, 1);
}
let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
let (depth_view, depth_load, depth_store) = context.get_depth_attachment("depth")?;
{
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Particle Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: color_load,
store: color_store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: depth_store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if self.use_additive_blending {
render_pass.set_pipeline(&self.render_pipeline_additive);
} else {
render_pass.set_pipeline(&self.render_pipeline);
}
render_pass.set_bind_group(0, &self.render_bind_group, &[]);
render_pass.set_bind_group(1, &self.texture_bind_group, &[]);
render_pass.draw_indirect(&self.draw_indirect_buffer, 0);
}
Ok(context.into_sub_graph_commands())
}
}