Search code examples
3drustlinear-algebraquaternions

Quaternion rotation causes scene to stretch


I have adapted a Vulkano, GLTF viewer example, to allow for keyboard and mouse input, to apply translation and rotation to the scene. The translation works fine, but the rotation causes the objects in the scene to move right up to the camera and stretch out until they look like garbage, as the rotation approaches between 90 - 180 degrees.

This is what the scene looks like before rotation:

This is what the scene looks like before rotation.

And this is when rotation is at approx 90 degrees:

And this is when rotation is at approx 90 degrees.

I'm using cgmath Quaternions for the rotation. I haven't used quaternions before, so I'm wondering if anyone could tell me if I'm using them correctly, I suspect this is where the problem lies.

This is how I'm creating the initial quaternion, using an equation I found here:

let rotation_speed_deg = 0.01;
let rotation_speed = rotation_speed_deg * f32::consts::PI / 180.0;
let rotation_angle_deg = 0.0;
let rotation_angle = rotation_angle_deg * f32::consts::PI / 180.0;
let rotation_axis = Vector3::new(0.0, 1.0, 0.0);
let rotation_scalar = (rotation_angle / 2.0 as f32).cos();
let rotation_vec = Vector3::new(
    rotation_axis.x * (rotation_angle / 2.0 as f32).sin(),
    rotation_axis.y * (rotation_angle / 2.0 as f32).sin(),
    rotation_axis.z * (rotation_angle / 2.0 as f32).sin());
let mut rotation_quat = Quaternion::from_sv(rotation_scalar, rotation_vec);

Then I'm modifying the quaternion to apply a rotation on mouse input like this:

let diff_x = position.0 - last_x;
if diff_x > 0.0 {
    // println!("turn right");
    rotation_quat.v.x += rotation_axis.x * ((rotation_angle + rotation_speed) / 2.0 as f32).sin();
    rotation_quat.v.y += rotation_axis.y * ((rotation_angle + rotation_speed) / 2.0 as f32).sin();
    rotation_quat.v.z += rotation_axis.z * ((rotation_angle + rotation_speed) / 2.0 as f32).sin();
    rotation_quat.s += ((rotation_angle + rotation_speed) / 2.0 as f32).cos();
} else if diff_x < 0.0 {
    // println!("turn left");
    rotation_quat.v.x += rotation_axis.x * ((rotation_angle - rotation_speed) / 2.0 as f32).sin();
    rotation_quat.v.y += rotation_axis.y * ((rotation_angle - rotation_speed) / 2.0 as f32).sin();
    rotation_quat.v.z += rotation_axis.z * ((rotation_angle - rotation_speed) / 2.0 as f32).sin();
    rotation_quat.s += ((rotation_angle - rotation_speed) / 2.0 as f32).cos();
}

last_x = position.0;

To apply the translation and rotation, I'm multiplying my matrices like this:

let fovy = Rad(60.0 * f32::consts::PI / 180.0);
let mut aspect = viewport_dimensions[0] as f32 / viewport_dimensions[1] as f32;
let near = 0.1;
let far = 100.0;
let mut proj = perspective(fovy, aspect, near, far);
let view = Matrix4::look_at(Point3::new(0.0, 1.0, -5.0), Point3::new(0.0, 1.0, 0.0), Vector3::new(0.0, -1.0, 0.0));
let trans_mat = Matrix4::from_translation(trans);
let rot_mat = Matrix4::from(rot);
builder = self.draw_node(node.index(), proj * view * trans_mat * rot_mat, viewport_dimensions, builder);

So you can see, I'm multiplying proj * view * trans_mat * rot_mat... Maybe that's the wrong order?

Any help would be much appreciated. I don't have a strong math background, and have been trying to learn CG for years on my own, so I don't know where else to turn. Some reading resources about this type of stuff would be appreciated too.


Solution

  • The reason for the stretching was because I was combining the initial and subsequent quaternions incorrectly. You can't just add them together, but the * operator works to concatenate rotations.

    So I changed the mouse input portion of the code to this and it works:

    let diff_x = position.0 - last_x;
    if diff_x > 0.0 {
        // println!("turn right");
        let rotation_scalar = ((rotation_angle + rotation_speed) / 2.0 as f32).cos();
        let rotation_vec = Vector3::new(
            rotation_axis.x * ((rotation_angle + rotation_speed) / 2.0 as f32).sin(),
            rotation_axis.y * ((rotation_angle + rotation_speed) / 2.0 as f32).sin(),
            rotation_axis.z * ((rotation_angle + rotation_speed) / 2.0 as f32).sin());
        let rotation_quat2 = Quaternion::from_sv(rotation_scalar, rotation_vec);
        rotation_quat = rotation_quat * rotation_quat2;
    } else if diff_x < 0.0 {
        // println!("turn left");
        let rotation_scalar = ((rotation_angle - rotation_speed) / 2.0 as f32).cos();
        let rotation_vec = Vector3::new(
            rotation_axis.x * ((rotation_angle - rotation_speed) / 2.0 as f32).sin(),
            rotation_axis.y * ((rotation_angle - rotation_speed) / 2.0 as f32).sin(),
            rotation_axis.z * ((rotation_angle - rotation_speed) / 2.0 as f32).sin());
        let rotation_quat2 = Quaternion::from_sv(rotation_scalar, rotation_vec);
        rotation_quat = rotation_quat * rotation_quat2;
    }
    last_x = position.0;