Skip to main content

Command Palette

Search for a command to run...

1.1 - Understanding the Graphics Pipeline

Updated
31 min read
1.1 - Understanding the Graphics Pipeline

What We're Learning

Welcome to the start of your shader programming journey! Before we can write a single line of WGSL code to create shimmering water or a glowing sword, we must first answer a fundamental question: what exactly is a shader?

At its core, a shader is a small, highly-focused program that you, the developer, write. Unlike the Rust code that runs on your computer's main processor (the CPU), a shader runs directly on the thousands of parallel cores of your Graphics Processing Unit (GPU). This gives you direct, low-level control over how your game's graphics are rendered. Shaders are the modern key to controlling the look, feel, and performance of everything you see in a real-time 3D application.

This first chapter pulls back the curtain on the rendering process. We will build a complete mental model of the graphics pipeline - the step-by-step assembly line a GPU uses to turn the raw data of your 3D models into the final, vibrant pixels on your screen.

Understanding this pipeline is the single most important foundation for shader programming. Without it, writing shader code is like trying to assemble a car without knowing what an engine or a wheel does. With it, every new concept will have a clear place to belong.

By the end of this chapter, you'll understand:

  • The Big Picture: The journey of your 3D model data from the CPU (your Bevy app) to the GPU (the rendering factory).

  • The "Why" of GPUs: Why we need a specialized processor for graphics and how its parallel design is perfect for rendering.

  • The Vertex's Journey: How a single point in your 3D model travels through a series of "coordinate spaces" - from its local origin to its final position on your screen.

  • The Two Programmable Stages: The specific jobs of the Vertex Shader (controlling shape and position) and the Fragment Shader (controlling color and appearance), and how your WGSL code fits into this process.

The Big Picture: CPU to Screen

Let's start with the fundamental question: How does a 3D model in your Bevy game become the final, colored pixels on your screen?

The process is a hand-off of instructions and data from a general-purpose processor (the CPU) to a highly specialized one (the GPU). The CPU decides what needs to be drawn, while the GPU figures out how to draw it at incredible speed. The GPU performs this task using a dedicated assembly line called the graphics pipeline.

Here is a simplified map of that journey:

┌───────────────────────────────────────────┐
│              CPU: The Director            │
│          (Your Bevy / Rust Code)          │
│                                           │
│  - Runs game logic, physics, AI, etc.     │
│  - Decides WHAT to draw this frame.       │
│  - Packages all necessary data for GPU.   │
└───────────────────────────────────────────┘
                       │
                       │ Sends a "Draw Command" with all required data
                       ↓
  ═════════════════════ GPU BOUNDARY ═════════════════════
                       ↓
┌───────────────────────────────────────────┐
│             GPU: The Factory              │
│       (Massively Parallel Hardware)       │
└───────────────────────────────────────────┘
                       │
                       ▼
┌───────────────────────────────────────────┐
│            1. VERTEX SHADER               │ «── [ YOUR WGSL CODE RUNS HERE ]
├───────────────────────────────────────────┤
│ INPUT:  A single vertex from a mesh       │
│         (e.g., its local position, UVs).  │
│                                           │
│ JOB:    Calculate the vertex's final      │
│         position on the screen.           │
│                                           │
│ OUTPUT: The vertex's position in          │
│         "Clip Space" & other data         │
│         (like normals) for the next stage.│
└───────────────────────────────────────────┘
                       │
                       │ (GPU groups 3 processed vertices into a triangle)
                       ↓
┌───────────────────────────────────────────┐
│            2. RASTERIZATION               │ «── [ AUTOMATIC HARDWARE STAGE ]
├───────────────────────────────────────────┤
│ INPUT:  A triangle in screen space.       │
│                                           │
│ JOB:    Determine which pixels the        │
│         triangle covers.                  │
│                                           │
│ OUTPUT: A stream of "Fragments."          │
│         (A fragment is a potential pixel  │
│         with smoothly interpolated data). │
└───────────────────────────────────────────┘
                       │
                       ↓
┌───────────────────────────────────────────┐
│           3. FRAGMENT SHADER              │ «── [ YOUR WGSL CODE RUNS HERE ]
├───────────────────────────────────────────┤
│ INPUT:  A single fragment with its        │
│         interpolated data (e.g., UVs).    │
│                                           │
│ JOB:    Calculate the final color for     │
│         that specific fragment.           │
│                                           │
│ OUTPUT: A single RGBA color.              │
└───────────────────────────────────────────┘
                       │
                       ↓
