# How to rotate 2D image in 3D space using a quaternion

I am trying to rotate a flat 2D image around a 2D origin in 3D space. The problem I am having is that the image becomes stretched after the rotation. I am using the WGSL from this tutorial, but with the camera logic removed. The clip-coordinates have to be a 3d vector + w.

Here is what the sprite is meant to look like:

Here is what the sprite looks like when rotated 45 degrees:

I am using the cgmath crate for the quaternions. I have tried creating a quarternion from the Z angle:

let rotation = Quaternion::from_angle_z(cgmath::Deg(rotation));


And also tried manually:

let rotation = Quaternion::from_axis_angle(
Vector3 {
x: 0.0,
y: 0.0,
z: 1.0,
},
cgmath::Deg(rotation).normalize(),
);


I tried making a matrix that looks like this:

$$\begin{bmatrix} \cos \theta & -\sin \theta & 0 & 0\\ \sin \theta & \cos \theta & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix}$$

and I have also tried calling from_axis_angle using the normalised position as the axis argument.

All of these solutions have resulted in the same effect.

I also tried manually rotating the vertices, however, this just caused the position of the 2D image to rotate further.

Vertex shader to manually rotate each vertex:

struct EntityInput {
@location(5) position: vec2<f32>,
@location(6) origin: vec2<f32>,
@location(7) rotation: f32,
@location(8) scale: f32,
}

@vertex
fn vs_main(
model: VertexInput,
entity: EntityInput,
) -> VertexOutput {
var out: VertexOutput;
var orig_x: f32 = model.position.x - entity.origin.x;
var orig_y: f32 = model.position.y - entity.origin.y;
var new_x = (orig_x * cos(entity.rotation)) - (orig_y * sin(entity.rotation));
var new_y = (orig_y * cos(entity.rotation)) + (orig_x * sin(entity.rotation));
var final_x = new_x + entity.origin.x;
var final_y = new_y + entity.origin.y;
var final_vec = (vec4<f32>(final_x, final_y, 1.0, 1.0) + vec4<f32>(entity.position, 0.0, 0.0));
final_vec.x = final_vec.x * entity.scale;
final_vec.y = final_vec.y * entity.scale;
out.tex_coords = model.tex_coords;
out.clip_position = final_vec;
return out;
}

• If you rotate the sprite 90 degrees, using the methods you mentioned, what would the result be? Would the sprite still look stretched? Commented Mar 17, 2023 at 16:25
• Yes it will be, it also appears it gets more stretched after every rotation. Commented Mar 17, 2023 at 16:48
• If the stretch gets bigger with each rotation, that should indicate an error in the math involved. I'm not familiar with cgmath so I can't help much there, but the rotation matrix you posted looks correct. I'm not sure if it would help if you'd post the steps you followed to calculate the rotation manually with the matrix. Commented Mar 17, 2023 at 16:55
• I multiplied them as per sotrh.github.io/learn-wgpu/beginner/tutorial7-instancing/…, however, I'll add the shader code which manually rotates each vertex. Commented Mar 17, 2023 at 17:02
• It seem the further stretching may be unrelated, this was reported to me by a friend, however I can't replicate it. Commented Mar 17, 2023 at 17:43

I managed to solve the problem, I was normalising the initial coordinates of where the vertexes should be for the sprite. For some reason, this cause the sprite to warp as it rotated, almost as if the vertexes were being normalised repeatedly against wrong values due to rotation.

The solution was to using pixel values only within the main Rust code, then normalise the very final clip coordinate at the end of the vertex shader:

@vertex
fn vs_main(
model: VertexInput,
entity: EntityInput,
) -> VertexOutput {
var screen_width = 562.0;
var screen_height = 1021.0;
var out: VertexOutput;
var orig_x: f32 = model.position.x - entity.origin.x;
var orig_y: f32 = model.position.y - entity.origin.y;
var new_x = (orig_x * cos(entity.rotation)) - (orig_y * sin(entity.rotation));
var new_y = (orig_y * cos(entity.rotation)) + (orig_x * sin(entity.rotation));
var final_x = new_x + entity.origin.x;
var final_y = new_y + entity.origin.y;
var final_vec = (vec4<f32>(final_x, final_y, 0.0, 0.0) + vec4<f32>(entity.position, 0.0, 0.0));
final_vec = vec4<f32>(normalise(vec2<f32>(final_vec.x, final_vec.y), screen_width, screen_height), 1.0, 1.0);
out.tex_coords = model.tex_coords;
out.clip_position = final_vec;
return out;
}

fn normalise(given: vec2<f32>, width: f32, height: f32) -> vec2<f32> {
var new_vec: vec2<f32>;
new_vec.x = ((2.0 * (given.x)) / width) - 1.0;
new_vec.y = ((2.0 * (given.y)) / height) - 1.0;
return new_vec;
}
$$$$
`