Skip to content

Commit

Permalink
Super simple sand fall example
Browse files Browse the repository at this point in the history
  • Loading branch information
hakolao committed Apr 13, 2024
1 parent c5fb19b commit 5781fe1
Show file tree
Hide file tree
Showing 4 changed files with 445 additions and 1 deletion.
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ egui_demo_lib = { version = "0.27", optional = true }
egui_plot = { version = "0.27", optional = true }

[dev-dependencies]
egui_demo_lib = { version = "0.27"}
egui_demo_lib = { version = "0.27" }
winit_input_helper = "0.15"
rapier2d = { version = "0.18", features = ["default", "debug-render"] }
rand = "0.8"

[lints.clippy]
blocks_in_conditions = "allow"
Expand Down Expand Up @@ -115,3 +116,12 @@ required-features = []
[package.metadata.example.lines]
name = "Line Draw"
description = "Example that draws lines"

[[example]]
name = "sand"
path = "examples/sand/main.rs"
required-features = ["egui_gui"]

[package.metadata.example.sand]
name = "Sand Sim"
description = "Example sand fall"
150 changes: 150 additions & 0 deletions examples/sand/grid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use glam::{IVec2, Vec2};
use glass::{pipelines::QuadPipeline, texture::Texture};
use image::RgbaImage;
use wgpu::{
BindGroup, Device, Extent3d, FilterMode, ImageCopyTexture, ImageDataLayout, Origin3d, Queue,
SamplerDescriptor, TextureAspect, TextureFormat, TextureUsages,
};

use crate::sand::{Sand, SandType};

pub struct Grid {
pub data: Vec<Sand>,
pub rgba: RgbaImage,
pub texture: Texture,
pub grid_bind_group: BindGroup,
pub width: u32,
pub height: u32,
changed: bool,
}

impl Grid {
pub fn new(device: &Device, quad: &QuadPipeline, width: u32, height: u32) -> Grid {
let data = vec![Sand::empty(); (width * height) as usize];
let rgba = RgbaImage::new(width, height);
let texture = Texture::empty(
device,
"grid",
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
1,
TextureFormat::Rgba8UnormSrgb,
&SamplerDescriptor {
mag_filter: FilterMode::Nearest,
min_filter: FilterMode::Nearest,
..Default::default()
},
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
);
let grid_bind_group = quad.create_bind_group(device, &texture.views[0], &texture.sampler);
Grid {
data,
rgba,
texture,
grid_bind_group,
width,
height,
changed: false,
}
}

fn index(&self, x: i32, y: i32) -> usize {
((self.height as i32 - y - 1) * self.width as i32 + x) as usize
}

pub fn draw_sand_radius(&mut self, x: i32, y: i32, sand: SandType, radius: f32) {
let y_start = y - radius as i32;
let y_end = y + radius as i32;
let x_start = x - radius as i32;
let x_end = x + radius as i32;
let center = Vec2::new(x as f32, y as f32);
for pixel_y in y_start..=y_end {
for pixel_x in x_start..=x_end {
let pos = IVec2::new(pixel_x, pixel_y);
let pos_f32 = pos.as_vec2();
if pos_f32.distance(center).round() < radius.round() {
self.draw_sand(pos.x, pos.y, Sand::from_type(sand, pos_f32));
}
}
}
}

pub fn draw_sand(&mut self, x: i32, y: i32, sand: Sand) {
if x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32 {
let index = self.index(x, y);
self.data[index] = sand;
self.rgba.put_pixel(
x as u32,
self.height - y as u32 - 1,
sand.color.to_srgba_unmultiplied().into(),
);

self.changed = true;
}
}

pub fn simulate(&mut self) {
for y in 0..self.height {
for x in 0..self.width {
// Current
let curr_index = self.index(x as i32, y as i32);
let curr = self.data[curr_index];
let curr_sand = curr.sand;
// Below
if !curr_sand.is_empty() && y as i32 - 1 >= 0 {
let below_index = self.index(x as i32, y as i32 - 1);
let below_sand = self.data[below_index].sand;
let swap_below = below_sand.is_empty();
let empty = Sand::empty();
if swap_below {
self.draw_sand(x as i32, y as i32, empty);
self.draw_sand(x as i32, y as i32 - 1, curr);
} else {
let p = rand::random::<f32>();
if p > 0.5 && x as i32 - 1 >= 0 {
let left_diag_index = self.index(x as i32 - 1, y as i32 - 1);
let left_diag_sand = self.data[left_diag_index].sand;
let swap_left_diag = left_diag_sand.is_empty();
if swap_left_diag {
self.draw_sand(x as i32, y as i32, empty);
self.draw_sand(x as i32 - 1, y as i32 - 1, curr);
}
} else if x as i32 + 1 < self.width as i32 {
let right_diag_index = self.index(x as i32 + 1, y as i32 - 1);
let right_diag_sand = self.data[right_diag_index].sand;
let swap_right_diag = right_diag_sand.is_empty();
if swap_right_diag {
self.draw_sand(x as i32, y as i32, empty);
self.draw_sand(x as i32 + 1, y as i32 - 1, curr);
}
}
}
}
}
}
}

pub fn update_texture(&mut self, queue: &Queue) {
if self.changed {
queue.write_texture(
ImageCopyTexture {
texture: &self.texture.texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
&self.rgba,
ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * self.width),
rows_per_image: None,
},
self.texture.texture.size(),
);
self.changed = false;
}
}
}
220 changes: 220 additions & 0 deletions examples/sand/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#![feature(more_float_constants)]

