mirror of
https://github.com/mwpenny/portal64-still-alive.git
synced 2024-10-19 22:27:36 -04:00
655 lines
17 KiB
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,
|
|
} |