Fix CFrame math

This commit is contained in:
Filip Tibell 2023-03-27 12:48:51 +02:00
parent 93b83a5874
commit a82624023f
No known key found for this signature in database
3 changed files with 139 additions and 32 deletions

View file

@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Fixed
- Fixed issues with CFrame math operations
## `0.6.4` - March 26th, 2023 ## `0.6.4` - March 26th, 2023
### Fixed ### Fixed

View file

@ -42,38 +42,36 @@ impl CFrame {
// Strict args constructors // Strict args constructors
datatype_table.set( datatype_table.set(
"lookAt", "lookAt",
lua.create_function( lua.create_function(|_, (from, to, up): (Vector3, Vector3, Option<Vector3>)| {
|_, (at, look_at, up): (Vector3, Vector3, Option<Vector3>)| { Ok(CFrame(look_at(
Ok(CFrame(Mat4::look_at_rh( from.0,
at.0, to.0,
look_at.0,
up.unwrap_or(Vector3(Vec3::Y)).0, up.unwrap_or(Vector3(Vec3::Y)).0,
))) )))
}, })?,
)?,
)?; )?;
datatype_table.set( datatype_table.set(
"fromEulerAnglesXYZ", "fromEulerAnglesXYZ",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::ZYX, rx, ry, rz))) Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
})?, })?,
)?; )?;
datatype_table.set( datatype_table.set(
"fromEulerAnglesYXZ", "fromEulerAnglesYXZ",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::ZXY, rx, ry, rz))) Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
})?, })?,
)?; )?;
datatype_table.set( datatype_table.set(
"Angles", "Angles",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::ZYX, rx, ry, rz))) Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
})?, })?,
)?; )?;
datatype_table.set( datatype_table.set(
"fromOrientation", "fromOrientation",
lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| {
Ok(CFrame(Mat4::from_euler(EulerRot::ZXY, rx, ry, rz))) Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
})?, })?,
)?; )?;
datatype_table.set( datatype_table.set(
@ -99,7 +97,7 @@ impl CFrame {
)?; )?;
// Dynamic args constructor // Dynamic args constructor
type ArgsPos = Vector3; type ArgsPos = Vector3;
type ArgsLook = (Vector3, Vector3); type ArgsLook = (Vector3, Vector3, Option<Vector3>);
type ArgsPosXYZ = (f32, f32, f32); type ArgsPosXYZ = (f32, f32, f32);
type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32); type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32);
type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32); type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32);
@ -110,8 +108,12 @@ impl CFrame {
Ok(CFrame(Mat4::IDENTITY)) Ok(CFrame(Mat4::IDENTITY))
} else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) { } else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) {
Ok(CFrame(Mat4::from_translation(pos.0))) Ok(CFrame(Mat4::from_translation(pos.0)))
} else if let Ok((pos, look_at)) = ArgsLook::from_lua_multi(args.clone(), lua) { } else if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) {
Ok(CFrame(Mat4::look_at_rh(pos.0, look_at.0, Vec3::Y))) Ok(CFrame(look_at(
from.0,
to.0,
up.unwrap_or(Vector3(Vec3::Y)).0,
)))
} else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args.clone(), lua) { } else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args.clone(), lua) {
Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z)))) Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z))))
} else if let Ok((x, y, z, qx, qy, qz, qw)) = } else if let Ok((x, y, z, qx, qy, qz, qw)) =
@ -208,20 +210,22 @@ impl LuaUserData for CFrame {
let pos = this.position(); let pos = this.position();
let (rx, ry, rz) = this.orientation(); let (rx, ry, rz) = this.orientation();
Ok(( Ok((
pos.x, pos.y, pos.z, pos.x, pos.y, -pos.z,
rx.x, rx.y, rx.z, rx.x, rx.y, rx.z,
ry.x, ry.y, ry.z, ry.x, ry.y, ry.z,
rz.x, rz.y, rz.z, rz.x, rz.y, rz.z,
)) ))
}); });
methods.add_method("ToEulerAnglesXYZ", |_, this, ()| { methods.add_method("ToEulerAnglesXYZ", |_, this, ()| {
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::ZYX)) Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::XYZ))
}); });
methods.add_method("ToEulerAnglesYXZ", |_, this, ()| { methods.add_method("ToEulerAnglesYXZ", |_, this, ()| {
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::ZXY)) let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
Ok((rx, ry, rz))
}); });
methods.add_method("ToOrientation", |_, this, ()| { methods.add_method("ToOrientation", |_, this, ()| {
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::ZXY)) let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
Ok((rx, ry, rz))
}); });
methods.add_method("ToAxisAngle", |_, this, ()| { methods.add_method("ToAxisAngle", |_, this, ()| {
let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle(); let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle();
@ -329,3 +333,22 @@ impl From<CFrame> for DomCFrame {
} }
} }
} }
/**
Creates a matrix at the position `from`, looking towards `to`.
[`glam`] does provide functions such as [`look_at_lh`], [`look_at_rh`] and more but
they all create view matrices for camera transforms which is not what we want here.
*/
fn look_at(from: Vec3, to: Vec3, up: Vec3) -> Mat4 {
let dir = (to - from).normalize();
let xaxis = up.cross(dir).normalize();
let yaxis = dir.cross(xaxis).normalize();
Mat4::from_cols(
Vec3::new(xaxis.x, yaxis.x, dir.x).extend(0.0),
Vec3::new(xaxis.y, yaxis.y, dir.y).extend(0.0),
Vec3::new(xaxis.z, yaxis.z, dir.z).extend(0.0),
from.extend(1.0),
)
}

View file

@ -2,6 +2,42 @@ local roblox = require("@lune/roblox") :: any
local CFrame = roblox.CFrame local CFrame = roblox.CFrame
local Vector3 = roblox.Vector3 local Vector3 = roblox.Vector3
local COMPONENT_NAMES =
{ "X", "Y", "Z", "R00", "R01", "R02", "R10", "R11", "R12", "R20", "R21", "R22" }
local function assertEq(actual, expected)
local actComps: { number } = { actual:GetComponents() }
local expComps: { number } = { expected:GetComponents() }
for index, actComp in actComps do
local expComp = expComps[index]
if math.abs(expComp - actComp) >= (1 / 512) then
local r0 = Vector3.new(actual:ToOrientation())
local r1 = Vector3.new(expected:ToOrientation())
error(
string.format(
"Expected component '%s' to be %.2f, got %.2f"
.. "\nActual: %.2f, %.2f, %.2f | %.2f, %.2f, %.2f"
.. "\nExpected: %.2f, %.2f, %.2f | %.2f, %.2f, %.2f",
COMPONENT_NAMES[index],
expComp,
actComp,
actual.Position.X,
actual.Position.Y,
actual.Position.Z,
math.deg(r0.X),
math.deg(r0.Y),
math.deg(r0.Z),
expected.Position.X,
expected.Position.Y,
expected.Position.Z,
math.deg(r1.X),
math.deg(r1.Y),
math.deg(r1.Z)
)
)
end
end
end
-- Constructors & properties -- Constructors & properties
CFrame.new() CFrame.new()
@ -23,20 +59,62 @@ assert(CFrame.new(1, 2, 3).X == 1)
assert(CFrame.new(1, 2, 3).Y == 2) assert(CFrame.new(1, 2, 3).Y == 2)
assert(CFrame.new(1, 2, 3).Z == 3) assert(CFrame.new(1, 2, 3).Z == 3)
assertEq(
CFrame.fromMatrix(
Vector3.new(1, 2, 3),
Vector3.new(1, 0, 0),
Vector3.new(0, 1, 0),
Vector3.new(0, 0, 1)
),
CFrame.new(1, 2, 3)
)
-- Constants -- Constants
assert(CFrame.identity == CFrame.new()) assertEq(CFrame.identity, CFrame.new())
assert(CFrame.identity == CFrame.new(0, 0, 0)) assertEq(CFrame.identity, CFrame.new(0, 0, 0))
assert(CFrame.identity == CFrame.Angles(0, 0, 0)) assertEq(CFrame.identity, CFrame.Angles(0, 0, 0))
assert(CFrame.identity == CFrame.fromOrientation(0, 0, 0)) assertEq(CFrame.identity, CFrame.fromOrientation(0, 0, 0))
-- Ops -- Ops
assert(CFrame.new(2, 4, 8) + Vector3.new(1, 1, 2) == CFrame.new(3, 5, 10)) assertEq(CFrame.new(2, 4, 8) + Vector3.new(1, 1, 2), CFrame.new(3, 5, 10))
assert(CFrame.new(2, 4, 8) - Vector3.new(1, 1, 2) == CFrame.new(1, 3, 6)) assertEq(CFrame.new(2, 4, 8) - Vector3.new(1, 1, 2), CFrame.new(1, 3, 6))
assert(CFrame.new(2, 4, 8) * CFrame.new(1, 1, 2) == CFrame.new(3, 5, 10)) assertEq(CFrame.new(2, 4, 8) * CFrame.new(1, 1, 2), CFrame.new(3, 5, 10))
assert(CFrame.new(2, 4, 8) * Vector3.new(1, 1, 2) == Vector3.new(3, 5, 10)) assert(CFrame.new(2, 4, 8) * Vector3.new(1, 1, 2) == Vector3.new(3, 5, 10))
-- TODO: Check mult ops with rotated CFrames -- Mult ops with rotated CFrames
-- TODO: Methods assertEq(
CFrame.fromOrientation(0, math.rad(90), 0) * CFrame.fromOrientation(math.rad(5), 0, 0),
CFrame.fromOrientation(math.rad(5), math.rad(90), 0)
)
assertEq(
CFrame.fromOrientation(0, math.rad(90), 0) * CFrame.new(0, 0, -5),
CFrame.new(-5, 0, 0) * CFrame.fromOrientation(0, math.rad(90), 0)
)
-- World & object space conversions
local offset = CFrame.new(0, 0, -5)
assert(offset:ToWorldSpace(offset).Z == offset.Z * 2)
assert(offset:ToObjectSpace(offset).Z == 0)
local world = CFrame.fromOrientation(0, math.rad(90), 0) * CFrame.new(0, 0, -5)
local world2 = CFrame.fromOrientation(0, -math.rad(90), 0) * CFrame.new(0, 0, -5)
assertEq(CFrame.identity:ToObjectSpace(world), world)
assertEq(
world:ToObjectSpace(world2),
CFrame.fromOrientation(0, math.rad(180), 0) * CFrame.new(0, 0, -10)
)
-- Look
assertEq(CFrame.fromOrientation(0, math.rad(90), 0), CFrame.lookAt(Vector3.zero, -Vector3.xAxis))
assertEq(CFrame.fromOrientation(0, -math.rad(90), 0), CFrame.lookAt(Vector3.zero, Vector3.xAxis))
assertEq(
CFrame.new(0, 0, -5) * CFrame.fromOrientation(0, math.rad(90), 0),
CFrame.lookAt(Vector3.new(0, 0, -5), Vector3.new(0, 0, -5) - Vector3.xAxis)
)
-- TODO: More methods