Skip to main content

Command Palette

Search for a command to run...

1.2 - WGSL Fundamentals - Data Types & Variables

Updated
25 min read
1.2 - WGSL Fundamentals - Data Types & Variables

What We're Learning

In our last article, we built a mental model of the graphics pipeline: a VFX studio where "Layout Artists" (Vertex Shaders) position geometry and "Coloring Artists" (Fragment Shaders) paint the final pixels. But what materials do these artists work with? How do we hand them a 3D coordinate to transform, a surface direction for lighting, or a final color to paint? How do we describe the data that flows through their assembly line?

This is where WGSL's data types come in. These aren't generic programming types; they are the fundamental vocabulary of the GPU - the digital clay, paint, and transformation tools our artists use to build a visual world. Learning them is like learning the alphabet before you can write a story.

At the lowest level, we have scalar types - the individual numbers that form the atoms of our data. But the real power comes from combining these into vectors, the workhorse of every shader, representing everything from a vec3<f32> position to a vec4<f32> RGBA color. And to manipulate these vectors - to move, rotate, and project them - we use matrices, the mathematical machines of transformation.

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

  • The atomic building blocks of all shader data: f32, i32, u32, and bool.

  • The workhorse of all shaders: how vectors (vec2, vec3, vec4) represent everything from 3D positions to RGBA colors.

  • The language of transformation: how matrices (mat4x4) are the machines that move, rotate, and scale your geometry.

  • The crucial difference between an immutable let (a constant) and a mutable var (a variable), and why you should almost always prefer let.

  • Powerful GPU-native shortcuts like constructors for building vectors and swizzling for efficiently accessing and rearranging their components.

Scalar Types: The Basics

At the heart of all shader calculations are scalar types. A scalar is simply a single value, like one number or a boolean state. WGSL provides four fundamental scalar types that serve as the atoms for all the more complex data we will build.

Floating-Point Numbers: f32

This is the single most important type in WGSL and your absolute workhorse for graphics programming. It represents a 32-bit floating-point number, which is essential for describing any data that is continuous and requires precision.

let pi: f32 = 3.14159;
let half: f32 = 0.5;
let negative_one_point_five: f32 = -1.5;

Key Rule: WGSL is very strict about types. A number literal without a decimal point is treated as an integer. To create an f32, you must include a decimal point, even for whole numbers.

let correct_float: f32 = 1.0; // ✓ Correct, this is a float.
let wrong: f32 = 1;           // ✗ COMPILE ERROR: Cannot assign an integer to an f32.

You will use f32 for almost everything, including:

  • Positions (X, Y, Z coordinates)

  • Colors (R, G, B channels)

  • Texture Coordinates (U, V)

  • Time, opacity, and any fractional value

Signed Integers: i32

This is a standard 32-bit signed integer, meaning it can represent both positive and negative whole numbers.

let count: i32 = 42;
let negative_ten: i32 = -10;
let zero: i32 = 0;

Use i32 when you need whole numbers and the possibility of a negative value is meaningful, such as for:

  • Loop counters that might count down

  • Calculations involving differences or offsets

  • Bitwise operations

Unsigned Integers: u32

This is a 32-bit unsigned integer, meaning it can only represent non-negative whole numbers (zero and positive values).

let index: u32 = 0u;
let size: u32 = 256u;

Key Rule: To distinguish an unsigned integer literal from a signed one, you must add a u suffix.

Use u32 when a value can never logically be negative. This is both a safety feature and a way to signal intent. Common uses include:

  • Array indices and lengths

  • Bevy's @builtin(instance_index)

  • Identifiers or flags packed into an integer

Booleans: bool

The bool type represents a simple true or false value.

let is_visible: bool = true;
let has_texture: bool = false;

Booleans are the foundation of all logic and control flow in your shaders. You'll use them for:

  • if/else statements and other conditional logic

  • Storing the result of a comparison (e.g., let is_close = distance < 1.0;)

  • Flags passed in from your Rust code to enable or disable shader features

Vector Types: Your New Best Friends

