From bd7f26817deadffa66c3a724c00c94e7c58cdc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Tue, 5 Nov 2024 01:23:04 +0100 Subject: [PATCH] shader --- assets/flag_shader.wgsl | 63 +++++++++ bevy-workshop/src/SUMMARY.md | 5 +- bevy-workshop/src/visuals/exercises.md | 10 ++ bevy-workshop/src/visuals/flag.md | 186 +++++++++++++++++++++++++ bevy-workshop/src/visuals/index.md | 8 ++ bevy-workshop/src/visuals/progress.md | 16 +++ src/game/flag.rs | 34 +++++ src/game/mod.rs | 50 ++++--- src/game/player.rs | 19 ++- 9 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 assets/flag_shader.wgsl create mode 100644 bevy-workshop/src/visuals/exercises.md create mode 100644 bevy-workshop/src/visuals/flag.md create mode 100644 bevy-workshop/src/visuals/index.md create mode 100644 bevy-workshop/src/visuals/progress.md create mode 100644 src/game/flag.rs diff --git a/assets/flag_shader.wgsl b/assets/flag_shader.wgsl new file mode 100644 index 0000000..0c318a4 --- /dev/null +++ b/assets/flag_shader.wgsl @@ -0,0 +1,63 @@ +#import bevy_sprite::{ + mesh2d_vertex_output::VertexOutput, + mesh2d_view_bindings::globals, +} + + +@group(2) @binding(0) var base_color_texture: texture_2d; +@group(2) @binding(1) var base_color_sampler: sampler; +@group(2) @binding(2) var index: vec4; +@group(2) @binding(3) var distance_to_player: vec4; + + +fn Hash12(t: f32) -> vec2 { + let x = fract(sin(t*748.32)*367.34); + let y = fract(sin((t+x)*623.785)*292.45); + + return vec2(x,y)-.5; +} + +fn Hash12Polar(t: f32) -> vec2 { + let a = fract(sin(t*748.31)*367.34)*6.2832; + let d = fract(sin((t+a)*623.785)*292.45); + + return vec2(cos(a),sin(a))*d; +} + +@fragment +fn fragment(mesh: VertexOutput) -> @location(0) vec4 { + let atlas_width = 1024.0; + let atlas_height = 512.0; + let sprite_size = 128.0; + + var texture = textureSample( + base_color_texture, + base_color_sampler, + vec2((mesh.uv.x + index.x) * sprite_size / atlas_width, (mesh.uv.y + index.y) * sprite_size / atlas_height) + ); + + let max_distance = 750.0; + + if distance_to_player.x < max_distance { + // Adapted from https://www.shadertoy.com/view/7sjfRy + for(var j = 0; j < 3; j++){ + for(var i = 0; i < 100; i++){ + + let t = fract(globals.time); + let bright = mix(0.002 * (1.0 - distance_to_player.x / max_distance), 0.001, smoothstep(0.025, 0.0, t) ); + let dir = Hash12Polar(f32(i)+1.); + let dist = distance(mesh.uv - vec2(0.5,0.5) - dir*t, vec2(0, 0)+(Hash12Polar(f32(j*i))/2.)); + + if bright / dist > 0.1 { + texture.r = bright / dist * 2.0; + texture.g = bright / dist / 2.0; + texture.b = bright / dist / 2.0; + texture.a = 1.0; + } + } + } + + } + + return texture; +} diff --git a/bevy-workshop/src/SUMMARY.md b/bevy-workshop/src/SUMMARY.md index a537b59..37c9a6e 100644 --- a/bevy-workshop/src/SUMMARY.md +++ b/bevy-workshop/src/SUMMARY.md @@ -35,7 +35,10 @@ - [Jumping](./sound/jumping.md) - [✍️ Exercises](./sound/exercises.md) - [Progress Report](./sound/progress.md) -- [Visual Effects](./visuals.md) +- [Visual Effects](./visuals/index.md) + - [Dynamic Flag](./visuals/flag.md) + - [✍️ Exercises](./visuals/exercises.md) + - [Progress Report](./visuals/progress.md) - [Enemies](./enemies.md) - [Platforms Support](./platforms.md) - [What's Next (Game)](./next_game.md) diff --git a/bevy-workshop/src/visuals/exercises.md b/bevy-workshop/src/visuals/exercises.md new file mode 100644 index 0000000..f3fc386 --- /dev/null +++ b/bevy-workshop/src/visuals/exercises.md @@ -0,0 +1,10 @@ +# ✍️ Exercises + +## Jumping + +Let's add a shader displaying an effect when jumping. + +Tips: +* Use the time the player started jumping in the material +* Use the current velocity in the material +* Try to find a cool effect on and port it diff --git a/bevy-workshop/src/visuals/flag.md b/bevy-workshop/src/visuals/flag.md new file mode 100644 index 0000000..995b370 --- /dev/null +++ b/bevy-workshop/src/visuals/flag.md @@ -0,0 +1,186 @@ +# Dynamic Flag + +We'll build a first shader adding some particles to the flag depending on how close the player is. + +## Custom GPU type + +First step is to declare the data we'll send to the GPU: + +```rust +# extern crate bevy; +# use bevy::{ +# prelude::*, +# render::render_resource::{AsBindGroup, ShaderRef}, +# sprite::{AlphaMode2d, Material2d, Material2dPlugin}, +# }; +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +pub struct FlagMaterial { + #[texture(0)] + #[sampler(1)] + pub atlas: Handle, + #[uniform(2)] + pub index: Vec4, + #[uniform(3)] + pub distance: Vec4, +} +``` + +By deriving the [`AsBindGroup`](https://docs.rs/bevy/0.15.0-rc.2/bevy/render/render_resource/trait.AsBindGroup.html) trait and annotating the field of the struct, Bevy will be able to know how to transform the data from Rust type to what is expected by the GPU: +* `atlas` has the handle to the spritesheet +* `index` is the index of the sprite in the spritesheet. Bevy uses a single `u32` for that, and get the number of rows and columns from the [`TextureAtlasLayout`](https://docs.rs/bevy/0.15.0-rc.2/bevy/prelude/struct.TextureAtlasLayout.html). We'll do simpler and hard code some values, and use `(i, j)` coordinatesto specify which sprite to use +* `distance` is the distance between the flag and the player + +
+ +`index` will have a `Vec2`, and `distance` a `f32`, but they are both defined as `Vec4`. This is for WebGL2 compatibility, where types must be aligned on 16 bytes. + +The two strategies to solve that are padding and packing. Padding is using bigger types than necessary and wasting memory, packing is grouping fields that have separate meaning in a single type. + +This workshop use padding as it's easier to read and the material is only used once, so doesn't waste a lot of memory. + +
+ +## Custom Material + +Next is to define the shader that will be used to render the data. This is done by implementing the [`Material2d`](https://docs.rs/bevy/0.15.0-rc.2/bevy/sprite/trait.Material2d.html) trait: + +```rust +# extern crate bevy; +# use bevy::{ +# prelude::*, +# render::render_resource::{AsBindGroup, ShaderRef}, +# sprite::{AlphaMode2d, Material2d, Material2dPlugin}, +# }; +# #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +# pub struct FlagMaterial {} +impl Material2d for FlagMaterial { + fn fragment_shader() -> ShaderRef { + "flag_shader.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Blend + } +} +``` + +The trait has more customisation than used here, and use sane defaults. By just using a string for the fragment shader, Bevy will load the file specified from the asset folder. + +This is a basic shader that will display the sprite selected by the `index` from a sprite sheet: + +```wgsl +#import bevy_sprite::{ + mesh2d_vertex_output::VertexOutput, + mesh2d_view_bindings::globals, +} + +@group(2) @binding(0) var base_color_texture: texture_2d; +@group(2) @binding(1) var base_color_sampler: sampler; +@group(2) @binding(2) var index: vec4; +@group(2) @binding(3) var distance_to_player: vec4; + +@fragment +fn fragment(mesh: VertexOutput) -> @location(0) vec4 { + let atlas_width = 1024.0; + let atlas_height = 512.0; + let sprite_size = 128.0; + + var texture = textureSample( + base_color_texture, + base_color_sampler, + vec2((mesh.uv.x + index.x) * sprite_size / atlas_width, (mesh.uv.y + index.y) * sprite_size / atlas_height) + ); + + return texture; +} +``` + +Bevy has some extensions to WGSL to allow imports and expose some helpful features. + +Variables with the `@group(2)` will match the bind group declared on Rust side. + +## Using the Material + +Our new material must be added to Bevy before it can be used. This can be done in a plugin: + +```rust +# extern crate bevy; +# use bevy::{ +# prelude::*, +# render::render_resource::{AsBindGroup, ShaderRef}, +# sprite::{AlphaMode2d, Material2d, Material2dPlugin}, +# }; +# #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +# pub struct FlagMaterial {} +# impl Material2d for FlagMaterial {} +pub struct FlagPlugin; + +impl Plugin for FlagPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(Material2dPlugin::::default()); + } +} +``` + +Then we can replace `Sprite` for the flag with our new material: + +```rust +# extern crate bevy; +# use bevy::{ +# prelude::*, +# render::render_resource::{AsBindGroup, ShaderRef}, +# sprite::{AlphaMode2d, Material2d, Material2dPlugin}, +# }; +# #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +# pub struct FlagMaterial { +# #[texture(0)] +# #[sampler(1)] +# pub atlas: Handle, +# #[uniform(2)] +# pub index: Vec4, +# #[uniform(3)] +# pub distance: Vec4, +# } +# impl Material2d for FlagMaterial {} +# enum Tile { Flag } +# #[derive(Component)] +# struct Flag; +# #[derive(Event)] +# struct ReachedFlag; +# fn reached_flag(_trigger: Trigger) {} +# struct GameAssets { +# items_image: Handle, +# items_layout: Handle, +# } +# #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)] +# enum GameState { #[default] Game } +fn display_tile( + // ... + meshes: &mut Assets, + flag_materials: &mut Assets, +) { + # let commands: Commands = unimplemented!(); + # let assets: GameAssets = unimplemented!(); + # let (x, y) = (0.0, 0.0); + # let tile = Tile::Flag; + match tile { + // ... + Tile::Flag => { + commands + .spawn(( + Mesh2d(meshes.add(Rectangle::default())), + MeshMaterial2d(flag_materials.add(FlagMaterial { + atlas: assets.items_image.clone(), + index: Vec4::new(0.0, 1.0, 0.0, 0.0), + distance: Vec4::ZERO, + })), + Transform::from_xyz(x, y, 1.0).with_scale(Vec3::splat(0.5) * 128.0), + StateScoped(GameState::Game), + Flag, + )) + .observe(reached_flag); + } + // ... + } +} +``` diff --git a/bevy-workshop/src/visuals/index.md b/bevy-workshop/src/visuals/index.md new file mode 100644 index 0000000..28ad4e3 --- /dev/null +++ b/bevy-workshop/src/visuals/index.md @@ -0,0 +1,8 @@ +# Visual Effects + +Visual effects can help your game pop up. This is commonly done with shaders, which are programs that execute on the GPU. The best languages to write them in Bevy is the [WebGPU Shading Language](https://www.w3.org/TR/WGSL/), and it will be translated as needed by the platform on which the application is running. + +Bevy offers several abstractions to render things on screen: +* Directly using images or colors or texture atlas, which is what we've been doing until now. The shaders are built-in Bevy, and use as many optimisation as possible at the cost of customisation. +* Custom materials, which we'll explore in this section. For 2d, you'll need to implement the [`Material2d`](https://docs.rs/bevy/0.15.0-rc.2/bevy/sprite/trait.Material2d.html) trait. +* Lower level abstractions, down to complete control on the whole rendering pipeline. This won't be in this workshop. diff --git a/bevy-workshop/src/visuals/progress.md b/bevy-workshop/src/visuals/progress.md new file mode 100644 index 0000000..557b6ef --- /dev/null +++ b/bevy-workshop/src/visuals/progress.md @@ -0,0 +1,16 @@ +# Progress Report + +## What You've learned + +* Defining a custom material + * With the [`AsBindGroup`](https://docs.rs/bevy/0.15.0-rc.2/bevy/render/render_resource/trait.AsBindGroup.html) derive and its attributes to handle data transfer to the GPU + * Implementing the [`Material2d`](https://docs.rs/bevy/0.15.0-rc.2/bevy/sprite/trait.Material2d.html) trait to define the shader + * And some basic WGSL +* And using that material + * Adding it to the app with the [`Material2dPlugin`](https://docs.rs/bevy/0.15.0-rc.2/bevy/sprite/struct.Material2dPlugin.html) + * With the [`Mesh2d`](https://docs.rs/bevy/0.15.0-rc.2/bevy/prelude/struct.Mesh2d.html) component to define the shape + * And the [`MeshMaterial2d`](https://docs.rs/bevy/0.15.0-rc.2/bevy/prelude/struct.MeshMaterial2d.html) component to define the material + +## Going Further + +Shaders and rendering is a *very big* domain. diff --git a/src/game/flag.rs b/src/game/flag.rs new file mode 100644 index 0000000..cf8ac85 --- /dev/null +++ b/src/game/flag.rs @@ -0,0 +1,34 @@ +use bevy::{ + prelude::*, + render::render_resource::{AsBindGroup, ShaderRef}, + sprite::{AlphaMode2d, Material2d, Material2dPlugin}, +}; + +pub struct FlagPlugin; + +impl Plugin for FlagPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(Material2dPlugin::::default()); + } +} + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +pub struct FlagMaterial { + #[texture(0)] + #[sampler(1)] + pub atlas: Handle, + #[uniform(2)] + pub index: Vec4, + #[uniform(3)] + pub distance: Vec4, +} + +impl Material2d for FlagMaterial { + fn fragment_shader() -> ShaderRef { + "flag_shader.wgsl".into() + } + + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Blend + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 19d328a..8ed94b5 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,6 +1,7 @@ use std::time::Duration; use bevy::{prelude::*, time::common_conditions::on_timer}; +use flag::FlagMaterial; use crate::{ level_loader::{Level, LoadedLevel, Tile}, @@ -8,6 +9,7 @@ use crate::{ }; mod audio; +mod flag; mod player; const SCALE: f32 = 0.5; @@ -16,7 +18,7 @@ pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_plugins((player::PlayerPlugin, audio::AudioPlugin)) + app.add_plugins((player::PlayerPlugin, audio::AudioPlugin, flag::FlagPlugin)) .add_systems(OnEnter(GameState::Game), display_level) .add_systems( Update, @@ -71,6 +73,8 @@ fn display_tile( y: f32, line: &[Tile], assets: &GameAssets, + meshes: &mut Assets, + flag_materials: &mut Assets, ) { match tile { Tile::Ground => { @@ -105,14 +109,13 @@ fn display_tile( Tile::Flag => { commands .spawn(( - Sprite::from_atlas_image( - assets.items_image.clone(), - TextureAtlas { - layout: assets.items_layout.clone(), - index: 6, - }, - ), - Transform::from_xyz(x, y, 1.0).with_scale(Vec3::splat(SCALE)), + Mesh2d(meshes.add(Rectangle::default())), + MeshMaterial2d(flag_materials.add(FlagMaterial { + atlas: assets.items_image.clone(), + index: Vec4::new(0.0, 1.0, 0.0, 0.0), + distance: Vec4::ZERO, + })), + Transform::from_xyz(x, y, 1.0).with_scale(Vec3::splat(SCALE) * 128.0), StateScoped(GameState::Game), Flag, )) @@ -127,6 +130,8 @@ fn display_level( assets: Res, level: Res, levels: Res>, + mut meshes: ResMut>, + mut flag_materials: ResMut>, ) { let level = levels.get(&level.level).unwrap(); @@ -136,18 +141,31 @@ fn display_level( (i as f32 - 9.0) * 128.0 * SCALE, -(j as f32 - 5.0) * 128.0 * SCALE, ); - display_tile(&mut commands, tile, i, x, y, line, &assets); + display_tile( + &mut commands, + tile, + i, + x, + y, + line, + &assets, + meshes.as_mut(), + flag_materials.as_mut(), + ); } } } -fn animate_level(mut flags: Query<&mut Sprite, With>) { - for mut flag in &mut flags { - let atlas = flag.texture_atlas.as_mut().unwrap(); - if atlas.index == 6 { - atlas.index = 12; +fn animate_level( + flags: Query<&MeshMaterial2d, With>, + mut flag_materials: ResMut>, +) { + for flag in &flags { + let material = flag_materials.get_mut(flag).unwrap(); + if material.index.y == 1.0 { + material.index.y = 2.0; } else { - atlas.index = 6; + material.index.y = 1.0; } } } diff --git a/src/game/player.rs b/src/game/player.rs index bd1163a..1e41413 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -5,7 +5,10 @@ use bevy::{ use crate::GameState; -use super::{AgainstWall, AudioTrigger, Flag, Ground, IsOnGround, Player, ReachedFlag, Velocity}; +use super::{ + flag::FlagMaterial, AgainstWall, AudioTrigger, Flag, Ground, IsOnGround, Player, ReachedFlag, + Velocity, +}; pub struct PlayerPlugin; @@ -192,15 +195,17 @@ fn death_by_fall( fn near_flag( mut commands: Commands, player_transform: Query<&Transform, With>, - flags: Query<(Entity, &Transform), With>, + flags: Query<(Entity, &Transform, &MeshMaterial2d), With>, + mut flag_materials: ResMut>, ) { let player_transform = player_transform.single(); - for (flag, flag_transform) in &flags { - if player_transform + for (flag, flag_transform, flag_material) in &flags { + let distance = player_transform .translation - .distance(flag_transform.translation) - < 50.0 - { + .distance(flag_transform.translation); + let material = flag_materials.get_mut(flag_material).unwrap(); + material.distance.x = distance; + if distance < 50.0 { commands.entity(flag).trigger(ReachedFlag); } }