Skip to content

Commit

Permalink
shader
Browse files Browse the repository at this point in the history
  • Loading branch information
mockersf committed Nov 5, 2024
1 parent cbab891 commit bd7f268
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 24 deletions.
63 changes: 63 additions & 0 deletions assets/flag_shader.wgsl
Original file line number Diff line number Diff line change
@@ -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<f32>;
@group(2) @binding(1) var base_color_sampler: sampler;
@group(2) @binding(2) var<uniform> index: vec4<f32>;
@group(2) @binding(3) var<uniform> distance_to_player: vec4<f32>;


fn Hash12(t: f32) -> vec2<f32> {
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<f32> {
let a = fract(sin(t*748.31)*367.34)*6.2832;
let d = fract(sin((t+a)*623.785)*292.45);

return vec2<f32>(cos(a),sin(a))*d;
}

@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
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<f32>((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;
}
5 changes: 4 additions & 1 deletion bevy-workshop/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions bevy-workshop/src/visuals/exercises.md
Original file line number Diff line number Diff line change
@@ -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 <https://www.shadertoy.com> and port it
186 changes: 186 additions & 0 deletions bevy-workshop/src/visuals/flag.md
Original file line number Diff line number Diff line change
@@ -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<Image>,
#[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

<div class="warning">

`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.

</div>

## 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<f32>;
@group(2) @binding(1) var base_color_sampler: sampler;
@group(2) @binding(2) var<uniform> index: vec4<f32>;
@group(2) @binding(3) var<uniform> distance_to_player: vec4<f32>;
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
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<f32>((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::<FlagMaterial>::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<Image>,
# #[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<ReachedFlag>) {}
# struct GameAssets {
# items_image: Handle<Image>,
# items_layout: Handle<TextureAtlasLayout>,
# }
# #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
# enum GameState { #[default] Game }
fn display_tile(
// ...
meshes: &mut Assets<Mesh>,
flag_materials: &mut Assets<FlagMaterial>,
) {
# 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);
}
// ...
}
}
```
8 changes: 8 additions & 0 deletions bevy-workshop/src/visuals/index.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions bevy-workshop/src/visuals/progress.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions src/game/flag.rs
Original file line number Diff line number Diff line change
@@ -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::<FlagMaterial>::default());
}
}

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct FlagMaterial {
#[texture(0)]
#[sampler(1)]
pub atlas: Handle<Image>,
#[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
}
}
Loading

0 comments on commit bd7f268

Please sign in to comment.