2014-12-08 05:42:21 -04:00

535 lines
9.8 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 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 a, b, c, d
-- copy
local arg = {...}
if #arg == 1 and type(arg[1]) == "table" then
a = arg[1].a
b = arg[1].b
c = arg[1].c
d = arg[1].d
-- four numbers
elseif #arg == 4 then
a = arg[1]
b = arg[2]
c = arg[3]
d = arg[4]
-- real number plus vector
elseif #arg == 2 then
a = arg[1]
b = arg[2].x
c = arg[2].y
d = arg[2].z
else
error("Incorrect number of arguments to quaternion")
end
return setmetatable({ a = a or 0, b = b or 0, c = c or 0, d = d or 0 }, quaternion)
end
function quaternion:to_axis_angle()
local tmp = self
if tmp.a > 1 then
tmp = tmp:normalize()
end
local angle = 2 * math.acos(tmp.a)
local s = math.sqrt(1-tmp.a*tmp.a)
local x, y, z
if s < constants.FLT_EPSILON then
x = tmp.b
y = tmp.c
z = tmp.d
else
x = tmp.b / s -- normalize axis
y = tmp.c / s
z = tmp.d / s
end
return angle, { x, y, z }
end
--[[
Test if we are zero.
--]]
function quaternion:is_zero()
-- are we the zero vector
if self.a ~= 0 or self.b ~= 0 or self.c ~= 0 or self.d ~= 0 then
return false
end
return true
end
--[[
Test if we are real.
--]]
function quaternion:is_real()
-- are we the zero vector
if self.b ~= 0 or self.c ~= 0 or self.d ~= 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.a ~= 0 then
return false
end
return true
end
--[[
Test for equality.
--]]
function quaternion:is_eq(q)
if self.a ~= q.a or self.b ~= q.b or self.c ~= q.c or self.d ~= q.d then
return false
end
return true
end
--[[
Defines the "==" shortcut.
--]]
function quaternion:__eq(q)
return self:is_eq(q)
end
--[[
The inner product of two quaternions.
--]]
function quaternion:dot(q)
return self.a * q.a + self.b * q.b + self.c * q.c + self.d * q.d
end
--[[
Makes "q .. p" return the inner product.
Probably a bad choice and likely to be removed in future versions.
--]]
function quaternion:__concat(q)
return self:dot(q)
end
--[[
Length of a quaternion.
--]]
function quaternion:len()
return math.sqrt(self:lensq())
end
--[[
Often enough to know the length squared, which is quicker.
--]]
function quaternion:lensq()
return self.a * self.a + self.b * self.b + self.c * self.c + self.d * self.d
end
--[[
Normalize a quaternion to have length 1, if possible.
--]]
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.a * l,self.b * l,self.c * l, self.d * l)
end
--[[
Add two quaternions. Or add a real number to a quaternion.
--]]
function quaternion:add(q)
if type(q) == "number" then
return new(self.a + q, self.b, self.c, self.d)
else
return new(self.a + q.a, self.b + q.b, self.c + q.c, self.d + q.d)
end
end
--[[
q + p
--]]
function quaternion:__add(q)
return self:add(q)
end
--[[
Subtraction
--]]
function quaternion:subtract(q)
return new(self.a - q.a, self.b - q.b, self.c - q.c, self.d - q.d)
end
--[[
q - p
--]]
function quaternion:__sub(q)
return self:subtract(q)
end
--[[
Negation (-q)
--]]
function quaternion:__unm()
return self:scale(-1)
end
--[[
Length (#q)
--]]
function quaternion:__len()
return self:len()
end
--[[
Multiply the current quaternion on the right.
Corresponds to composition of rotations.
--]]
function quaternion:multiplyRight(q)
local a,b,c,d
a = self.a * q.a - self.b * q.b - self.c * q.c - self.d * q.d
b = self.a * q.b + self.b * q.a + self.c * q.d - self.d * q.c
c = self.a * q.c - self.b * q.d + self.c * q.a + self.d * q.b
d = self.a * q.d + self.b * q.c - self.c * q.b + self.d * q.a
return new(a,b,c,d)
end
--[[
q * p
--]]
function quaternion:__mul(q)
if type(q) == "number" then
return self:scale(q)
elseif type(q) == "table" then
return self:multiplyRight(q)
end
end
--[[
Multiply the current quaternion on the left.
Corresponds to composition of rotations.
--]]
function quaternion:multiplyLeft(q)
return q:multiplyRight(self)
end
--[[
Conjugation (corresponds to inverting a rotation).
--]]
function quaternion:conjugate()
return new(self.a, - self.b, - self.c, - self.d)
end
function quaternion:co()
return self:conjugate()
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:lensq()
q = q:scale(1/l)
return q
end
--[[
Integral powers.
--]]
function quaternion:power(n)
if n ~= math.floor(n) then
error("Only able to do integer powers")
return false
end
if n == 0 then
return new(1,0,0,0)
elseif n > 0 then
return self:multiplyRight(self:power(n-1))
elseif n < 0 then
return self:reciprocal():power(-n)
end
end
--[[
q^n
This is overloaded so that a non-number exponent returns the
conjugate. This means that one can write things like q^* or q^"" to
get the conjugate of a quaternion.
--]]
function quaternion:__pow(n)
if type(n) == "number" then
return self:power(n)
else
return self:conjugate()
end
end
--[[
Division: q/p
--]]
function quaternion:__div(q)
if type(q) == "number" then
return self:scale(1/q)
elseif type(q) == "table" then
return self:multiplyRight(q:reciprocal())
end
end
--[[
Returns the real part.
--]]
function quaternion:real()
return self.a
end
--[[
Returns the vector (imaginary) part as a Vec3 object.
--]]
function quaternion:vector()
return Vec3(self.b, self.c, self.d)
end
--[[
Represents a quaternion as a string.
--]]
function quaternion:__tostring()
local s
local im ={{self.b,"i"},{self.c,"j"},{self.d,"k"}}
if self.a ~= 0 then
s = self.a
end
for k,v in pairs(im) do
if v[1] ~= 0 then
if s then
if v[1] > 0 then
if v[1] == 1 then
s = s .. " + " .. v[2]
else
s = s .. " + " .. v[1] .. v[2]
end
else
if v[1] == -1 then
s = s .. " - " .. v[2]
else
s = s .. " - " .. (-v[1]) .. v[2]
end
end
else
if v[1] == 1 then
s = v[2]
elseif v[1] == - 1 then
s = "-" .. v[2]
else
s = v[1] .. v[2]
end
end
end
end
if s then
return s
else
return "0"
end
end
--[[
(Not a class function)
Returns a quaternion corresponding to the current gravitational vector
so that after applying the corresponding rotation, the y-axis points
in the gravitational direction and the x-axis is in the plane of the
iPad screen.
When we have access to the compass, the x-axis behaviour might change.
--]]
--[[
function quaternion.gravity()
local gxy, gy, gygxy, a, b, c, d
if Gravity.x == 0 and Gravity.y == 0 then
return new(1,0,0,0)
else
gy = - Gravity.y
gxy = math.sqrt(math.pow(Gravity.x,2) + math.pow(Gravity.y,2))
gygxy = gy/gxy
a = math.sqrt(1 + gxy - gygxy - gy)/2
b = math.sqrt(1 - gxy - gygxy + gy)/2
c = math.sqrt(1 - gxy + gygxy - gy)/2
d = math.sqrt(1 + gxy + gygxy + gy)/2
if Gravity.y > 0 then
a = a
b = b
end
if Gravity.z < 0 then
b = - b
c = - c
end
if Gravity.x > 0 then
c = - c
d = - d
end
return new(a,b,c,d)
end
end
--]]
--[[
Converts a rotation to a quaternion. The first argument is the angle
to rotate, the rest must specify an axis, either as a Vec3 object or
as three numbers.
--]]
function quaternion.rotation(a,...)
local q,c,s
q = new(0,...)
q = q:normalize()
c = math.cos(a/2)
s = math.sin(a/2)
q = q:scale(s)
q = q:add(c)
return q
end
function quaternion:to_euler()
local sqw = self.a*self.a
local sqx = self.b*self.b
local sqy = self.c*self.c
local sqz = self.d*self.d
-- if normalised is one, otherwise is correction factor
local unit = sqx + sqy + sqz + sqw
local test = self.b*self.c + self.d*self.a
local pitch, yaw, roll
-- singularity at north pole
if test > 0.499*unit then
yaw = 2 * math.atan2(self.b,self.a)
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.b,self.a)
pitch = -math.pi/2
roll = 0
return pitch, yaw, roll
end
yaw = math.atan2(2*self.c*self.a-2*self.b*self.d , sqx - sqy - sqz + sqw)
pitch = math.asin(2*test/unit)
roll = math.atan2(2*self.b*self.a-2*self.c*self.d , -sqx + sqy - sqz + sqw)
return pitch, roll, yaw
end
--[[
The unit quaternion.
--]]
function quaternion.unit()
return new(1,0,0,0)
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 },
{ __call = function(_, ...) return new(...) end })