Introducing a library for Quaternions and Quaternion Springs
TLDR: This is collection of classes, one for quaternions, one for quaternion springs, and one for regular springs.
About
Hello! For the past 3 or so months I have been working on a library for handling Quaternions and Quaternion Springs (with a vector equivalent), inspired by the spring class on NevermoreEngine.
What are Quaternions?
Quaternions are a neat way to compactly store rotations using just 4 numbers, compared to rotation matrices (CFrames) which use 9 numbers. Since there are less numbers involved, this also means that calculations are much faster to complete, and we can manipulate rotations in interesting ways and do things which would otherwise be difficult using other representations. Quaternions do not suffer from gimbal lock, and also have a double-cover property, which means that q0
and -q0
encode the same rotation.
Note that there are actually four common ways to represent rotations: Euler Angles, Rotation Matrices, Quaternions, and Axis Angles (including compact axis-angles, also known as a rotation vector).
Luckily, there are video guides on youtube explaining how Quaternions work:
3Blue1Brown - Visualizing quaternions (4d numbers) with stereographic projection
3Blue1Brown - Quaternions and 3d rotation, explained interactively
What are the pros and cons of each representation?
Euler Angles:
Pros:
- Intuitive: Euler angles are easy to understand because they directly represent rotations around the XYZ axes.
- Easy to Visualize: They provide a clear mental picture of how an object is oriented.
- Simple for Limited Rotations: Useful for simple cases with limited or constrained rotations.
Cons:
- Gimbal Lock: Euler angles can suffer from gimbal lock, where certain combinations of rotations lead to a loss of one degree of freedom and numerical instability.
- Order Dependency: The order in which rotations are applied (e.g., pitch, yaw, roll) can significantly affect the final orientation, leading to ambiguity.
- Interpolation Challenges: Interpolating between Euler angles can result in undesirable motion paths.
Rotation Matrices (CFrame’s Rotational Component)
Pros:
- Direct Transformation: Rotation matrices directly apply rotations to vectors through matrix multiplication, making them easy to use for transforming points.
- No Gimbal Lock: They do not suffer from gimbal lock and are free from order dependence.
- Well-Defined Composition: Composing multiple rotations is straightforward using matrix multiplication.
Cons:
- Numerical Instability: Numerical errors can accumulate in rotation matrices, leading to potential issues with accuracy over multiple operations.
- Storage and Memory: Storing a 3x3 matrix for each rotation can be memory-intensive in certain applications.
- Interpolation Challenges: Interpolating between matrices can be complex and may require additional techniques like spherical linear interpolation (slerp).
Quaternions:
Pros:
- No Gimbal Lock: Quaternions do not suffer from gimbal lock, making them suitable for continuous interpolation.
- Efficient Interpolation: Quaternion interpolation (slerp) provides smooth and efficient transitions between rotations.
- Compact Representation: Quaternions are more memory-efficient than rotation matrices, requiring only four components.
- Numerical Stability: They are less prone to numerical instability compared to rotation matrices.
Cons:
- Complexity: Understanding and visualizing quaternions can be challenging for those not familiar with them.
- Additional Conversions: Converting between quaternions and other representations may be necessary for certain operations.
- Normalization Required: Quaternions must be normalized to maintain their properties, which can add computational overhead.
Axis-Angle:
Pros:
- Intuitive: Axis-angle representation is intuitive, as it combines a familiar axis direction with an angle of rotation.
- Efficient Interpolation: Interpolation between axis-angle representations is straightforward and computationally efficient.
- Compact: Like quaternions, it is a relatively compact representation, requiring only four components.
Cons:
- Conversion Overhead: Converting between axis-angle and other representations (e.g., rotation matrices) can involve trigonometric functions.
- Lack of Direct Transformation: Unlike rotation matrices, axis-angle representations do not directly transform vectors; they require conversion to other forms for this purpose.
- Ambiguity: Multiple representations can describe the same rotation (180-degree rotations, in particular, have multiple valid axis-angle representations).
Why use Quaternions over CFrames?
Here’s the thing: You probably don’t need to use Quaternions. You can do everything you might possibly want to do with CFrames (rotation matrices).
So why bother making an entire library and post about Quaternions?
If you remember earlier in this post, I said that quaternions use just 4 numbers to store rotations compared to rotation matrices 9 numbers. While the memory requirements in the modern world are practically irrelevant, there are performance considerations that should be made. When combining rotations with quaternions, you can save 17 floating point operations compared to multiplying rotation matrices together. While this may seem insignificant, this can quickly add up if you are combining multiple rotations and is a consideration worth making. It’s also important to consider that its far quicker and more efficient to renormalize a quaternion than it is to renormalize a rotation matrix. Quaternions can just be treated as 4D vectors that need have unit length (1), meanwhile rotation matrices may need orthonormalization applied to them, which is a comparatively expensive computation to carry out.
However, the flip side to this is trying to compute a rotated vector from a quaternion takes 26 more floating point operations, so you should consider whether a bulk of your operations is chaining rotations together or computing rotated vectors.
For example, say you have a computation that needs to track the orientation of a rigid body, its all about chaining operations so you can reduce the number of operations needed (There is no effective reduction in memory since roblox requires quaternions be converted into CFrames to be used in parts.)
Github and using the libraries
You can find these on my github. When you open the repository, simply go to the src
folder and select the class you desire to use!
Simply copy the raw contents clicking the overlayed square icons (next to the button called raw
) on the top right of the code screen, creating a new module script in studio, and then pasting the code into that file, and renaming it to the appropriate class name!
And then use the library as following:
local Quaternion = require(game.PATH.TO.Quaternion)
I also created a documentation website you can refer to, however note that all of the documentation is in the code when you copy it. This is just a pretty representation of it!
What’s that about Quaternion springs?
Oh that’s right, I almost forgot! This comes with quaternion springs which allow you to achieve some pretty cool behaviours! Here is a video demonstration, which combines the quaternion spring with a vector spring:
What’s special about these is that they are lazily evaluated which means they only update when they are indexed, and you can specify different things like damping and speed of the spring
Here’s an example of how you might use this:
local runService = game:GetService("RunService")
local replicatedStorage = game:GetService("ReplicatedStorage")
local Spring = require(replicatedStorage.Spring)
local Quaternion = require(replicatedStorage.Quaternion)
local QuaternionSpring = require(replicatedStorage.QuaternionSpring)
local targetPart = workspace.TargetPart
local targetPart2 = workspace.TargetPart2
local springPart = workspace.SpringPart
local springPart2 = workspace.SpringPart2
local rq = Quaternion.RandomQuaternion(0)
local initialQuaternion = Quaternion.fromCFrame(targetPart.CFrame)
local speed = 8
local damping = 0.4
local vectorSpring = Spring.new(targetPart.Position)
local quaternionSpring = QuaternionSpring.new(initialQuaternion)
vectorSpring.s = speed
vectorSpring.d = damping
quaternionSpring.s = speed
quaternionSpring.d = damping
workspace.select:GetPropertyChangedSignal("Value"):Connect(function()
local targ = workspace.select.Value and targetPart or targetPart2
targ.CFrame = CFrame.new(targ.Position) * rq():ToCFrame()
local tcf = targ.CFrame
local tq = Quaternion.fromCFrame(tcf)
vectorSpring.t = tcf.Position
quaternionSpring.t = tq
end)
local stepped
stepped = runService.Stepped:Connect(function(_, dt)
springPart2.CFrame = CFrame.new(vectorSpring.p) * quaternionSpring.p:ToCFrame()
end)
(this is the code used to run the above example)
Thanks for reading!
I hope this library is of use to some of you great people in the roblox community! Don’t forget, this is open source software so if you see a mistake feel free to contribute!
Credits for help along the way:
There are things that wouldn’t have been possible without these good people: