Skip to content

Commit

Permalink
Add a rudimentary compute shader for collisions
Browse files Browse the repository at this point in the history
  • Loading branch information
bas-ie committed Jan 19, 2025
1 parent b94d812 commit d82fcbb
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 6 deletions.
24 changes: 24 additions & 0 deletions assets/shaders/collision.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Collision detection

// What do we need back from the compute shader?
//
// 1. Ground collision: we need to know when to stop falling.
// - if falling and collision occurs, stop falling
// 2. Forward collision: we need to know when to turn around.
// - if walking and collision occrs, turn around
//
// Given the above, we may not need to know much about the collision itself, only that it has
// occurred. We can do any finer detail collision (e.g. between blocker and non-blocker characters)
// on the CPU side with no dramas.
//
// We just need to return an identifier, and an on-off bit determining if the preceding identifier
// has collided with something.

@group(0) @binding(0) var<storage, read_write> collisions: array<u32>;

@compute @workgroup_size(1)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
// We use the global_id to index the array to make sure we don't
// access data used in another workgroup.
data[global_id.x] += 1u;
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl Plugin for GamePlugin {
assets::plugin,
screens::plugin,
game::plugin,
physics::plugin,
physics::PhysicsPlugin,
));

#[cfg(feature = "dev")]
Expand Down
159 changes: 156 additions & 3 deletions src/physics.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,88 @@
use bevy::prelude::*;
use bevy::{
prelude::*,
render::{
Render, RenderApp, RenderSet,
extract_resource::{ExtractResource, ExtractResourcePlugin},
gpu_readback::{Readback, ReadbackComplete},
render_asset::RenderAssets,
render_graph::{self, RenderGraph, RenderLabel},
render_resource::{
binding_types::{storage_buffer, texture_storage_2d},
*,
},
renderer::{RenderContext, RenderDevice},
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
},
};

use crate::game::yup::CharacterState;

pub fn plugin(app: &mut App) {
app.add_systems(FixedUpdate, gravity);
const SHADER_ASSET_PATH: &str = "shaders/collision.wgsl";

pub struct PhysicsPlugin;

impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, init);
app.add_systems(FixedUpdate, gravity);
app.add_plugins(ExtractResourcePlugin::<CollisionsBuffer>::default());
}

fn finish(&self, app: &mut App) {
let render_app = app.sub_app_mut(RenderApp);
render_app
.init_resource::<CollisionsPipeline>()
.add_systems(
Render,
prepare_bind_group
.in_set(RenderSet::PrepareBindGroups)
// We don't need to recreate the bind group every frame
.run_if(not(resource_exists::<CollisionsBufferBindGroup>)),
);

// Add the compute node as a top level node to the render graph
// (this means it will only execute once per frame). See:
// https://github.com/bevyengine/bevy/blob/main/examples/shader/gpu_readback.rs
render_app
.world_mut()
.resource_mut::<RenderGraph>()
.add_node(CollisionsNodeLabel, CollisionsNode::default());
}
}

#[derive(Resource, ExtractResource, Clone)]
struct CollisionsBuffer(Handle<ShaderStorageBuffer>);

#[derive(Resource)]
struct CollisionsBufferBindGroup(BindGroup);

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct CollisionsNodeLabel;

#[derive(Component, Debug)]
pub struct Gravity;

fn init(mut commands: Commands, mut buffers: ResMut<Assets<ShaderStorageBuffer>>) {
// TODO: figure out magic number avoidance later!
// TODO: can we use a dynamic-sized buffer with web builds?
let mut collisions = ShaderStorageBuffer::from(vec![0u32; 200]);
// TODO: do we need DST here?
collisions.buffer_description.usage |= BufferUsages::COPY_SRC;
let collisions = buffers.add(collisions);

commands
.spawn(Readback::buffer(collisions.clone()))
.observe(|trigger: Trigger<ReadbackComplete>| {
// This matches the type which was used to create the `ShaderStorageBuffer` above,
// and is a convenient way to interpret the data.
let data: Vec<u32> = trigger.event().to_shader_type();
info!("Buffer {:?}", data);
});
// NOTE: need to make sure nothing accesses this resource before OnEnter(Screen::InGame), or
// else init the resource with a default.
commands.insert_resource(CollisionsBuffer(collisions));
}

fn gravity(mut has_gravity: Query<(&CharacterState, &mut Transform), With<Gravity>>) {
for (state, mut t) in &mut has_gravity {
if *state == CharacterState::Falling {
Expand All @@ -17,6 +91,84 @@ fn gravity(mut has_gravity: Query<(&CharacterState, &mut Transform), With<Gravit
}
}

fn prepare_bind_group(
buffers: Res<RenderAssets<GpuShaderStorageBuffer>>,
collisions: Res<CollisionsBuffer>,
mut commands: Commands,
pipeline: Res<CollisionsPipeline>,
render_device: Res<RenderDevice>,
) {
let shader_storage = buffers.get(&collisions.0).unwrap();
let bind_group = render_device.create_bind_group(
None,
&pipeline.layout,
&BindGroupEntries::sequential((shader_storage.buffer.as_entire_buffer_binding(),)),
);
commands.insert_resource(CollisionsBufferBindGroup(bind_group));
}

#[derive(Resource)]
struct CollisionsPipeline {
layout: BindGroupLayout,
pipeline: CachedComputePipelineId,
}

impl FromWorld for CollisionsPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
None,
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(storage_buffer::<Vec<u32>>(false),),
),
);
let shader = world.load_asset(SHADER_ASSET_PATH);
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("Collisions compute shader".into()),
layout: vec![layout.clone()],
push_constant_ranges: Vec::new(),
shader: shader.clone(),
shader_defs: Vec::new(),
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
CollisionsPipeline { layout, pipeline }
}
}

#[derive(Default)]
struct CollisionsNode {}

impl render_graph::Node for CollisionsNode {
fn run(
&self,
_graph: &mut render_graph::RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), render_graph::NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<CollisionsPipeline>();
let bind_group = world.resource::<CollisionsBufferBindGroup>();

if let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) {
let mut pass =
render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor {
label: Some("Collisions compute pass"),
..default()
});

pass.set_bind_group(0, &bind_group.0, &[]);
pass.set_pipeline(init_pipeline);
// TODO: figure out config values/constants.
pass.dispatch_workgroups(200, 1, 1);
}
Ok(())
}
}
// fn collision(
// camera: Single<(&Camera, &GlobalTransform), With<MainCamera>>,
// images: Res<Assets<Image>>,
Expand All @@ -41,3 +193,4 @@ fn gravity(mut has_gravity: Query<(&CharacterState, &mut Transform), With<Gravit
// }
// }
// }
//
5 changes: 3 additions & 2 deletions src/screens/ingame/playing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ pub fn init(
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::default(),
);
// TODO: feels like we need DST but not SRC here? Find out for sure.
// TODO: feels like we need DST but not SRC here? Find out for sure. This even seems to work
// without COPY_DST. Ask Discord?
image.texture_descriptor.usage =
TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT;
TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT;
minimap.texture = images.add(image);

// Source camera
Expand Down

0 comments on commit d82fcbb

Please sign in to comment.