Learning WGSL Shaders with Bevy 0.16: A Practical Journey

Does This Sound Familiar?
You're a Bevy developer. You love Rust's power and safety, and you've started to feel comfortable building scenes and game logic. But now you want to create something truly unique - a shimmering shield, a stylized water surface, a custom lighting model - and you realize you need to write a shader.
So you open a .wgsl file... and you hit a wall.
You look for tutorials, but they're almost all for GLSL or Unity, using a different language and a different engine. You look at the official Bevy examples, but they seem to assume you already know how shaders work. You're stuck in a frustrating gap: you know the engine, you know the language, but you don't know how to bridge the two to speak directly to the GPU.
I created this series because I was that developer.
This is not a theoretical textbook or a high-level overview. It is the practical, step-by-step guide I wish I had when I was starting. It's the result of systematically navigating that gap, figuring out the fundamentals, hitting the common pitfalls, and documenting what finally made the concepts "click."
My goal is to provide a clear, linear path that takes you from the absolute basics of the graphics pipeline all the way to advanced techniques, with every single concept explained and demonstrated inside a working Bevy project. This is the journey of learning how to think in shaders, and I'm thrilled to share it with you.
Our Approach: A Practical, Incremental Journey
This is a practical, hands-on series. We will learn by building, not just by reading. Every single article is built around a working Bevy project that you can run, modify, and experiment with. Our philosophy is that you don't truly understand a concept until you've seen it work, broken it, and fixed it again.
Our journey is structured to build your knowledge from the ground up, ensuring you have a solid foundation before moving on to more complex topics. Here's the path we'll take:
First, we'll build the foundation. We will demystify the Graphics Pipeline and learn the fundamental "alphabet" of WGSL - its data types, variables, and functions. You'll understand where your code runs and the language it speaks.
Then, we'll take control of geometry. We'll dive deep into the Vertex Shader, where you will learn to manipulate the very shape of your 3D models. You'll create waving flags, rippling water, and fields of animated grass, learning how to breathe life into static meshes.
Next, we'll learn to paint those shapes with light and color. We'll master the Fragment Shader, moving beyond simple colors to create procedural patterns, sample textures, and implement our own lighting models. This is where you'll define the unique visual identity of your projects.
Finally, we'll explore the advanced frontier. With the fundamentals in place, we'll unlock the true power of the GPU, exploring post-processing effects, compute shaders, performance optimization, and how to achieve specific artistic styles.
By the end of this series, you won't just know how to copy and paste shader code - you will have the confidence and the deep understanding to create your own custom rendering effects from scratch.
Who This Series Is For (And What You'll Need)
This series is designed for the curious Bevy developer who is ready to take the next step in their creative journey. If you're comfortable with the basics of Rust and Bevy but feel like the GPU is still a "black box," you're in the perfect place.
The Prerequisites
This is not a "from zero" programming course. We'll be moving at a steady pace, and I'll assume you have a solid footing in the following areas:
A Good Grasp of Rust: You don't need to be a systems-level expert, but you should be comfortable with core concepts like structs, traits, ownership, and the module system.
Bevy Fundamentals: You should have worked through the official Bevy book or built a small project. You know what Components, Systems, and Resources are, and you feel comfortable setting up a basic scene.
Basic Vector Math Intuition: You don't need a math degree, but you should know what a vector is (
Vec3) and have a general idea of what operations like adding, subtracting, or normalizing them mean. We'll review the more complex math (like dot products and matrices) as we need it, focusing on intuition over raw proofs.
What You Don't Need
This is just as important. You do not need:
Any prior shader programming experience (that's what we're here to learn!).
A deep background in low-level graphics APIs like OpenGL, Vulkan, or DirectX.
To be a math wizard. A willingness to engage with the concepts is far more important than a formal education in linear algebra.
Our goal is to build up your knowledge from first principles, right here in the Bevy ecosystem.
Setting Up Your Development Playground
A fast, smooth iteration loop is the secret to enjoying shader development. You want to be able to make a small change to your shader code and see the result instantly, without fighting with long compile times. Let's create a Bevy project specifically configured for this rapid, creative workflow.
Recommended Tools
While you can use any text editor, this series is written with a specific, highly effective setup in mind:
- Editor: Visual Studio Code (VS Code) is strongly recommended. Its rust-analyzer extension provides top-tier support for Rust, and there are excellent extensions that add syntax highlighting for WGSL, making your shader code much easier to read.
Graphics Debugger (Optional but Powerful): For advanced debugging, a dedicated tool that can capture and inspect a single frame is invaluable. We won't need this for the early articles, but knowing it exists is important.
Windows / Linux: RenderDoc is the open-source industry standard.
macOS: Apple's Metal GPU Debugger and Frame Capture, which is built directly into Xcode, is the essential tool for debugging Metal applications.
Step 1: Install the Essentials
First, ensure you have the Rust toolchain installed. If you're already a Rust developer, you can likely skip this.
# Install Rust if you don't have it
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Step 2: Create the Project and Configure Cargo.toml
Let's create a new Bevy project that will be our home for this entire series.
mkdir bevy-shader-journey
cd bevy-shader-journey
cargo init
Now, replace the contents of your Cargo.toml file with the following configuration. We're adding a few key dependencies and settings to optimize our development experience.
[package]
name = "bevy-shader-journey"
version = "0.1.0"
edition = "2024"
[dependencies]
# The core Bevy engine, version 0.16
bevy = { version = "0.16", features = ["file_watcher"] }
# For faster compile times in debug builds, you can enable this feature.
# bevy = { version = "0.16", features = ["dynamic_linking", "file_watcher"] }
# Interactive demo selection - makes it easy to jump between examples
inquire = "0.9"
# Dependencies for procedural generation and randomness
noise = "0.9"
rand = "0.9"
# An incredibly useful debugging tool we'll use in later articles
# to tweak shader values in real-time.
bevy-inspector-egui = "0.35"
# This section makes our local code compile faster (opt-level = 1)
# while keeping our dependencies fully optimized (opt-level = 3).
# It's a great trick for improving Bevy's debug build times.
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
Step 3: Create the Project Directory Structure
A good project organization will make life much easier as we add more examples. Create the necessary directories and files with this command:
mkdir -p assets/shaders src/demos src/materials
# Create the module files
touch src/demos/mod.rs
touch src/materials/mod.rs
This gives us:
assets/shaders/: Where all our.wgslshader files will live.src/demos/: Where each article's interactive example will go.src/materials/: Where the Rust "glue" code for our custom materials will go.
Step 4: Set Up the Demo Selection System
Here's where things get interesting. Instead of creating separate example files or constantly commenting and uncommenting code, we're going to build a smart demo selection system. This will let you easily run any example from the series with a simple command.
Replace the contents of src/main.rs with this code:
use std::env;
mod demos;
mod materials;
struct Demo {
number: &'static str,
title: &'static str,
run: fn(),
}
impl Demo {
fn matches(&self, query: &str) -> bool {
let query_lower = query.to_lowercase();
self.number.contains(&query_lower) || self.title.to_lowercase().contains(&query_lower)
}
fn display(&self) -> String {
format!("{} - {}", self.number, self.title)
}
}
fn main() {
// Registry of all available demos
let demos = vec![
Demo {
number: "0.0",
title: "Basic Scene Setup",
run: demos::d00_00_basic_scene::run,
},
];
let args: Vec<String> = env::args().skip(1).collect();
let demo = if args.is_empty() {
// No arguments - show interactive selection
select_demo_interactive(&demos)
} else {
// Arguments provided - try to match
let query = args.join(" ");
find_and_select_demo(&demos, &query)
};
match demo {
Some(d) => {
println!("\n๐ Running: {}\n", d.display());
(d.run)();
}
None => {
println!("No demo selected. Exiting.");
std::process::exit(0);
}
}
}
fn find_and_select_demo<'a>(demos: &'a [Demo], query: &str) -> Option<&'a Demo> {
let matches: Vec<&Demo> = demos.iter().filter(|d| d.matches(query)).collect();
match matches.len() {
0 => {
eprintln!("โ No demos match '{}'\n", query);
show_available_demos(demos);
println!();
select_demo_interactive(demos)
}
1 => Some(matches[0]),
_ => {
eprintln!("โ ๏ธ Multiple demos match '{}':\n", query);
select_from_list(&matches)
}
}
}
fn select_demo_interactive(demos: &[Demo]) -> Option<&Demo> {
println!("Available demos:");
show_available_demos(demos);
println!();
// Convert &[Demo] to Vec<&Demo> for select_from_list
let demo_refs: Vec<&Demo> = demos.iter().collect();
select_from_list(&demo_refs)
}
fn show_available_demos(demos: &[Demo]) {
for demo in demos {
println!(" โข {}", demo.display());
}
}
fn select_from_list<'a>(demos: &[&'a Demo]) -> Option<&'a Demo> {
use inquire::Select;
let options: Vec<String> = demos.iter().map(|d| d.display()).collect();
let selected = Select::new("Select a demo to run:", options).prompt();
match selected {
Ok(selected_text) => {
// Find the demo that matches the selected display text
demos.iter().find(|d| d.display() == selected_text).copied()
}
Err(_) => {
println!("Selection cancelled.");
None
}
}
}
What does this do? This system lets you run any demo from the series with intuitive commands:
# Run by article number
cargo run 1.1
# Run by partial title match
cargo run pipeline
# Multiple matches? You'll get an interactive menu with arrow keys
cargo run shader
# No arguments? See all available demos and select with arrow keys
cargo run
As you progress through the series, you'll simply add new demos to the demos vector in main.rs, and they'll automatically be available through this selection system. No managing multiple example files or hunting through code!
Step 5: Create Your First Demo Module
Now let's create the first demo that we registered in our system. Create src/demos/d00_00_basic_scene.rs:
use bevy::prelude::*;
pub fn run() {
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
// Enable hot reloading
watch_for_changes_override: Some(true),
..default()
}))
.add_systems(Startup, setup)
.add_systems(Update, rotate_camera)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Add a simple scene we can apply our shaders to
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
));
// Light
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
// Simple camera rotation to see our shaders from different angles
fn rotate_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera3d>>) {
for mut transform in camera_query.iter_mut() {
let radius = 9.0;
let angle = time.elapsed_secs() * 0.5;
transform.translation.x = angle.cos() * radius;
transform.translation.z = angle.sin() * radius;
transform.look_at(Vec3::ZERO, Vec3::Y);
}
}
And register it in src/demos/mod.rs:
pub mod d00_00_basic_scene;
The pattern you'll follow for every article:
Create a new module in
src/demos/(e.g.,d01_01_first_shader.rs)Implement a
pub fn run()function that starts a Bevy appAdd the module to
src/demos/mod.rsRegister it in the
demosvector inmain.rs
That's it! The demo selection system handles the rest.
Step 5: Test Your Setup
You're all set! Let's make sure everything works:
# Run the basic scene demo directly
cargo run 0.0
# Or try the interactive selection
cargo run
You should see a simple scene with a rotating camera orbiting around a cube.
Try experimenting with the selection system:
cargo run basic # Matches "Basic Scene Setup"
cargo run scene # Same result
cargo run xyz # No match - shows all demos and lets you select
Congratulations! You now have a robust development environment with:
Fast iteration with hot-reloading for shader changes
An intuitive demo selection system for jumping between examples
A clean project structure that will scale as you learn
When you save changes to a shader file in the assets directory, Bevy will automatically detect the change and reload it in real-time. This instant feedback is the key to learning and experimenting with shaders effectively.
Understanding the Project Structure
Here's how everything fits together:
bevy-shader-journey/
โโโ assets/
โ โโโ shaders/ # Your .wgsl shader files
โ โโโ d01_01_first_shader.wgsl # (We'll create these as we go)
โโโ src/
โ โโโ main.rs # Demo selection system
โ โโโ demos/ # One module per article
โ โ โโโ mod.rs
โ โ โโโ d00_00_basic_scene.rs # Article 0.0
โ โ โโโ d01_01_first_shader.rs # (Article 1.1, etc.)
โ โโโ materials/ # Custom material definitions
โ โโโ mod.rs
โโโ Cargo.toml
The workflow:
Read an article to understand the concepts
Run the demo:
cargo run [article-number]Look at the code in
src/demos/dXX_XX_[name].rsExamine the shader in
assets/shaders/dXX_XX_[name].wgslExperiment - change values, break things, see what happens!
Move to the next article when ready
A Mindset for Success
You are now ready to begin. As you dive into the world of shaders, keeping a few practical tips in mind will make the learning process smoother and more enjoyable.
Start Simple, Then Iterate. The golden rule of shader development. Always begin with the absolute most basic version of an effect that works, even if it's just a solid color. Once you have a working baseline, add complexity one small step at a time. This makes debugging infinitely easier.
Embrace the Visual Debugging Loop. Your primary debugging tool is the screen itself. Is the effect too bright? Multiply by 0.5. Is it upside down? Multiply a coordinate by
-1.0. Get comfortable making small, incremental changes and immediately observing the visual result.Use Descriptive Variable Names. Shaders can quickly become a maze of vector math. A variable named
surfaceToLightDirectionis a thousand times clearer thanvecL. Your future self, trying to debug the code, will thank you.Save Your Progress Frequently. Shader development is highly experimental. You'll often go down a path that leads to a visual mess. Using a version control system like Git to commit frequently after each small success will give you the freedom to experiment without fear of losing your working code.
Jump Between Examples Freely. One of the beauties of our demo selection system is that you can easily revisit previous techniques. Forgot how normal mapping works? Just
cargo run normaland refresh your memory. Want to compare two approaches? Run them side by side!
Let's Get Started!
Your development environment is configured, your mindset is right, and the path is laid out before you. That blank .wgsl file is no longer an intimidating obstacle - it's a canvas waiting for your creativity.
The demo selection system gives you the freedom to explore at your own pace, jumping between concepts and building your understanding incrementally. Each article in this series adds a new demo to your toolkit, and by the end, you'll have a comprehensive library of shader techniques at your fingertips.
It's time to dive in and learn how to speak directly to the GPU.