mod grid;
mod sand;

use glam::Vec2;
use glass::{
device_context::DeviceConfig,
pipelines::QuadPipeline,
window::{GlassWindow, WindowConfig},
Glass, GlassApp, GlassConfig, GlassContext, GlassError, RenderData,
};
use wgpu::{
Color, Limits, LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
TextureViewDescriptor,
};
use winit::{
event::{ElementState, Event, MouseButton, WindowEvent},
event_loop::EventLoopWindowTarget,
};

use crate::{grid::Grid, sand::SandType};

const CANVAS_SIZE: u32 = 256;
const CANVAS_SCALE: u32 = 4;

fn main() -> Result<(), GlassError> {
Glass::new_and_run(config(), |_e, context| {
Box::new(SandSim::new(context)) as Box<dyn GlassApp>
})
}

struct SandSim {
grid: Grid,
quad_pipeline: QuadPipeline,
cursor_pos: Vec2,
draw_sand: bool,
draw_empty: bool,
}

impl SandSim {
pub fn new(context: &GlassContext) -> SandSim {
let quad_pipeline = QuadPipeline::new(context.device(), wgpu::ColorTargetState {
format: GlassWindow::default_surface_format(),
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent::OVER,
alpha: wgpu::BlendComponent::OVER,
}),
write_mask: wgpu::ColorWrites::ALL,
});
let grid = Grid::new(context.device(), &quad_pipeline, CANVAS_SIZE, CANVAS_SIZE);
SandSim {
grid,
quad_pipeline,
cursor_pos: Vec2::ZERO,
draw_sand: false,
draw_empty: false,
}
}
}

impl GlassApp for SandSim {
fn input(
&mut self,
_context: &mut GlassContext,
_event_loop: &EventLoopWindowTarget<()>,
event: &Event<()>,
) {
if let Event::WindowEvent {
event, ..
} = event
{
match event {
WindowEvent::CursorMoved {
position, ..
} => {
self.cursor_pos = Vec2::new(position.x as f32, position.y as f32);
}
WindowEvent::MouseInput {
button: MouseButton::Left,
state,
..
} => {
self.draw_sand = state == &ElementState::Pressed;
}
WindowEvent::MouseInput {
button: MouseButton::Right,
state,
..
} => {
self.draw_empty = state == &ElementState::Pressed;
}
_ => (),
}
}
}

fn update(&mut self, context: &mut GlassContext) {
if self.draw_sand || self.draw_empty {
let screen_size = context.primary_render_window().surface_size();
let scale_factor = context.primary_render_window().window().scale_factor() as f32;
let pos = cursor_to_canvas(
self.cursor_pos / scale_factor,
screen_size[0] as f32 / scale_factor,
screen_size[1] as f32 / scale_factor,
);
let rounded = pos.round().as_ivec2();
self.grid.draw_sand_radius(
rounded.x,
rounded.y,
if self.draw_sand {
SandType::Sand
} else {
SandType::Empty
},
5.0,
);
}
self.grid.simulate();
self.grid.simulate();
self.grid.update_texture(context.queue());
}

fn render(&mut self, _context: &GlassContext, render_data: RenderData) {
let SandSim {
grid,
quad_pipeline,
..
} = self;
let RenderData {
encoder,
frame,
window,
..
} = render_data;
let (width, height) = {
let scale_factor = window.window().scale_factor() as f32;
let size = window.window().inner_size();
(
size.width as f32 / scale_factor,
size.height as f32 / scale_factor,
)
};
let view = frame.texture.create_view(&TextureViewDescriptor::default());

{
let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Color::BLACK),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
quad_pipeline.draw(
&mut rpass,
&grid.grid_bind_group,
[0.0; 4],
camera_projection([width, height]).to_cols_array_2d(),
[
grid.texture.size[0] * CANVAS_SCALE as f32,
grid.texture.size[1] * CANVAS_SCALE as f32,
],
1.0,
);
}
}
}

fn cursor_to_canvas(cursor: Vec2, screen_width: f32, screen_height: f32) -> Vec2 {
let half_screen = Vec2::new(screen_width, screen_height) / 2.0;
Vec2::new(1.0, -1.0) * (cursor - half_screen) / CANVAS_SCALE as f32 + CANVAS_SIZE as f32 * 0.5
}

fn camera_projection(screen_size: [f32; 2]) -> glam::Mat4 {
let half_width = screen_size[0] / 2.0;
let half_height = screen_size[1] / 2.0;
OPENGL_TO_WGPU
* glam::Mat4::orthographic_rh(
-half_width,
half_width,
-half_height,
half_height,
0.0,
1000.0,
)
}

#[rustfmt::skip]
pub const OPENGL_TO_WGPU: glam::Mat4 = glam::Mat4::from_cols_array(&[
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.0, 0.0, 0.5, 1.0,
]);

fn config() -> GlassConfig {
GlassConfig {
device_config: DeviceConfig {
limits: Limits {
// Needed for push constants
max_push_constant_size: 128,
..Default::default()
},
..DeviceConfig::performance()
},
window_configs: vec![WindowConfig {
width: CANVAS_SIZE * CANVAS_SCALE,
height: CANVAS_SIZE * CANVAS_SCALE,
exit_on_esc: true,
..WindowConfig::default()
}],
}
}
Loading

0 comments on commit 5781fe1

Please sign in to comment.