Skip to content

Commit

Permalink
Working image pass to compute shader?
Browse files Browse the repository at this point in the history
  • Loading branch information
bas-ie committed Jan 29, 2025
1 parent d26fad8 commit 7c6d942
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 22 deletions.
2 changes: 2 additions & 0 deletions assets/shaders/collision.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
// has collided with something.

@group(0) @binding(0) var<storage, read_write> collisions: array<u32>;
@group(0) @binding(1) var texture: texture_storage_2d<rgba8unorm, read>;

@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.
collisions[global_id.x] += 1u;
// textureStore(texture, vec2<i32>(i32(global_id.x), 0), vec4<u32>(data[global_id.x], 0, 0, 0));
}
5 changes: 4 additions & 1 deletion src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ pub enum Game {
/// Player has hit the pause key, pause menu shows.
Paused,
/// Active gameplay.
#[default]
Playing,
/// This isn't a great name, however we need a state that isn't Playing where prep/setup-type
/// tasks can take place.
#[default]
Loading,
}

pub fn plugin(app: &mut App) {
Expand Down
4 changes: 2 additions & 2 deletions src/game/level.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::{
prelude::*,
render::{
extract_resource::ExtractResource,
render_resource::{AsBindGroup, ShaderRef},
view::RenderLayers,
},
Expand All @@ -16,7 +17,6 @@ const SHADER_ASSET_PATH: &str = "shaders/terrain.wgsl";

pub fn plugin(app: &mut App) {
app.add_plugins(Material2dPlugin::<LevelMaterial>::default());
app.init_resource::<LevelRenderTargets>();
app.add_systems(OnEnter(Screen::InGame), init.in_set(GameSet::Init));
app.add_systems(Update, update_cursor_position.in_set(GameSet::RecordInput));
app.add_systems(
Expand Down Expand Up @@ -58,7 +58,7 @@ impl Material2d for LevelMaterial {
// The image we'll use to display the rendered output. Everything on the main game screen and in
// the minimap is rendered to this image, which is swapped (via "ping-pong buffering") each frame
// with the handle attached to LevelMaterial.
#[derive(Resource, Default)]
#[derive(Resource, ExtractResource, Default, Clone)]
pub struct LevelRenderTargets {
pub destination: Handle<Image>,
pub source: Handle<Image>,
Expand Down
18 changes: 15 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ use bevy::{
asset::AssetMetaCheck,
audio::{AudioPlugin, Volume},
prelude::*,
render::view::RenderLayers,
render::{
RenderPlugin,
settings::{RenderCreation, WgpuFeatures, WgpuSettings},
view::RenderLayers,
},
window::WindowResolution,
};
use game::{Game, rendering::GameRenderLayers};
use game::{Game, level::LevelRenderTargets, rendering::GameRenderLayers};
use screens::Screen;

pub struct GamePlugin;
Expand Down Expand Up @@ -46,6 +50,14 @@ impl Plugin for GamePlugin {
..default()
})
.set(ImagePlugin::default_nearest())
// .set(RenderPlugin {
// render_creation: RenderCreation::Automatic(WgpuSettings {
// // WARN this is a native only feature. It will not work with webgl or webgpu
// features: WgpuFeatures::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
// ..default()
// }),
// ..default()
// })
.set(WindowPlugin {
primary_window: Window {
title: "all the way home".to_string(),
Expand All @@ -71,8 +83,8 @@ impl Plugin for GamePlugin {

app.add_plugins((
assets::plugin,
screens::plugin,
game::plugin,
screens::plugin,
physics::PhysicsPlugin,
));

Expand Down
70 changes: 64 additions & 6 deletions src/physics.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy::{
asset::RenderAssetUsages,
prelude::*,
render::{
Render, RenderApp, RenderSet,
Expand All @@ -9,10 +10,13 @@ use bevy::{
render_resource::{binding_types::storage_buffer, *},
renderer::{RenderContext, RenderDevice},
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
texture::GpuImage,
},
};
use binding_types::texture_storage_2d;
use tiny_bail::prelude::*;

use crate::game::yup::CharacterState;
use crate::game::{level::LevelRenderTargets, yup::CharacterState};

const SHADER_ASSET_PATH: &str = "shaders/collision.wgsl";

Expand All @@ -22,7 +26,10 @@ impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, init);
app.add_systems(FixedUpdate, gravity);

// Without these, the resources are not available in the pipeline.
app.add_plugins(ExtractResourcePlugin::<CollisionsBuffer>::default());
app.add_plugins(ExtractResourcePlugin::<LevelRenderTargets>::default());
}

fn finish(&self, app: &mut App) {
Expand All @@ -33,7 +40,6 @@ impl Plugin for PhysicsPlugin {
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>)),
);

Expand All @@ -59,7 +65,11 @@ struct CollisionsNodeLabel;
#[derive(Component, Debug)]
pub struct Gravity;

fn init(mut commands: Commands, mut buffers: ResMut<Assets<ShaderStorageBuffer>>) {
fn init(
mut commands: Commands,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
mut images: ResMut<Assets<Image>>,
) {
// 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]);
Expand All @@ -78,6 +88,31 @@ fn init(mut commands: Commands, mut buffers: ResMut<Assets<ShaderStorageBuffer>>
// 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));

// Ensure sensibly-formatted render target image exists to initialise the compute pipeline.
// These will get replaced once level loading begins in Screen::Intro.
let mut blank_image = Image::new_fill(
Extent3d {
// TODO: can we really get away with this, or do we need to use the 2k image size like
// the real levels? Inquiring minds want to know.
width: 2560,
height: 1440,
..default()
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Rgba8Unorm,
// We don't care if this image ever exists in the main world. It's entirely a GPU resource.
RenderAssetUsages::RENDER_WORLD,
);
blank_image.texture_descriptor.usage |=
TextureUsages::COPY_SRC | TextureUsages::STORAGE_BINDING;
let blank_handle = images.add(blank_image);

commands.insert_resource(LevelRenderTargets {
destination: blank_handle.clone(),
source: blank_handle.clone(),
});
}

fn gravity(mut has_gravity: Query<(&CharacterState, &mut Transform), With<Gravity>>) {
Expand All @@ -92,14 +127,27 @@ fn prepare_bind_group(
buffers: Res<RenderAssets<GpuShaderStorageBuffer>>,
collisions: Res<CollisionsBuffer>,
mut commands: Commands,
mut images: ResMut<RenderAssets<GpuImage>>,
level_targets: Res<LevelRenderTargets>,
pipeline: Res<CollisionsPipeline>,
render_device: Res<RenderDevice>,
) {
let shader_storage = buffers.get(&collisions.0).unwrap();
let destination_image = r!(images.get_mut(&level_targets.destination));

// NOTE: forcing this from the Srgb variant will lead to incorrect gamma values. Since we're
// mostly interested in the alpha, which remains the same, this shouldn't be a problem. See
// https://docs.rs/bevy_color/latest/bevy_color/#conversion. We could also do away with this if
// compute shaders ever support storage textures that are Rgba8UnormSrgb.
destination_image.texture_format = TextureFormat::Rgba8Unorm;

let bind_group = render_device.create_bind_group(
None,
&pipeline.layout,
&BindGroupEntries::sequential((shader_storage.buffer.as_entire_buffer_binding(),)),
&BindGroupEntries::sequential((
shader_storage.buffer.as_entire_buffer_binding(),
destination_image.texture_view.into_binding(),
)),
);
commands.insert_resource(CollisionsBufferBindGroup(bind_group));
}
Expand All @@ -117,7 +165,10 @@ impl FromWorld for CollisionsPipeline {
None,
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(storage_buffer::<Vec<u32>>(false),),
(
storage_buffer::<Vec<u32>>(false),
texture_storage_2d(TextureFormat::Rgba8Unorm, StorageTextureAccess::ReadWrite),
),
),
);
let shader = world.load_asset(SHADER_ASSET_PATH);
Expand Down Expand Up @@ -147,7 +198,14 @@ impl render_graph::Node for CollisionsNode {
) -> Result<(), render_graph::NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<CollisionsPipeline>();
let bind_group = world.resource::<CollisionsBufferBindGroup>();

// NOTE: we need to alter the bindgroup for each new level, because the image we're passing
// to the compute shader CHANGES each time we load a level.
let Some(bind_group) = world.get_resource::<CollisionsBufferBindGroup>() else {
// TODO: this is not great, but lets us await the eventual insertion of the above
// resource when we arrive at the intro screen for each level?
return Ok(());
};

if let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) {
let mut pass =
Expand Down
2 changes: 1 addition & 1 deletion src/screens/ingame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::game::Game;
use bevy::prelude::*;

pub fn plugin(app: &mut App) {
// app.add_systems(OnEnter(Screen::Playing), spawn_level);
app.add_plugins(pause::plugin);

// app.load_resource::<PlayingMusic>();
// app.add_systems(OnEnter(Screen::Playing), play_gameplay_music);
Expand Down
28 changes: 19 additions & 9 deletions src/screens/intro.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use bevy::{prelude::*, render::render_resource::TextureUsages};
use bevy::{
prelude::*,
render::render_resource::{TextureFormat, TextureUsages},
};
use tiny_bail::prelude::*;

use crate::{
GameSet, assets::Levels, game::level::LevelRenderTargets, screens::Screen, ui::Containers,
assets::Levels,
game::{Game, level::LevelRenderTargets},
screens::Screen,
ui::Containers,
};

const INTRO_TIMER_DURATION_SECS: f32 = 0.5;
Expand All @@ -15,10 +21,9 @@ pub(super) fn plugin(app: &mut App) {
app.add_systems(OnExit(Screen::Intro), remove_intro_timer);
app.add_systems(
Update,
(
tick_intro_timer.in_set(GameSet::TickTimers),
check_intro_timer.in_set(GameSet::Update),
),
(tick_intro_timer, check_intro_timer)
.chain()
.run_if(in_state(Screen::Intro)),
);
}

Expand All @@ -44,7 +49,7 @@ fn spawn_intro_screen(mut commands: Commands) {
});
}

fn prepare_level_images(
pub fn prepare_level_images(
mut images: ResMut<Assets<Image>>,
mut level_targets: ResMut<LevelRenderTargets>,
levels: Res<Levels>,
Expand All @@ -53,7 +58,7 @@ fn prepare_level_images(
// be modified in order to use the image as a render target. Here, we create two copies of the
// level image: one to use as "source", the other "destination". These will be swapped after
// rendering each frame.
let level_image = r!(images.get(&levels.level.clone()));
let level_image = r!(images.get_mut(&levels.level.clone()));
let mut source_image = level_image.clone();
source_image.texture_descriptor.usage =
TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT;
Expand All @@ -80,8 +85,13 @@ fn tick_intro_timer(time: Res<Time>, mut timer: ResMut<IntroTimer>) {
timer.0.tick(time.delta());
}

fn check_intro_timer(timer: ResMut<IntroTimer>, mut next_screen: ResMut<NextState<Screen>>) {
fn check_intro_timer(
timer: ResMut<IntroTimer>,
mut next_screen: ResMut<NextState<Screen>>,
mut next_game_state: ResMut<NextState<Game>>,
) {
if timer.0.just_finished() {
next_screen.set(Screen::InGame);
next_game_state.set(Game::Playing);
}
}

0 comments on commit 7c6d942

Please sign in to comment.