Shader concept
Rotating in a shader: sin, cos and rotation matrices
To rotate a point around the origin by an angle, you mix its x and y with the sine and cosine of that angle. A shader rotates a picture by rotating each pixel's position the opposite way before drawing.
Running live in your browser. The code below is exactly what draws it.
The formula
Rotating the point (x, y) by an angle a gives (x*cos(a) - y*sin(a), x*sin(a) + y*cos(a)). As a matrix that is the 2x2 grid with cos and -sin in the first row and sin and cos in the second, which is why you will see the word matrix. The function rot above is that formula written out, which works in every shader language without worrying how each one multiplies matrices.
The direction trick
A shader does not move shapes; it asks, for each pixel, what the picture contains at that position. To turn the shape clockwise you therefore rotate the lookup position anticlockwise. In practice: rotate p first, then evaluate your shape on the rotated p, as the picture does with sdBox(rot(p, time), ...). If it spins the wrong way, negate the angle.
Rotating about a point other than the centre
Subtract the pivot, rotate, add the pivot back: rot(p - pivot, a) + pivot. Rotating around the origin of the coordinates only matches the middle of the screen if you centred p first.
Common mistakes
- Rotating un-centred uv (where the origin is the bottom-left corner): everything swings around the corner.
- Rotating before correcting for the aspect ratio, which turns circles into squashed ovals mid-spin. Fix the aspect first.
- Mixing up matrix multiplication order between HLSL (
mul(M, v)) and the others. Writing the formula out avoids the problem.
The code, in every shader language
The same example for each language family. Open one to see it.
HLSL Unity, Unreal
float2 rot(float2 p, float a)
{
float c = cos(a);
float s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float sdBox(float2 p, float2 b)
{
float2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float d = sdBox(rot(p, time), float2(0.22, 0.09)); // a spinning bar
float fill = 1.0 - smoothstep(0.0, 0.006, d);
float3 col = lerp(float3(0.07, 0.09, 0.2), float3(0.95, 0.55, 0.35), fill);
return float4(col, 1.0);
} GLSL Shadertoy, three.js, OpenGL
vec2 rot(vec2 p, float a)
{
float c = cos(a);
float s = sin(a);
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float sdBox(vec2 p, vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
float d = sdBox(rot(p, time), vec2(0.22, 0.09)); // a spinning bar
float fill = 1.0 - smoothstep(0.0, 0.006, d);
vec3 col = mix(vec3(0.07, 0.09, 0.2), vec3(0.95, 0.55, 0.35), fill);
return vec4(col, 1.0);
} WGSL WebGPU, Bevy
fn rot(p: vec2f, a: f32) -> vec2f
{
let c = cos(a);
let s = sin(a);
return vec2f(c * p.x - s * p.y, s * p.x + c * p.y);
}
fn sdBox(p: vec2f, b: vec2f) -> f32
{
let d = abs(p) - b;
return length(max(d, vec2f(0.0))) + min(max(d.x, d.y), 0.0);
}
fn frag(uv: vec2f) -> vec4f
{
let p = (uv - 0.5) * vec2f(aspect, 1.0);
let d = sdBox(rot(p, time), vec2f(0.22, 0.09)); // a spinning bar
let fill = 1.0 - smoothstep(0.0, 0.006, d);
let col = mix(vec3f(0.07, 0.09, 0.2), vec3f(0.95, 0.55, 0.35), fill);
return vec4f(col, 1.0);
} Godot Godot 4 shading language
shader_type canvas_item;
vec2 rot(vec2 p, float a)
{
float c = cos(a);
float s = sin(a);
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float sdBox(vec2 p, vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0);
float d = sdBox(rot(p, TIME), vec2(0.22, 0.09)); // a spinning bar
float fill = 1.0 - smoothstep(0.0, 0.006, d);
vec3 col = mix(vec3(0.07, 0.09, 0.2), vec3(0.95, 0.55, 0.35), fill);
return vec4(col, 1.0);
}
void fragment()
{
// Godot's UV starts at the top-left, the lessons use bottom-left
COLOR = frag(vec2(UV.x, 1.0 - UV.y));
} Metal iOS, macOS
float2 rot(float2 p, float a)
{
float c = cos(a);
float s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
float sdBox(float2 p, float2 b)
{
float2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float d = sdBox(rot(p, time), float2(0.22, 0.09)); // a spinning bar
float fill = 1.0 - smoothstep(0.0, 0.006, d);
float3 col = mix(float3(0.07, 0.09, 0.2), float3(0.95, 0.55, 0.35), fill);
return float4(col, 1.0);
} Practise it in the lessons
- Turning a quarter turnDraw a horizontal bar with a square on its right end, turned by a quarter turn using p = float2(-p.y, p.x).…
- Turning by any angle (sin and cos)Take the same bar-with-a-square shape and turn it by 0.5 radians (about 29 degrees) using c and s, no…
- Rotation & matricesSpin the box: rotate p by an angle equal to time (in radians) before computing the distance.
Questions
Do I need a matrix to rotate in a shader?
No. The sin and cos formula does the same thing and is easier to read. Matrices matter when you combine many transformations or rotate in 3D.
Why does my shape rotate the wrong way?
A shader transforms the lookup position, so the effect on the picture is the opposite of the effect on the position. Negate the angle.