This commit is contained in:
Matthew Deville 2025-08-31 13:47:28 +02:00
parent 3611612d47
commit b8aee4788d
3 changed files with 135 additions and 128 deletions

View file

@ -1,16 +1,10 @@
//! A shader that uses the WGSL shading language. //! A shader that uses the WGSL shading language.
mod material;
mod systems;
use bevy::{ use bevy::{input::common_conditions::input_just_pressed, prelude::*, sprite::Material2dPlugin};
asset::AssetEvent,
input::common_conditions::*,
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, Shader, ShaderRef},
sprite::{AlphaMode2d, Material2d, Material2dPlugin},
window::{PrimaryWindow, WindowMode},
};
const SHADER_ASSET_PATH: &str = "shaders/default.wgsl"; use material::CustomMaterial;
fn main() { fn main() {
App::new() App::new()
@ -18,128 +12,16 @@ fn main() {
DefaultPlugins, DefaultPlugins,
Material2dPlugin::<CustomMaterial>::default(), Material2dPlugin::<CustomMaterial>::default(),
)) ))
.add_systems(Startup, setup) .add_systems(Startup, systems::setup)
.add_systems( .add_systems(
Update, Update,
( (
update_material_time, material::update_material_time,
resize_fullscreen_quad, material::shader_hot_reload,
shader_hot_reload, systems::resize_fullscreen_quad,
exit_app.run_if(input_just_pressed(KeyCode::Escape)), systems::exit_app.run_if(input_just_pressed(KeyCode::Escape)),
toggle_fullscreen.run_if(input_just_pressed(KeyCode::F11)), systems::toggle_fullscreen.run_if(input_just_pressed(KeyCode::F11)),
), ),
) )
.run(); .run();
} }
/// set up a simple 2D-screen-like surface
fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
mut commands: Commands,
) {
primary_window.decorations = false;
commands.spawn(Camera2d);
// quad that fills the whole window
let screen_size = Vec2::new(
primary_window.resolution.width(),
primary_window.resolution.height(),
);
// create material with initial time = 0.0
let material_handle = materials.add(CustomMaterial {
time: 0.0,
resolution: screen_size,
});
commands.spawn((
Mesh2d(meshes.add(Rectangle::default())),
MeshMaterial2d(material_handle),
Transform::from_scale(Vec3::new(screen_size.x, screen_size.y, 1.0)),
FullscreenQuad,
));
}
/// Marker component for the fullscreen quad so we can update it on window resize
#[derive(Component)]
struct FullscreenQuad;
/// Update fullscreen quad transforms when the primary window is resized.
fn resize_fullscreen_quad(
primary_window: Single<&Window, With<PrimaryWindow>>,
mut transform: Single<&mut Transform, With<FullscreenQuad>>,
) {
let size = Vec2::new(
primary_window.resolution.width(),
primary_window.resolution.height(),
);
transform.scale = Vec3::new(size.x, size.y, 1.0);
}
fn exit_app(mut exit: EventWriter<AppExit>) {
exit.write(AppExit::Success);
}
fn toggle_fullscreen(mut primary_window: Single<&mut Window, With<PrimaryWindow>>) {
primary_window.mode = match primary_window.mode {
WindowMode::BorderlessFullscreen(_) => WindowMode::Windowed,
WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
WindowMode::Fullscreen(_, _) => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
};
}
/// Listen for shader asset changes and force materials to update so the new shader is used.
fn shader_hot_reload(
mut shader_events: EventReader<AssetEvent<Shader>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
let mut reload_needed = false;
for event in shader_events.read() {
match event {
AssetEvent::Modified { .. } => {
info!("shader changed: {event:?}");
reload_needed = true;
}
_ => {}
}
}
if reload_needed {
// iterate all materials and 'touch' them so Bevy updates bindings using the recompiled shader
for (_handle, material) in materials.iter_mut() {
// assign to itself to mark changed
let cloned = material.clone();
*material = cloned;
}
}
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Clone)]
struct CustomMaterial {
#[uniform(0)]
time: f32,
#[uniform(1)]
resolution: Vec2,
}
fn update_material_time(mut materials: ResMut<Assets<CustomMaterial>>, time: Res<Time>) {
let t = time.elapsed_secs();
for (_handle, material) in materials.iter_mut() {
material.time = t;
}
}
/// The Material2d trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material2d api docs for details!
impl Material2d for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Opaque
}
}

61
src/material.rs Normal file
View file

@ -0,0 +1,61 @@
use bevy::{
asset::AssetEvent,
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, Shader, ShaderRef},
sprite::{AlphaMode2d, Material2d},
};
const SHADER_ASSET_PATH: &str = "shaders/default.wgsl";
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Clone)]
pub struct CustomMaterial {
#[uniform(0)]
pub time: f32,
#[uniform(1)]
pub resolution: Vec2,
}
pub fn update_material_time(mut materials: ResMut<Assets<CustomMaterial>>, time: Res<Time>) {
let t = time.elapsed_secs();
for (_handle, material) in materials.iter_mut() {
material.time = t;
}
}
/// Listen for shader asset changes and force materials to update so the new shader is used.
pub fn shader_hot_reload(
mut shader_events: EventReader<AssetEvent<Shader>>,
mut materials: ResMut<Assets<CustomMaterial>>,
) {
let mut reload_needed = false;
for event in shader_events.read() {
match event {
AssetEvent::Modified { .. } => {
info!("shader changed: {event:?}");
reload_needed = true;
}
_ => {}
}
}
if reload_needed {
// iterate all materials and 'touch' them so Bevy updates bindings using the recompiled shader
for (_handle, material) in materials.iter_mut() {
// assign to itself to mark changed
let cloned = material.clone();
*material = cloned;
}
}
}
impl Material2d for CustomMaterial {
fn fragment_shader() -> ShaderRef {
SHADER_ASSET_PATH.into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Opaque
}
}

64
src/systems.rs Normal file
View file

@ -0,0 +1,64 @@
use bevy::{
prelude::*,
window::{PrimaryWindow, WindowMode},
};
use crate::material::CustomMaterial;
/// Marker component for the fullscreen quad so we can update it on window resize
#[derive(Component)]
pub struct FullscreenQuad;
/// set up a simple 2D-screen-like surface
pub fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<CustomMaterial>>,
mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
mut commands: Commands,
) {
primary_window.decorations = false;
commands.spawn(Camera2d);
// quad that fills the whole window
let screen_size = Vec2::new(
primary_window.resolution.width(),
primary_window.resolution.height(),
);
// create material with initial time = 0.0
let material_handle = materials.add(CustomMaterial {
time: 0.0,
resolution: screen_size,
});
commands.spawn((
Mesh2d(meshes.add(Rectangle::default())),
MeshMaterial2d(material_handle),
Transform::from_scale(Vec3::new(screen_size.x, screen_size.y, 1.0)),
FullscreenQuad,
));
}
/// Update fullscreen quad transforms when the primary window is resized.
pub fn resize_fullscreen_quad(
primary_window: Single<&Window, With<PrimaryWindow>>,
mut transform: Single<&mut Transform, With<FullscreenQuad>>,
) {
let size = Vec2::new(
primary_window.resolution.width(),
primary_window.resolution.height(),
);
transform.scale = Vec3::new(size.x, size.y, 1.0);
}
pub fn exit_app(mut exit: EventWriter<AppExit>) {
exit.write(AppExit::Success);
}
pub fn toggle_fullscreen(mut primary_window: Single<&mut Window, With<PrimaryWindow>>) {
primary_window.mode = match primary_window.mode {
WindowMode::BorderlessFullscreen(_) => WindowMode::Windowed,
WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
WindowMode::Fullscreen(_, _) => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
};
}