┌───────────────────────────────────────────┐
│           4. OUTPUT MERGER                │ «── [ AUTOMATIC HARDWARE STAGE ]
├───────────────────────────────────────────┤
│ JOB:    Take the colored fragment and     │
│         merge it into the final image.    │
│                                           │
│       - Performs Depth Test (is this      │
│         fragment behind something else?). │
│       - Performs Blending (for            │
│         transparency effects).            │
│       - Writes the final color.           │
└───────────────────────────────────────────┘
                       │
                       ▼
            ▓▓▓ PIXELS ON SCREEN ▓▓▓

This process spans two distinct worlds, each with a specific job.

The CPU Side (Bevy/Rust): The Director

Think of your CPU and your Bevy code as the director of a film. It's smart, flexible, and responsible for all the high-level decision-making. For every single frame (60 times a second!), it runs your game logic, updates physics, handles input, and determines what needs to be on screen and where it should be.

Its final job for rendering is to prepare a detailed set of instructions and data for the GPU. This "frame package" includes:

  • Mesh Data: The raw vertex information (positions, normals, UV coordinates) for each model.

  • Material Data: Your shader's settings, like colors, roughness values, and which textures to use.

  • Transformation Data: The matrices that define each object's position, rotation, and scale in the world.

  • Global Data: Information about the entire scene, like the camera's position (View Matrix), its lens settings (Projection Matrix), and the location of lights.

Once this package is assembled, the CPU sends it across the bus to the GPU and effectively says, "Here, render this." The CPU's main rendering job for this frame is now complete.

The GPU Side (Shaders): The VFX Studio

Think of the GPU as a state-of-the-art VFX studio with an army of digital artists, each with a very specific job. This studio is built for one purpose: turning the director's brief into a final, beautifully rendered image with incredible speed. Their process is the graphics pipeline.

When the brief arrives at the studio:

  1. The Geometry & Layout Artists (Vertex Shader): The first team gets the brief. This team consists of thousands of artists. Each artist is assigned a single corner (a vertex) of an actor or prop. Their only job is to calculate exactly where that specific corner will appear in the final camera shot, based on the actor's position and the camera's lens. They don't color anything; they just map out the structure of the scene from the camera's perspective.

  2. The Rendering Artists (Fragment Shader): After the layout is done, a second, even larger army of artists takes over. There are millions of them - one for every pixel of the final image. Each artist is assigned a single pixel to paint (a fragment). They look at the director's notes for that surface (the material, the textures) and the lighting setup. Then, they calculate and apply the final, precise color for their one tiny pixel.

This analogy directly maps to the GPU's strengths:

  • Specialization: The geometry artists only do positioning; the rendering artists only do coloring.

  • Parallelism: Millions of rendering artists can paint their individual pixels all at the exact same time, without needing to talk to each other. This is what makes the GPU so fast.

Your WGSL shader code is the set of instructions - the "artistic direction" - you give to these two teams of digital artists.

Why the GPU? Understanding Parallelism

It's a fair question: your computer's Central Processing Unit (CPU) is an incredibly powerful and fast processor. Why can't it just draw the triangles? Why do we need a separate, specialized piece of hardware like a Graphics Processing Unit (GPU)?

The answer lies not in raw clock speed, but in a fundamentally different architectural philosophy: Serial vs. Parallel processing.

The CPU: A Master Chef

Think of your CPU as a master chef in a world-class kitchen.

  • It is brilliant and versatile. It can follow any recipe (run any program), handle complex sequential steps, improvise when needed (handle interrupts and varied tasks), and manage the entire kitchen (the operating system).

  • It has a few, very powerful cores. Like having 8 or 16 highly trained sous-chefs, it can work on a handful of complex, different dishes at once.

If you asked this master chef to prepare a single, elaborate seven-course meal, they would excel. But if you asked them to make ten million identical hamburgers, the entire restaurant would grind to a halt. The chef's genius is wasted on such a simple, repetitive task; its strength is in complexity and flexibility, not mass production.

The GPU: A Hyper-Efficient Assembly Line

Now, think of your GPU as a massive hamburger assembly line that stretches for miles.

  • It is highly specialized. It's not designed to create new recipes. It's designed to execute one simple recipe over and over again with breathtaking speed.

  • It has thousands of simple cores. Instead of a few master chefs, you have thousands of line cooks. Each cook is trained for just one or two simple tasks - place the patty, add the cheese - but they all work at the exact same time.

This factory can't prepare a seven-course meal, but it can produce those ten million identical hamburgers in the blink of an eye. This is what we call "pleasingly parallel" work.

Graphics is a "Pleasingly Parallel" Problem

Rendering a 3D scene is the ultimate assembly-line task. The core operations are simple, repetitive, and most importantly, independent.

  • The calculation for vertex A's final position does not depend on vertex B's position.

  • The calculation for pixel #1's color does not depend on pixel #2's color.

They can all be processed simultaneously.

