-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
445 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}], | ||
} | ||
} |
Oops, something went wrong.