mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
Fix example game of life (#12897)
# Objective - Example `compute_shader_game_of_life` is random and not following the rules of the game of life: at each steps, it randomly reads some pixel of the current step and some of the previous step instead of only from the previous step - Fixes #9353 ## Solution - Adopted from #9678 - Added a switch of the texture displayed every frame otherwise the game of life looks wrong - Added a way to display the texture bigger so that I could manually check everything was right --------- Co-authored-by: Sludge <96552222+SludgePhD@users.noreply.github.com> Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
This commit is contained in:
parent
b9a232966b
commit
de3ec47f3f
2 changed files with 91 additions and 41 deletions
|
@ -1,4 +1,12 @@
|
|||
@group(0) @binding(0) var texture: texture_storage_2d<r32float, read_write>;
|
||||
// The shader reads the previous frame's state from the `input` texture, and writes the new state of
|
||||
// each pixel to the `output` texture. The textures are flipped each step to progress the
|
||||
// simulation.
|
||||
// Two textures are needed for the game of life as each pixel of step N depends on the state of its
|
||||
// neighbors at step N-1.
|
||||
|
||||
@group(0) @binding(0) var input: texture_storage_2d<r32float, read>;
|
||||
|
||||
@group(0) @binding(1) var output: texture_storage_2d<r32float, write>;
|
||||
|
||||
fn hash(value: u32) -> u32 {
|
||||
var state = value;
|
||||
|
@ -19,15 +27,15 @@ fn randomFloat(value: u32) -> f32 {
|
|||
fn init(@builtin(global_invocation_id) invocation_id: vec3<u32>, @builtin(num_workgroups) num_workgroups: vec3<u32>) {
|
||||
let location = vec2<i32>(i32(invocation_id.x), i32(invocation_id.y));
|
||||
|
||||
let randomNumber = randomFloat(invocation_id.y * num_workgroups.x + invocation_id.x);
|
||||
let randomNumber = randomFloat(invocation_id.y << 16u | invocation_id.x);
|
||||
let alive = randomNumber > 0.9;
|
||||
let color = vec4<f32>(f32(alive));
|
||||
|
||||
textureStore(texture, location, color);
|
||||
textureStore(output, location, color);
|
||||
}
|
||||
|
||||
fn is_alive(location: vec2<i32>, offset_x: i32, offset_y: i32) -> i32 {
|
||||
let value: vec4<f32> = textureLoad(texture, location + vec2<i32>(offset_x, offset_y));
|
||||
let value: vec4<f32> = textureLoad(input, location + vec2<i32>(offset_x, offset_y));
|
||||
return i32(value.x);
|
||||
}
|
||||
|
||||
|
@ -59,7 +67,5 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
|
|||
}
|
||||
let color = vec4<f32>(f32(alive));
|
||||
|
||||
storageBarrier();
|
||||
|
||||
textureStore(texture, location, color);
|
||||
}
|
||||
textureStore(output, location, color);
|
||||
}
|
||||
|
|
|
@ -7,34 +7,42 @@ use bevy::{
|
|||
prelude::*,
|
||||
render::{
|
||||
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||
render_asset::RenderAssetUsages,
|
||||
render_asset::RenderAssets,
|
||||
render_asset::{RenderAssetUsages, RenderAssets},
|
||||
render_graph::{self, RenderGraph, RenderLabel},
|
||||
render_resource::*,
|
||||
render_resource::{binding_types::texture_storage_2d, *},
|
||||
renderer::{RenderContext, RenderDevice},
|
||||
Render, RenderApp, RenderSet,
|
||||
},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
const SIZE: (u32, u32) = (1280, 720);
|
||||
const DISPLAY_FACTOR: u32 = 4;
|
||||
const SIZE: (u32, u32) = (1280 / DISPLAY_FACTOR, 720 / DISPLAY_FACTOR);
|
||||
const WORKGROUP_SIZE: u32 = 8;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::BLACK))
|
||||
.add_plugins((
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
// uncomment for unthrottled FPS
|
||||
// present_mode: bevy::window::PresentMode::AutoNoVsync,
|
||||
DefaultPlugins
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
resolution: (
|
||||
(SIZE.0 * DISPLAY_FACTOR) as f32,
|
||||
(SIZE.1 * DISPLAY_FACTOR) as f32,
|
||||
)
|
||||
.into(),
|
||||
// uncomment for unthrottled FPS
|
||||
// present_mode: bevy::window::PresentMode::AutoNoVsync,
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
})
|
||||
.set(ImagePlugin::default_nearest()),
|
||||
GameOfLifeComputePlugin,
|
||||
))
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, switch_textures)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
@ -52,19 +60,34 @@ fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
|||
);
|
||||
image.texture_descriptor.usage =
|
||||
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
|
||||
let image = images.add(image);
|
||||
let image0 = images.add(image.clone());
|
||||
let image1 = images.add(image);
|
||||
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite {
|
||||
custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)),
|
||||
..default()
|
||||
},
|
||||
texture: image.clone(),
|
||||
texture: image0.clone(),
|
||||
transform: Transform::from_scale(Vec3::splat(DISPLAY_FACTOR as f32)),
|
||||
..default()
|
||||
});
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
|
||||
commands.insert_resource(GameOfLifeImage { texture: image });
|
||||
commands.insert_resource(GameOfLifeImages {
|
||||
texture_a: image0,
|
||||
texture_b: image1,
|
||||
});
|
||||
}
|
||||
|
||||
// Switch texture to display every frame to show the one that was written to most recently.
|
||||
fn switch_textures(images: Res<GameOfLifeImages>, mut displayed: Query<&mut Handle<Image>>) {
|
||||
let mut displayed = displayed.single_mut();
|
||||
if *displayed == images.texture_a {
|
||||
*displayed = images.texture_b.clone_weak();
|
||||
} else {
|
||||
*displayed = images.texture_a.clone_weak();
|
||||
}
|
||||
}
|
||||
|
||||
struct GameOfLifeComputePlugin;
|
||||
|
@ -76,7 +99,7 @@ impl Plugin for GameOfLifeComputePlugin {
|
|||
fn build(&self, app: &mut App) {
|
||||
// Extract the game of life image resource from the main world into the render world
|
||||
// for operation on by the compute shader and display on the sprite.
|
||||
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImage>::default());
|
||||
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImages>::default());
|
||||
let render_app = app.sub_app_mut(RenderApp);
|
||||
render_app.add_systems(
|
||||
Render,
|
||||
|
@ -94,29 +117,35 @@ impl Plugin for GameOfLifeComputePlugin {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Deref, ExtractResource, AsBindGroup)]
|
||||
struct GameOfLifeImage {
|
||||
#[storage_texture(0, image_format = R32Float, access = ReadWrite)]
|
||||
texture: Handle<Image>,
|
||||
#[derive(Resource, Clone, ExtractResource)]
|
||||
struct GameOfLifeImages {
|
||||
texture_a: Handle<Image>,
|
||||
texture_b: Handle<Image>,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct GameOfLifeImageBindGroup(BindGroup);
|
||||
struct GameOfLifeImageBindGroups([BindGroup; 2]);
|
||||
|
||||
fn prepare_bind_group(
|
||||
mut commands: Commands,
|
||||
pipeline: Res<GameOfLifePipeline>,
|
||||
gpu_images: Res<RenderAssets<Image>>,
|
||||
game_of_life_image: Res<GameOfLifeImage>,
|
||||
game_of_life_images: Res<GameOfLifeImages>,
|
||||
render_device: Res<RenderDevice>,
|
||||
) {
|
||||
let view = gpu_images.get(&game_of_life_image.texture).unwrap();
|
||||
let bind_group = render_device.create_bind_group(
|
||||
let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap();
|
||||
let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap();
|
||||
let bind_group_0 = render_device.create_bind_group(
|
||||
None,
|
||||
&pipeline.texture_bind_group_layout,
|
||||
&BindGroupEntries::single(&view.texture_view),
|
||||
&BindGroupEntries::sequential((&view_a.texture_view, &view_b.texture_view)),
|
||||
);
|
||||
commands.insert_resource(GameOfLifeImageBindGroup(bind_group));
|
||||
let bind_group_1 = render_device.create_bind_group(
|
||||
None,
|
||||
&pipeline.texture_bind_group_layout,
|
||||
&BindGroupEntries::sequential((&view_b.texture_view, &view_a.texture_view)),
|
||||
);
|
||||
commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1]));
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
|
@ -129,7 +158,16 @@ struct GameOfLifePipeline {
|
|||
impl FromWorld for GameOfLifePipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
let texture_bind_group_layout = GameOfLifeImage::bind_group_layout(render_device);
|
||||
let texture_bind_group_layout = render_device.create_bind_group_layout(
|
||||
"GameOfLifeImages",
|
||||
&BindGroupLayoutEntries::sequential(
|
||||
ShaderStages::COMPUTE,
|
||||
(
|
||||
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly),
|
||||
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
|
||||
),
|
||||
),
|
||||
);
|
||||
let shader = world.load_asset("shaders/game_of_life.wgsl");
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
||||
|
@ -160,7 +198,7 @@ impl FromWorld for GameOfLifePipeline {
|
|||
enum GameOfLifeState {
|
||||
Loading,
|
||||
Init,
|
||||
Update,
|
||||
Update(usize),
|
||||
}
|
||||
|
||||
struct GameOfLifeNode {
|
||||
|
@ -193,10 +231,16 @@ impl render_graph::Node for GameOfLifeNode {
|
|||
if let CachedPipelineState::Ok(_) =
|
||||
pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)
|
||||
{
|
||||
self.state = GameOfLifeState::Update;
|
||||
self.state = GameOfLifeState::Update(1);
|
||||
}
|
||||
}
|
||||
GameOfLifeState::Update => {}
|
||||
GameOfLifeState::Update(0) => {
|
||||
self.state = GameOfLifeState::Update(1);
|
||||
}
|
||||
GameOfLifeState::Update(1) => {
|
||||
self.state = GameOfLifeState::Update(0);
|
||||
}
|
||||
GameOfLifeState::Update(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,7 +250,7 @@ impl render_graph::Node for GameOfLifeNode {
|
|||
render_context: &mut RenderContext,
|
||||
world: &World,
|
||||
) -> Result<(), render_graph::NodeRunError> {
|
||||
let texture_bind_group = &world.resource::<GameOfLifeImageBindGroup>().0;
|
||||
let bind_groups = &world.resource::<GameOfLifeImageBindGroups>().0;
|
||||
let pipeline_cache = world.resource::<PipelineCache>();
|
||||
let pipeline = world.resource::<GameOfLifePipeline>();
|
||||
|
||||
|
@ -214,8 +258,6 @@ impl render_graph::Node for GameOfLifeNode {
|
|||
.command_encoder()
|
||||
.begin_compute_pass(&ComputePassDescriptor::default());
|
||||
|
||||
pass.set_bind_group(0, texture_bind_group, &[]);
|
||||
|
||||
// select the pipeline based on the current state
|
||||
match self.state {
|
||||
GameOfLifeState::Loading => {}
|
||||
|
@ -223,13 +265,15 @@ impl render_graph::Node for GameOfLifeNode {
|
|||
let init_pipeline = pipeline_cache
|
||||
.get_compute_pipeline(pipeline.init_pipeline)
|
||||
.unwrap();
|
||||
pass.set_bind_group(0, &bind_groups[0], &[]);
|
||||
pass.set_pipeline(init_pipeline);
|
||||
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
||||
}
|
||||
GameOfLifeState::Update => {
|
||||
GameOfLifeState::Update(index) => {
|
||||
let update_pipeline = pipeline_cache
|
||||
.get_compute_pipeline(pipeline.update_pipeline)
|
||||
.unwrap();
|
||||
pass.set_bind_group(0, &bind_groups[index], &[]);
|
||||
pass.set_pipeline(update_pipeline);
|
||||
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue