Portal64/skelatool64/lua/sk_math.lua

655 lines
17 KiB
Lua

--- @module sk_math
local Vector3 = {}
local Box3 = {}
local Quaternion = {}
local Color4 = {}
local Plane3 = {}
--- creates a new 3d vector
--- @function vector3
--- @tparam number x the x value for the vector
--- @tparam number y the x value for the vector
--- @tparam number z the x value for the vector
--- @treturn Vector3
local function vector3(x, y, z)
return setmetatable({ x = x or 0, y = y or 0, z = z or 0 }, Vector3)
end
--- determines if the input is a Vector3
--- @function isVector3
--- @tparam any obj
--- @treturn boolean
local function isVector3(obj)
return type(obj) == 'table' and type(obj.x) == 'number' and type(obj.y) == 'number' and type(obj.z) == 'number' and obj.w == nil
end
--- creates a box3
--- @function box3
--- @tparam Vector3 min
--- @tparam Vector3 max
--- @treturn Box3
local function box3(min, max)
return setmetatable({ min = min or vector3(), max = max or vector3() }, Box3)
end
--- creates a new quaternion
--- @function quaternion
--- @tparam number x the x value for the quaternion
--- @tparam number y the x value for the quaternion
--- @tparam number z the x value for the quaternion
--- @tparam number w the x value for the quaternion
--- @treturn Quaternion
local function quaternion(x, y, z, w)
return setmetatable({ x = x, y = y, z = z, w = w }, Quaternion)
end
--- creates a new quaternion with an axis and an angle in radians
--- @function quaternion
--- @tparam Vector3 axis
--- @tparam number angle
--- @treturn Quaternion
local function axis_angle(axis, angle)
local normalized_axis = axis:normalized()
local cos_angle = math.cos(angle * 0.5)
local sin_angle = math.sin(angle * 0.5)
return quaternion(
normalized_axis.x * sin_angle,
normalized_axis.y * sin_angle,
normalized_axis.z * sin_angle,
cos_angle
)
end
--- determines if the input is a Quaternion
--- @function isQuaternion
--- @tparam any obj
--- @treturn boolean
local function isQuaternion(obj)
return type(obj) == 'table' and type(obj.x) == 'number' and type(obj.y) == 'number' and type(obj.z) == 'number' and type(obj.w) == 'number'
end
--- creates a new Plane3
--- @function plane3
--- @tparam Vector3 normal the normal of the plane
--- @tparam number d the distance to the origin
--- @treturn Plane3
local function plane3(normal, d)
return setmetatable({ normal = normal, d = d }, Plane3)
end
--- creates a new Plane3 using a point and a normal
--- @function plane3
--- @tparam Vector3 normal the normal of the plane
--- @tparam Vector3 point a point on the plane
--- @treturn Plane3
local function plane3_with_point(normal, point)
if not isVector3(normal) then
error('plane3_with_point expected vector as first operand got ' .. type(b), 2)
end
if not isVector3(point) then
error('plane3_with_point expected vector as second operand got ' .. type(b), 2)
end
return setmetatable({ normal = normal, d = -normal:dot(point) }, Plane3)
end
--- @type Vector3
--- @tfield number x
--- @tfield number y
--- @tfield number z
Vector3.__index = Vector3;
--- @function __eq
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__eq(a, b)
if (type(a) == 'number') then
return a == b.x and a == b.y and a == b.z
end
if (type(b) == 'number') then
return a.x == b and a.y == b and a.z + b
end
if (not isVector3(b)) then
error('Vector3.__eq expected another vector as second operand', 2)
end
return a.x == b.x and a.y == b.y and a.z == b.z
end
--- @function __add
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__add(a, b)
if (type(a) == 'number') then
return vector3(a + b.x, a + b.y, a + b.z)
end
if (type(b) == 'number') then
return vector3(a.x + b, a.y + b, a.z + b)
end
if (not isVector3(b)) then
error('Vector3.__add expected another vector as second operand got ' .. type(b), 2)
end
return vector3(a.x + b.x, a.y + b.y, a.z + b.z)
end
--- @function __sub
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__sub(a, b)
if (type(a) == 'number') then
return vector3(a - b.x, a - b.y, a - b.z)
end
if (type(b) == 'number') then
return vector3(a.x - b, a.y - b, a.z - b)
end
if (not isVector3(b)) then
error('Vector3.__sub expected another vector as second operand', 2)
end
if (a == nil) then
print(debug.traceback())
end
return vector3(a.x - b.x, a.y - b.y, a.z - b.z)
end
--- @function __unm
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__unm(a)
return vector3(-a.x, -a.y, -a.z)
end
--- @function __mul
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__mul(a, b)
if (type(a) == 'number') then
return vector3(a * b.x, a * b.y, a * b.z)
end
if (type(b) == 'number') then
return vector3(a.x * b, a.y * b, a.z * b)
end
if (not isVector3(b)) then
error('Vector3.__mul expected another vector or number as second operand got ' .. type(b), 2)
end
return vector3(a.x * b.x, a.y * b.y, a.z * b.z)
end
--- @function __div
--- @tparam number|Vector3 b
--- @treturn Vector3
function Vector3.__div(a, b)
if (type(a) == 'number') then
return vector3(a / b.x, a / b.y, a / b.z)
end
if (type(b) == 'number') then
local mul_value = 1 / b
return vector3(a.x * mul_value, a.y * mul_value, a.z * mul_value)
end
if (not isVector3(b)) then
error('Vector3.__div expected another vector as second operand', 2)
end
return vector3(a.x / b.x, a.y / b.y, a.z / b.z)
end
function Vector3.__tostring(v)
return 'vector3(' .. v.x .. ', ' .. v.y .. ', ' .. v.z .. ')'
end
--- Get the magnitude of the vector
--- @function magnitude
--- @treturn number
function Vector3.magnitude(v)
return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
end
--- Get the magnitude squared of the vector
--- @function magnitudeSqrd
--- @treturn number
function Vector3.magnitudeSqrd(v)
return v.x * v.x + v.y * v.y + v.z * v.z
end
--- Returns a normalized version of this vector
--- @function normalized
--- @treturn Vector3
function Vector3.normalized(v)
local magnitude = v:magnitude()
if (magnitude == 0) then
return vector3(0, 0, 0)
end
return v / magnitude
end
--- Get the magnitude of the vector
--- @function min
--- @tparam Vector3 other vector
--- @treturn Vector3
function Vector3.min(a, b)
return vector3(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z))
end
--- Get the magnitude of the vector
--- @function max
--- @tparam Vector3 other vector
--- @treturn Vector3
function Vector3.max(a, b)
return vector3(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z))
end
--- Returns a copy of this vector
--- @function max
--- @treturn Vector3
function Vector3.copy(vector)
return vector3(vector.x, vector.y, vector.z)
end
--- Get the dot product between two vectors
--- @function dot
--- @tparam Vector3 b
--- @treturn number
function Vector3.dot(a, b)
if not isVector3(b) then
error('Vector3.dot expected another vector as second operand', 2)
end
return a.x * b.x + a.y * b.y + a.z * b.z
end
--- Get the cross product between two vectors
--- @function cross
--- @tparam Vector3 b
--- @treturn Vector3
function Vector3.cross(a, b)
return vector3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
)
end
--- Linearly interpolates between two points
--- @function lerp
--- @tparam Vector3 b
--- @treturn Vector3
function Vector3.lerp(a, b, lerp)
if not isVector3(b) then
error('Vector3.lerp expected another vector as second operand', 2)
end
if type(lerp) ~= 'number' then
error('Vector3.lerp expected number as third operand', 2)
end
return a * (1 - lerp) + b * lerp
end
--- @type Box3
--- @tfield Vector3 min
--- @tfield Vector3 max
Box3.__index = Box3;
--- Returns the point inside or on the box that is nearest to the given point
--- @function nearest_point_in_box
--- @tparam Vector3 point
--- @treturn Vector3
function Box3.nearest_point_in_box(box, point)
return Vector3.min(box.max, point):max(box.min)
end
--- Returns true of the two bounding boxes have some volume in common
--- @function overlaps
--- @tparam Vector3|Box3 box_or_point
--- @treturn boolean
function Box3.overlaps(box, box_or_point)
if isVector3(box_or_point) then
return box_or_point.x >= box.min.x and box_or_point.x <= box.max.x and
box_or_point.y >= box.min.y and box_or_point.y <= box.max.y and
box_or_point.z >= box.min.z and box_or_point.z <= box.max.z
end
return box.min.x < box_or_point.max.x and box_or_point.min.x < box.max.x and
box.min.y < box_or_point.max.y and box_or_point.min.y < box.max.y and
box.min.z < box_or_point.max.z and box_or_point.min.z < box.max.z;
end
--- @function __mul
--- @tparam number|Box3 b
--- @treturn Box3
function Box3.__mul(a, b)
if type(a) == 'number' then
return box3(a * b.min, a * b.max)
end
if type(b) == 'number' then
return box3(a.min * b, a.max * b)
end
return box3(a.min * b.min, a.max * b.max)
end
--- Gets the distance from the box to the point
--- If the box contains the point then the negative distance to
--- the nearest edge is returned
--- @function distance_to_point
--- @tparam Vector3 point
--- @treturn number
function Box3.distance_to_point(box, point)
local nearest_point = Box3.nearest_point_in_box(box, point)
if (nearest_point == point) then
local max_offset = Vector3.__sub(point, box.max)
local min_offset = Vector3.__sub(box.min, point)
return math.max(
max_offset.x, max_offset.y, max_offset.z,
min_offset.x, min_offset.y, min_offset.z
)
end
return (nearest_point - point):magnitude()
end
--- Linearly interpolates between the min and max of the box
--- @function lerp
--- @treturn Vector3
function Box3.lerp(box, lerp)
return Vector3.lerp(box.min, box.max, lerp)
end
--- Finds a lerp value, x, such that box:lerp(x) == pos
--- @function pos
--- @treturn Vector3
function Box3.unlerp(box, pos)
return (pos - box.min) / (box.max - box.min)
end
--- Returns the box that encloses the two boxes
--- @function union
--- @tparam Vector3|Box3 box_or_point
--- @treturn Box3
function Box3.union(box, box_or_point)
if isVector3(box_or_point) then
return box3(box.min:min(box_or_point), box.max:max(box_or_point))
end
return box3(box.min:min(box_or_point.min), box.max:max(box_or_point.max))
end
--- Returns the box that overlaps both a and b
--- @function intersection
--- @tparam Box3 b
--- @treturn Box3
function Box3.intersection(a, b)
local min = a.min:max(b.min)
local max = a.max:min(b.max)
max = min:max(max)
return box3(min, max)
end
--- Returns the volume of the box
--- @function volume
--- @treturn number
function Box3.volume(box)
local side_length = box.max - box.min
return side_length.x * side_length.y * side_length.z
end
--- Returns the area of the box
--- @function area
--- @treturn number
function Box3.area(box)
local side_length = box.max - box.min
return (side_length.x * side_length.y + side_length.y * side_length.z + side_length.z * side_length.x) * 2
end
--- Returns a deep copy of the box
--- @function copy
--- @treturn Box3
function Box3.copy(box)
return box3(box.min:copy(), box.max:copy())
end
function Box3.__tostring(b)
return 'box3(' .. tostring(b.min) .. ', ' .. tostring(b.max) .. ')'
end
--- @type Quaternion
--- @tfield number x
--- @tfield number y
--- @tfield number z
--- @tfield number w
Quaternion.__index = Quaternion;
--- @function conjugate
--- @treturn Quaternion
function Quaternion.conjugate(input)
return quaternion(-input.x, -input.y, -input.z, input.w)
end
function Quaternion.__tostring(v)
return 'quaternion(' .. v.x .. ', ' .. v.y .. ', ' .. v.z .. ', ' .. v.w .. ')'
end
function Quaternion.__mul(a, b)
if (isVector3(b)) then
local result = a * quaternion(b.x, b.y, b.z, 0) * a:conjugate()
return vector3(result.x, result.y, result.z)
elseif (isQuaternion(b)) then
return quaternion(
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
)
else
error("Expected vector3 or quaternion got " .. tostring(b), 2)
end
end
--- @function slerp
--- @tparam Quaternion b
--- @tparam number t
--- @treturn Quaternion
function Quaternion.slerp(a, b, t)
-- calc cosine theta
local cosom = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
-- adjust signs (if necessary)
local endQ = quaternion(b.x, b.y, b.z, b.w);
if cosom < 0 then
cosom = -cosom
-- Reverse all signs
endQ.x = -endQ.x
endQ.y = -endQ.y
endQ.z = -endQ.z
endQ.w = -endQ.w
end
-- Calculate coefficients
local sclp, sclq;
if (1 - cosom) > 0.0001 then
-- Standard case (slerp)
local omega = math.acos(cosom); -- extract theta from dot product's cos theta
local sinom = math.sin( omega);
sclp = math.sin( (1 - t) * omega) / sinom;
sclq = math.sin( t * omega) / sinom;
else
-- Very close, do linear interp (because it's faster)
sclp = 1 - t;
sclq = t;
end
return quaternion(
sclp * a.x + sclq * endQ.x,
sclp * a.y + sclq * endQ.y,
sclp * a.z + sclq * endQ.z,
sclp * a.w + sclq * endQ.w
)
end
--- creates a new 4d color
--- @function color
--- @tparam number r
--- @tparam number g
--- @tparam number b
--- @tparam number a
--- @treturn Color4
local function color4(r, g, b, a)
return setmetatable({ r = r or 1, g = g or 1, b = b or 1, a = a or 1 }, Color4)
end
--- determines if the input is a Vector3
--- @function isColor4
--- @tparam any obj
--- @treturn boolean
local function isColor4(obj)
return type(obj) == 'table' and type(obj.r) == 'number' and type(obj.g) == 'number' and type(obj.b) == 'number' and type(obj.a) == 'number'
end
--- @type Color4
--- @tfield number r
--- @tfield number g
--- @tfield number b
--- @tfield number a
Color4.__index = Color4;
--- @function __eq
--- @tparam number|Color4 b
--- @treturn Color4
function Color4.__eq(a, b)
if (type(a) == 'number') then
return a == b.r and a == b.g and a == b.b and a == b.a
end
if (type(b) == 'number') then
return a.r == b and a.g == b and a.b + b and a.a == b
end
if (not isColor4(b)) then
error('Color4.__eq expected another vector as second operand', 2)
end
return a.r == b.r and a.g == b.g and a.b == b.b and a.a == a.a
end
--- @function __add
--- @tparam number|Color4 b
--- @treturn Color4
function Color4.__add(a, b)
if (type(a) == 'number') then
return color4(a + b.r, a + b.g, a + b.b, a + b.a)
end
if (type(b) == 'number') then
return color4(a.r + b, a.g + b, a.b + b, a.a + b)
end
if (not isColor4(b)) then
error('Color4.__add expected another vector as second operand got ' .. type(b), 2)
end
return color4(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a)
end
--- @function __sub
--- @tparam number|Color4 b
--- @treturn Color4
function Color4.__sub(a, b)
if (type(a) == 'number') then
return color4(a - b.r, a - b.g, a - b.b, a - b.a)
end
if (type(b) == 'number') then
return color4(a.r - b, a.g - b, a.b - b, a.a - b)
end
if (not isColor4(b)) then
error('Color4.__sub expected another vector as second operand', 2)
end
if (a == nil) then
print(debug.traceback())
end
return color4(a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a)
end
--- @function __mul
--- @tparam number|Color4 b
--- @treturn Color4
function Color4.__mul(a, b)
if (type(a) == 'number') then
return color4(a * b.r, a * b.g, a * b.b, a * b.a)
end
if (type(b) == 'number') then
return color4(a.r * b, a.g * b, a.b * b, a.a * b)
end
if (not isColor4(b)) then
error('Color4.__mul expected another vector or number as second operand got ' .. type(b), 2)
end
return color4(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a)
end
function Color4.__tostring(v)
return 'color4(' .. v.r .. ', ' .. v.g .. ', ' .. v.b .. ', ' .. v.a .. ')'
end
--- Linearly interpolates between two points
--- @function lerp
--- @tparam Color4 b
--- @treturn Color4
function Color4.lerp(a, b, lerp)
return a * (1 - lerp) + b * lerp
end
--- @type Plane3
--- @tfield Vector3 normal
--- @tfield number d
Plane3.__index = Plane3;
function Plane3.distance_to_point(plane, point)
return plane.normal:dot(point) + plane.d
end
return {
vector3 = vector3,
Vector3 = Vector3,
isVector3 = isVector3,
box3 = box3,
Box3 = Box3,
Quaternion = Quaternion,
quaternion = quaternion,
axis_angle = axis_angle,
isQuaternion = isQuaternion,
color4 = color4,
isColor4 = isColor4,
plane3 = plane3,
plane3_with_point = plane3_with_point,
Plane3 = Plane3,
}