summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-20 23:20:21 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-20 23:20:21 -0700
commit533da47f2cc03a8b60a7c6c0bb5a938a7e523a03 (patch)
treee4709c9d56954c0a9b2bd3e21f64c036490ed070 /src/renderer
parent29575e8592f5d6d566abb0ce9beaf794fb98a4e4 (diff)
particles
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/mod.rs248
1 files changed, 207 insertions, 41 deletions
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index ff503c8..2aabd48 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -13,6 +13,7 @@ use crate::{
sim::{
cell::materials::MaterialId,
cell_sim::world::World,
+ particle_sim::{ParticleManager, particle::Particle},
rb_sim::{RbSimManager, debug_render::DebugVertex, rb_entity::RbEntity},
},
};
@@ -21,12 +22,13 @@ use crate::{
const CHUNK_SLOTS: usize = 11 * 200;
const RB_ENTITY_SLOTS: usize = 64;
const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS;
+const PARTICLE_SLOTS: usize = 10_000;
const INITIAL_DEBUG_VERTEX_CAPACITY: usize = 4096;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
-struct Instance {
+struct RendererInstance {
centre: [f32; 2],
cos_sin: [f32; 2],
half_size: [f32; 2],
@@ -35,6 +37,14 @@ struct Instance {
_padding: u32,
}
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+struct RendererParticle {
+ position: [f32; 2],
+ data: u32,
+ _padding: u32,
+}
+
fn srgb_to_linear(channel: u8) -> f32 {
let channel = channel as f32 / 255.0;
if channel <= 0.04045 {
@@ -70,12 +80,19 @@ pub struct RendererState {
pub egui_state: egui_winit::State,
egui_renderer: egui_wgpu::Renderer,
- // world pixels
- pixels_pipeline: wgpu::RenderPipeline,
+ // common
camera_uniform_buffer: wgpu::Buffer,
+
+ // world pixels
+ instance_pipeline: wgpu::RenderPipeline,
instance_buffer: wgpu::Buffer,
cell_buffer: wgpu::Buffer,
- pixels_bind_group: wgpu::BindGroup,
+ instance_bind_group: wgpu::BindGroup,
+
+ // particles
+ particle_pipeline: wgpu::RenderPipeline,
+ particle_buffer: wgpu::Buffer,
+ particle_bind_group: wgpu::BindGroup,
// physics debug lines
debug_pipeline: wgpu::RenderPipeline,
@@ -157,7 +174,26 @@ impl RendererState {
}
});
- let pixels_bind_group_layout =
+ // --- PALETTE ---
+ let mut palette = [0.0f32; MaterialId::ALL.len() * 4];
+ for material in MaterialId::ALL {
+ let def = material.def();
+ let i = material as usize * 4;
+ palette[i] = srgb_to_linear(def.color.0);
+ palette[i + 1] = srgb_to_linear(def.color.1);
+ palette[i + 2] = srgb_to_linear(def.color.2);
+ palette[i + 3] = def.color.3 as f32 / 255.0;
+ }
+
+ let palette_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Palette buffer"),
+ size: size_of_val(&palette) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ // --- INSTANCES ---
+ let instance_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
@@ -208,29 +244,29 @@ impl RendererState {
],
});
- let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader"),
- source: wgpu::ShaderSource::Wgsl(include_str!("../shader/shader.wgsl").into()),
+ let instance_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Instance shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/instance.wgsl").into()),
});
- let pixels_pipeline_layout =
+ let instance_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
immediate_size: 0,
- bind_group_layouts: &[Some(&pixels_bind_group_layout)],
+ bind_group_layouts: &[Some(&instance_bind_group_layout)],
});
- let pixels_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ let instance_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
- layout: Some(&pixels_pipeline_layout),
+ layout: Some(&instance_pipeline_layout),
vertex: wgpu::VertexState {
- module: &shader,
+ module: &instance_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
- module: &shader,
+ module: &instance_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
@@ -261,28 +297,11 @@ impl RendererState {
mapped_at_creation: false,
});
- let mut palette = [0.0f32; MaterialId::ALL.len() * 4];
- for material in MaterialId::ALL {
- let def = material.def();
- let i = material as usize * 4;
- palette[i] = srgb_to_linear(def.color.0);
- palette[i + 1] = srgb_to_linear(def.color.1);
- palette[i + 2] = srgb_to_linear(def.color.2);
- palette[i + 3] = def.color.3 as f32 / 255.0;
- }
-
- let palette_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Palette buffer"),
- size: size_of_val(&palette) as u64,
- usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
queue.write_buffer(&palette_buffer, 0, bytemuck::cast_slice(&palette));
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Instance buffer"),
- size: (CELL_SLOTS * size_of::<Instance>()) as u64,
+ size: (CELL_SLOTS * size_of::<RendererInstance>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
@@ -294,9 +313,9 @@ impl RendererState {
mapped_at_creation: false,
});
- let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ let instance_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
- layout: &pixels_bind_group_layout,
+ layout: &instance_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
@@ -317,6 +336,120 @@ impl RendererState {
],
});
+ // --- PARTICLES ---
+ let particle_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: None,
+ entries: &[
+ // camera uniform
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 0,
+ count: None,
+ visibility: wgpu::ShaderStages::VERTEX,
+ },
+ // palette
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 1,
+ count: None,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ },
+ // instance buffer
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 2,
+ count: None,
+ visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+ },
+ ],
+ });
+
+ let particle_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Particle shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/particle.wgsl").into()),
+ });
+
+ let particle_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: None,
+ immediate_size: 0,
+ bind_group_layouts: &[Some(&particle_bind_group_layout)],
+ });
+
+ let particle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: None,
+ layout: Some(&particle_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &particle_shader,
+ entry_point: Some("vs_main"),
+ buffers: &[],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &particle_shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleStrip,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+
+ let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Particle buffer"),
+ size: (PARTICLE_SLOTS * size_of::<RendererParticle>()) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let particle_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: None,
+ layout: &particle_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: camera_uniform_buffer.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: palette_buffer.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: particle_buffer.as_entire_binding(),
+ },
+ ],
+ });
+
+ // --- DEBUG ---
let debug_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Debug bind group layout"),
@@ -409,11 +542,16 @@ impl RendererState {
egui_state,
egui_renderer,
- pixels_pipeline,
camera_uniform_buffer,
+
+ instance_pipeline,
instance_buffer,
cell_buffer,
- pixels_bind_group,
+ instance_bind_group,
+
+ particle_pipeline,
+ particle_buffer,
+ particle_bind_group,
debug_pipeline,
debug_bind_group,
@@ -437,6 +575,7 @@ impl RendererState {
&mut self,
world: &mut World,
rb_sim_manager: &mut RbSimManager,
+ particle_manager: &mut ParticleManager,
camera: &mut Camera,
config: &mut Config,
diagnostics: &Diagnostics,
@@ -528,7 +667,7 @@ impl RendererState {
);
let mut cell_buffer: [u8; CELLS_IN_CHUNK] = [0; CELLS_IN_CHUNK];
- let mut instances: Vec<Instance> = Vec::new();
+ let mut instances: Vec<RendererInstance> = Vec::new();
{
puffin::profile_scope!("Upload chunk cells");
@@ -587,7 +726,7 @@ impl RendererState {
for cx in cxl..=cxu {
for cy in cyl..=cyu {
if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) {
- instances.push(Instance {
+ instances.push(RendererInstance {
centre: [
(cx * CHUNK_SIZE) as f32 + half_size[0],
(cy * CHUNK_SIZE) as f32 + half_size[1],
@@ -606,7 +745,7 @@ impl RendererState {
if let Some(&RbEntity { width, height, .. }) = rb_sim_manager.rb_entities.get(id)
&& let Some((x, y, cos, sin)) = rb_sim_manager.get_rb_entity_transform(*id)
{
- instances.push(Instance {
+ instances.push(RendererInstance {
centre: [x, y],
cos_sin: [cos, sin],
half_size: [width as f32 / 2.0, height as f32 / 2.0],
@@ -621,6 +760,25 @@ impl RendererState {
.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
}
+ let mut particles: Vec<RendererParticle> = Vec::new();
+
+ {
+ puffin::profile_scope!("Build particles");
+ // TODO could cull this to visible, maybe it's slower?
+ particles = particle_manager
+ .particles
+ .iter()
+ .map(|p| RendererParticle {
+ position: [p.position.x, p.position.y],
+ data: p.material as u32,
+ _padding: 0,
+ })
+ .collect();
+
+ self.queue
+ .write_buffer(&self.particle_buffer, 0, bytemuck::cast_slice(&particles));
+ }
+
let debug_vertex_count = if config.debug_render {
puffin::profile_scope!("Build physics debug lines");
let vertices = rb_sim_manager.debug_render(config.debug_render_mode);
@@ -671,6 +829,7 @@ impl RendererState {
multiview_mask: None,
});
+ // this is kind of a hack
let instances_start = u32::min(
if config.cells_render {
0
@@ -680,10 +839,17 @@ impl RendererState {
instances.len() as u32 - 1,
);
- render_pass.set_pipeline(&self.pixels_pipeline);
- render_pass.set_bind_group(0, &self.pixels_bind_group, &[]);
+ // instances
+ render_pass.set_pipeline(&self.instance_pipeline);
+ render_pass.set_bind_group(0, &self.instance_bind_group, &[]);
render_pass.draw(0..4, instances_start..instances.len() as u32);
+ // particles
+ render_pass.set_pipeline(&self.particle_pipeline);
+ render_pass.set_bind_group(0, &self.particle_bind_group, &[]);
+ render_pass.draw(0..4, 0..particles.len() as u32);
+
+ // debug
if debug_vertex_count > 0 {
render_pass.set_pipeline(&self.debug_pipeline);
render_pass.set_bind_group(0, &self.debug_bind_group, &[]);