From a82624023fd7a69420140db26ae997778cdaf9e2 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Mon, 27 Mar 2023 12:48:51 +0200 Subject: [PATCH] Fix CFrame math --- CHANGELOG.md | 6 ++ .../lib-roblox/src/datatypes/types/cframe.rs | 69 ++++++++----- tests/roblox/datatypes/CFrame.luau | 96 +++++++++++++++++-- 3 files changed, 139 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e29ea8..d6be909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 ### Fixed diff --git a/packages/lib-roblox/src/datatypes/types/cframe.rs b/packages/lib-roblox/src/datatypes/types/cframe.rs index 1601b9b..b46c090 100644 --- a/packages/lib-roblox/src/datatypes/types/cframe.rs +++ b/packages/lib-roblox/src/datatypes/types/cframe.rs @@ -42,38 +42,36 @@ impl CFrame { // Strict args constructors datatype_table.set( "lookAt", - lua.create_function( - |_, (at, look_at, up): (Vector3, Vector3, Option)| { - Ok(CFrame(Mat4::look_at_rh( - at.0, - look_at.0, - up.unwrap_or(Vector3(Vec3::Y)).0, - ))) - }, - )?, + lua.create_function(|_, (from, to, up): (Vector3, Vector3, Option)| { + Ok(CFrame(look_at( + from.0, + to.0, + up.unwrap_or(Vector3(Vec3::Y)).0, + ))) + })?, )?; datatype_table.set( "fromEulerAnglesXYZ", 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( "fromEulerAnglesYXZ", 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( "Angles", 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( "fromOrientation", 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( @@ -99,7 +97,7 @@ impl CFrame { )?; // Dynamic args constructor type ArgsPos = Vector3; - type ArgsLook = (Vector3, Vector3); + type ArgsLook = (Vector3, Vector3, Option); type ArgsPosXYZ = (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); @@ -110,8 +108,12 @@ impl CFrame { Ok(CFrame(Mat4::IDENTITY)) } else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) { Ok(CFrame(Mat4::from_translation(pos.0))) - } else if let Ok((pos, look_at)) = ArgsLook::from_lua_multi(args.clone(), lua) { - Ok(CFrame(Mat4::look_at_rh(pos.0, look_at.0, Vec3::Y))) + } else if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) { + 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) { Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z)))) } else if let Ok((x, y, z, qx, qy, qz, qw)) = @@ -208,20 +210,22 @@ impl LuaUserData for CFrame { let pos = this.position(); let (rx, ry, rz) = this.orientation(); Ok(( - pos.x, pos.y, pos.z, - rx.x, rx.y, rx.z, - ry.x, ry.y, ry.z, - rz.x, rz.y, rz.z, + pos.x, pos.y, -pos.z, + rx.x, rx.y, rx.z, + ry.x, ry.y, ry.z, + rz.x, rz.y, rz.z, )) }); 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, ()| { - 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, ()| { - 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, ()| { let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle(); @@ -329,3 +333,22 @@ impl From 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), + ) +} diff --git a/tests/roblox/datatypes/CFrame.luau b/tests/roblox/datatypes/CFrame.luau index 8d5b094..5ae3b81 100644 --- a/tests/roblox/datatypes/CFrame.luau +++ b/tests/roblox/datatypes/CFrame.luau @@ -2,6 +2,42 @@ local roblox = require("@lune/roblox") :: any local CFrame = roblox.CFrame 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 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).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 -assert(CFrame.identity == CFrame.new()) -assert(CFrame.identity == CFrame.new(0, 0, 0)) -assert(CFrame.identity == CFrame.Angles(0, 0, 0)) -assert(CFrame.identity == CFrame.fromOrientation(0, 0, 0)) +assertEq(CFrame.identity, CFrame.new()) +assertEq(CFrame.identity, CFrame.new(0, 0, 0)) +assertEq(CFrame.identity, CFrame.Angles(0, 0, 0)) +assertEq(CFrame.identity, CFrame.fromOrientation(0, 0, 0)) -- Ops -assert(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)) -assert(CFrame.new(2, 4, 8) * CFrame.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)) +assertEq(CFrame.new(2, 4, 8) - Vector3.new(1, 1, 2), CFrame.new(1, 3, 6)) +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)) --- 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