mirror of
https://github.com/Ukendio/jecs.git
synced 2026-02-04 15:15:21 +00:00
188 lines
4.3 KiB
Text
188 lines
4.3 KiB
Text
|
|
--!native
|
||
|
|
--!optimize 2
|
||
|
|
--!strict
|
||
|
|
|
||
|
|
local Spring_Vector3 = require("./vector3")
|
||
|
|
|
||
|
|
export type Spring = {
|
||
|
|
type: "CFrame",
|
||
|
|
position: Spring_Vector3.Spring,
|
||
|
|
rotation: Spring_Rotation,
|
||
|
|
d: number,
|
||
|
|
f: number,
|
||
|
|
g: CFrame,
|
||
|
|
p: CFrame
|
||
|
|
}
|
||
|
|
|
||
|
|
type Spring_Rotation = {
|
||
|
|
d: number,
|
||
|
|
f: number,
|
||
|
|
g: CFrame,
|
||
|
|
p: CFrame,
|
||
|
|
v: Vector3
|
||
|
|
}
|
||
|
|
|
||
|
|
-- evaluate dot products in high precision
|
||
|
|
local function dot(v0: Vector3, v1: Vector3)
|
||
|
|
return v0.X*v1.X + v0.Y*v1.Y + v0.Z*v1.Z
|
||
|
|
end
|
||
|
|
|
||
|
|
local function angleDiff(c0: CFrame, c1: CFrame)
|
||
|
|
local x = dot(c0.XVector, c1.XVector)
|
||
|
|
local y = dot(c0.YVector, c1.YVector)
|
||
|
|
local z = dot(c0.ZVector, c1.ZVector)
|
||
|
|
local w = x + y + z - 1
|
||
|
|
return math.atan2(math.sqrt(math.max(0, 1 - w*w*0.25)), w*0.5)
|
||
|
|
end
|
||
|
|
|
||
|
|
-- gives approx. 21% accuracy improvement over CFrame.fromAxisAngle near poles
|
||
|
|
local function fromAxisAngle(axis: Vector3, angle: number)
|
||
|
|
local c = math.cos(angle)
|
||
|
|
local s = math.sin(angle)
|
||
|
|
local x, y, z = axis.X, axis.Y, axis.Z
|
||
|
|
|
||
|
|
local mxy = x*y*(1 - c)
|
||
|
|
local myz = y*z*(1 - c)
|
||
|
|
local mzx = z*x*(1 - c)
|
||
|
|
|
||
|
|
local rx = Vector3.new(x*x*(1 - c) + c, mxy + z*s, mzx - y*s)
|
||
|
|
local ry = Vector3.new(mxy - z*s, y*y*(1 - c) + c, myz + x*s)
|
||
|
|
local rz = Vector3.new(mzx + y*s, myz - x*s, z*z*(1 - c) + c)
|
||
|
|
|
||
|
|
return CFrame.fromMatrix(Vector3.zero, rx, ry, rz):Orthonormalize()
|
||
|
|
end
|
||
|
|
|
||
|
|
local function rotateAxis(r0: Vector3, c1: CFrame)
|
||
|
|
local c0 = CFrame.identity
|
||
|
|
local mag = r0.Magnitude
|
||
|
|
if mag > 1e-6 then
|
||
|
|
c0 = fromAxisAngle(r0.Unit, mag)
|
||
|
|
end
|
||
|
|
return c0 * c1
|
||
|
|
end
|
||
|
|
|
||
|
|
-- axis*angle difference between two cframes
|
||
|
|
local function axisAngleDiff(c0: CFrame, c1: CFrame)
|
||
|
|
-- use native axis (stable enough)
|
||
|
|
local axis = (c0*c1:Inverse()):ToAxisAngle()
|
||
|
|
|
||
|
|
-- use full-precision angle calculation to minimize truncation
|
||
|
|
local angle = angleDiff(c0, c1)
|
||
|
|
return axis.Unit*angle
|
||
|
|
end
|
||
|
|
|
||
|
|
local function rotation_step(spring: Spring_Rotation, dt: number): CFrame
|
||
|
|
debug.profilebegin("rotation")
|
||
|
|
local d = spring.d
|
||
|
|
local f = spring.f*(2*math.pi)
|
||
|
|
local g = spring.g
|
||
|
|
local p0 = spring.p
|
||
|
|
local v0 = spring.v
|
||
|
|
|
||
|
|
local offset = axisAngleDiff(p0, g)
|
||
|
|
local decay = math.exp(-d*f*dt)
|
||
|
|
|
||
|
|
local pt: CFrame
|
||
|
|
local vt: Vector3
|
||
|
|
|
||
|
|
if d == 1 then -- critically damped
|
||
|
|
pt = rotateAxis((offset*(1 + f*dt) + v0*dt)*decay, g)
|
||
|
|
vt = (v0*(1 - dt*f) - offset*(dt*f*f))*decay
|
||
|
|
|
||
|
|
elseif d < 1 then -- underdamped
|
||
|
|
local c = math.sqrt(1 - d*d)
|
||
|
|
|
||
|
|
local i = math.cos(dt*f*c)
|
||
|
|
local j = math.sin(dt*f*c)
|
||
|
|
|
||
|
|
local y = j/(f*c)
|
||
|
|
local z = j/c
|
||
|
|
|
||
|
|
pt = rotateAxis((offset*(i + z*d) + v0*y)*decay, g)
|
||
|
|
vt = (v0*(i - z*d) - offset*(z*f))*decay
|
||
|
|
else -- overdamped
|
||
|
|
local c = math.sqrt(d*d - 1)
|
||
|
|
|
||
|
|
local r1 = -f*(d + c)
|
||
|
|
local r2 = -f*(d - c)
|
||
|
|
|
||
|
|
local co2 = (v0 - offset*r1)/(2*f*c)
|
||
|
|
local co1 = offset - co2
|
||
|
|
|
||
|
|
local e1 = co1*math.exp(r1*dt)
|
||
|
|
local e2 = co2*math.exp(r2*dt)
|
||
|
|
|
||
|
|
pt = rotateAxis(e1 + e2, g)
|
||
|
|
vt = e1*r1 + e2*r2
|
||
|
|
end
|
||
|
|
|
||
|
|
spring.p = pt
|
||
|
|
spring.v = vt
|
||
|
|
|
||
|
|
debug.profileend()
|
||
|
|
|
||
|
|
return pt
|
||
|
|
end
|
||
|
|
|
||
|
|
local SLEEP_ROTATION_DIFF = math.rad(0.01) -- rad
|
||
|
|
local SLEEP_ROTATION_VELOCITY = math.rad(0.1) -- rad/s
|
||
|
|
|
||
|
|
local function areRotationsClose(c0: CFrame, c1: CFrame)
|
||
|
|
local rx = dot(c0.XVector, c1.XVector)
|
||
|
|
local ry = dot(c0.YVector, c1.YVector)
|
||
|
|
local rz = dot(c0.ZVector, c1.ZVector)
|
||
|
|
local trace = rx + ry + rz
|
||
|
|
return trace > 1 + 2*math.cos(SLEEP_ROTATION_DIFF)
|
||
|
|
end
|
||
|
|
|
||
|
|
local function rotation_can_sleep(spring: Spring_Rotation): boolean
|
||
|
|
local sleepP = areRotationsClose(spring.p, spring.g)
|
||
|
|
local sleepV = spring.v.Magnitude < SLEEP_ROTATION_VELOCITY
|
||
|
|
return sleepP and sleepV
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Very important to remember that the origo and the goal's Rotation are
|
||
|
|
-- orthonormalized!
|
||
|
|
local function create(d: number, f: number, origo: CFrame, goal: CFrame): Spring
|
||
|
|
return {
|
||
|
|
type = "CFrame",
|
||
|
|
position = {
|
||
|
|
type = "Vector3",
|
||
|
|
d = d,
|
||
|
|
f = f,
|
||
|
|
g = goal.Position,
|
||
|
|
p = origo.Position,
|
||
|
|
v = Vector3.zero,
|
||
|
|
},
|
||
|
|
rotation = {
|
||
|
|
d = d,
|
||
|
|
f = f,
|
||
|
|
g = goal.Rotation:Orthonormalize(),
|
||
|
|
p = origo.Rotation:Orthonormalize(),
|
||
|
|
v = Vector3.zero
|
||
|
|
},
|
||
|
|
d = d,
|
||
|
|
f = f,
|
||
|
|
p = origo,
|
||
|
|
g = goal
|
||
|
|
}
|
||
|
|
end
|
||
|
|
|
||
|
|
local function step(spring: Spring, dt: number): CFrame
|
||
|
|
local p = Spring_Vector3.step(spring.position, dt)
|
||
|
|
local r = rotation_step(spring.rotation, dt)
|
||
|
|
local cframe = r + p
|
||
|
|
spring.p = cframe
|
||
|
|
return cframe
|
||
|
|
end
|
||
|
|
|
||
|
|
local function can_sleep(spring: Spring)
|
||
|
|
return Spring_Vector3.can_sleep(spring.position) and rotation_can_sleep(spring.rotation)
|
||
|
|
end
|
||
|
|
|
||
|
|
return {
|
||
|
|
create = create,
|
||
|
|
step = step,
|
||
|
|
can_sleep = can_sleep
|
||
|
|
}
|