Tutorial 43: Quaternions for Smooth Rotation
Quaternions represent 3D rotations as a four-component value (X, Y, Z, W). They are the standard tool for smooth 3D rotation in games because they avoid gimbal lock, interpolate cleanly, and compose without numerical drift.
Why quaternions?
Euler angles (yaw/pitch/roll) are intuitive but suffer from gimbal lock: when two rotation axes align, you lose a degree of freedom and animation breaks. Rotation matrices avoid gimbal lock but are expensive to interpolate and accumulate floating-point error after many multiplications. Quaternions give you:
- No gimbal lock
- Smooth spherical interpolation with
Slerp - Compact storage (4 floats vs 9 for a matrix)
- Stable composition via multiplication
Gimbal lock explained
Consider a flight model using three independent Euler angles. If you pitch up 90 degrees, your local roll axis now aligns with the world yaw axis — any subsequent roll rotation has the same effect as a yaw. You have lost one degree of freedom. This manifests as uncontrollable sudden jumps when rotations approach singularities. Quaternions represent all orientations uniformly and never hit this singularity.
CreateFromAxisAngle
#include "Microsoft/Xna/Framework/Quaternion.hpp"
using namespace Microsoft::Xna::Framework;
// Rotate 45 degrees around the Y axis
Quaternion q = Quaternion::CreateFromAxisAngle(Vector3::Up,
MathHelper::ToRadians(45.0f));
// Identity (no rotation)
Quaternion identity = Quaternion::Identity; // (0, 0, 0, 1)
CreateFromYawPitchRoll
// Convert Euler angles to a quaternion (applied as Yaw * Pitch * Roll)
float yaw = MathHelper::ToRadians(90.0f); // rotate around Y
float pitch = MathHelper::ToRadians(30.0f); // rotate around X
float roll = MathHelper::ToRadians(0.0f); // rotate around Z
Quaternion q2 = Quaternion::CreateFromYawPitchRoll(yaw, pitch, roll);
Multiply — compose rotations
Multiplying quaternions combines their rotations. Order matters: q1 * q2 applies q1 first, then q2 (same as matrix multiplication with row vectors).
Quaternion spinY = Quaternion::CreateFromAxisAngle(Vector3::Up, 0.01f);
Quaternion tiltX = Quaternion::CreateFromAxisAngle(Vector3::Right, 0.005f);
// Apply tilt first, then spin (think: local then world)
Quaternion combined = tiltX * spinY;
// Accumulate orientation each frame
orientation_ = Quaternion::Normalize(orientation_ * combined);
Slerp — smooth interpolation
Slerp (Spherical Linear Interpolation) traces the shortest arc between two orientations at constant angular velocity. Use it for camera transitions, AI state blending, or any smooth rotation tween.
Quaternion startRot = Quaternion::Identity;
Quaternion endRot = Quaternion::CreateFromAxisAngle(Vector3::Up,
MathHelper::Pi); // 180 degrees
float t = 0.5f; // halfway through the rotation
Quaternion mid = Quaternion::Slerp(startRot, endRot, t);
// mid ≈ 90-degree rotation around Y
ToMatrix — convert for rendering
Quaternion q3 = Quaternion::CreateFromAxisAngle(Vector3::Forward, 1.2f);
Matrix rotMat = Matrix::CreateFromQuaternion(q3);
// Use as the rotation part of a world matrix
Matrix world = Matrix::CreateScale(1.0f)
* rotMat
* Matrix::CreateTranslation(0, 0, -5);
Conjugate and Inverse
Quaternion q4 = Quaternion::CreateFromAxisAngle(Vector3::Up, 1.0f);
// Conjugate: same axis, negated angle (flips X,Y,Z, keeps W)
Quaternion conj = Quaternion::Conjugate(q4);
// Inverse: for unit quaternions, Inverse == Conjugate
Quaternion inv = Quaternion::Inverse(q4);
// Applying q then inv gets you back to identity
Quaternion result = q4 * inv; // ≈ Quaternion::Identity
Normalize
Quaternion multiplication can accumulate floating-point error over many frames. Normalise periodically (every frame is fine) to keep the rotation pure:
orientation_ = Quaternion::Normalize(orientation_);
Code example: smooth orbit with Slerp, spacecraft rotation
class SpacecraftDemo final : public Game {
public:
SpacecraftDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void Initialize() override {
Game::Initialize();
orientation_ = Quaternion::Identity;
targetOrient_ = Quaternion::Identity;
slerpT_ = 1.0f; // no pending interpolation
}
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
effect_->setVertexColorEnabled(true);
// ... load spacecraft mesh into vb_ ...
}
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.ElapsedGameTime.TotalSeconds());
auto kb = Keyboard::GetState();
// Queue a new target orientation on keypress
if (kb.IsKeyDown(Keys::Left) && slerpT_ >= 1.0f) {
targetOrient_ = orientation_ *
Quaternion::CreateFromAxisAngle(Vector3::Up,
MathHelper::ToRadians(-90.0f));
slerpT_ = 0.0f;
}
if (kb.IsKeyDown(Keys::Up) && slerpT_ >= 1.0f) {
targetOrient_ = orientation_ *
Quaternion::CreateFromAxisAngle(Vector3::Right,
MathHelper::ToRadians(-45.0f));
slerpT_ = 0.0f;
}
// Advance slerp
if (slerpT_ < 1.0f) {
slerpT_ = std::min(1.0f, slerpT_ + dt * 2.0f); // 0.5s transition
orientation_ = Quaternion::Slerp(orientation_, targetOrient_, slerpT_);
orientation_ = Quaternion::Normalize(orientation_);
}
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
Matrix world = Matrix::CreateFromQuaternion(orientation_);
Matrix view = Matrix::CreateLookAt(
Vector3(0, 3, 8), Vector3::Zero, Vector3::Up);
Matrix proj = Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f);
effect_->setWorld(world);
effect_->setView(view);
effect_->setProjection(proj);
gd.setVertexBuffer(*vb_);
for (auto& pass : effect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, primitiveCount_);
}
gd.Present();
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> vb_;
int primitiveCount_ = 0;
Quaternion orientation_;
Quaternion targetOrient_;
float slerpT_ = 1.0f;
};
Orbit camera with Quaternion.Slerp
// Smooth camera orbit: interpolate between two look directions
Quaternion camOrbit = Quaternion::CreateFromAxisAngle(
Vector3::Up, totalTime_ * 0.3f);
// Camera starts behind the player, orbits around Y
Vector3 baseCamOffset(0, 3, 8);
Vector3 orbitedOffset = Vector3::Transform(baseCamOffset,
Matrix::CreateFromQuaternion(camOrbit));
Matrix view = Matrix::CreateLookAt(
orbitedOffset, Vector3::Zero, Vector3::Up);