Skip to content

Instantly share code, notes, and snippets.

@loopspace
Created January 11, 2014 19:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loopspace/8375858 to your computer and use it in GitHub Desktop.
Save loopspace/8375858 to your computer and use it in GitHub Desktop.
Spitfire II Release v1.4 -Using quaternions to orient a plane.
Spitfire II Tab Order Version: 1.4
------------------------------
This file should not be included in the Codea project.
#Main
#Andrew
#Quaternion
#Q
#Plane
--Andrew
--the RotateAndDraw function needs code
VERSION = 1.4
function setup()
if AutoGist then
autogist = AutoGist("Spitfire II","Using quaternions to orient a plane.",VERSION)
autogist:backup(true)
end
--** Plane settings **
plane=Plane() --plane mesh
planeQ=vec4(1,0,0,0) --plane quaternion
planeQEXZ = vec4(1,0,0,0) --initialise quaternion to (1,0,0,0)
planeQEZX = vec4(1,0,0,0)
planeQT = vec4(1,0,0,0)
planePos=vec3(0,0,0) --starting position (plane doesn't change position)
--** Joystick sttings **
joyPos=vec2(5,5) --
joy=vec2(0,0) --rotation vector derived from joystick position, both values between -1 and +1
oldJoy = vec2(0,0)
--**Flight controls **
--for each of pitch and yaw, a true value below means that the plane moves directly in line
--with the joystick, with a maximum of 90 degrees, which I've called "slaved"
--false means that existing rotation is incremented by the joystick movement
--(this means the plane can rotate indefinitely, and loop the loop and do barrel rolls)
--yaw is assumed to always be incremented, based on the current roll setting, so there is no parameter,
--but I have coded for yaw to be allowed the option to be fixed, just in case
parameter.boolean("Pitch_Slaved",true,Reset)
parameter.boolean("Roll_Slaved",true,Reset)
parameter.boolean("Yaw_Slaved",false,Reset)
yaw = 1
modes = {"Absolute", "Increment", "Delta", "Parameter"}
parameter.boolean("Apply_Yaw",false,function() yaw = 1-yaw end)
parameter.integer("mode",1,#modes,1,function(m) Mode = modes[m] end)
parameter.watch("Mode")
--Yaw_Slaved=false --hard code this for now
--** rotation settings and limits **
--x, y, z map to pitch, yaw, roll
--limits for slave rotation
limit=vec3(90,90,90)*math.pi/180
--sensitivity of rotation for incremental rotation
sensitivity=vec3(1,1,1)
parameter.watch("joy")
parameter.watch("deltaJoy")
atRest = true
print(Q:EulerToQuat(1,1,0))
print(qEuler(0,1,1,{"Z","Y","X"})) -- YX,XZ
print(qRotation(1,0,1,0)*qRotation(1,1,0,0))
end
function Reset() --reset quaternion if settings changed
planeQEXZ = vec4(1,0,0,0) --initialise quaternion to (1,0,0,0)
planeQEZX = vec4(1,0,0,0)
planeQT = vec4(1,0,0,0)
end
function draw()
background(179, 205, 220, 255)
fill(255)
perspective()
camera(0,0,100,0,0,0)
RotateAndDraw()
DrawJoystick()
end
--this function adjusts the rotation based on joystick position, and draws the plane
function RotateAndDraw()
--joy.x, joy.y hold joystick values ranging from -1 to +1
--plane position is in the (unchanging) vec3 planePos
--your task, should you choose to accept it, is to rotate and draw the plane
--the pitch and roll rotations may either be slaved to absolute joystick values
--each has a parameter which the user can set individually
--if slaved, the rotation is in line with the joystick position
--pitch = -joy.y * limit.x (negative because pulling the stick back lifts the nose)
--yaw is not slaved, so not applicable
--roll = joy.x * limit.z
--if not slaved, the rotation is increased from its previous value, by
--pitch = -joy.y * sensitivity.x
--yaw = joy.x * sensitivity.y
--roll = joy.x * sensitivity.z
deltaJoy = joy - oldJoy
oldJoy = joy
if inTouch then
atRest = false
local v
if mode == 1 then
v = vec3(-joy.y*limit.x,yaw*joy.x*limit.y,joy.x*limit.z)
planeQEXZ = qEuler(v.y,v.x,v.z,{"Y","X","Z"})
planeQEZX = qEuler(v.y,v.z,v.x,{"Y","Z","X"})
planeQT = qTangent(v)
elseif mode == 2 then
v = vec3(-joy.y*sensitivity.x*DeltaTime,
yaw*joy.x*sensitivity.y*DeltaTime,
joy.x*sensitivity.z*DeltaTime)
planeQEXZ = planeQEXZ * qEuler(v.y,v.x,v.z,{"Y","X","Z"})
planeQEZX = planeQEZX * qEuler(v.y,v.z,v.x,{"Y","Z","X"})
planeQT = planeQT * qTangent(v)
elseif mode == 3 then
v = vec3(-deltaJoy.y*limit.x,
yaw*deltaJoy.x*limit.y,
deltaJoy.x*limit.z)
planeQEXZ = planeQEXZ * qEuler(v.y,v.x,v.z,{"Y","X","Z"})
planeQEZX = planeQEZX * qEuler(v.y,v.z,v.x,{"Y","Z","X"})
planeQT = planeQT * qTangent(v)
elseif mode == 4 then
v = vec3()
if Pitch_Slaved then
v.x = -deltaJoy.y*limit.x
else
v.x = -joy.y*sensitivity.x*DeltaTime
end
if Yaw_Slaved then
v.y = yaw*deltaJoy.x*limit.y
else
v.y = yaw*joy.x*sensitivity.y*DeltaTime
end
if Roll_Slaved then
v.z = deltaJoy.x*limit.z
else
v.z = joy.x*sensitivity.z*DeltaTime
end
planeQEXZ = planeQEXZ * qEuler(v.y,v.x,v.z,{"Y","X","Z"})
planeQEZX = planeQEZX * qEuler(v.y,v.z,v.x,{"Y","Z","X"})
planeQT = planeQT * qTangent(v)
end
else
if not atRest then
if not inSlerp then
planeSlerpEXZ = planeQEXZ:make_slerp()
planeSlerpEZX = planeQEZX:make_slerp()
planeSlerpT = planeQT:make_slerp()
inSlerp = true
stime = ElapsedTime + 5
end
if stime - ElapsedTime > 0 then
planeQEXZ = planeSlerpEXZ(
smootherstep((stime - ElapsedTime)/5))
planeQEZX = planeSlerpEZX(
smootherstep((stime - ElapsedTime)/5))
planeQT = planeSlerpT(
smootherstep((stime - ElapsedTime)/5))
else
planeQEXZ = vec4(1,0,0,0)
planeQEZX = vec4(1,0,0,0)
planeQT = vec4(1,0,0,0)
inSlerp = false
atRest = true
end
end
end
pushMatrix()
translate(0,15,0)
translate(planePos)
rotate(planeQEXZ)
plane:draw()
popMatrix()
pushMatrix()
translate(planePos)
rotate(planeQEZX)
plane:draw()
popMatrix()
pushMatrix()
translate(0,-15,0)
translate(planePos)
rotate(planeQT)
plane:draw()
popMatrix()
ortho()
viewMatrix(matrix())
resetMatrix()
fontSize(40)
textMode(CORNER)
text("Euler X,Z",20,HEIGHT/2+250)
text("Euler Z,X",20,HEIGHT/2+50)
text("Tangential",20,HEIGHT/2-150)
end
function smoothstep(t)
return t*t*(3 - 2*t)
end
function smootherstep(t)
return t*t*t*(t*(t*6 - 15) + 10)
end
--** Utility functions - joystick and touch ***
--manage joystick
function touched(t)
if t.state~=ENDED then
local v = vec2(t.x,t.y)
if v:dist(centre)>radius-joystick then
v = (v - centre):normalize()*(radius - joystick) + centre
end --set x,y values for joy based on touch
joyPos=v - centre
joy=joyPos/(radius-joystick)
inTouch = true
else --reset joystick to centre when touch ends
joyPos=vec2(0,0)
joy=vec2(0,0)
inTouch = false
end
end
function DrawJoystick()
ortho()
viewMatrix(matrix())
radius,joystick=100,30
location=1 --1 for bottom left, 2 for bottom right
if location==1 then centre=vec2(radius+5,radius+5) else centre=vec2(WIDTH-radius-5,radius+5) end
pushStyle()
fill(160, 182, 191, 50)
stroke(118, 154, 195, 100)
strokeWidth(1)
ellipse(centre.x,centre.y,2*radius)
fill(78, 131, 153, 50)
ellipse(centre.x+joyPos.x,centre.y+joyPos.y,joystick*2)
popStyle()
end
--------------------------------------------------------------
--Project: Flying Library
--Version: alpha 1.0
--Author: Andrew Stacey, ignatz
--The MIT License (MIT)
--
--Copyright (c) 2014 Andrew Stacey, ignatz
--
--Permission is hereby granted, free of charge, to any person obtaining a copy
--of this software and associated documentation files (the "Software"), to deal
--in the Software without restriction, including without limitation the rights
--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--copies of the Software, and to permit persons to whom the Software is
--furnished to do so, subject to the following conditions:
--
--The above copyright notice and this permission notice shall be included in all
--copies or substantial portions of the Software.
--
--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--SOFTWARE.
-- Flying
function setup()
--** Plane settings **
plane=Plane() --plane mesh
planeQ=Q(plane) --plane quaternion
planePos=vec3(0,0,0) --starting position (plane doesn't change position)
--** Joystick sttings **
joyPos=vec2(5,5) --
joy=vec2(0,0) --rotation vector derived from joystick position, both values between -1 and +1
--**Flight controls **
--for each of pitch and yaw, a true value below means that the plane moves directly in line
--with the joystick, with a maximum of 90 degrees, which I've called "slaved"
--false means that existing rotation is incremented by the joystick movement
--(this means the plane can rotate indefinitely, and loop the loop and do barrel rolls)
--yaw is assumed to always be incremented, based on the current roll setting, so there is no parameter,
--but I have coded for yaw to be allowed the option to be fixed, just in case
parameter.boolean("Pitch_Slaved",true,Reset)
parameter.boolean("Roll_Slaved",true,Reset)
Yaw_Slaved=false --hard code this for now
--** rotation settings and limits **
--x, y, z map to pitch, yaw, roll
--limits for slave rotation
limit=vec3(90,90,90)
--sensitivity of rotation for incremental rotation
sensitivity=vec3(1,.5,1)
end
function Reset() --reset quaternion if settings changed
planeQ:Reset() --initialise quaternion to (1,0,0,0)
end
function draw()
background(179, 205, 220, 255)
fill(255)
perspective()
camera(0,0,100,0,0,0)
RotateAndDraw()
DrawJoystick()
end
function RotateAndDraw()
--** manage rotation changes here **
--joy.x, joy.y hold joystick values ranging from -1 to +1
--plane position is in the (unchanging) vec3 planePos
--my approach is to have two quaternions, one for slaved rotations and one for incremental
--the slaved one is created freshly each time, the incremental one is stored in the quaternion class
local slave,increment=vec3(0,0,0),vec3(0,0,0)
if Pitch_Slaved then
slave.x=-joy.y*limit.x --pitch up if joystick pulled back (down) and vice versa
else
increment.x=-joy.y*sensitivity.x
end
if Yaw_Slaved then
slave.y=joy.x*limit.y --yaw based on roll
else
--increment.y=joy.x*sensitivity.y
end
if Roll_Slaved then
slave.z=joy.x*limit.z
else
increment.z=joy.x*sensitivity.z
end
planeQ:AdjustRotation(slave,increment)
planeQ:Draw(plane,planePos)
end
--** Utility functions - joystick and touch ***
--manage joystick
function touched(t)
if t.state~=ENDED then
if vec2(t.x,t.y):dist(centre)>radius-joystick then return
else --set x,y values for joy based on touch
joyPos=vec2(t.x-centre.x,t.y-centre.y)
joy=vec2(joyPos.x,joyPos.y)/(radius-joystick)
end
else --reset joystick to centre when touch ends
joyPos=vec2(0,0)
joy=vec2(0,0)
end
end
function DrawJoystick()
ortho()
viewMatrix(matrix())
radius,joystick=100,30
location=1 --1 for bottom left, 2 for bottom right
if location==1 then centre=vec2(radius+5,radius+5) else centre=vec2(WIDTH-radius-5,radius+5) end
pushStyle()
fill(160, 182, 191, 50)
stroke(118, 154, 195, 100)
strokeWidth(1)
ellipse(centre.x,centre.y,2*radius)
fill(78, 131, 153, 50)
ellipse(centre.x+joyPos.x,centre.y+joyPos.y,joystick*2)
popStyle()
end
--plane
Plane=class()
function Plane:init()
a={}
c1=color(107, 139, 184, 255)
c2=color(72, 194, 81, 255)
c3=color(176, 159, 101, 255)
c4=color(255,0,0)
c5=color(187, 53, 53, 255)
a[1]=Block(3,3,25,c1,vec3(0,0,0))
a[2]=Block(15,1,5,c2,vec3(-9,1,-6))
a[3]=Block(15,1,5,c5,vec3(9,1,-6))
a[4]=Block(1,4,3,c2,vec3(0,3.5,11))
a[5]=Block(4,1,2,c2,vec3(-3.5,1,11))
a[6]=Block(4,1,2,c5,vec3(3.5,1,11))
a[7]=Block(3,2,5,c3,vec3(0,2.5,-6))
a[8]=Block(3,3,1,c4,vec3(0,0,-13))
self.blocks=a
end
function Plane:draw()
for i=1,#self.blocks do
self.blocks[i]:draw()
end
end
Block = class() --taken from 3D lab project
function Block:init(w,h,d,c,p,t,r) --width,height,depth,colour,position,texture, (optional) texture range (see above)
self.width=w
self.height=h
self.depth=d
self.color=c
self.pos=p
self.tex=t
--if no limits specified on which part of image to draw, set to 0,1
if r~=nil then self.texR=r else self.texR={0,0,1,1} end
self.blk=self:createBlock()
end
function Block:createBlock()
-- all the unique vertices that make up a block
--There are only 8 corners in a cube - we define them as vertices
--all measurements are taken from the centre of the block
--so bottom left front has x of -1/2 width, y of -1/2 height, and z of 1/2 depth
local w,h,d=self.width,self.height,self.depth
local x,y,z=self.pos.x,self.pos.y,self.pos.z
local v = {
vec3(x-0.5*w, y-0.5*h, z+0.5*d), -- Left bottom front
vec3(x+0.5*w, y-0.5*h, z+0.5*d), -- Right bottom front
vec3(x+0.5*w, y+0.5*h, z+0.5*d), -- Right top front
vec3(x-0.5*w, y+0.5*h, z+0.5*d), -- Left top front
vec3(x-0.5*w, y-0.5*h, z-0.5*d), -- Left bottom back
vec3(x+0.5*w, y-0.5*h, z-0.5*d), -- Right bottom back
vec3(x+0.5*w, y+0.5*h, z-0.5*d), -- Right top back
vec3(x-0.5*w, y+0.5*h, z-0.5*d), -- Left top back
}
local cubeverts = {
-- Front, Right, Back, Left, Top, Bottom
v[1], v[2], v[3], v[1], v[3], v[4],
v[2], v[6], v[7], v[2], v[7], v[3],
v[6], v[5], v[8], v[6], v[8], v[7],
v[5], v[1], v[4], v[5], v[4], v[8],
v[4], v[3], v[7], v[4], v[7], v[8],
v[5], v[6], v[2], v[5], v[2], v[1],
}
local cc={}
--assign colors
local c=self.color
local x=0.5
local BL=color(c.r,c.g,c.b) --bottom left
local BR=color(c.r,c.g,c.b) --bottom right
local TR=color(c.r*x,c.g*x,c.b*x) --top right
local TL=color(c.r*x,c.g*x,c.b*x) --top left
for i=1,6 do
cc[#cc+1]=BL
cc[#cc+1]=BR
cc[#cc+1]=TR
cc[#cc+1]=BL
cc[#cc+1]=TR
cc[#cc+1]=TL
end
--put it all together
local ms = mesh()
ms.vertices = cubeverts
if self.tex then
ms.texture = self.tex
ms.texCoords = cubetexCoords
else
ms.colors=cc
end
--ms:setColors(self.color)
return ms
end
function Block:draw()
self.blk:draw()
end
--Quaternions - dermot
--has two quaternions, one for incremental changes, and the other for total rotation
--the incremental one is updated and stored each draw
--the total one is based on the incremental one plus any slaved rotations, and is temporary
Q=class()
function Q:init()
self:Reset()
end
function Q:AdjustRotation(a,b) --a,b are vec3 angle rotations, a is slave, b is increment
local qa=Q:EulerToQuat(math.rad(a.x),math.rad(a.y),math.rad(a.z))
local qb=Q:EulerToQuat(math.rad(b.x),math.rad(b.y),math.rad(b.z))
self.incrementQ=Q:Multiply(qb,self.incrementQ):normalize() --add incremental rotation
self.totalQ=Q:Multiply(qa,self.incrementQ):normalize() --calculate total rotation
return self.totalQ --return in case it's needed
end
function Q:Rotate()
modelMatrix(self:QToMatrix(0,0,0,self.totalQ))
end
function Q:Draw(m,p)
pushMatrix()
p=p or vec3(0,0,0)
modelMatrix(self:QToMatrix(p.x,p.y,p.z,self.totalQ))
m:draw()
popMatrix()
end
function Q:EulerToQuat(p, y, r)
-- compute all trigonometric values used to compute the quaternion
local cp = math.cos(p/2)
local cy = math.cos(y/2)
local cr = math.cos(r/2)
local sp = math.sin(p/2)
local sy = math.sin(y/2)
local sr = math.sin(r/2)
local cycr = cy * cr
local sysr = sy * sr
-- combine values to generate the vector and scalar for the quaternion
--[[
ijk = -1
ij = k, jk = i, ki = j
kji = - kij = ikj = - ijk
(cp + sp i)
(cy + sy j)
(cr + sr k)
=
cp cy cr +
cp cy sr k +
cp sy cr j +
cp sy sr i + (jk)
sp cy cr i +
- sp cy sr j + (ik)
sp sy cr k + (ij)
- sp sy sr (ijk)
--]]
local w = cp * cycr + sp * sysr
local x = sp * cycr - cp * sysr
local y = cp * sy * cr + sp * cy * sr
local z = cp * cy * sr - sp * sy * cr
return vec4(w,x,y,z):normalize()
end
function Q:Multiply(q1,q2)
local w1,x1,y1,z1=q1.x,q1.y,q1.z,q1.w
local w2,x2,y2,z2=q2.x,q2.y,q2.z,q2.w
local w = w1*w2 - x1*x2 - y1*y2 - z1*z2
local x = w1*x2 + x1*w2 + y1*z2 - z1*y2
local y = w1*y2 - x1*z2 + y1*w2 + z1*x2
local z = w1*z2 + x1*y2 - y1*x2 + z1*w2
return vec4(w,x,y,z):normalize()
end
function Q:QToMatrix(px,py,pz)
local q=self.totalQ
-- calculate coefficients used for building the matrix
local w,x,y,z=q.x,q.y,q.z,q.w
local x2,y2,z2 = x + x,y + y,z + z
local xx,xy,xz = x * x2,x * y2,x * z2
local yy,yz,zz = y * y2,y * z2,z * z2
local wx,wy,wz = w * x2,w * y2,w * z2
-- fill in matrix positions with them
return matrix(1-(yy+zz),xy-wz,xz+wy,0,xy+wz,1-(xx+zz),yz-wx,0,xz-wy,yz+wx,1-(xx+yy),0,px,py,pz,1)
end
function Q:Reset()
self.incrementQ=vec4(1,0,0,0)
self.totalQ=vec4(1,0,0,0)
end
-- Quaternion library wherein vec4s are promoted to quaternions
-- Author: Andrew Stacey
-- Website: http://www.math.ntnu.no/~stacey/HowDidIDoThat/iPad/Codea.html
-- Licence: CC0 http://wiki.creativecommons.org/CC0
--[[
This provides code for handling rotations. Internally, the rotations
are implemented as quaternions which are in turn implemented by
extending the methods available for vec4 userdata.
Due to how vec4s are implemented, we cannot have methods beginning with
letters a, b, g, r, w, x, y, z
--]]
-- Simplistic error handling
local error = error or print
-- Localise the maths functions/constants that we use for faster lookup.
local abs = math.abs
local pow = math.pow
local sqrt = math.sqrt
local sin = math.sin
local cos = math.cos
local acos = math.acos
local pi = math.pi
local floor = math.floor
--[[
The function "is_a" extends the capabilities of the method "is_a" which is automatically defined by Codea for classes.
Parameters:
a: object to be tested
b: test
The tests work as follows.
1. If the type of b is a string, it is taken as the name of a type to test a against.
2. If the type of b is a table, it is assumed to be a class to test if a is an instance thereof.
3. If the type of b is a userdata, the test is to see if a is the same type of object.
4. If b is a function, then it is replaced by the value of that function.
--]]
local function is_a(a,b)
if type(b) == "function" then
b = b()
end
if type(b) == "string" then
return type(a) == b
end
if type(b) == "table" then
if type(a) == "table"
and a.is_a
and a:is_a(b) then
return true
else
return false
end
end
if type(b) == "userdata" then
if type(a) == "userdata" then
a = getmetatable(a)
b = getmetatable(b)
return a == b
else
return false
end
end
return false
end
-- Import the metatable of vec4s for extending
local mtq = getmetatable(vec4())
-- We run into difficulties if our vec4s contain NaNs or infs. This
-- tests for those.
mtq["is_finite"] = function(q)
if q.x < math.huge and q.x > -math.huge
and q.y < math.huge and q.y > -math.huge
and q.z < math.huge and q.z > -math.huge
and q.w < math.huge and q.w > -math.huge
then
return true
end
return false
end
--[[
Test if we are real.
--]]
mtq["is_real"] = function (self)
if self.y ~= 0 or self.z ~= 0 or self.w ~= 0 then
return false
end
return true
end
--[[
Test if the real part is zero.
--]]
mtq["is_imaginary"] = function (self)
if self.x ~= 0 then
return false
end
return true
end
--[[
Normalise a quaternion to have length 1, safely. Here "safely" means
that we ensure that we do not have NaNs or infs. If we do, the
returned quaternion is the unit.
--]]
mtq["normalise"] = function (q)
q = q:normalize()
if q:is_finite() then
return q
else
return vec4(1,0,0,0)
end
end
--[[
Add two quaternions inline, including promotion of a number.
q + p
--]]
local __add = mtq["__add"]
mtq["__add"] = function (a,b)
if is_a(a,"number") then
a = vec4(a,0,0,0)
end
if is_a(b,"number") then
b = vec4(b,0,0,0)
end
return __add(a,b)
end
--[[
Same for inline subtraction.
q - p
--]]
local __sub = mtq["__sub"]
mtq["__sub"] = function (a,b)
if is_a(a,"number") then
a = vec4(a,0,0,0)
end
if is_a(b,"number") then
b = vec4(b,0,0,0)
end
return __sub(a,b)
end
--[[
For inline multiplication, we also allow multiplication by a matrix.
In this case, we use the fact that we're viewing quaternions as
rotations and matrices as affine transformations, so the best result
of multiplication is as the matrix representing the appropriate
composition.
q * p
--]]
local __mul = mtq["__mul"]
mtq["__mul"] = function (a,b)
if is_a(a,"number") then
return __mul(a,b)
end
if is_a(b,"number") then
return __mul(a,b)
end
if is_a(a,matrix) then
return a:__mul(b:tomatrixleft())
end
if is_a(b,matrix) then
a = a:tomatrixleft()
return a:__mul(b)
end
local x,y,z,w
x = a.x * b.x - a.y * b.y - a.z * b.z - a.w * b.w
y = a.x * b.y + a.y * b.x + a.z * b.w - a.w * b.z
z = a.x * b.z - a.y * b.w + a.z * b.x + a.w * b.y
w = a.x * b.w + a.y * b.z - a.z * b.y + a.w * b.x
return vec4(x,y,z,w)
end
--[[
Conjugation (corresponds to inverting a rotation).
--]]
mtq["conjugate"] = function (q)
return vec4(q.x, - q.y, - q.z, - q.w)
end
mtq["co"] = mtq["conjugate"]
--[[
Inline division, including division of and by a number.
--]]
local __div = mtq["__div"]
mtq["__div"] = function (a,b)
if is_a(b,"number") then
return __div(a,b)
end
local l = b:lenSqr()
b = vec4(b.x/l,-b.y/l,-b.z/l,-b.w/l)
if is_a(a,"number") then
return vec4(a*b.x,a*b.y,a*b.z,a*b.w)
end
local x,y,z,w
x = a.x * b.x - a.y * b.y - a.z * b.z - a.w * b.w
y = a.x * b.y + a.y * b.x + a.z * b.w - a.w * b.z
z = a.x * b.z - a.y * b.w + a.z * b.x + a.w * b.y
w = a.x * b.w + a.y * b.z - a.z * b.y + a.w * b.x
return vec4(x,y,z,w)
end
--[[
Powers. Integer powers are defined iteratively. Non-integer powers
use the slerp function.
TODO: test whether the slerp is faster than the iterative method for
integer powers as well.
--]]
local function intpower(q,n)
if n ~= floor(n) then
error("Only able to do integer powers")
return false
end
if n == 0 then
return vec4(1,0,0,0)
elseif n > 0 then
return q:__mul(power(q,n-1))
elseif n < 0 then
local l = q:lenSqr()
q = vec4(q.x/l,-q.y/l,-q.z/l,-q.w/l)
return q:power(-n)
end
end
local function power(q,n)
if n == floor(n) then
return intpower(q,n)
end
local l = q:len()
q = q:normalise()
return l^n * q:slerp(n)
end
--[[
Inline exponentiation.
q^n
The behaviour depends on the type of the exponent:
* number: compute the power.
* vec4: conjugate by the exponent.
* other: return the conjugate.
--]]
mtq["__pow"] = function (q,n)
if is_a(n,"number") then
return power(q,n)
elseif is_a(n,vec4) then
return n:__mul(q):__div(n)
else
return q:conjugate()
end
end
--[[
Interpolation functions, we assume the input to be already normalised
for speed. If you cannot guarantee this, renormalise the input first.
The constructor functions do do the renormalisation.
Parameters:
q initial rotation
qq final rotation
t parameter (only for the direct functions)
If the input does not provide enough parameters, it is assumed that
the missing one is the initial rotation and that this should be taken
to be the identity rotation (represented by vec4(1,0,0,0)).
--]]
--[[
Linear interpolation, renormalised.
--]]
mtq["lerp"] = function (q,qq,t)
if not t then
return vec4(1,0,0,0):lerp(q,qq)
end
local v
if (q + qq):len() == 0 then
-- antipodal points, need a midpoint
v = vec4(q.y,-q.x,q.w,-q.z)
v = (1 - 2*t)*q + (1-abs(2*t-1))*v
else
v = (1-t)*q + t*qq
end
return v:normalise()
end
--[[
Spherical interpolation.
We have to be quite careful here not to emit anything with NaNs or
infs as there are several opportunities to create them due to the
limits of finite precision mathematics.
--]]
mtq["slerp"] = function (q,qq,t)
if not t then
return vec4(1,0,0,0):slerp(q,qq)
end
local v
if (q + qq):len() == 0 then
-- antipodal points, need a midpoint
v = vec4(q.y,-q.x,q.w,-q.z)
t = 2*t
elseif (q - qq):len() == 0 then
return q
else
v = qq
end
local ca = q:dot(v)
local sa = sqrt(1 - pow(ca,2))
if sa == 0 or sa ~= sa then
return q
end
local a = acos(ca)
sa = sin(a*t)/sa
v = (cos(a*t) - ca*sa)*q+ sa*v
return v
end
--[[
Constructor for normalised linear interpolation.
--]]
mtq["make_lerp"] = function (q,qq)
if not qq then
return vec4(1,0,0,0):make_lerp(q)
end
local v,w
w = q:normalise()
if (q + qq):len() == 0 then
-- antipodal points, need a midpoint
v = vec4(w.y,-w.x,w.w,-w.z)
return function(t)
local u = (1 - 2*t)*w + (1-abs(2*t-1))*v
return u:normalise()
end
else
v = qq:normalise()
return function(t)
local u = (1-t)*w + t*v
return u:normalise()
end
end
end
--[[
Spherical interpolation
--]]
mtq["make_slerp"] = function (q,qq)
if not qq then
q,qq = vec4(1,0,0,0),q
end
local v,f,u
if (q + qq):len() == 0 then
-- antipodal points, need a midpoint
v = vec4(q.y,-q.x,q.w,-q.z)
f = 2
elseif (q - qq):len() == 0 then
return function(t)
return q
end
else
v = qq
f = 1
end
v = v:normalise()
u = q:normalise()
local ca = u:dot(v)
local sa = sqrt(1 - pow(ca,2))
if sa == 0 or sa ~= sa then
return function(t)
return q
end
end
local a = acos(ca)
v = (v - ca*q)/sa
return function(t)
return cos(a*f*t)*q + sin(a*f*t)*v
end
end
--[[
Returns the real part.
--]]
mtq["toreal"] = function (q)
return q.x
end
--[[
Returns the vector (imaginary) part as a vec3 object.
--]]
mtq["vector"] = function (q)
return vec3(q.y, q.z, q.w)
end
mtq["tovector"] = mtq["vector"]
--[[
Represents a quaternion as a string.
--]]
mtq["tostring"] = function (q)
local s
local im = {{q.y,"i"},{q.z,"j"},{q.w,"k"}}
if q.x ~= 0 then
s = string.format("%.3f",q.x)
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 .. " + " ..
string.format("%.3f",v[1]) .. v[2]
end
else
if v[1] == -1 then
s = s .. " - " .. v[2]
else
s = s .. " - " ..
string.format("%.3f",-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 = string.format("%.3f",v[1]) .. v[2]
end
end
end
end
if s then
return s
else
return "0"
end
end
mtq["__concat"] = function (q,s)
if is_a(s,"string") then
return q:tostring() .. s
else
return q .. s:tostring()
end
end
--[[
Converts the quaternion to a matrix.
We distinguish between left and right actions. A rotation matrix
acting on the right is the transpose of the matrix on the left.
--]]
mtq["tomatrixleft"] = function (q)
q = q:normalise()
local a,b,c,d = q.x,q.y,q.z,q.w
local ab = 2*a*b
local ac = 2*a*c
local ad = 2*a*d
local bb = 2*b*b
local bc = 2*b*c
local bd = 2*b*d
local cc = 2*c*c
local cd = 2*c*d
local dd = 2*d*d
return matrix(
1 - cc - dd,
bc - ad,
ac + bd,
0,
bc + ad,
1 - bb - dd,
cd - ab,
0,
bd - ac,
cd + ab,
1 - bb - cc,
0,
0,0,0,1
)
end
mtq["tomatrixright"] = function (q)
q = q:normalise()
local a,b,c,d = q.x,-q.y,-q.z,-q.w
local ab = 2*a*b
local ac = 2*a*c
local ad = 2*a*d
local bb = 2*b*b
local bc = 2*b*c
local bd = 2*b*d
local cc = 2*c*c
local cd = 2*c*d
local dd = 2*d*d
return matrix(
1 - cc - dd,
bc - ad,
ac + bd,
0,
bc + ad,
1 - bb - dd,
cd - ab,
0,
bd - ac,
cd + ab,
1 - bb - cc,
0,
0,0,0,1
)
end
mtq["tomatrix"] = mtq["tomatrixright"]
--[[
Converts the quaternion to an "angle-axis" representation.
--]]
mtq["toangleaxis"] = function (q)
q = q:normalise()
local a = 2*acos(q.x)
local v = vec3(q.y,q.z,q.w)
if v == vec3(0,0,0) then
return 0,vec3(0,0,1)
end
return a,v:normalise()
end
--[[
The following code modifies various of Codea's functions to enable
them to take a quaternion as input. We have to be careful as for some
passing nil is distinct to passing an empty input.
The other issue is as to the distinction between left and right
actions of matrices. In OpenGL, matrices act on the right. However,
in standard linear algebra, the convention is for matrices to act on
the left.
--]]
local __modelMatrix = modelMatrix
function modelMatrix(m)
if m then
if is_a(m,vec4) then
m = m:tomatrixright()
end
return __modelMatrix(m)
else
return __modelMatrix()
end
end
local __applyMatrix = applyMatrix
function applyMatrix(m)
if m then
if is_a(m,vec4) then
m = m:tomatrixright()
end
return __applyMatrix(m)
else
return __applyMatrix()
end
end
local __viewMatrix = viewMatrix
function viewMatrix(m)
if m then
if is_a(m,vec4) then
m = m:tomatrixright()
end
return __viewMatrix(m)
else
return __viewMatrix()
end
end
local __projectionMatrix = projectionMatrix
function projectionMatrix(m)
if m then
if is_a(m,vec4) then
m = m:tomatrixright()
end
return __projectionMatrix(m)
else
return __projectionMatrix()
end
end
local __rotate = rotate
function rotate(a,x,y,z)
if is_a(a,vec4) then
local v
a,v = a:toangleaxis()
x,y,z = v.x,v.y,v.z
a = a*180/pi
end
__rotate(a,x,y,z)
end
local __translate = translate
function translate(x,y,z)
if not y then
x,y,z = x.x,x.y,x.z
end
__translate(x,y,z)
end
--[[
We also define some extensions to the vec3 type.
--]]
local mt = getmetatable(vec3())
-- Same as for vec4.
mt["is_finite"] = function(v)
if v.x < math.huge and v.x > -math.huge
and v.y < math.huge and v.y > -math.huge
and v.z < math.huge and v.z > -math.huge
then
return true
end
return false
end
--[[
Promote to a quaternion with 0 real part.
--]]
mt["toQuaternion"] = function (v)
return vec4(0,v.x,v.y,v.z)
end
--[[
Apply a quaternion as a rotation (assumes unit quaternion for speed)
using conjugation.
--]]
mt["applyQuaternion"] = function (v,q)
v = v:toQuaternion()
v = q:__mul(v)
v = v:__mul(q:conjugate())
return v:vector()
end
-- There is no native rotate method for a vec3.
mt["rotate"] = mt["applyQuaternion"]
-- We use the exponential notation for writing the action of a
-- quaternion on a vector (this is consistent with group theory
-- notation).
mt["__pow"] = function (v,q)
if is_a(q,vec4) then
return v:applyQuaternion(q)
end
return false
end
mt["__concat"] = function (u,s)
if is_a(s,"string") then
return u:__tostring() .. s
else
return u .. s:__tostring()
end
end
--[[
The rotateTo method produces a rotation that rotates the first vector
to the second about an axis that is perpendicular to both. So long as
the two are not collinear, the axis is unique. The angle is taken to
be the smallest angle. If the vectors point in opposite directions,
an orthogonal axis is chosen in such a way as to minimise precision
error.
TODO: Allow for a third parameter to specify the axis in case of
ambiguity.
--]]
mt["rotateTo"] = function (u,v)
if v:lenSqr() == 0 or u:lenSqr() == 0 then
return vec4(1,0,0,0)
end
u = u:normalise()
v = u + v:normalise()
if v:lenSqr() == 0 then
-- Opposite vectors, no canonical direction
local a,b,c = abs(u.x), abs(u.y), abs(u.z)
if a < b and a < c then
v = vec3(0,-u.z,u.y)
elseif b < c then
v = vec3(u.z,0,-u.x)
else
v = vec3(u.y,-u.x,0)
end
end
v = v:normalise()
local d = u:dot(v)
u = u:cross(v)
return vec4(d,u.x,u.y,u.z)
end
--[[
Safe renormalisation, as for quaternions. Except that there is no
"cannonical" unit vec3, so we return the unit vector in the z
direction. The rationale for that is that if this is an axis, this
vector represents the axis out of the screen.
--]]
mt["normalise"] = function (v)
v = v:normalize()
if v:is_finite() then
return v
else
return vec3(0,0,1)
end
end
--[[
Inline multiplication extended to allow for multiplication by a matrix.
We interpret the vec3 as vertical or horizontal depending on whether
the matrix is on the right or left. Multiplication by a matrix is
viewed as applying an affine transformation.
--]]
local __mulv = mt["__mul"]
mt["__mul"] = function(m,v)
if is_a(m,vec3) and is_a(v,"number") then
return __mulv(m,v)
end
if is_a(m,"number") and is_a(v,vec3) then
return __mulv(m,v)
end
if is_a(m,matrix) and is_a(v,vec3) then
local l = m[13]*v.x + m[14]*v.y + m[15]*v.z + m[16]
return vec3(
(m[1]*v.x + m[2]*v.y + m[3]*v.z + m[4])/l,
(m[5]*v.x + m[6]*v.y + m[7]*v.z + m[8])/l,
(m[9]*v.x + m[10]*v.y + m[11]*v.z + m[12])/l
)
end
if is_a(m,vec3) and is_a(v,matrix) then
m,v = v,m
local l = m[4]*v.x + m[8]*v.y + m[12]*v.z + m[16]
return vec3(
(m[1]*v.x + m[5]*v.y + m[9]*v.z + m[13])/l,
(m[2]*v.x + m[6]*v.y + m[10]*v.z + m[14])/l,
(m[3]*v.x + m[7]*v.y + m[11]*v.z + m[15])/l
)
end
end
--[[
Extensions to the matrix class.
--]]
local mtm = getmetatable(matrix())
--[[
Inline multiplication by either quaternion or vector.
--]]
local __mulm = mtm["__mul"]
mtm["__mul"] = function (m,mm)
if is_a(m,matrix) and is_a(mm,matrix) then
return __mulm(m,mm)
end
if is_a(m,matrix) and is_a(mm,vec4) then
return __mulm(m,mm:tomatrix())
end
if is_a(m,vec4) and is_a(mm,matrix) then
return __mulm(m:tomatrix(),mm)
end
if is_a(m,matrix) and is_a(mm,vec3) then
local l = m[13]*mm.x + m[14]*mm.y + m[15]*mm.z + m[16]
return vec3(
(m[1]*mm.x + m[2]*mm.y + m[3]*mm.z + m[4])/l,
(m[5]*mm.x + m[6]*mm.y + m[7]*mm.z + m[8])/l,
(m[9]*mm.x + m[10]*mm.y + m[11]*mm.z + m[12])/l
)
end
if is_a(m,vec3) and is_a(mm,matrix) then
local l = mm[4]*m.x + mm[8]*m.y + mm[12]*m.z + mm[16]
return vec3(
(mm[1]*m.x + mm[5]*m.y + mm[9]*m.z + mm[13])/l,
(mm[2]*m.x + mm[6]*m.y + mm[10]*m.z + mm[14])/l,
(mm[3]*m.x + mm[7]*m.y + mm[11]*m.z + mm[15])/l
)
end
end
-- Extending the rotate method to take a quaternion.
local __mrotate = mtm["rotate"]
mtm["rotate"] = function(m,a,x,y,z)
if is_a(a,vec4) then
a,x = a:toangleaxis()
x,y,z = x.x,x.y,x.z
end
m:__mrotate(a,x,y,z)
end
--[[
The following functions are intended to be more "user friendly" and
provide solutions to common problems.
The primary functions construct a rotation dependent on some initial
data. The simplest are to define a rotation from some other method of
specifying rotations: angle-axis or Euler angles. More complicated
are methods to define a rotation by giving two (orthogonal) frames and
computing the rotation to get from the first to the last. In
particular, there are certain common frames that might be expected to
be used frequently:
1. The initial orientation of the iPad.
2. A gravitational frame wherein the Gravity vector is "straight
down". This is not completely defined as there is currently no way to
choose a corresponding horizontal direction (access to the compass
would provide this). Two possibilities are: to choose the x-axis so
that it is always in the plane of the iPad, and to use RotationRate to
try to keep track of the initial x-axis.
3. A "DeltaOrientation" in which the change in orientation from one
frame to the next is used (this uses RotationRate).
There are obvious limits in accuracy in using these.
--]]
--[[
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.
--]]
local function qGravity()
local gxy, gy, gygxy, a, b, c, d
if Gravity.x == 0 and Gravity.y == 0 then
return vec4(1,0,0,0)
else
gy = - Gravity.y
gxy = sqrt(pow(Gravity.x,2) + pow(Gravity.y,2))
gygxy = gy/gxy
a = sqrt(1 + gxy - gygxy - gy)/2
b = sqrt(1 - gxy - gygxy + gy)/2
c = sqrt(1 - gxy + gygxy - gy)/2
d = 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 vec4(a,b,c,d)
end
end
function qrGravity(q)
local qg,qx
if not q then
qx = ReferenceFrame()
else
qx = q
end
local y = vec3(0,-1,0)^qx
qg = y:rotateTo(Gravity)
return qg*qx
end
mtq["gravity"] = qrGravity
local frame = {
vec3(1,0,0),
vec3(0,1,0),
vec3(0,0,1)
}
local qzyx = vec4(1,0,0,0)
--[[
Needs to be run once every frame!
--]]
function updateReferenceFrame(q)
local x,y,z
--[[
if CurrentOrientation == PORTRAIT then
x,y,z = -frame[1],-frame[2],-frame[3]
elseif CurrentOrientation == PORTRAIT_UPSIDE_DOWN then
x,y,z = frame[1],frame[2],-frame[3]
elseif CurrentOrientation == LANDSCAPE_LEFT then
x,y,z = frame[2],-frame[1],-frame[3]
elseif CurrentOrientation == LANDSCAPE_RIGHT then
x,y,z = -frame[2],frame[1],-frame[3]
end
--]]
x,y,z = unpack(frame)
local qz = qRotation(RotationRate.z*DeltaTime,z.x,z.y,z.z)
local qy = qRotation(RotationRate.y*DeltaTime,y.x,y.y,y.z)
local qx = qRotation(RotationRate.x*DeltaTime,x.x,x.y,x.z)
if q then
local qq = qz * qy * qx * q
q.x,q.y,q.z,q.w = qq.x,qq.y,qq.z,qq.w
else
qzyx = qz * qy * qx * qzyx
return qzyx
end
end
function ReferenceFrame(q)
return q or qzyx
end
mtq["updateReferenceFrame"] = updateReferenceFrame
mtq["ReferenceFrame"] = ReferenceFrame
--[[
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 qRotation(a,x,y,z)
local q,c,s
if not y then
x,y,z = x.x,x.y,x.z
end
q = vec4(0,x,y,z)
q = q:normalise()
if q == vec4(1,0,0,0) then
return q
end
c = cos(a/2)
s = sin(a/2)
q = q:__mul(s)
q = q:__add(c)
return q
end
--[[
The qEuler function handles conversion from Euler angles. As there
are many ways to specify Euler angles, we have to allow for quite a
variety. The table __euler contains the code necessary for allowing
all of this flexibility.
The last parameter is the specification, which is a table taken from
the alphabet "xXyYzZ". This ought to correspond to the axes of
rotation, using lowercase to mean the absolute frame and uppercase to
mean the transformed frame.
TODO: Test this.
If only two parameters are passed, the first is taken to be a vec3 or
a table containing the Euler angles.
--]]
local __euler = {}
__euler.x = function(q)
return vec3(1,0,0)^q
end
__euler.X = function(q)
return vec3(1,0,0)
end
__euler.y = function(q)
return vec3(0,1,0)^q
end
__euler.Y = function(q)
return vec3(0,1,0)
end
__euler.z = function(q)
return vec3(0,0,1)^q
end
__euler.Z = function(q)
return vec3(0,0,1)
end
function qEuler(a,b,c,v)
if c then
a = {a,b,c}
else
if is_a(a,vec3) then
a = {a.x,a.y,a.z}
end
v = b
end
v = v or {"x","y","z"}
local q = vec4(1,0,0,0)
local w
for k,u in ipairs(v) do
w = __euler[u](q)
q = q * qRotation(a[k],w)
end
return q
end
-- v is a tangent vector at the identity
function qTangent(x,y,z)
local q
if y then
q = vec4(0,x,y,z)
else
q = vec4(0,x.x,x.y,x.z)
end
local qn = q:normalise()
if qn == vec4(1,0,0,0) then
return qn
end
local a = q:len()/2
return cos(a)*vec4(1,0,0,0) + sin(a)*qn
end
function qRotationRate()
return qTangent(DeltaTime * RotationRate)
end
local iGravity = vec3(0,-1,0)
Frame = {
Roll = vec4(1,0,0,0),
Pitch = vec4(1,0,0,0),
Yaw = vec4(1,0,0,0),
Gravity = vec4(1,0,0,0),
Rotation = vec4(1,0,0,0),
AdjustedRotation = vec4(1,0,0,0),
RotationRate = vec4(1,0,0,0)
}
local update_fn
update_fn = function()
local q = vec3(iGravity.x,iGravity.y,0):rotateTo(
vec3(Gravity.x,Gravity.y,0))
Frame.Roll.x = q.x
Frame.Roll.y = q.y
Frame.Roll.z = q.z
Frame.Roll.w = q.w
q = vec3(0,iGravity.y,iGravity.z):rotateTo(
vec3(0,Gravity.y,Gravity.z))
Frame.Pitch.x = q.x
Frame.Pitch.y = q.y
Frame.Pitch.z = q.z
Frame.Pitch.w = q.w
q = vec3(iGravity.x,0,iGravity.z):rotateTo(
vec3(Gravity.x,0,Gravity.z))
Frame.Yaw.x = q.x
Frame.Yaw.y = q.y
Frame.Yaw.z = q.z
Frame.Yaw.w = q.w
q = iGravity:rotateTo(Gravity)
Frame.Gravity.x = q.x
Frame.Gravity.y = q.y
Frame.Gravity.z = q.z
Frame.Gravity.w = q.w
q = vec4(0,RotationRate.x,RotationRate.y,RotationRate.z)
local qn = q:normalise()
if qn ~= vec4(1,0,0,0) then
local a = q:len()*DeltaTime/2
qn = cos(a)*vec4(1,0,0,0) + sin(a)*qn
end
Frame.RotationRate.x = qn.x
Frame.RotationRate.y = qn.y
Frame.RotationRate.z = qn.z
Frame.RotationRate.w = qn.w
q = qn*Frame.Rotation
Frame.Rotation.x = q.x
Frame.Rotation.y = q.y
Frame.Rotation.z = q.z
Frame.Rotation.w = q.w
q = qn*Frame.AdjustedRotation
local g = iGravity^(q)
qn = g:rotateTo(iGravity)
q = q*qn
Frame.AdjustedRotation.x = q.x
Frame.AdjustedRotation.y = q.y
Frame.AdjustedRotation.z = q.z
Frame.AdjustedRotation.w = q.w
tween.delay(0,update_fn)
end
tween.delay(.5,function()
iGravity = vec3(Gravity.x,Gravity.y,Gravity.z)
tween.delay(0,update_fn)
end)
--[[
A suite of test functions.
--]]
function testRotations()
print("Rotation tests:")
local tolerance = 10^(-6)
local vars = [[local q1,q2,i,j,k
q1 = vec4(1,2,3,4)
q2 = vec4(.5,.5,-.5,-.5)
i = vec4(0,1,0,0)
j = vec4(0,0,1,0)
k = vec4(0,0,0,1)
q3 = vec4(1/math.sqrt(2),0,0,1/math.sqrt(2))
v1 = vec3(1,0,0)
v2 = vec3(0,1,0)
v3 = vec3(1,-1,2)
m = matrix(
10,10,5,1,
4,6,4,1,
1,3,3,1,
0,1,2,1
)
return ]]
local tests = {
{"q1 * q2", vec4(3,2,4,-1)},
{"q1 + q2", vec4(1.5,2.5,2.5,3.5)},
{"q1 - q2", vec4(.5,1.5,3.5,4.5)},
{"q1 / q2", vec4(-2,0,-1,5)},
{"q1^q2", vec4(1,-4,-2,3)},
{"q1^\"\"", vec4(1,-2,-3,-4)},
{"i * j", vec4(0,0,0,1)},
{"j * k", vec4(0,1,0,0)},
{"k * i", vec4(0,0,1,0)},
{"v1^q3", vec3(0,1,0)},
{"q3:tomatrixleft()*v1", vec3(0,1,0)},
{"v1:rotate(q3)", vec3(0,1,0)},
{"v2^q3", vec3(-1,0,0)},
{"q3:tomatrixleft()*v2", vec3(-1,0,0)},
{"v2:rotate(q3)", vec3(-1,0,0)},
{"qRotation(math.pi/2,0,0,1)", vec4(1/math.sqrt(2),0,0,1/math.sqrt(2))},
{"v1:rotateTo(v2)", vec4(1/math.sqrt(2),0,0,1/math.sqrt(2))},
{"v3 * m", vec3(8,11,9)/3},
{"m * v3", vec3(11,7,5)/4}
}
local t,a,m,n,tn
tn,n = 0,0
for k,u in ipairs(tests) do
tn = tn + 1
t = loadstring(vars .. u[1])
a = t()
if a == u[2] then
m = "OK"
n = n + 1
elseif a.dist and a:dist(u[2]) < tolerance then
m = "OK (" .. a:dist(u[2]) .. ")"
n = n + 1
else
m = "not OK (expected " .. u[2] .. ")"
end
print(u[1] .. " = " .. a .. " : " .. m)
end
print(n .. " tests passed out of " .. tn)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment