Rotations
rotation() or quaternion() create a rotation from its components:
myRot = rotation(1, 1, 1, 0) or myRot = quaternion(1, 1, 1, 0).
- It’s not possible to assign a value to a component. We need to create a new rotation.
- We can get a component from the return value of a function, not only from a variable.
- Components are stored in 32 bits floats.
- Vectors can use uppercase components (X, Y, Z) but rotations can’t. Let’s use always lowercase components.
-- rotations (SLua)
local myRot = rotation(1, 1, 1, 0)
local myRot2 = quaternion(2, 2, 2, 0)
myRot = rotation(myRot.x, myRot.y, myRot.z, -myRot.s) -- we can't assign a value to a component
local rotS = ll.GetRot().s
torotation() or toquaternion() create a rotation from a string:
myRot = torotation("<1, 1, 1, 0>") or myRot = quaternion( torotation("<1, 1, 1, 0>")).
- they return nil when are passed an invalid rotation.
We have the datatypes rotation and quaternion, but internally only exists the type quaternion, rotation is just an alias:
if typeof(myVar) == "quaternion" then -- NOT "rotation", it would never happen!
-- do rotations stuff
end
typeof( ZERO_ROTATION ) -- > quaternion. There is no constant ZERO_QUATERNION.
The rotation (or quaternion) library
SLua has a library with functions for rotations. We can’t use the :method notation, we have to call them on rotation (or quaternion):
rotation.create( x, y, z, s ) : creates a new rotation.
- same as rotation( x, y, z, s )
rotation.conjugate( rot ) : returns a rotation, the “inverse” or opposite of the rotation. If rot rotates 90° left, conjugate(rot) rotates 90° right.
-- rotation.conjugate() written in SLua
local function conjugate(rot)
return rotation(-rot.x, -rot.y, -rot.z, rot.s)
end
rotation.dot( rot1, rot2 ) : returns a number, the dot product of the rotations. It measures how “similar” two rotations are.
- If the result is 1 or -1, the rotations are identical.
- If the result is 0, they are 180 degrees apart.
-- rotation.dot() written in SLua
local function dot(a, b)
return a.x * b.x + a.y * b.y + a.z * b.z + a.s * b.s
end
rotation.magnitude( rot ) : returns a number, the magnitude of the rotation.
-- rotation.magnitude() written in SLua
local function magnitude(rot)
return math.sqrt(rot.x * rot.x + rot.y * rot.y + rot.z * rot.z + rot.s * rot.s)
end
rotation.normalize( rot ) : returns a rotation, the normalized version (unit rotation) of the rotation.
-- rotation.normalize() written in SLua
local function normalize(rot)
local mag = math.sqrt(rot.x * rot.x + rot.y * rot.y + rot.z * rot.z + rot.s * rot.s) -- magnitude
if mag > 0.0000001 then
if math.abs(1 - mag) > 0.000001 then
local oomag = 1 / mag
return rotation(rot.x * oomag, rot.y * oomag, rot.z * oomag, rot.s * oomag)
else
return rot
end
end
return rotation(0, 0, 0 ,1)
end
rotation.slerp( rot1, rot2, interpolation ) : returns a rotation, uses Spherical Linear Interpolation to calculate a smooth rotation between a start rotation and a target rotation based on a percent (interpolation between 0 and 1).
-- rotation.slerp() written in SLua
local function slerp(a, b, u)
local cos_t = a.x * b.x + a.y * b.y + a.z * b.z + a.s * b.s -- dot product
local bflip = false
if cos_t < 0.0 then
cos_t = -cos_t
bflip = true
end
local alpha, beta
if (1 - cos_t) < 0.00001 then
beta = 1 - u
alpha = u
else
local theta = math.acos(cos_t)
local sin_t = math.sin(theta)
beta = math.sin(theta - u * theta) / sin_t
alpha = math.sin(u * theta) / sin_t
end
if bflip then
beta = -beta
end
return rotation(
beta * a.x + alpha * b.x,
beta * a.y + alpha * b.y,
beta * a.z + alpha * b.z,
beta * a.s + alpha * b.s
)
end
rotation.tofwd( rot ) : returns a vector, the Forward vector (local X-axis) of the rotation.
- same as ll.Rot2Fwd( rot )
rotation.toleft( rot ) : returns a vector, the Left vector (local Y-axis) of the rotation.
- same as ll.Rot2Left( rot )
rotation.toup( rot ) : returns a vector, the Up vector (local Z-axis) of the rotation.
- same as ll.Rot2Up( rot )
rotation.identity : returns the identity rotation.
- same as ZERO_ROTATION
Rotations in tables
rotation is a reference type. Its value is stored on the memory heap, and variables or table nodes store a reference to this memory location.
When we use a rotation as a key in a table its reference is stored in the table. When we access the table, SLua looks for the reference and not for its value.
-- rotations as table keys
local t = {}
-- storing a rotation
t[rotation(1, 0, 0, 0)] = true
print(t[rotation(1, 0, 0, 0)]) -- > nil
for k, v in t do
print(k, v)
end
-- > <1, 0, 0, 0> true
-- removing the rotation by its value
-- but rotation() creates a new rotation with a new reference
-- the new reference is not in the table and setting to nil does nothing
t[rotation(1, 0, 0, 0)] = nil
for k, v in t do
print(k, v)
end
-- > <1, 0, 0, 0> true
-- updating the rotation by its value
-- but again, it's a different reference
-- instead of updating, a new key is created with a rotation of the same value
t[rotation(1, 0, 0, 0)] = true
print(t[rotation(1, 0, 0, 0)]) -- > nil
for k, v in t do
print(k, v)
end
-- > <1, 0, 0, 0> true
-- > <1, 0, 0, 0> true
We can solve this by storing the rotations as strings:
-- rotations (as strings) as table keys
local t = {}
-- storing the rotation as string
t[tostring(rotation(1, 0, 0, 0))] = true
print(t[tostring(rotation(1, 0, 0, 0))]) -- > true
for k, v in t do
print(torotation(k), v)
end
-- > <1, 0, 0, 0> true
-- removing the rotation
t[tostring(rotation(1, 0, 0, 0))] = nil
for k, v in t do
print(torotation(k), v)
end
-- >
-- storing again
t[tostring(rotation(1, 0, 0, 0))] = true
-- updating the rotation
t[tostring(rotation(1, 0, 0, 0))] = true
print(t[tostring(rotation(1, 0, 0, 0))]) -- > true
for k, v in t do
print(torotation(k), v)
end
-- > <1, 0, 0, 0> true
string is also a reference type.
Why does it work with string and not with rotation?
This works because string uses string interning (explained here string interning). All strings with the same value use the same reference. uuid also uses string interning.
Another solution is to use a metatable:
-- rotations (with a metatable) as table keys
local rotation_mt = {
__index = function(t, key) -- not found in the table when reading
for k, v in t do
if k == key then
return v -- return the value if the key is in the table
end
end
return nil -- otherwise return nil
end,
__newindex = function(t, key, val) -- not found in the table when writing
for k, v in t do
if k == key then
t[k] = val -- update the value if the key is in the table
return
end
end
rawset(t, key, val) -- otherwise create a new key
end
}
local t = setmetatable({}, rotation_mt)
-- storing a rotation
t[rotation(1, 0, 0, 0)] = true
print(t[rotation(1, 0, 0, 0)]) -- > true -- value from __index
for k, v in t do
print(k, v)
end
-- > <1, 0, 0, 0> true
-- removing the rotation
t[rotation(1, 0, 0, 0)] = nil -- removed in __newindex
for k, v in t do
print(k, v)
end
-- >
-- storing again
t[rotation(1, 0, 0, 0)] = true
-- updating the rotation
t[rotation(1, 0, 0, 0)] = true -- updated in __newindex
print(t[rotation(1, 0, 0, 0)]) -- > true -- value from __index
for k, v in t do
print(k, v)
end
-- > <1, 0, 0, 0> true
The operator ==, with reference types, compares the references.
Why does it work with k == key in the metatable but not when accessing the table with a rotation?
This works because a rotation is created with a metatable, with the metamethods: __eq, __add, __sub, __mul, __div, __unm, __tostring.
The operator == calls the __eq metamethod. Accessing a table only looks for the reference and doesn’t use __eq.
Data types, like tables, can have metatables.
-- just an example, don't do this!!!
local r1 = rotation(1, 0, 0, 0)
local r2 = rotation(1, 0, 0, 0)
local r3 = rotation(0, 1, 0, 0)
print(getmetatable(r1).__eq(r1, r2)) -- > true
print(getmetatable(r1).__eq(r2, r3)) -- > false
A rotation uses 16 bytes in memory and another 16 bytes for its reference stored in a table or a variable.
Each call to rotation() creates a new reference, which consumes additional memory:
local t = {}
local m = ll.GetUsedMemory()
for i = 1, 16 do
table.insert(t, (select(math.random(3), rotation(0.707, 0, 0, 0.707), rotation(0, 0.707, 0, 0.707), rotation(0, 0, 0.707, 0.707))))
end
print(ll.GetUsedMemory() - m) -- > 512 (16*16 indexes + 16*16 rotations)
Storing rotations that are used several times in variables saves memory:
local t = {}
local m = ll.GetUsedMemory()
local X90 = rotation(0.707, 0, 0, 0.707)
local Y90 = rotation(0, 0.707, 0, 0.707)
local Z90 = rotation(0, 0, 0.707, 0.707)
for i = 1, 16 do
table.insert(t, (select(math.random(3), X90, Y90, Z90)))
end
print(ll.GetUsedMemory() - m) -- > 304 (16*16 indexes + 16*3 rotations)