Let's put this into perspective. When rendering a moderately complex scene with 100,000 triangles on a standard 1080p display, for a single frame, you are asking the hardware to perform approximately:

  • 300,000 vertex shader executions (one for each vertex)

  • Millions of fragment shader executions (one for each pixel covered by a triangle)

And this has to happen 60 times every second for smooth gameplay.

This is the "ten million hamburgers" problem. A CPU, with its few brilliant cores, would be overwhelmed trying to handle these tasks one by one. But a GPU, with its thousands of simple cores, can process huge batches of vertices and pixels all at once.

This is why we write shaders for the GPU. We are providing the "recipe" for the assembly line workers. The GPU's architecture is not just "more cores"; it's a completely different philosophy of computation, one that is perfectly and beautifully matched to the massive, repetitive, and parallel nature of computer graphics.

The Rendering Pipeline in Detail

Let's zoom in on the VFX studio's assembly line. Each stage has a specific responsibility, taking a particular kind of data as input and producing a new kind of data for the next stage to work on.

Stage 1: The Application Stage (CPU - The Director's Brief)

This stage isn't on the GPU at all - it's your Bevy application running on the CPU. Think of it as the "pre-production" step where the director prepares the detailed brief. Before any rendering can happen, your application needs to tell the GPU everything it needs to know about the world for the upcoming frame. Bevy's renderer orchestrates this for you.

For every frame, Bevy traverses your scene's Entity-Component-System (ECS) world and gathers all the necessary information:

// You write this in Bevy, describing the "what" and "where"
commands.spawn((
 Mesh3d(meshes.add(Sphere::new(1.0))),
 MeshMaterial3d(materials.add(my_custom_material)),
));

From code like this, Bevy assembles the "shot list":

  • What to draw: A list of meshes (the sphere's vertices and triangles).

  • How to draw it: The material to use and its properties (colors, textures).

  • Where it is: The object's world position, rotation, and scale (the Transform).

  • From where to view it: The camera's position and perspective settings.

The final output of this stage is a highly-organized package of data and commands, which Bevy then sends over to the GPU to begin the actual rendering process.

Stage 2: The Vertex Shader (GPU - The Layout Artists)

This is the first programmable stage on the GPU, where your first piece of WGSL code runs. The vertex shader's fundamental job is to answer one question for every single vertex of a mesh: "Where on the screen does this vertex end up?"

  • Input: It receives the data for a single vertex at a time (its position in local model space, its normal vector, its UV coordinates, etc.).

  • The Job: Its one mandatory task is to perform mathematical operations (usually matrix multiplications) to transform the vertex's 3D position into a final 4D "clip space" position. This clip space coordinate is what the GPU hardware needs to figure out the 2D location on your monitor.

  • Output: It must output that final clip space position. It can also pass along any other data it received or calculated (like colors, normals, or UVs) to be used later by the fragment shader.

Here's a conceptual view of what your WGSL code will do:

@vertex
fn vertex(input: VertexInput) -> VertexOutput {
    var output: VertexOutput;

    // The primary job: Transform the 3D local position into a final 2D screen position.
    output.position = project_to_screen(view_matrix * model_matrix * input.position);

    // A secondary job: Pass necessary data to the next stage.
    // Here, we're just passing the vertex's normal along for lighting calculations later.
    output.normal = input.normal;
    return output;
}

Key Insight: Because this shader runs on every vertex, it gives you the power to manipulate the shape and position of your geometry in real-time. This is not just about moving objects around (which is best done by changing the Transform in Bevy); it's about deforming the mesh itself. This is how you create dynamic effects like:

  • Waving flags

  • Rippling water surfaces

  • Procedurally animated grass swaying in the wind

Stage 3: The Rasterizer (GPU - Automatic Hardware)

This stage is a piece of dedicated, non-programmable hardware on the GPU. It's an automatic, incredibly fast process that you don't write code for. The rasterizer takes the processed vertices from the vertex shader (three at a time to form a triangle) and figures out which pixels on the screen that triangle covers.

For every single pixel it covers, it generates a "fragment." A fragment is a "potential pixel" - it contains all the information needed to calculate a final color.

This is also where the magic of interpolation happens. The rasterizer looks at the data you passed out of the vertex shader for each of the triangle's three vertices and smoothly blends it across the surface of the triangle for each fragment.

Think of it like this:

  • A fragment near the top will receive a reddish color.

  • A fragment on the left edge will get a purplish color (red + blue).

  • A fragment right in the middle will get a muddy, grayish color, which is the mathematical average of red, green, and blue.

The rasterizer does this for every single piece of data you passed along - colors, UV coordinates, normals, etc. The output is a massive stream of fragments, each one "pre-loaded" with its own unique, interpolated data, ready to be colored.

Stage 4: The Fragment Shader (GPU - The Rendering Artists)

This is the second programmable stage, and it's where most of the visual artistry happens. The fragment shader's job is to answer one simple question for every single fragment generated by the rasterizer: "What color is this pixel?"

  • Input: It receives the interpolated data for a single fragment. This includes its position on the screen and the smoothly blended values (like normals and UVs) that were passed from the vertex shader.

  • The Job: To use this input data to perform calculations and return a single, final color.

  • Output: It must output a vec4<f33> representing an RGBA color. This is the color that will be written to the screen (assuming it passes final tests like depth testing).

Here's a conceptual view of what your WGSL code will do:

@fragment
fn fragment(input: FragmentInput) -> @location(0) vec4<f32> {
    // The input data (e.g., input.normal) has already been smoothly interpolated for us.
    // We can use it to calculate lighting for this specific pixel.
    let lighting = calculate_light(input.normal, light_direction);

    // We could also sample a texture using the interpolated UVs.
    let surface_color = sample_texture(input.uv);

    // Combine them to get the final color.
    let final_color = surface_color * lighting;
    return vec4<f32>(final_color, 1.0); // Return the final RGBA value
}

Key Insight: The fragment shader runs for potentially millions of pixels every frame. This is your chance to define the appearance of your object's surface with incredible detail. This is where you:

  • Apply lighting calculations to create highlights and shadows.

  • Sample textures to give a surface detail and color.

  • Create procedural patterns like stripes, checkerboards, or noise.

  • Implement special effects like glowing, outlines, or distortion.

The Coordinate Space Journey

One of the most initially confusing, but ultimately powerful, concepts in graphics programming is the journey a vertex takes through different coordinate spaces. Each space is a different frame of reference, like describing a location from a different point of view. The vertex shader's main job is to transform a vertex from one space to the next, like a series of conversions, until it lands in the right spot on the screen.

Think of it like giving directions to a friend:

  1. "The book is on the second shelf of the bookcase." (Local Space: The position is relative to the bookcase).

  2. "The bookcase is against the north wall of the living room." (World Space: Now the bookcase's position is relative to the entire house).

  3. "Stand in the doorway and look towards the fireplace." (View Space: Now everything is described from your friend's point of view).

  4. "The book you're looking for should be in the upper-left part of your vision." (Clip Space: This is what's in their field of view).

In 3D graphics, we perform these transformations using matrix mathematics.

Note: Don't worry if the matrix math looks intimidating right now. We have a dedicated chapter later that explains how matrices work. For now, just focus on the purpose of each transformation - what question it answers.

1. Local Space (or Model Space)

The Question: "What does this object look like by itself?"

This is the object's blueprint. When an artist creates a 3D model of a car, they don't care where it will be in your game world. They model it at the center of its own universe, the origin (0, 0, 0). The coordinates of every vertex are relative only to the car's own center point.

A car's front-right tire vertex in Local Space might be:
(0.8, 0.3, 1.5)

2. World Space

The Question: "Where is this object in the game world?"

This is the shared, global coordinate system of your entire scene. It's the common frame of reference where all your objects, lights, and the camera coexist. When you give an entity a Transform in Bevy, you are defining its position, rotation, and scale within this World Space.

// This Transform moves the object from its local origin to a specific spot in the world.
Transform::from_xyz(10.0, 0.0, -20.0)

The Model Matrix, which Bevy derives from this Transform, is the mathematical tool that converts vertices from Local Space to World Space. Your vertex shader applies it to every vertex.

Local Position × Model Matrix = World Position

3. View Space (or Camera Space)

The Question: "How does the world look from the camera's perspective?"

Once everything is placed in the world, we need to view it. To make the math simpler for the next step, we transform the entire world so that the camera is at the origin (0, 0, 0) and looking down a specific axis (typically negative Z). Everything in the world is now positioned relative to the camera. An object in front of the camera will have a negative Z coordinate, an object to the camera's left will have a negative X, and so on.

The View Matrix performs this transformation. It's calculated from the camera's own world-space transform.

World Position × View Matrix = View Position

4. Clip Space

The Question: "Is this vertex inside the viewable area, and if so, where?"

This is the final and most abstract space that the vertex shader is responsible for creating. It's a standardized, cube-like volume that represents everything the camera can see. The transformation into this space, performed by the Projection Matrix, does two magical things:

  1. It applies perspective. It mathematically squishes the 3D scene so that objects farther away from the camera appear smaller than objects that are closer. This is what creates the illusion of depth.

  2. It normalizes the coordinates. Everything that will be visible on screen is mapped into a neat box where the X and Y coordinates range from -1 to 1. The Z coordinate is also remapped (usually to a [0, 1] range) to represent depth.

The GPU now has a simple job: any vertex with X or Y coordinates outside of this [-1, 1] range is "clipped" and discarded, as it is off-screen. The Z coordinate will be used in a later stage for depth testing (figuring out if one object is in front of another).

View Position × Projection Matrix = Clip Position

The mandatory output of your vertex shader is this final Clip Space position.

5. Screen Space

The Question: "Which specific pixel on my monitor does this correspond to?"

This final step is handled automatically by the GPU after the vertex shader is done. The hardware takes the [-1, 1] Clip Space coordinates and maps them to the actual pixel coordinates of your window (e.g., from (0, 0) in the top-left to (1920, 1080) in the bottom-right). This is not something you calculate in your shaders; it's the final output of the fixed-function part of the pipeline before the fragment shader runs.

Putting It All Together: The Life of a Single Vertex

Let's trace the complete life of a single vertex, from its creation in a modeling tool to its final appearance as a colored pixel on your screen. We'll follow a vertex at the very top of a sphere model.

1. The Blueprint (CPU - Local Space)

It begins its life in a 3D modeling program. An artist defines a sphere, and our vertex is created at the very top. Relative to the sphere's own center, its position is simply (0.0, 1.0, 0.0). This is its Local Space position. This data is loaded into Bevy as part of a Mesh asset.

2. Setting the Scene (CPU - The Director's Brief)

Your Bevy application decides where this sphere belongs in the game world. You assign it a Transform to place it, for example, at world coordinates (5.0, 2.0, -3.0). The CPU doesn't move the vertex itself; instead, it calculates a Model Matrix from this transform and packages it up, ready to be sent to the GPU's VFX studio.

3. The Great Transformation (GPU - Vertex Shader)

Now the director's brief is uploaded to the GPU, and our programmable Vertex Shader takes over. This is where the "Layout Artists" get to work. The shader receives our vertex's original local position, (0, 1, 0), along with the Model, View, and Projection matrices. It then performs the crucial sequence of multiplications to find the vertex's final on-screen position:

  • To World Space: The shader multiplies the local position by the Model Matrix. This moves the vertex into the shared World Space. Our vertex at (0, 1, 0) is now effectively at (5.0, 3.0, -3.0) in the game world (position + model's height).

  • To View Space: Next, it multiplies the new world position by the View Matrix. This transforms the vertex into View Space, making its coordinates relative to the camera's perspective. Its position might now be something like (-2.0, 1.0, -5.0), meaning it's slightly to the camera's left, above its center, and some distance in front of it.

  • To Clip Space: Finally, it multiplies the view position by the Projection Matrix. This applies perspective and transforms the vertex into the final, required Clip Space. The position might now be (-0.4, 0.2, 0.8). This tells the GPU the vertex is on-screen (since X and Y are between -1 and 1) and provides its depth. This vec4 is the mandatory output of the vertex shader.

Simultaneously, the vertex shader also prepares any other data needed for coloring, like the vertex's color or normal vector, and passes it along.

4. The Triangle Factory (GPU - Rasterizer)

The vertex, now just a point in Clip Space, is grouped by the GPU with two other processed vertices to form a triangle. The hardware Rasterizer takes over, calculating exactly which screen pixels this triangle covers. For each covered pixel, it generates a "fragment" and interpolates all the data that the vertex shaders passed out (like colors or UVs), creating a smooth gradient of values across the triangle's face.

5. The Coloring Book (GPU - Fragment Shader)

A single fragment, born from the rasterizer, arrives at our programmable Fragment Shader. Now the "Rendering Artists" do their job. The fragment carries its own unique, interpolated data (e.g., a color that is a blend of the three corner vertices' colors). The fragment shader's sole job is to use this information to calculate a final RGBA color. It might sample a texture, calculate lighting, or perform any number of other operations.

6. The Final Pixel (GPU - Output Merger)

The fragment shader outputs its calculated color. This color, along with the fragment's depth, is sent to the final hardware stage. The GPU performs a depth test to see if this fragment is in front of whatever is already on the screen at that pixel. If it is, its color is written to the framebuffer, becoming one of the millions of pixels that form the final image you see. Our vertex has completed its journey.

A Mental Model for Your Shaders

The rendering pipeline can seem complex, but by holding on to our "Director and VFX Studio" analogy, we can assign a clear, relatable job to each programmable stage.

1. The Vertex Shader: The VFX Layout Artist

Your WGSL code in the vertex shader provides the instructions for the layout artists at the VFX studio.

AnalogyRoleShader Code Focus
Staging the scene for the camera.Your job is to take the 3D models (defined in their own local space) and place them correctly in the world and frame them perfectly in the camera's shot.Transformation. You use matrix math to move each vertex of a model from its origin to its final position relative to the camera's view. You can also dynamically move vertices here to create animations like waving flags or rippling water.
Defining the final composition.You must output the final projected position of each vertex. This is the main deliverable for this stage.output.position = projection * view * model * local_pos;
Prepping for the colorists.You pass along any surface information that the next team will need, like texture coordinates (uv) or which way the surface is facing (normal).output.world_normal = ..., output.uv = ...

2. The Rasterizer: The Digital Render Farm

This is an automated, non-programmable hardware stage. You don't write code for it, but you need to know what it does.

AnalogyRoleShader Code Focus
Automated rendering setup.The GPU's hardware takes the 3D triangles you've positioned in the previous step...None. This is a fixed, non-programmable step.
...and projects them onto a 2D grid.The hardware determines exactly which pixels on the screen are covered by each triangle. For each covered pixel, it creates a "fragment" and interpolates (smoothly blends) the data from the triangle's corners.A fragment in the middle of a triangle gets a perfectly blended uv coordinate and normal vector, ready for the next stage.

3. The Fragment Shader: The VFX Coloring & Lighting Artist

Your WGSL code in the fragment shader provides the instructions for the massive team of coloring and lighting artists.

AnalogyRoleShader Code Focus
Painting one pixel at a time.Your job is to look at a single, uncolored pixel (a fragment) and decide what its final color should be. This is where all the visual artistry happens.Color Calculation. You have complete control over the pixel's final RGBA value.
Using the prepped materials.You use the interpolated data from the previous stage to perform your work.return vec4<f32>(final_color, alpha);
Applying textures and lighting.You can sample textures using the interpolated uv coordinates and use the interpolated normal vector to calculate realistic lighting and shadows.textureSample(...), dot(normal, light_dir)

Complete Example: Visualizing the Pipeline

Theory is essential, but seeing is believing. To make these abstract concepts concrete, we'll build a simple, interactive shader in Bevy. This shader won't create a realistic object; instead, it will act as a diagnostic tool, allowing us to "see" the data at different stages of the pipeline.

A Note Before We Begin: You will see new WGSL syntax (@location, @group, etc.) and Bevy patterns (AsBindGroup, MaterialPlugin) in the code below. Do not worry about understanding every line right now. We will break down all of these concepts in detail in the upcoming articles.

The goal of this example is to observe the visual output of each mode and connect it back to the high-level pipeline concepts we just learned:

  • How data is interpolated across a surface (the smooth normal colors).

  • How the fragment shader runs for every pixel (the sharp checkerboard).

  • How data from the vertex stage is used in the fragment stage (the height gradient).

Focus on the "what you're seeing" part, and treat the code as a preview of what you'll soon master.

Our Goal

We will create a custom material for a sphere that can cycle through three different visualization modes by pressing a key. Each mode will highlight a different core concept of the rendering pipeline that we've just discussed.

What This Project Demonstrates

  • Data Flow and Interpolation: How data (like normal vectors) passed from the vertex shader is smoothly interpolated by the rasterizer before reaching the fragment shader.

  • Per-Fragment Processing: Proof that the fragment shader runs independently for every single pixel, allowing it to create complex patterns based on a fragment's world position.

  • Vertex Data in Fragment Shaders: How to use data prepared by the vertex shader (like the final world position) to drive calculations in the fragment shader.

The Shader (assets/shaders/d01_01_debug_pipeline.wgsl)

This single WGSL file contains both our vertex and fragment shaders. The key element is the material.mode uniform, a number we can change from our Rust code to switch the logic inside the fragment shader.

  • The vertex shader is standard: it transforms the vertex position into clip space and also passes the vertex's world position and world normal along to the fragment stage.

  • The fragment shader uses an if/else if chain based on material.mode to decide how to color the current pixel.

#import bevy_pbr::mesh_functions
#import bevy_pbr::view_transformations::position_world_to_clip
#import bevy_pbr::forward_io::VertexOutput

struct DebugMaterial {
    mode: u32,
}

@group(2) @binding(0)
var<uniform> material: DebugMaterial;

@vertex
fn vertex(
    @builtin(instance_index) instance_index: u32,
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
) -> VertexOutput {
    var out: VertexOutput;

    // Get the model transformation matrix
    let world_from_local = mesh_functions::get_world_from_local(instance_index);

    // Transform position to world space
    let world_position = mesh_functions::mesh_position_local_to_world(
        world_from_local,
        vec4<f32>(position, 1.0)
    );

    // Transform to clip space (final vertex shader output)
    out.position = position_world_to_clip(world_position.xyz);

    // Transform normal to world space
    out.world_normal = mesh_functions::mesh_normal_local_to_world(
        normal,
        instance_index
    );

    // Pass world position to fragment shader
    out.world_position = world_position;

    return out;
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // Mode 0: Show normals (demonstrates interpolation between vertices)
    if material.mode == 0u {
        // Normals range from -1 to 1, convert to 0-1 for RGB color
        // Red = X direction, Green = Y direction, Blue = Z direction
        let color = (in.world_normal + 1.0) * 0.5;
        return vec4<f32>(color, 1.0);
    }

    // Mode 1: Checkerboard pattern (demonstrates per-pixel fragment shader work)
    if material.mode == 1u {
        // Create a 3D checkerboard pattern
        let scale = 3.0;
        let x = i32(floor(in.world_position.x * scale));
        let y = i32(floor(in.world_position.y * scale));
        let z = i32(floor(in.world_position.z * scale));

        // Use bitwise AND to alternate between 0 and 1
        let checker = (x + y + z) & 1;

        if checker == 0 {
            return vec4<f32>(0.9, 0.9, 0.9, 1.0); // Light gray
        } else {
            return vec4<f32>(0.2, 0.2, 0.8, 1.0); // Blue
        }
    }

    // Mode 2: Height-based gradient (demonstrates math in fragment shader)
    if material.mode == 2u {
        // Color based on Y position (height) in world space
        // Map from -2 to 2 range to 0-1 range for color
        let height = (in.world_position.y + 2.0) / 4.0;
        let color = vec3<f32>(height, 0.5, 1.0 - height);
        return vec4<f32>(color, 1.0);
    }

    // Default: Solid color
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

The Rust Material (src/materials/d01_01_debug_pipeline.rs)

This is the Rust-side definition of our material. It's a simple struct that mirrors the DebugMaterial struct in our shader, allowing Bevy's rendering engine to send our chosen mode value to the GPU.

use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct DebugPipelineMaterial {
    #[uniform(0)]
    pub mode: u32,
}

impl Material for DebugPipelineMaterial {
    fn fragment_shader() -> ShaderRef {
        "shaders/d01_01_debug_pipeline.wgsl".into()
    }

    fn vertex_shader() -> ShaderRef {
        "shaders/d01_01_debug_pipeline.wgsl".into()
    }
}

Don't forget to add it to src/materials/mod.rs:

// ... other materials
pub mod d01_01_debug_pipeline;

The Demo Module (src/demos/d01_01_debug_pipeline.rs)

This Rust module sets up our Bevy scene. It spawns a sphere with our custom DebugPipelineMaterial, adds a camera and light, and sets up the UI text. Most importantly, it contains the cycle_debug_mode system, which listens for the spacebar press and updates the mode field on our material, triggering the change in the shader.

use crate::materials::d01_01_debug_pipeline::DebugPipelineMaterial;
use bevy::prelude::*;

pub fn run() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MaterialPlugin::<DebugPipelineMaterial>::default())
        .add_systems(Startup, setup)
        .add_systems(Update, (rotate_camera, cycle_debug_mode))
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<DebugPipelineMaterial>>,
) {
    // Spawn a sphere with our debug material (better for showing interpolation)
    commands.spawn((
        Mesh3d(meshes.add(Sphere::new(1.0))),
        MeshMaterial3d(materials.add(DebugPipelineMaterial { mode: 0 })),
    ));

    // 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),
    ));

    // UI to show current mode
    commands.spawn((
        Text::new("Press SPACE to cycle debug modes\nMode 0: Normals (shows interpolation)"),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(10.0),
            left: Val::Px(10.0),
            ..default()
        },
    ));
}

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);
    }
}

fn cycle_debug_mode(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut materials: ResMut<Assets<DebugPipelineMaterial>>,
    mut text_query: Query<&mut Text>,
) {
    if keyboard.just_pressed(KeyCode::Space) {
        for (_, material) in materials.iter_mut() {
            material.mode = (material.mode + 1) % 3;

            for mut text in text_query.iter_mut() {
                **text = match material.mode {
                    0 => "Press SPACE to cycle debug modes\nMode 0: Normals (shows interpolation)".to_string(),
                    1 => "Press SPACE to cycle debug modes\nMode 1: Checkerboard (per-pixel processing)".to_string(),
                    2 => "Press SPACE to cycle debug modes\nMode 2: Height Gradient (fragment math)".to_string(),
                    _ => "Unknown mode".to_string(),
                };
            }
        }
    }
}

Don't forget to add it to src/demos/mod.rs:

// ... other demoss
pub mod d01_01_debug_pipeline;

And register it in src/main.rs:

Demo {
    number: "1.1",
    title: "Understanding the Graphics Pipeline",
    run: demos::d01_01_debug_pipeline::run,
},

Running the Demo

When you run the project, you will see a sphere. Pressing the spacebar will cycle through the three different debug visualizations, each revealing a different aspect of the pipeline's inner workings.

Controls

ControlAction
SPACECycle to the next visualization mode (0 -> 1 -> 2 -> 0).

What You're Seeing

ModeDescriptionWhat It Proves
0 - NormalsThe sphere is colored based on the direction its surface is facing. Red points right (+X), Green points up (+Y), and Blue points forward (+Z). You see smooth gradients of color across the entire surface.This demonstrates interpolation. The normals are defined only at the vertices, but the rasterizer smoothly blends them between those points, giving every single pixel its own unique normal vector to use for coloring.
1 - CheckerboardThe sphere is covered in a 3D checkerboard pattern that appears fixed in the world as the sphere rotates through it. The pattern is sharp and blocky.This demonstrates per-fragment processing. The fragment shader calculates which color to be (light gray or blue) for every single pixel independently, based on that pixel's position in world space.
2 - Height GradientThe sphere is colored with a gradient based on its height in the world. The bottom is magenta, and the top is cyan.This demonstrates using vertex shader data in the fragment shader. The vertex shader calculates the world_position for each vertex. The rasterizer interpolates it, and the fragment shader uses the Y-component of that position to calculate a color.

Key Takeaways

This chapter covered a lot of ground. Before moving on, take a moment to solidify these five core concepts. They are the foundation for everything that follows.

  1. The Pipeline is a CPU-to-GPU Process.
    Rendering is a collaboration. Your Bevy code on the CPU acts as the Director, preparing the scene's data (meshes, materials, transforms). It then hands this "shot list" to the GPU, a specialized VFX Studio that executes the rendering process through a hardware assembly line.

  2. Shaders are Your Instructions for the VFX Artists.
    You cannot change the hardware pipeline itself, but you can write small, highly focused programs called shaders that run at critical, programmable stages. The GPU's massively parallel architecture executes your shader code for millions of vertices and pixels per second, which is what makes real-time 3D graphics possible.

  3. The Vertex Shader's Job is to POSITION Geometry.
    This is your "Layout Artist" stage. The vertex shader runs once for every vertex in your mesh. Its primary responsibility is to take that vertex's 3D position from the original model and transform it through a series of coordinate spaces until it has its final, correct position on the 2D screen.

  4. The Fragment Shader's Job is to COLOR Pixels.
    This is your "Coloring & Lighting Artist" stage. After the hardware rasterizer determines which pixels a triangle covers, the fragment shader runs once for every single one of those "fragments." Its sole responsibility is to calculate and return the final RGBA color for that specific spot on the screen. This is where you apply textures, lighting, and visual effects.

  5. Data Flows and is Interpolated from Vertex to Fragment.
    The two shaders are connected. The vertex shader can pass data (like UV coordinates or normal vectors) to the next stage. The hardware Rasterizer automatically interpolates (smoothly blends) this data across the face of the triangle, making a unique version of it available to the fragment shader for every single pixel.

What's Next?

You now have the essential mental model for the rendering pipeline - you understand where your shader code runs and the specific job of each stage. With this "map" in hand, we are finally ready to learn the language of the GPU's artists themselves.

In the next article, we will dive into the fundamental building blocks of the WGSL language: its data types and variables. You'll learn how to represent positions, colors, and transformations in your code using scalars, vectors, and matrices.

Next up: 1.2 - WGSL Fundamentals - Data Types & Variables


Quick Reference

A summary of the core concepts for quick lookup.

The Pipeline Stages & Their Jobs

StageAnalogyRole (What it does)Programmable?
1. Application (CPU)The DirectorPrepares all scene data: meshes, materials, transforms, camera info.Yes (Rust)
2. Vertex ShaderThe Layout ArtistRuns per-vertex to calculate its final on-screen position (in Clip Space).Yes (WGSL)
3. RasterizerThe Render FarmAutomatic hardware step that turns 3D triangles into a 2D grid of fragments.No
4. Fragment ShaderThe Coloring ArtistRuns per-fragment to calculate its final RGBA color.Yes (WGSL)
5. Output MergerThe Final PrintAutomatic hardware step that performs depth tests and writes the final color.No

The Coordinate Space Journey

This is the required path a single vertex position takes through the Vertex Shader.

Local Space → World Space → View Space → Clip Space

(The GPU then automatically handles the final conversion to Screen Space)

Core Concepts

  • Parallelism: The GPU's core strength. It processes thousands of vertices and millions of fragments simultaneously. Your shader code is the instruction set for this parallel work.

  • Interpolation: The process by which the Rasterizer automatically and smoothly blends data (like colors, UVs, or normals) that was output by the Vertex Shader at the corners of a triangle. This provides a unique value for that data to the Fragment Shader for every pixel the triangle covers.