#Part 6 - 2D Transformations
Static images are a start, but real graphics need to move. In this part we apply a transformation matrix to the vertex shader so we can rotate and scale the textured quad every frame.
Add glam to Cargo.toml:
[dependencies]
wgpu = "0.20"
winit = "0.29"
pollster = "0.3"
env_logger = "0.11"
log = "0.4"
bytemuck = { version = "1.14", features = ["derive"] }
image = "0.25"
glam = { version = "0.27", features = ["bytemuck"] }
What is a Transformation Matrix?
A 4×4 transformation matrix encodes translation (position), rotation, and scale in a single structure. Multiplying a vertex position by the matrix produces the transformed position — all three operations in one GPU instruction.
We need 4×4 rather than 3×3 because translation can’t be expressed as a pure linear operation in 3D space. The solution is homogeneous coordinates: we represent a 3D position as a 4D vector (x, y, z, 1.0), which allows translation to be encoded as a matrix multiplication.
Introducing glam
glam is Rust’s standard linear algebra crate for graphics. It provides Mat4, Vec3, quaternions, and more. The bytemuck feature enables safe byte-casting of Mat4 so we can upload it to the GPU as a uniform.
use glam::Mat4;
The TransformUniform Struct
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct TransformUniform {
matrix: [[f32; 4]; 4],
}
impl TransformUniform {
fn from_mat4(m: Mat4) -> Self {
Self { matrix: m.to_cols_array_2d() }
}
}
to_cols_array_2d() converts Mat4 into a [[f32; 4]; 4] that bytemuck can safely cast to bytes.
Updating the Vertex Shader
struct TransformUniform {
matrix: mat4x4<f32>,
}
@group(1) @binding(0)
var<uniform> transform: TransformUniform;
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.tex_coords = in.tex_coords;
out.clip_position = transform.matrix * vec4<f32>(in.position, 1.0);
return out;
}
The texture bind group stays at @group(0). The transform gets @group(1) — multiple bind groups let you update them independently without invalidating each other.
The update() Method
fn update(&mut self) {
let t = self.start_time.elapsed().as_secs_f32();
let rotation = Mat4::from_rotation_z(t);
let scale = Mat4::from_scale(glam::Vec3::splat(0.5 + 0.2 * t.sin()));
let transform = TransformUniform::from_mat4(rotation * scale);
self.queue.write_buffer(&self.transform_buffer, 0, bytemuck::cast_slice(&[transform]));
}
Matrix multiplication order matters: rotation * scale applies scale first, then rotation. Reversing the order would produce a different result — the object would scale along its rotated axes rather than its original axes.
Multiple Bind Groups
The pipeline layout now takes both bind group layouts:
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[&texture_bind_group_layout, &transform_bind_group_layout],
push_constant_ranges: &[],
});
And the render pass binds both:
render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]);
render_pass.set_bind_group(1, &self.transform_bind_group, &[]);
Running It
cargo run
You should see the textured quad spinning and pulsing in size. Try modifying the rotation speed or adding a translation (Mat4::from_translation) to move it around the screen.
What’s Next
The foundations arc is complete. From here the series can go in many directions:
- Part 7: A 3D Camera — projection matrices, view matrices, depth buffer
- Part 8: Instancing — drawing thousands of objects with a single draw call
- Part 9: Lighting — diffuse and specular shading with normal vectors
Each of these builds naturally on the pipeline you’ve built here.
Further reading:
FAQ
What is a transformation matrix? A 4×4 matrix that encodes position, rotation, and scale in a single structure. Multiplying a vertex position by the matrix applies all three transformations in one operation on the GPU.
Why 4×4 instead of 3×3? In 3D space, translation cannot be expressed as a linear operation. Adding a fourth coordinate (homogeneous coordinates) allows translation to be encoded as a matrix multiplication, so all transforms can be combined and applied in one step.
What is glam?
glam is a fast, SIMD-optimised linear algebra crate for Rust, widely used in games and graphics. It provides Mat4, Vec3, Quat, and related types with an ergonomic API and bytemuck integration for GPU uploads.
Why does matrix multiplication order matter for transforms?
Matrix multiplication is not commutative: A * B ≠ B * A. For transforms, the rightmost matrix in a product is applied first. rotation * scale scales the geometry first (in object space), then rotates the result. scale * rotation rotates first, then scales along the world axes — producing a different shape when the scale is non-uniform.
Continue Learning
Part 5 - Textures and Image Loading
Load an image file and render it as a texture on a quad with wgpu in Rust — covers texture creation, samplers, UV coordinates, and index buffers.
Part 4 - Colors and Uniforms
Learn how to pass data from your Rust program to the GPU using uniform buffers and bind groups in wgpu — and animate a colour tint in real time.
Part 3 - Drawing Your First Triangle
Draw your first triangle with wgpu in Rust — define vertex data, upload it to the GPU with a vertex buffer, and write your first WGSL shaders.
Part 2 - The Render Pipeline
Initialise the wgpu render pipeline in Rust — create a GPU device, configure a surface, and clear the screen to a colour using a render pass.