If scalars are the atoms of our data, vectors are the molecules. They are the heart and soul of shader programming. A vector is an ordered collection of scalars, but in graphics, it's a powerful tool for representing multi-dimensional data like positions, colors, and directions.

The GPU is a parallel processing beast. It is specifically designed to perform the same mathematical operation on large batches of data all at once. Vectors are the native language of this architecture. When you add two vec3 values together, the GPU can perform all three additions (x+x, y+y, z+z) simultaneously. Using vectors effectively is the key to unlocking the GPU's performance and writing elegant shader code.

Vector Syntax

WGSL defines vectors with a simple, clear syntax: vecN<T>, where N is the number of components (2, 3, or 4) and T is the scalar type of those components. While you can create vectors of any scalar type, floating-point vectors are used in over 99% of graphics operations.

TypeCommon Usage
vec2<f32>2D positions, and especially texture coordinates (UVs).
vec3<f32>The workhorse for 3D graphics: 3D positions, RGB colors, surface normals, direction vectors.
vec4<f32>RGBA colors (with an alpha channel for transparency) and homogeneous coordinates, such as the final clip-space position from a vertex shader.
vec2<i32>Less common, but useful for things like integer grid coordinates or texture dimensions in pixels.

Creating Vectors (Constructors)

WGSL provides a flexible set of "constructors" to build vectors in whatever way is most convenient. This lets you write clean, readable code without needing to manually handle every single component.

// 1. From individual scalar components: The most direct method.
let pos = vec3<f32>(1.0, 2.0, 3.0);
let red = vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red with full alpha

// 2. From a single value (a "splat"): A useful shortcut.
// This creates a vector where all components have the same value.
let gray = vec3<f32>(0.5); // Equivalent to vec3<f32>(0.5, 0.5, 0.5)

// 3. Combining smaller vectors and scalars: Extremely common and powerful.
// The components are simply concatenated in order to form the new vector.
let xy = vec2<f32>(1.0, 2.0);
let xyz = vec3<f32>(xy, 3.0);       // -> Creates vec3(1.0, 2.0, 3.0)
let xyzw = vec4<f32>(xy, 3.0, 4.0); // -> Creates vec4(1.0, 2.0, 3.0, 4.0)

// 4. From another vector and a scalar: The classic way to add a dimension.
// A common example is adding an alpha channel to an existing RGB color.
let rgb_color = vec3<f32>(1.0, 0.5, 0.0); // An orange color
let rgba_color = vec4<f32>(rgb_color, 1.0); // -> Creates vec4(1.0, 0.5, 0.0, 1.0)

Accessing Vector Components

To work with vectors, you need to access their individual components. WGSL gives you several accessor "schemes" which are aliases for each other. Using the right scheme is a best practice that makes your code much more readable by signaling your intent.

let my_vector = vec4<f32>(1.0, 0.5, 0.0, 1.0);

// The "spatial" or "geometric" scheme: x, y, z, w
// Use this when the vector represents a position, direction, or texture coordinate.
let x_pos = my_vector.x; // 1.0

// The "color" scheme: r, g, b, a
// Use this when the vector represents a color. This makes the code self-documenting.
let green_channel = my_vector.g; // 0.5

// By index (0-based array-style access)
// This is less common for direct access but is useful for programmatic access,
// for instance, if you were iterating through a vector's components in a loop.
let first_component = my_vector[0]; // 1.0

Vector Swizzling: A GPU Superpower

"Swizzling" is a powerful and highly efficient GPU feature that lets you create a new vector by rearranging, duplicating, or selecting components from an existing vector. It's not just a syntax shortcut; it compiles to a single, incredibly fast hardware instruction.

let color = vec4<f32>(1.0, 0.5, 0.0, 1.0); // A bright orange color

// 1. Extract a subset of components
// A very common use is to get the RGB part of an RGBA color.
let rgb = color.rgb; // -> Creates a new vec3<f32> with (1.0, 0.5, 0.0)
let rg = color.rg;   // -> Creates a new vec2<f32> with (1.0, 0.5)

// 2. Reorder components
// Useful for converting between different data layouts (e.g., from an image format).
let bgr = color.bgr; // -> Creates vec3(0.0, 0.5, 1.0). The components are reversed!

// 3. Duplicate components
// A great way to create grayscale values or fill a vector from a single channel.
let grayscale_from_red = color.rrr; // -> Creates vec3(1.0, 1.0, 1.0)

// 4. Mix and match to build new vectors
// This is where swizzling shines for geometric operations.
let pos = vec3<f32>(1.0, 2.0, 3.0);
let yxz = pos.yxz; // -> Creates vec3(2.0, 1.0, 3.0)

// A classic use case: using the X and Z components of a 3D position
// for 2D calculations, like looking up a texture for a ground plane.
let ground_coords = pos.xz; // -> Creates vec2(1.0, 3.0)

You can use the accessor schemes xyzw, rgba, or stpq (a convention for texture coordinates) for swizzling. They all access the same underlying components (0, 1, 2, 3), but you cannot mix schemes in a single swizzle (e.g., color.xb is illegal).

Vector Arithmetic

Arithmetic operations on vectors are performed component-wise. This means the operation is applied independently to each pair of corresponding components. This is a fundamental concept for everything from blending colors to manipulating positions.

let a = vec3<f32>(1.0, 2.0, 3.0);
let b = vec3<f32>(4.0, 5.0, 6.0);

// Addition: result.x = a.x + b.x, result.y = a.y + b.y, etc.
// Use case: Combining light sources, offsetting a position.
let sum = a + b; // -> Result: vec3(5.0, 7.0, 9.0)

// Subtraction: result.x = a.x - b.x, etc.
// Use case: Finding the direction vector from point A to point B (B - A).
let diff = b - a; // -> Result: vec3(3.0, 3.0, 3.0)

// Multiplication (component-wise!): result.x = a.x * b.x, etc.
// Use case: Tinting. Multiplying a base color by a texture color.
let product = a * b; // -> Result: vec3(4.0, 10.0, 18.0)

// Operations with a scalar: The scalar is applied to every component.
// Use case: Uniform scaling an object or changing the brightness of a color.
let scaled_up = a * 2.0;   // -> Result: vec3(2.0, 4.0, 6.0)
let scaled_down = b / 2.0; // -> Result: vec3(2.0, 2.5, 3.0)

CRITICAL: Standard vector multiplication (*) is component-wise multiplication, often called the Hadamard product. It is NOT a dot product or a cross product! WGSL provides separate built-in functions (dot(), cross()) for those, which we will cover in a later article. This is one of the most common points of confusion for beginners.

Matrix Types: For Transformations

If vectors represent points and directions, then a matrix is the machine that transforms them. Matrices are the mathematical tool we use to perform all the essential 3D operations we discussed in the graphics pipeline: moving (translation), rotating, and scaling objects. They are the engine that powers the journey of a vertex through different coordinate spaces.

A matrix is a grid of numbers arranged in columns and rows. In WGSL, the syntax is matCxR<T>, where C is the number of columns, R is the number of rows, and T is the scalar type (almost always f32).

Common Matrix Types

While various sizes exist, you will primarily work with mat4x4<f32> in 3D graphics.

TypeCommon Usage
mat4x4<f32>The cornerstone of 3D graphics. A single 4x4 matrix can store a combination of translation, rotation, and scale for a 3D object. Bevy's Model, View, and Projection matrices are all of this type.
mat3x3<f32>Often used for transforming 3D direction vectors like normals, where translation information should be ignored.
mat2x2<f32>Used for 2D transformations, for example, rotating or scaling UV coordinates in a fragment shader.

In your Bevy shaders, you'll mostly receive mat4x4<f32> matrices from the engine and use them to transform your vertex positions.

Creating Matrices and Column-Major Order

You will rarely build transformation matrices from scratch inside a shader - Bevy's Transform component does that complex work for you on the CPU. However, it's crucial to understand how they are constructed. The constructor takes the values for each column, one after the other.

Key Concept: WGSL matrices, like those in most modern graphics APIs, are column-major. This defines how the numbers you provide are stored in memory. It means you provide all the data for the first column, then all for the second, and so on.

Think of it as filling the matrix grid vertically, one column at a time:

// A 4x4 identity matrix (which represents "no transformation").
let identity = mat4x4<f32>(
//  Col 0     Col 1     Col 2     Col 3
    1.0, 0.0, 0.0, 0.0,  // Row 0
    0.0, 1.0, 0.0, 0.0,  // Row 1
    0.0, 0.0, 1.0, 0.0,  // Row 2
    0.0, 0.0, 0.0, 1.0   // Row 3
);

// This is wrong! The constructor takes columns, not rows.
// let wrong = mat4x4<f32>(row0, row1, row2, row3); // ✗

// You can also construct a matrix from column vectors. This is often clearer.
let col0 = vec4<f32>(1.0, 0.0, 0.0, 0.0);
let col1 = vec4<f32>(0.0, 1.0, 0.0, 0.0);
let col2 = vec4<f32>(0.0, 0.0, 1.0, 0.0);
let col3 = vec4<f32>(0.0, 0.0, 0.0, 1.0);
let from_vectors = mat4x4<f32>(col0, col1, col2, col3);

Accessing Matrix Elements

Accessing matrix data follows this column-major logic. The most important rule to remember is: column-first, then row.

let m = ... // some mat4x4<f32>

// Get an entire column as a vector
let second_column: vec4<f32> = m[1]; // Index 1 gives you the second column.

// Access a specific element: matrix[column_index][row_index]
let element_c1_r2 = m[1][2]; // Get the element in Column 1, Row 2.

The Magic: Matrix-Vector Multiplication

The real power of a matrix is revealed when you multiply it by a vector. This operation applies the transformation stored in the matrix to the vector, producing a new, transformed vector.

This is NOT component-wise multiplication! It's a special mathematical operation that correctly rotates, scales, and translates the vector. We will dive deep into the math in a later article. For now, just focus on the syntax and the result.

// Conceptual vertex shader code
// These matrices are provided by Bevy's renderer.
let model_matrix: mat4x4<f32> = ...;
let view_matrix: mat4x4<f32> = ...;
let projection_matrix: mat4x4<f32> = ...;

// The original vertex position from the mesh.
// We use a vec4 with w=1.0 for positions so that translation works correctly.
let local_position = vec4<f32>(1.0, 2.0, 3.0, 1.0);

// Apply the transformations in sequence.
// Note the order: Projection * View * Model * Position
let world_position = model_matrix * local_position;
let view_position = view_matrix * world_position;
let clip_position = projection_matrix * view_position;

This sequence of multiplications is the absolute core of every 3D vertex shader. It's the code that executes the "Coordinate Space Journey" we learned about previously, taking a vertex from its local blueprint all the way to its final place on the screen.

Variables: var vs let

WGSL provides two keywords for declaring a named value: let and var. The choice between them is one of the most important you'll make, as it signals your intent and has major consequences for safety and performance. The core difference is mutability: can the value change after it's been declared?

let: Immutable Constants

The let keyword declares a runtime constant. Once you assign a value to a let binding, it is frozen and cannot be changed for the rest of its scope. You should think of it as a named placeholder for a fixed value.

let pi = 3.14159;
let base_color = vec3<f32>(1.0, 0.5, 0.2); // An orange color

// The following lines would cause a compilation error:
// pi = 3.14;                 // ✗ ERROR: Cannot reassign a `let` constant.
// base_color.r = 0.8;        // ✗ ERROR: Cannot modify a component of a `let` constant.

Why You Should Prefer let

As a best practice, you should use let for every value you declare by default. This habit makes your code safer, more readable, and often more performant.

  1. Safety and Readability: When you see let, you have a guarantee that the value will never change within that part of the code. This makes shaders much easier to read and reason about, as you don't need to track potential modifications. It prevents a whole class of bugs caused by accidental reassignment.

  2. Performance and Optimization: Declaring a value as immutable gives the shader compiler critical information. The compiler knows the value is constant, which allows for powerful optimizations. It might perform calculations at compile time ("constant folding"), store the value more efficiently in a register, or remove entire branches of code. In a highly parallel environment like a GPU, working with immutable data is fundamentally more efficient.

var: Mutable Variables

For situations where a value must change after it has been declared, WGSL provides the var keyword. This declares a mutable variable, the traditional variable type you might be used to from other languages. Its value can be changed at any time.

// Declare a mutable variable for a counter
var counter = 0;
counter = counter + 1; // ✓ This is perfectly valid.

// Declare a mutable color to be modified
var color = vec3<f32>(1.0, 0.0, 0.0);
color.r = 0.5; // Modify the red component
color.gb = vec2<f32>(0.2, 0.8); // Modify the green and blue components with a swizzle

When to Use var

You should only resort to using var when immutability is not an option. Legitimate scenarios include:

  • Accumulators: When you are summing up values in a loop, such as calculating the total light contribution from multiple light sources.
var total_light = vec3<f32>(0.0);
for (var i = 0; i < 4; i = i + 1) {
    total_light = total_light + calculate_light_contribution(i);
}
  • Loop Counters: The counter in a for loop must be a var.

  • Step-by-Step Modification: When you start with a base value and apply a series of complex, conditional modifications to it.

var final_color = texture_color;
if (is_in_shadow) {
    final_color = final_color * 0.5;
}
final_color = apply_fog(final_color, distance);

Even in this case, it's often possible (and clearer) to rewrite the logic using let for each stage, as it forces a more explicit data flow.

Type Inference

WGSL's compiler is smart and can often infer a variable's type from the value you assign to it.

let x = 1.0;                    // Inferred as f32
let pos = vec3(1.0, 2.0, 3.0);  // Components are f32, so inferred as vec3<f32>
let count = 0;                  // Inferred as i32
let flag = true;                // Inferred as bool

Technically, number literals like 1 or 1.0 start as "abstract" integers or floats. The compiler gives them a concrete type (i32, f32, etc.) the first time they are used in a context that requires one. For let count = 0;, the default concrete type for an abstract integer is i32. This is why type inference works so smoothly.

While inference is convenient, being explicit about types can sometimes make your code clearer and prevent subtle bugs, especially when you intend to use a less common type like u32.

// Explicit types for clarity
let x: f32 = 1.0;
let pos: vec3<f32> = vec3(1.0, 2.0, 3.0);
let frame_index: u32 = 0u; // Explicitly an unsigned integer

Putting It All Together: A Common Workflow

We've learned about scalars, vectors, and the let keyword as separate concepts. Now, let's put them all together. The following snippet demonstrates a common and powerful workflow you'll use constantly in your shaders: starting with base values and creating new ones in a series of clear, immutable steps.

This isn't a runnable demo, but rather a perfect illustration of how the pieces fit together to achieve a result.

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // Step 1: Define our starting point with `let` and a vector constructor.
    // This base value itself won't change; we will create new constants based on it.
    let base_color = vec3<f32>(1.0, 0.5, 0.2);

    // Step 2: Adjust brightness with scalar-vector multiplication.
    // In a real shader, this could be a shadow calculation. We're just making
    // the base color 50% darker via component-wise multiplication.
    let darker_orange = base_color * 0.5;

    // Step 3: Blend with another color using vector arithmetic.
    // We create a final color that is 70% our `darker_orange` and 30% a
    // cool blue accent color. This is a manual linear interpolation.
    let accent_color = vec3<f32>(0.2, 0.3, 0.8);
    let blended_color = darker_orange * 0.7 + accent_color * 0.3;

    // Step 4: Construct the final `vec4` output.
    // The fragment shader must return an RGBA color. We construct this
    // by combining our final `vec3` result with a scalar `f32` for the alpha.
    let final_output = vec4<f32>(blended_color, 1.0);

    return final_output;
}

Deconstructing the Process

