Skip to content

Commit

Permalink
Fairly reliable terrain destruction
Browse files Browse the repository at this point in the history
  • Loading branch information
bas-ie committed Jan 23, 2025
1 parent 58126e4 commit 78ab611
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 108 deletions.
2 changes: 1 addition & 1 deletion assets/shaders/collision.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
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;
collisions[global_id.x] += 1u;
}
47 changes: 24 additions & 23 deletions assets/shaders/mouse_shader.wgsl
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
#import bevy_sprite::mesh2d_vertex_output::VertexOutput

const MESH_DIMENSIONS: vec2<f32> = vec2<f32>(2560., 1440.);

@group(2) @binding(0) var<uniform> cursor_position: vec2<f32>;
@group(2) @binding(1) var<uniform> level_viewport: vec2<f32>;
@group(2) @binding(2) var terrain_texture: texture_2d<f32>;
@group(2) @binding(3) var terrain_texture_sampler: sampler;
@group(2) @binding(4) var mask_texture: texture_2d<f32>;
@group(2) @binding(5) var mask_texture_sampler: sampler;
@group(2) @binding(1) var terrain_texture: texture_2d<f32>;
@group(2) @binding(2) var terrain_texture_sampler: sampler;
@group(2) @binding(3) var mask_texture: texture_2d<f32>;
@group(2) @binding(4) var mask_texture_sampler: sampler;

@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
var terrain_color = textureSample(terrain_texture, terrain_texture_sampler, mesh.uv);
if terrain_color.a == 0. {
// Exit early, we don't need to do anything else. Note that this might not always be true,
// if we end up introducing mechanics by which empty space can be "filled in". For example,
// could we implement builders this way?
return terrain_color;
}

// For centre of screen:
// level_viewport here is 640, 360 (centred)
// mesh.position.xy is 1280, 720 because it's the middle of a 2k screen
// cursor_position will be 640, 360 (centre of screen)
// adding level_viewport + mesh.position.xy == 1920, 1080 which makes little sense
// instead, our target value should be the centre of 2k which is 1280, 720
// it also needs to work when viewport is 0, 0 or 1920, 720, the min and max possible for viewport
// let adjusted_position = mesh.position.xy + level_viewport;
// let diff = adjusted_position - cursor_position;

let diff = mesh.uv - cursor_position;

if all(abs(diff) < vec2<f32>(0.01, 0.01)) {
// TODO: should this be different to adjust for actual mesh position?
let mask_uv = (diff + vec2<f32>(1., 1.)) / 2.;
// Normalise pos to dimensions of underlying mesh (NOT window dimensions).
let npos = cursor_position / MESH_DIMENSIONS;
let diff = mesh.uv - npos;
// Scaling the diff by aspect ratio avoids the "squashed circle" problem.
let scaled_diff = vec2<f32>(diff.x * (MESH_DIMENSIONS.x / MESH_DIMENSIONS.y), diff.y);
let distance = length(scaled_diff);

// Compare with the radius of the 20px mask texture, converted to UV via the smaller of the mesh
// dimensions. Also, don't bother doing anything
if distance < (10. / MESH_DIMENSIONS.y) {
// Convert NDC value to UV for mask texture sampling.
let mask_uv = (diff + vec2<f32>(1.0)) / 2.0;
var mask_color = textureSample(mask_texture, mask_texture_sampler, mask_uv);
if terrain_color.a != 0. {
terrain_color.a = mask_color.a;
}
terrain_color.a = mask_color.a;
}

return terrain_color;
Expand Down
80 changes: 22 additions & 58 deletions src/game/minimap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,76 +10,40 @@ use bevy::{
use crate::screens::Screen;

pub fn plugin(app: &mut App) {
app.init_resource::<MinimapRenderTarget>();
app.init_resource::<LevelRenderTarget>();
app.add_systems(OnEnter(Screen::InGame), init);
}

#[derive(Component)]
pub struct MinimapCamera;
pub struct LevelCamera;

#[derive(Component)]
pub struct Minimap;

// 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)]
pub struct MinimapRenderTarget {
pub struct LevelRenderTarget {
pub texture: Handle<Image>,
}

fn get_minimap_transform(image_size: &Vec2, screen_size: &Vec2, scale_factor: f32) -> Transform {
let actual_size = image_size / 2. * scale_factor;
Transform::from_xyz(
20. - (screen_size.x / 2.) + actual_size.x,
// TODO: fix
-150. + (screen_size.y / 2.) + actual_size.y,
0.,
)
.with_scale(Vec3::splat(scale_factor))
}

fn init(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut minimap: ResMut<MinimapRenderTarget>,
window: Single<&Window>,
mut level: ResMut<LevelRenderTarget>,
) {
// Render to image for minimap.
let mut minimap_image = Image::new_fill(
Extent3d {
width: 2560,
height: 1440,
..default()
},
TextureDimension::D2,
&[0, 0, 0, 0],
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::default(),
);
// TODO: feels like we need DST but not SRC here? Find out for sure. This even seems to work
// without COPY_DST. Ask Discord?
minimap_image.texture_descriptor.usage =
TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT;
minimap.texture = images.add(minimap_image);

commands.spawn((
Name::new("Minimap Camera"),
MinimapCamera,
Camera2d,
Camera {
clear_color: Color::WHITE.into(),
// Render this first.
order: -1,
target: minimap.texture.clone().into(),
..default()
},
StateScoped(Screen::InGame),
));

commands.spawn((
Name::new("Minimap"),
RenderLayers::layer(1),
Sprite {
image: minimap.texture.clone(),
..Default::default()
},
StateScoped(Screen::InGame),
// TODO: magic numbers
get_minimap_transform(&Vec2::new(2560., 1440.), &window.size(), 0.1),
));
// TODO: eventually, spawn a minimap image here
// commands.spawn((
// Name::new("Minimap"),
// Minimap,
// RenderLayers::layer(1),
// Sprite {
// image: level.texture.clone(),
// ..Default::default()
// },
// StateScoped(Screen::InGame),
// Transform::from_scale(Vec3::splat(0.1)),
// ));
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ fn spawn_camera(mut commands: Commands) {
MainCamera,
Camera2d,
IsDefaultUiCamera,
// NOTE: this camera needs to "see" both the main screen and the minimap.
// This camera needs to be able to see all our render layers in order to composite the
// level background and the sprites together into one view.
RenderLayers::from_layers(&[0, 1]),
));
}
39 changes: 36 additions & 3 deletions src/physics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ use bevy::{
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
},
};
use tiny_bail::prelude::*;

use crate::game::yup::CharacterState;
use crate::{
game::{
minimap::{LevelCamera, LevelRenderTarget},
yup::CharacterState,
},
screens::{
Screen,
ingame::playing::{Level, LevelMaterial},
},
};

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

Expand All @@ -23,6 +33,12 @@ impl Plugin for PhysicsPlugin {
app.add_systems(Startup, init);
app.add_systems(FixedUpdate, gravity);
app.add_plugins(ExtractResourcePlugin::<CollisionsBuffer>::default());
app.add_systems(
FixedUpdate,
swap_textures
.in_set(RenderSet::PostCleanup)
.run_if(in_state(Screen::InGame)),
);
}

fn finish(&self, app: &mut App) {
Expand All @@ -47,6 +63,23 @@ impl Plugin for PhysicsPlugin {
}
}

fn swap_textures(
mut cam: Single<&mut Camera, With<LevelCamera>>,
level: Query<&MeshMaterial2d<LevelMaterial>, With<Level>>,
mut materials: ResMut<Assets<LevelMaterial>>,
mut target: ResMut<LevelRenderTarget>,
) {
let l = r!(level.get_single());
let level_material = r!(materials.get_mut(&l.0));
let old_target_texture = target.texture.clone();
target.texture = level_material.terrain_texture.clone();
level_material.terrain_texture = old_target_texture;
cam.target = target.texture.clone().into();

// Trigger change detection
let _ = r!(materials.get_mut(&l.0));
}

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

Expand All @@ -72,8 +105,8 @@ fn init(mut commands: Commands, mut buffers: ResMut<Assets<ShaderStorageBuffer>>
.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);
// 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.
Expand Down
80 changes: 58 additions & 22 deletions src/screens/ingame/playing.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use bevy::{
prelude::*,
render::render_resource::{AsBindGroup, ShaderRef},
render::{
render_resource::{AsBindGroup, ShaderRef, TextureUsages},
view::RenderLayers,
},
sprite::{Material2d, Material2dPlugin},
};
use tiny_bail::prelude::*;

use crate::{
MainCamera,
assets::{Levels, Masks},
game::minimap::MinimapRenderTarget,
game::minimap::{LevelCamera, LevelRenderTarget},
screens::Screen,
};

Expand All @@ -34,14 +37,11 @@ pub struct MovementSpeed(pub f32);
pub struct LevelMaterial {
#[uniform(0)]
pub cursor_position: Vec2,
#[uniform(1)]
pub level_viewport: Vec2,
// TODO: find out more about samplers!
#[texture(2)]
#[sampler(3)]
#[texture(1)]
#[sampler(2)]
pub terrain_texture: Handle<Image>,
#[texture(4)]
#[sampler(5)]
#[texture(3)]
#[sampler(4)]
pub mask_texture: Handle<Image>,
}

Expand All @@ -67,32 +67,56 @@ impl Material2d for LevelMaterial {

pub fn init(
mut commands: Commands,
level_viewport: Res<LevelViewport>,
mut images: ResMut<Assets<Image>>,
mut level_target: ResMut<LevelRenderTarget>,
masks: Res<Masks>,
mut materials: ResMut<Assets<LevelMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
textures: Res<Levels>,
window: Single<&Window>,
) {
let cursor_position = r!(window.physical_cursor_position());
let base_level_image = r!(images.get_mut(&textures.level.clone()));
let mut target1 = base_level_image.clone();
target1.texture_descriptor.usage =
TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT;
let target2 = target1.clone();
let handle1 = images.add(target1);
level_target.texture = images.add(target2);

commands.spawn((
Name::new("Level"),
Level,
Mesh2d(meshes.add(Rectangle::new(2560., 1440.))),
MeshMaterial2d(materials.add(LevelMaterial {
cursor_position,
level_viewport: **level_viewport,
mask_texture: masks.cursor.clone(),
terrain_texture: textures.level.clone(),
terrain_texture: handle1.clone(),
})),
RenderLayers::layer(1),
StateScoped(Screen::InGame),
));

commands.spawn((
Name::new("Level Camera"),
LevelCamera,
Camera2d,
Camera {
// Render this first.
order: -1,
target: level_target.texture.clone().into(),
..default()
},
// Only the level background lives on render layer 1, everything else is rendered normally
// including sprites, etc.
RenderLayers::layer(1),
StateScoped(Screen::InGame),
));
}

fn draw_alpha_gpu(
level: Query<&MeshMaterial2d<LevelMaterial>, With<Level>>,
level_viewport: Res<LevelViewport>,
camera: Single<(&Camera, &GlobalTransform), With<MainCamera>>,
level: Query<(&MeshMaterial2d<LevelMaterial>, &Transform), With<Level>>,
mut materials: ResMut<Assets<LevelMaterial>>,
mouse_button: Res<ButtonInput<MouseButton>>,
window: Single<&Window>,
Expand All @@ -101,14 +125,26 @@ fn draw_alpha_gpu(
return;
}

let l = r!(level.get_single());
let level_material = r!(materials.get_mut(&l.0));
let (cam, cam_transform) = *camera;
let (material_handle, material_transform) = r!(level.get_single());
let level_material = r!(materials.get_mut(&material_handle.0));
if let Some(cursor_pos) = window.cursor_position() {
let uv = Vec2::new(
cursor_pos.x / window.width(),
cursor_pos.y / window.height(),
);
level_material.cursor_position = uv;
level_material.level_viewport = **level_viewport;
// Convert the cursor pos to world coords. So, for the centre of the window, (640, 360)
// will become (0, 0). Note that this flips the y value in Bevy, so we'll need to flip it
// again later. This step should allow us to scroll the image and still get a reliable
// cursor position.
let world_pos = r!(cam.viewport_to_world_2d(cam_transform, cursor_pos));

// Convert the world pos to pixel coords within the mesh texture.
let texture_pos = material_transform
.compute_matrix()
.inverse()
.transform_point3(world_pos.extend(0.));

// This final step is necessary to offset the position passed to the shader by the window
// dimensions. Without it, we'll get a "ghost image" showing in the bottom right corner of
// the window when the shader draws to the centre of it! We also invert the y value again.
// TODO: magic numbers.
level_material.cursor_position = Vec2::new(texture_pos.x + 1280., -texture_pos.y + 720.);
}
}

0 comments on commit 78ab611

Please sign in to comment.