346 lines
7.6 KiB
Lua

-- quaternions
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
This is a class for handling quaternion numbers. It was originally
designed as a way of encoding rotations of 3 dimensional space.
--]]
local current_folder = (...):gsub('%.[^%.]+$', '') .. "."
local constants = require(current_folder .. "constants")
local vec3 = require(current_folder .. "vec3")
local quaternion = {}
quaternion.__index = quaternion
--[[
A quaternion can either be specified by giving the four coordinates as
real numbers or by giving the scalar part and the vector part.
--]]
local function new(...)
local x, y, z, w
-- copy
local arg = { select(1, ...) or 0, select(2, ...) or 0, select(3, ...) or 0, select(4, ...) or 0 }
local n = select('#', ...)
if n == 1 and type(arg[1]) == "table" then
x = arg[1].x or arg[1][1]
y = arg[1].y or arg[1][2]
z = arg[1].z or arg[1][3]
w = arg[1].w or arg[1][4]
-- four numbers
elseif n == 4 then
x = arg[1]
y = arg[2]
z = arg[3]
w = arg[4]
-- real number plus vector
elseif n == 2 then
x = arg[1].x or arg[1][1]
y = arg[1].y or arg[1][2]
z = arg[1].z or arg[1][3]
w = arg[2]
else
print(string.format("%s %s %s %s", select(1, ...), select(2, ...), select(3, ...), select(4, ...)))
error("Incorrect number of arguments to quaternion")
end
return setmetatable({ x = x or 0, y = y or 0, z = z or 0, w = w or 1 }, quaternion)
end
function quaternion.__add(a, b)
if type(b) == "number" then
return new(a.x, a.y, a.z, a.w + b)
end
return new(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w)
end
function quaternion.__sub(a, b)
return new(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w)
end
function quaternion:__unm()
return self:scale(-1)
end
function quaternion.__mul(a, b)
-- quat * number
if type(b) == "number" then
return a:scale(b)
-- quat * quat
elseif type(b) == "table" and b.w then
local x, y, z, w
x = a.x * b.w + a.w * b.x + a.y * b.z - a.z * b.y
y = a.y * b.w + a.w * b.y + a.z * b.x - a.x * b.z
z = a.z * b.w + a.w * b.z + a.x * b.y - a.y * b.x
w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
return new(x, y, z, w)
else
local qv = vec3(a.x, a.y, a.z)
local uv = qv:cross(b)
local uuv = qv:cross(uv)
return b + ((uv * a.w) + uuv) * 2
end
end
function quaternion.__div(a, b)
if type(b) == "number" then
return a:scale(1 / b)
elseif type(b) == "table" then
return a * b:reciprocal()
end
end
function quaternion:__pow(n)
if n == 0 then
return self.unit()
elseif n > 0 then
return self * self^(n-1)
elseif n < 0 then
return self:reciprocal()^(-n)
end
end
function quaternion.__eq(a, b)
if a.x ~= b.x or a.y ~= b.y or a.z ~= b.z or a.w ~= b.w then
return false
end
return true
end
function quaternion:__tostring()
return string.format("(%0.3f,%0.3f,%0.3f,%0.3f)", self.x, self.y, self.z, self.x)
end
function quaternion:unpack()
return self.x, self.y, self.z, self.w
end
function quaternion.unit()
return new(0, 0, 0, 1)
end
function quaternion:to_axis_angle()
if self.w > 1 then
self = self:normalize()
end
local angle = 2 * math.acos(self.w)
local s = math.sqrt(1-self.w*self.w)
local x, y, z
if s < constants.FLT_EPSILON then
x = self.x
y = self.y
z = self.z
else
x = self.x / s -- normalize axis
y = self.y / s
z = self.z / s
end
return angle, { x, y, z }
end
-- Test if we are zero
function quaternion:is_zero()
-- are we the zero vector
if self.x ~= 0 or self.y ~= 0 or self.z ~= 0 or self.w ~= 0 then
return false
end
return true
end
-- Test if we are real
function quaternion:is_real()
-- are we the zero vector
if self.x ~= 0 or self.y ~= 0 or self.z ~= 0 then
return false
end
return true
end
-- Test if the real part is zero
function quaternion:is_imaginary()
-- are we the zero vector
if self.w ~= 0 then
return false
end
return true
end
-- The dot product of two quaternions
function quaternion.dot(a, b)
return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
end
function quaternion.cross(a, b)
return new(
a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
a.w * b.y + a.y * b.w + a.z * b.x - a.x * b.z,
a.w * b.z + a.z * b.w + a.x * b.y - a.y * b.x,
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
)
end
-- Length of a quaternion
function quaternion:len()
return math.sqrt(self:len2())
end
-- Length squared of a quaternion
function quaternion:len2()
return self:dot(self)
end
-- Normalize a quaternion to have length 1
function quaternion:normalize()
if self:is_zero() then
error("Unable to normalize a zero-length quaternion")
return false
end
local l = 1 / self:len()
return self:scale(l)
end
-- Scale the quaternion
function quaternion:scale(l)
return new(self.x * l, self.y * l, self.z * l, self.w * l)
end
-- Conjugation (corresponds to inverting a rotation)
function quaternion:conjugate()
return new(-self.x, -self.y, -self.z, self.w)
end
function quaternion:inverse()
return self:conjugate():normalize()
end
-- Reciprocal: 1/q
function quaternion:reciprocal()
if self.is_zero() then
error("Cannot reciprocate a zero quaternion")
return false
end
local q = self:conjugate()
local l = self:len2()
q = q:scale(1 / l)
return q
end
-- Returns the real part
function quaternion:real()
return self.w
end
function quaternion:clone()
return new(self.x, self.y, self.z, self.w)
end
-- Returns the vector (imaginary) part as a Vec3 object
function quaternion:to_vec3()
return vec3(self.x, self.y, self.z)
end
--[[
Converts a rotation to a quaternion. The first argument is the angle
to rotate, the second must specify an axis as a Vec3 object.
--]]
local function rotate(angle, axis)
local len = axis:len()
if math.abs(len - 1) > 0.001 then
axis.x = axis.x / len
axis.y = axis.y / len
axis.z = axis.z / len
end
local sin = math.sin(angle * 0.5)
local cos = math.cos(angle * 0.5)
return new(axis.x * sin, axis.y * sin, axis.z * sin, cos)
end
function quaternion:to_euler()
local sqx = self.x*self.x
local sqy = self.y*self.y
local sqz = self.z*self.z
local sqw = self.w*self.w
-- if normalised is one, otherwise is correction factor
local unit = sqx + sqy + sqz + sqw
local test = self.x*self.y + self.z*self.w
local pitch, yaw, roll
-- singularity at north pole
if test > 0.499*unit then
yaw = 2 * math.atan2(self.x,self.w)
pitch = math.pi/2
roll = 0
return pitch, yaw, roll
end
-- singularity at south pole
if test < -0.499*unit then
yaw = -2 * math.atan2(self.x,self.w)
pitch = -math.pi/2
roll = 0
return pitch, yaw, roll
end
yaw = math.atan2(2*self.y*self.w-2*self.x*self.z , sqx - sqy - sqz + sqw)
pitch = math.asin(2*test/unit)
roll = math.atan2(2*self.x*self.w-2*self.y*self.z , -sqx + sqy - sqz + sqw)
return pitch, roll, yaw
end
-- http://keithmaggio.wordpress.com/2011/02/15/math-magician-lerp-slerp-and-nlerp/
-- non-normalized rotations do not work out for quats!
function quaternion.lerp(a, b, s)
local v = a + (b - a) * s
return v:normalize()
end
-- http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/
function quaternion.slerp(a, b, s)
local function clamp(n, low, high) return math.min(math.max(n, low), high) end
local dot = a:dot(b)
-- http://www.gamedev.net/topic/312067-shortest-slerp-path/#entry2995591
if dot < 0 then
a = -a
dot = -dot
end
if dot > constants.DOT_THRESHOLD then
return quaternion.lerp(a, b, s)
end
clamp(dot, -1, 1)
local theta = math.acos(dot) * s
local c = (b - a * dot):normalize()
return a * math.cos(theta) + c * math.sin(theta)
end
-- return quaternion
-- the module
return setmetatable({ new = new, rotate = rotate },
{ __call = function(_, ...) return new(...) end })