This short example is a microcosm of everyday shader programming, demonstrating several key concepts in a practical flow:

  1. Immutability by Default: We use let to define each stage of the color. This makes the code easy to follow - base_color is always the original orange, darker_orange is always the half-bright version, and so on. There are no surprise mutations, which makes the logic simple to debug.

  2. Vector as a Unit: We treat base_color and accent_color as single entities, even though they contain three separate f32 values. All our math (* and +) operates on them as a whole, which is clean and intuitive.

  3. Component-Wise Math in Action: The multiplication darker_orange * 0.7 isn't a dot product; it's a scaling of each color channel (R, G, and B) independently. This component-wise behavior is exactly what we need for tinting and blending colors.

  4. Vector Construction Flexibility: We see two kinds of construction: vec3<f32>(r, g, b) to define the initial colors from scalars, and the powerful vec4<f32>(vec3, a) to compose the final output by combining an existing vec3 with a new scalar alpha channel.

By following this pattern - starting with base values, applying a series of transformations and blends to create new let constants, and finally constructing the required output - you can build complex visual effects in a clean, readable, and performant way.


Complete Example: Vector Operations Visualizer

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" vector operations in real-time.

Our Goal

We will create a custom material for a sphere that can cycle through five different visualization modes by pressing a key. Each mode will apply a different vector operation to the sphere's base color, visually demonstrating concepts like component access, swizzling, and arithmetic.

What This Project Demonstrates

  • Component Access: How to isolate and use a single component of a vector (e.g., .r).

  • Swizzling: The power of rearranging vector components on the fly (e.g., .bgr).

  • Vector Arithmetic: The visual result of component-wise addition and multiplication.

  • Vector Construction: How to build new vectors by combining smaller vectors and scalars.

  • Bevy Integration: The basic structure of a custom Material in Bevy 0.16 and how to pass data (our demo_mode) from Rust to a WGSL shader.

The Shader (assets/shaders/d01_02_vector_demo.wgsl)

This single WGSL file contains both our vertex and fragment shaders. The vertex shader is standard; it calculates the world position and normal for each vertex and passes them to the next stage.

The fragment shader is where the magic happens. It first calculates a base color from the interpolated world normal. Then, using an if chain, it checks the material.demo_mode uniform (sent from our Rust code) to decide which vector operation to perform, altering the final color of the pixel accordingly.

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

struct VectorDemoMaterial {
    demo_mode: u32,
}

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

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

    let world_from_local = mesh_functions::get_world_from_local(instance_index);
    let world_position = mesh_functions::mesh_position_local_to_world(
        world_from_local,
        vec4<f32>(position, 1.0)
    );

    out.position = position_world_to_clip(world_position.xyz);
    out.world_normal = mesh_functions::mesh_normal_local_to_world(normal, instance_index);
    out.world_position = world_position;

    return out;
}

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // Create a base color from the normal
    let base = (in.world_normal + 1.0) * 0.5;

    // Mode 0: RGB channels separately
    if material.demo_mode == 0u {
        // Show only red channel
        return vec4<f32>(base.r, 0.0, 0.0, 1.0);
    }

    // Mode 1: Swizzling demonstration
    if material.demo_mode == 1u {
        // Reverse the RGB channels (BGR)
        let swizzled = base.bgr;
        return vec4<f32>(swizzled, 1.0);
    }

    // Mode 2: Vector arithmetic
    if material.demo_mode == 2u {
        // Add a constant color
        let added = base + vec3<f32>(0.0, 0.3, 0.0);  // Add green
        return vec4<f32>(added, 1.0);
    }

    // Mode 3: Vector multiplication
    if material.demo_mode == 3u {
        // Multiply by a color (component-wise)
        let tinted = base * vec3<f32>(1.0, 0.5, 0.5);  // Reduce green and blue
        return vec4<f32>(tinted, 1.0);
    }

    // Mode 4: Component extraction and reconstruction
    if material.demo_mode == 4u {
        // Extract xy, ignore z, add new z
        let xy = base.xy;
        let reconstructed = vec3<f32>(xy, 0.5);  // Force z to 0.5
        return vec4<f32>(reconstructed, 1.0);
    }

    // Default: Original
    return vec4<f32>(base, 1.0);
}

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

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

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

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

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

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

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

// ... other materials
pub mod d01_02_vector_demo;

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

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

use crate::materials::d01_02_vector_demo::VectorDemoMaterial;
use bevy::prelude::*;

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

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<VectorDemoMaterial>>,
) {
    // Spawn a sphere
    commands.spawn((
        Mesh3d(meshes.add(Sphere::new(1.0).mesh().uv(32, 18))),
        MeshMaterial3d(materials.add(VectorDemoMaterial { demo_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
    commands.spawn((
        Text::new("Press SPACE to cycle modes\nMode 0: Red channel only"),
        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_demo_mode(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut materials: ResMut<Assets<VectorDemoMaterial>>,
    mut text_query: Query<&mut Text>,
) {
    if keyboard.just_pressed(KeyCode::Space) {
        for (_, material) in materials.iter_mut() {
            material.demo_mode = (material.demo_mode + 1) % 5;

            for mut text in text_query.iter_mut() {
                **text = match material.demo_mode {
                    0 => "Press SPACE to cycle modes\nMode 0: Red channel only".to_string(),
                    1 => "Press SPACE to cycle modes\nMode 1: BGR swizzle (reversed)".to_string(),
                    2 => "Press SPACE to cycle modes\nMode 2: Add green (vector addition)"
                        .to_string(),
                    3 => "Press SPACE to cycle modes\nMode 3: Color tint (vector multiplication)"
                        .to_string(),
                    4 => "Press SPACE to cycle modes\nMode 4: XY extraction with new Z".to_string(),
                    _ => "Unknown mode".to_string(),
                };
            }
        }
    }
}

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

// ... other demoss
pub mod d01_02_vector_demo;

And register it in src/main.rs:

Demo {
    number: "1.2",
    title: "WGSL Fundamentals - Data Types & Variables",
    run: demos::d01_02_vector_demo::run,
},

Running the Demo

When you run the project, you will see a multi-colored sphere. Pressing the spacebar will cycle through the five different debug visualizations, each revealing how a different vector operation affects the final color.

Controls

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

What You're Seeing

Each mode corresponds to a different branch in our fragment shader's logic.

ModeDescriptionWhat It Proves
0 - Red Channel OnlyThe sphere is colored only with the red component of the base color.Demonstrates component access (base.r). We've isolated a single f32 from a vec3.
1 - BGR SwizzleThe colors are reversed. Areas that were red are now blue, and vice-versa.Demonstrates swizzling (base.bgr). We created a new vec3 by reordering the components of the original.
2 - Vector AdditionThe entire sphere appears brighter and greener.Demonstrates component-wise addition. We added (0.0, 0.3, 0.0) to every single pixel's color, increasing its green channel.
3 - Color TintThe sphere appears more reddish-orange, with the greens and blues muted.Demonstrates component-wise multiplication. We scaled the G and B channels by 0.5, effectively tinting the entire result.
4 - XY ReconstructionThe color is now only composed of red and green, with a constant blue value of 0.5 across the whole sphere.Demonstrates construction and extraction. We extracted a vec2 (base.xy) and used it to construct a new vec3.

Key Takeaways

This article introduced you to the alphabet of the WGSL language. Before moving on, ensure you have a solid grasp of these core concepts, as they are the foundation upon which all shader logic is built.

  1. Scalars are the Atomic Units, with f32 Being King.
    WGSL provides the basic building blocks of f32 (floats), i32/u32 (integers), and bool (booleans). Remember that nearly all graphics math is done with f32, and you must be explicit with literals (use 1.0 for a float, 1u for an unsigned integer).

  2. Vectors are the Workhorse for All Graphics Data.
    Positions, colors, directions, texture coordinates - everything is represented by vectors (vec2, vec3, vec4). Learning to think in terms of vectors is the most important step in becoming a shader programmer. They are not just data containers; they are the native language of the GPU.

  3. Vector Operations are Fast, Powerful, and Component-Wise.
    The GPU is optimized to perform math on vectors. Operations like addition, subtraction, and multiplication are applied to each component independently. This is perfect for tasks like blending colors or offsetting positions. Swizzling (.rgba, .xy, .bgr) is a zero-cost hardware feature that lets you efficiently rearrange and create new vectors from existing ones.

  4. Matrices are the Machines of Transformation.
    While we'll cover the deep math later, understand now that matrices (especially mat4x4<f32>) are the tools used to move, rotate, scale, and project your vectors from one coordinate space to another. The core of a vertex shader is applying matrix transformations to vertex positions.

  5. Prefer Immutable let for Safety and Performance.
    Use let to declare constants by default. This makes your code safer, easier to reason about, and provides crucial information to the shader compiler, enabling powerful optimizations. Only use var for mutable variables when you have a specific need to change a value after its initial declaration, such as an accumulator in a loop.

What's Next?

You have now learned the "nouns" of the WGSL language - the fundamental data types and variables used to represent all the data in your shader, from a single boolean flag to a complex transformation matrix. You know how to create, store, and perform basic math on this data.

But data on its own is static. To bring our shaders to life, we need to create logic. In the next article, we will learn the "verbs" and "grammar" of WGSL. We will explore how to organize your code into reusable functions and how to make decisions and repeat operations using control flow structures like if/else and for loops.

Next up: 1.3 - WGSL Fundamentals - Functions & Control Flow


Quick Reference

A cheat sheet for the fundamental building blocks of the WGSL language.

Scalar Types

TypeDescriptionExample Literal
f3232-bit floating-point number. The default for most math.1.0, -0.5, 3.14
i3232-bit signed integer. The default for integers.42, -10, 0
u3232-bit unsigned integer (non-negative).0u, 255u, 100u
boolA boolean value.true, false

Variable Declaration

  • let name = value;: Declares an immutable constant. Its value cannot be changed. Use this by default.

  • var name = value;: Declares a mutable variable. Its value can be reassigned. Use only when necessary (e.g., accumulators, loop counters).

Vector Types

The workhorse of shader programming. T can be f32, i32, u32, or bool.

Syntax: vecN<T> (e.g., vec3<f32>)

Construction

// From components
let v3 = vec3<f32>(1.0, 2.0, 3.0);

// From a single value ("splat")
let v3 = vec3<f32>(0.5); // -> vec3(0.5, 0.5, 0.5)

// Combining smaller types
let xy = vec2<f32>(1.0, 2.0);
let v3 = vec3<f32>(xy, 3.0); // -> vec3(1.0, 2.0, 3.0)
let v4 = vec4<f32>(v3, 1.0); // -> vec4(1.0, 2.0, 3.0, 1.0)

Access & Swizzling

let v = vec4<f32>(1.0, 2.0, 3.0, 4.0);

// Access (all are equivalent for the first component)
let c0_a = v.x; // -> 1.0
let c0_b = v.r; // -> 1.0
let c0_c = v[0]; // -> 1.0

// Swizzling (creating new vectors)
let v2 = v.xy;      // -> vec2(1.0, 2.0)
let v3 = v.bgr;     // -> vec3(3.0, 2.0, 1.0) (reordered)
let v3 = v.rrr;     // -> vec3(1.0, 1.0, 1.0) (duplicated)
let v2 = v.wz;      // -> vec2(4.0, 3.0)

Arithmetic (Component-Wise)

let a = vec2<f32>(1.0, 2.0);
let b = vec2<f32>(3.0, 4.0);

let sum = a + b;    // -> vec2(4.0, 6.0)
let prod = a * b;   // -> vec2(3.0, 8.0)
let scaled = a * 2.0; // -> vec2(2.0, 4.0)

Matrix Types

Used for transformations. C\=Columns, R\=Rows.

Syntax: matCxR<T> (e.g., mat4x4<f32>)

  • Column-Major: Data is organized by columns.

  • Access: matrix[column_index][row_index]

  • Multiplication: transformed_vector = matrix * vector (This is a mathematical transformation, not component-wise).