From 513532a93cf8dca768784c3a48b125fb9c6919a1 Mon Sep 17 00:00:00 2001 From: Lars Mueller Date: Sat, 10 May 2025 20:38:44 +0200 Subject: [PATCH] WIP matrix & rotation lua APIs --- doc/lua_api.md | 158 ++++++++ games/devtest/mods/unittests/init.lua | 1 + games/devtest/mods/unittests/matrix4.lua | 287 ++++++++++++++ games/devtest/mods/unittests/rotation.lua | 67 ++++ irr/include/matrix4.h | 42 ++ irr/include/quaternion.h | 6 + src/script/common/helper.cpp | 42 +- src/script/common/helper.h | 7 + src/script/lua_api/CMakeLists.txt | 2 + src/script/lua_api/l_matrix4.cpp | 458 ++++++++++++++++++++++ src/script/lua_api/l_matrix4.h | 114 ++++++ src/script/lua_api/l_rotation.cpp | 239 +++++++++++ src/script/lua_api/l_rotation.h | 69 ++++ src/script/scripting_server.cpp | 6 + src/unittest/test_irr_rotation.cpp | 16 + 15 files changed, 1509 insertions(+), 5 deletions(-) create mode 100644 games/devtest/mods/unittests/matrix4.lua create mode 100644 games/devtest/mods/unittests/rotation.lua create mode 100644 src/script/lua_api/l_matrix4.cpp create mode 100644 src/script/lua_api/l_matrix4.h create mode 100644 src/script/lua_api/l_rotation.cpp create mode 100644 src/script/lua_api/l_rotation.h diff --git a/doc/lua_api.md b/doc/lua_api.md index afb44d489..1b7bafe98 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -4151,6 +4151,164 @@ For example: * `core.hash_node_position` (Only works on node positions.) * `core.dir_to_wallmounted` (Involves wallmounted param2 values.) +Rotations +========= + +As abusing vectors of euler angles is discouraged as error-prone, +Luanti provides a proper helper class for working with 3d rotations. + +You must not rely on the specific type or imprecision of the current implementation. + +Constructors +------------ + +* `Rotation.identity()`: Constructs a no-op rotation. +* `Rotation.quaternion(x, y, z, w)`: + Constructs a rotation from a quaternion (which need not be normalized) +* `Rotation.axis_angle(axis, angle)`: + Constructs a rotation around the given axis by the given angle + * `axis` is a vector, which need not be normalized + * `angle` is in radians + * Shorthands for rotations around the respective axes: + * `Rotation.x(pitch)` + * `Rotation.y(yaw)` + * `Rotation.z(roll)` +* `Rotation.euler_angles(pitch, yaw, roll)` + * All angles in radians. + * Rotation order is ZYX: First pitch is applied, then yaw, then roll. Equivalent to + `Rotation.compose(Rotation.z(roll), Rotation.y(yaw), Rotation.x(pitch))`. + * Consistent with the euler angles that can be used for bones. + +Conversions +----------- + +Corresponding to the constructors, quaternions can be converted +to different representations; note that you need not get the same values out - +you merely get values that produce the same rotation when passed to the corresponding constructor: + +* `x, y, z, w = Rotation:to_quaternion()` + * Returns the normalized quaternion representation. +* `axis, angle = Rotation:to_axis_angle()` + * `axis` is a normalized vector. + * `angle` is in radians. +* `pitch, yaw, roll = Rotation:to_euler_angles()` + * Angles are all in radians. + * `pitch`, `yaw`, `roll`: Rotation around the X-, Y-, and Z-axis respectively. + * Rotation order is ZYX: First pitch is applied, then yaw, then roll. + * Coordinate system is right-handed + +Methods +------- + +* `Rotation:apply(vec)`: Returns the result of applying the rotation to the given vector. +* `Rotation.compose(...)`: Returns the composition of the given rotations, + in right-to-left order: `second:compose(first):apply(v)` + is equivalent to `second:apply(first:apply(v))`. + `Rotation.compose()` is an alias for `Rotation.identity()`, + `Rotation:compose()` copies the rotation. +* `Rotation:invert()`: Returns the inverse rotation. +* `Rotation:slerp(from, to, time)`: Interpolate from one rotation to another. + * `time = 0` is all `from`, `time = 1` is all `to`. +* `Rotation:angle_to(other)`: Returns the absolute angle between two quaternions. + * Useful to measure similarity. + + +Matrices +======== + +Luanti uses 4x4 matrices to represent transformations of 3d vectors. +The matrices use row-major conventions: +The first row is the image of the vector (1, 0, 0, 0), +the second row is the image of (0, 1, 0, 0), and so on. +Thus the translation is in the last row. + +You must account for reasonable imprecisions in matrix calculations, +as they currently use 32-bit floats; they may use 64-bit floats in the future. +You must not rely on the internal representation or type of matrices; +e.g. they may be implemented in pure Lua as a table in the future. + +Matrices are very suitable for constructing, composing and applying +linear transformations; they are not so useful for exact storage of transformations, +decomposition into rotation and scale will not be exact. + +Constructors +------------ + +* `Matrix4.new(r1c1, r1c2, ..., r4c4)`: + Constructs a matrix from the given 16 numbers in row-major order. +* `Matrix4.identity()`: Constructs an identity matrix. +* `Matrix4.all(number)`: Constructs a matrix where all entries are the given number. +* `Matrix4.translation(vec)`: Constructs a matrix that translates vectors by the given `vector`. +* `Matrix4.rotation(rot)`: Constructs a matrix that applies the given `Rotation` to vectors. +* `Matrix4.scale(vec)`: Constructs a matrix that applies the given + component-wise scaling factors to vectors. +* `Matrix4.reflection(normal)`: Constructs a matrix that reflects vectors + at the plane with the given plane normal vector (which need not be normalized). + +Methods +------- + +Storage: + +* `Matrix4:get(row, col)`: Get an entry. + * `row` and `col` range from 1 to 4 +* `Matrix4:set(row, col, element)`: Set an entry. + * `row` and `col` range from 1 to 4 +* `x, y, z, w = Matrix4:get_row(row)` +* `Matrix4:set_row(row, x, y, z, w)` +* `x, y, z, w = Matrix4:get_column(col)` +* `Matrix4:set_column(col, x, y, z, w)` +* `Matrix4:copy()`: Copy the matrix. +* `... = Matrix4:unpack()`: Get the entries of the matrix in row-major order. + +Linear algebra: + +* `Matrix4.compose(...)`: Returns the composition of the given matrices. +* `Matrix4:transpose()`: Returns the transpose of the matrix. +* `Matrix4:invert()`: Returns the inverse, or `nil` if the matrix is (close to being) singular. +* `x, y, z, w = Matrix4:transform_4d(x, y, z, w)`: Apply the matrix to a 4d vector. +* `Matrix4:transform_position(pos)`: + * Apply the matrix to a vector representing a position. + * Applies the transformation as if w = 1 and discards the resulting w component. +* `Matrix4:transform_direction(dir)`: + * Apply the matrix to a vector representing a direction. + * Ignores the fourth row and column; does not apply the translation (w = 0). +* `Matrix4:equals(other, [tolerance = 0])`: + Returns whether all components differ in absolute value at most by the given tolerance. +* `Matrix4:is_affine_transform([tolerance = 0])`: + Whether the matrix is an affine transformation in 3d space, + meaning it is a 3d linear transformation plus a translation. + (This is the case if the last column is approximately 0, 0, 0, 1.) + +For working with affine transforms, the following methods are available: + +* `Matrix4:get_translation()`: + Returns the translation as a vector. +* `Matrix4:set_translation(vec)` + +For TRS transforms specifically, +let `self = Matrix4.compose(Matrix4.translation(t), Matrix4.rotation(r), Matrix4.scale(s))`. +Then we can decompose `self` further. Note that `self` must not shear or reflect. + +* `rotation, scale = Matrix4:get_rs()`: + Extracts a `Rotation` equivalent to `r`. + along with the corresponding component-wise scaling factors as a vector. + + +Operators +--------- + +Similar to vectors, matrices define some arithmetic operators: + +* `m1 == m2`: Returns whether `m1` and `m2` are identical. +* `-m`: Returns the additive inverse. +* `m1 + m2`: Returns the sum of both matrices. +* `m1 - m2`: Shorthand for `m1 + (-m2)`. +* `m * s` or `s * m`: Returns the matrix `m` scaled by the scalar `s`. + * Note: *All* entries are scaled, including the last row. + +Matrices also define a `__tostring` metamethod. +This is only intended for human readability and not for serialization. diff --git a/games/devtest/mods/unittests/init.lua b/games/devtest/mods/unittests/init.lua index 22057f26a..40200e972 100644 --- a/games/devtest/mods/unittests/init.lua +++ b/games/devtest/mods/unittests/init.lua @@ -201,6 +201,7 @@ dofile(modpath .. "/inventory.lua") dofile(modpath .. "/load_time.lua") dofile(modpath .. "/on_shutdown.lua") dofile(modpath .. "/color.lua") +dofile(modpath .. "/matrix4.lua") -------------- diff --git a/games/devtest/mods/unittests/matrix4.lua b/games/devtest/mods/unittests/matrix4.lua new file mode 100644 index 000000000..88fc548dc --- /dev/null +++ b/games/devtest/mods/unittests/matrix4.lua @@ -0,0 +1,287 @@ +local function describe(_, func) + func() +end + +local function it(section_name, func) + print("Running test: " .. section_name) + func() +end + +local assert = require("luassert") + +local function assert_close(a, b) + assert(a:equals(b, 1e-4)) +end + +local mat1 = Matrix4.new( + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 +) + +it("identity", function() + assert.equals(Matrix4.new( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ), Matrix4.identity()) +end) + +describe("getters & setters", function() + it("get & set", function() + local mat = Matrix4.all(0) + local i = 0 + for row = 1, 4 do + for col = 1, 4 do + i = i + 1 + assert.equals(0, mat:get(row, col)) + mat:set(row, col, i) + assert.equals(i, mat:get(row, col)) + end + end + assert.equals(mat1, mat) + end) + it("get_row & set_row", function() + local mat = mat1:copy() + assert.same({5, 6, 7, 8}, {mat:get_row(2)}) + mat:set_row(2, 1, 2, 3, 4) + assert.same({1, 2, 3, 4}, {mat:get_row(2)}) + end) + it("get_column & set_column", function() + local mat = mat1:copy() + assert.same({3, 7, 11, 15}, {mat:get_column(3)}) + mat:set_column(3, 1, 2, 3, 4) + assert.same({1, 2, 3, 4}, {mat:get_column(3)}) + end) +end) + +it("copy", function() + assert.equals(mat1, mat1:copy()) +end) + +it("unpack", function() + assert.equals(mat1, Matrix4.new(mat1:unpack())) +end) + +describe("transform", function() + it("4d", function() + assert.same({ + 1 * 1 + 2 * 5 + 3 * 9 + 4 * 13, + 1 * 2 + 2 * 6 + 3 * 10 + 4 * 14, + 1 * 3 + 2 * 7 + 3 * 11 + 4 * 15, + 1 * 4 + 2 * 8 + 3 * 12 + 4 * 16, + }, {mat1:transform_4d(1, 2, 3, 4)}) + end) + it("position", function() + assert.equals(vector.new( + 1 * 1 + 2 * 5 + 3 * 9, + 1 * 2 + 2 * 6 + 3 * 10, + 1 * 3 + 2 * 7 + 3 * 11 + ):offset(13, 14, 15), mat1:transform_position(vector.new(1, 2, 3))) + end) + it("direction", function() + assert.equals(vector.new( + 1 * 1 + 2 * 5 + 3 * 9, + 1 * 2 + 2 * 6 + 3 * 10, + 1 * 3 + 2 * 7 + 3 * 11 + ), mat1:transform_direction(vector.new(1, 2, 3))) + end) +end) + + +local mat2 = Matrix4.new( + 16, 15, 14, 13, + 12, 11, 10, 9, + 8, 7, 6, 5, + 4, 3, 2, 1 +) + +describe("composition", function() + it("identity for empty argument list", function() + assert(Matrix4.identity():equals(Matrix4.compose())) + end) + it("same matrix for single argument", function() + local mat = Matrix4.new( + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 + ) + assert(mat:equals(mat:compose())) + end) + it("matrix multiplication for two arguments", function() + local composition = mat1:compose(mat2) + assert.equals(Matrix4.new( + 386, 444, 502, 560, + 274, 316, 358, 400, + 162, 188, 214, 240, + 50, 60, 70, 80 + ), composition) + assert.same({mat1:transform_4d(mat2:transform_4d(1, 2, 3, 4))}, + {composition:transform_4d(1, 2, 3, 4)}) + end) + it("supports multiple arguments", function() + local fib = Matrix4.new( + 0, 1, 0, 0, -- x' = y + 1, 1, 0, 0, -- y' = x + y + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + local function rep(v, n) + if n == 0 then + return + end + return v, rep(v, n - 1) + end + local result = Matrix4.compose(rep(fib, 10)) + assert.equals(55, result:get(2, 1)) + end) +end) + +local function random_matrix4() + local t = {} + for i = 1, 16 do + t[i] = math.random() + end + return Matrix4.new(unpack(t)) +end + +it("determinant", function() + assert.equal(42, Matrix4.scale(vector.new(2, 3, 7)):determinant()) +end) + +describe("inversion", function() + it("simple permutation", function() + assert_close(Matrix4.new( + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + 0, 0, 0, 1 + ), Matrix4.new( + 0, 0, 1, 0, + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 1 + ):invert()) + end) + it("random matrices", function() + for _ = 1, 100 do + local mat = random_matrix4() + if math.abs(mat:determinant()) > 1e-3 then + assert_close(Matrix4.identity(), mat:invert():compose(mat)) + assert_close(Matrix4.identity(), mat:compose(mat:invert())) + end + end + end) +end) + +it("transpose", function() + assert.equals(Matrix4.new( + 1, 5, 9, 13, + 2, 6, 10, 14, + 3, 7, 11, 15, + 4, 8, 12, 16 + ), mat1:transpose()) +end) + +describe("affine transform constructors", function() + it("translation", function() + assert.equals(Matrix4.new( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 2, 3, 1 + ), Matrix4.translation(vector.new(1, 2, 3))) + end) + it("scale", function() + assert.equals(Matrix4.new( + -1, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 3, 0, + 0, 0, 0, 1 + ), Matrix4.scale(vector.new(-1, 2, 3))) + end) + it("rotation", function() + assert_close(Matrix4.new( + 0, -1, 0, 0, + 1, 0, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ), Matrix4.rotation(Rotation.z(math.pi / 2))) + end) + it("reflection", function() + assert_close(Matrix4.identity() - 2/(1^2 + 2^2 + 3^2) * Matrix4.new( + 1, 2, 3, 0, + 2, 4, 6, 0, + 3, 6, 9, 0, + 0, 0, 0, 0 + ), Matrix4.reflection(vector.new(1, 2, 3))) + end) +end) + +describe("affine transform methods", function() + local t = vector.new(4, 5, 6) + local r = Rotation.z(math.pi / 2) + local s = vector.new(1, 2, 3) + local trs = Matrix4.compose( + Matrix4.translation(t), + Matrix4.rotation(r), + Matrix4.scale(s) + ) + it("is affine", function() + assert(trs:is_affine_transform()) + assert(not mat1:is_affine_transform()) + end) + it("get translation", function() + assert.equals(t, trs:get_translation()) + end) + it("get rotation & scale", function() + print(trs) + local rotation, scale = trs:get_rs() + print(rotation, scale) + print(r) + assert(r:angle_to(rotation) < 1e-4) + assert(s:distance(scale) < 1e-4) + end) + it("set translation", function() + local mat = trs:copy() + local v = vector.new(1, 2, 3) + mat:set_translation(v) + assert.equals(v, mat:get_translation()) + end) +end) + +describe("metamethods", function() + it("addition", function() + assert.equals(Matrix4.all(17), mat1 + mat2) + end) + it("subtraction", function() + assert.equals(Matrix4.all(0), mat1 - mat1) + end) + it("unary minus", function() + assert.equals(-1 * mat1, -mat1) + end) + it("scalar multiplication", function() + assert.equals(2 * mat1, mat1 * 2) + assert.equals(2 * mat1, mat1:compose(Matrix4.new( + 2, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 2, 0, + 0, 0, 0, 2 + ))) + end) + it("equals", function() + local mat1 = Matrix4.identity() + local mat2 = Matrix4.identity() + assert(mat1:equals(mat2)) + mat2:set(1, 1, 0) + assert(not mat1:equals(mat2)) + mat2:set(1, 1, 0.999) + assert(mat1:equals(mat2, 0.01)) + end) + it("tostring", function() + assert(tostring(Matrix4.scale(vector.new(12345, 0, 0))):find"12345") + end) +end) diff --git a/games/devtest/mods/unittests/rotation.lua b/games/devtest/mods/unittests/rotation.lua new file mode 100644 index 000000000..354fd6ddb --- /dev/null +++ b/games/devtest/mods/unittests/rotation.lua @@ -0,0 +1,67 @@ +local function describe(_, func) + func() +end + +local function it(section_name, func) + print("Running test: " .. section_name) + func() +end + +local function assert_close(expected, actual) + assert(expected:angle_to(actual) < 1e-4) +end + +describe("constructors", function() + it("identity", function() + local rot = Rotation.identity() + assert.same({0, 0, 0, 1}, {rot:to_quaternion()}) + end) + it("quaternion", function() + local rot = Rotation.quaternion(0, 0, 0, 1) + assert_close(rot, Rotation.identity()) + end) + it("axis angle", function() end) + it("axis-angle shorthands", function() + + end) +end) + +describe("composition", function() + it("is the identity for an empty argument list", function() + assert_close(Rotation.identity(), Rotation.compose()) + end) + it("is the same rotation for a single argument", function() + local rot = Rotation.x(math.pi / 2) + assert_close(rot, rot:compose()) + end) + it("is consistent with application", function() + + end) +end) + +local function random_quaternion() + local x = math.random() + local y = math.random() + local z = math.random() + local w = math.random() + return Rotation.quaternion(x, y, z, w) +end + +describe("inversion", function() + it("random quaternions", function() + for _ = 1, 100 do + local rot = random_quaternion() + assert_close(Rotation.identity(), rot:inverse():compose(rot)) + assert_close(Rotation.identity(), rot:compose(rot:inverse())) + end + end) + it("inverts the angle", function() + for _ = 1, 100 do + local rot = random_quaternion() + local axis, angle = rot:axis_angle() + local inv_axis, inv_angle = rot:inverse():axis_angle() + assert(axis:distance(inv_axis) < 1e-4) + assert(math.abs(angle + inv_angle) < 1e-4) + end + end) +end) \ No newline at end of file diff --git a/irr/include/matrix4.h b/irr/include/matrix4.h index 0a9f6fd26..65927a141 100644 --- a/irr/include/matrix4.h +++ b/irr/include/matrix4.h @@ -12,6 +12,7 @@ #include "aabbox3d.h" #include "rect.h" #include +#include namespace irr { @@ -213,6 +214,9 @@ public: //! Get Scale vector3d getScale() const; + //! Scale the matrix rows ("axes") by the components of a vector + void scaleAxes(const vector3d &v); + //! Translate a vector by the inverse of the translation part of this matrix. void inverseTranslateVect(vector3df &vect) const; @@ -220,6 +224,7 @@ public: [[nodiscard]] vector3d scaleThenInvRotVect(const vector3d &vect) const; //! Rotate and scale a vector. Applies both rotation & scale part of the matrix. + // TODO rename to transformDirection [[nodiscard]] vector3d rotateAndScaleVect(const vector3d &vect) const; //! Transforms the vector by this matrix @@ -426,6 +431,16 @@ public: //! Compare two matrices using the equal method bool equals(const CMatrix4 &other, const T tolerance = (T)ROUNDING_ERROR_f64) const; + //! Check whether matrix is a 3d affine transform (last column is approximately 0, 0, 0, 1) + bool isAffine(const T tolerance = (T)ROUNDING_ERROR_f64) const + { + const auto &m = *this; + return core::equals(m(0, 3), (T) 0) && + core::equals(m(1, 3), (T) 0) && + core::equals(m(2, 3), (T) 0) && + core::equals(m(3, 3), (T) 1); + } + private: template vector3d getRotation(const vector3d &scale) const; @@ -751,6 +766,20 @@ inline vector3d CMatrix4::getScale() const }; } +template +void CMatrix4::scaleAxes(const vector3d &v) +{ + auto scale_row = [this](int row, T scale) { + auto &m = *this; + m(row, 0) *= scale; + m(row, 1) *= scale; + m(row, 2) *= scale; + }; + scale_row(0, v.X); + scale_row(1, v.Y); + scale_row(2, v.Z); +} + template inline CMatrix4 &CMatrix4::setRotationDegrees(const vector3d &rotation) { @@ -1631,6 +1660,19 @@ inline void CMatrix4::getTransposed(CMatrix4 &o) const o[15] = M[15]; } +template +std::ostream& operator<<(std::ostream& os, const CMatrix4& matrix) +{ + for (int row = 0; row < 4; ++row) { + for (int col = 0; col < 4; ++col) { + os << "\t"; + os << matrix(row, col); + } + os << "\n"; + } + return os; +} + // used to scale <-1,-1><1,1> to viewport template inline CMatrix4 &CMatrix4::buildNDCToDCMatrix(const rect &viewport, f32 zScale) diff --git a/irr/include/quaternion.h b/irr/include/quaternion.h index e23b1317d..00ec8a16d 100644 --- a/irr/include/quaternion.h +++ b/irr/include/quaternion.h @@ -91,6 +91,12 @@ public: //! Calculates the dot product inline f32 dotProduct(const quaternion &other) const; + //! Calculates the (unsigned) angle between two quaternions + inline f32 angleTo(const quaternion &other) const + { + return acosf(std::abs(dotProduct(other))); + } + //! Sets new quaternion inline quaternion &set(f32 x, f32 y, f32 z, f32 w); diff --git a/src/script/common/helper.cpp b/src/script/common/helper.cpp index 26fb56a91..7c5e633d5 100644 --- a/src/script/common/helper.cpp +++ b/src/script/common/helper.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-2.1-or-later // Copyright (C) 2018 nerzhul, Loic Blot +#include "irrTypes.h" extern "C" { #include } @@ -36,13 +37,44 @@ int LuaHelper::readParam(lua_State *L, int index) } template <> -float LuaHelper::readParam(lua_State *L, int index) +f32 LuaHelper::readParam(lua_State *L, int index) { - lua_Number v = luaL_checknumber(L, index); - if (std::isnan(v) && std::isinf(v)) - throw LuaError("Invalid float value (NaN or infinity)"); + f64 v = luaL_checknumber(L, index); + return static_cast(v); +} - return static_cast(v); +template <> +f64 LuaHelper::readParam(lua_State *L, int index) +{ + return luaL_checknumber(L, index); +} + +template <> +LuaHelper::Finite LuaHelper::readParam(lua_State *L, int index) +{ + f64 original_value = luaL_checknumber(L, index); + f32 v = static_cast(original_value); + if (std::isfinite(v)) + return {v}; + if (std::isnan(original_value)) + luaL_argerror(L, index, "number is NaN"); + if (!std::isfinite(original_value)) + luaL_argerror(L, index, "number is not finite"); + assert(!std::isfinite(v)); + luaL_argerror(L, index, "number is out-of-bounds for a 32-bit float"); + IRR_CODE_UNREACHABLE(); +} + +template <> +LuaHelper::Finite LuaHelper::readParam(lua_State *L, int index) +{ + f64 v = luaL_checknumber(L, index); + if (std::isfinite(v)) + return {v}; + if (std::isnan(v)) + luaL_argerror(L, index, "number is NaN"); + luaL_argerror(L, index, "number is not finite"); + IRR_CODE_UNREACHABLE(); } template <> diff --git a/src/script/common/helper.h b/src/script/common/helper.h index 29f5915a1..0932029e1 100644 --- a/src/script/common/helper.h +++ b/src/script/common/helper.h @@ -4,6 +4,7 @@ #pragma once +#include #include extern "C" { @@ -24,6 +25,12 @@ protected: template static T readParam(lua_State *L, int index); + /// Type to represent a restriction to finite floats + template + struct Finite { + T value; + }; + /** * Read a value using a template type T from Lua state L at index * diff --git a/src/script/lua_api/CMakeLists.txt b/src/script/lua_api/CMakeLists.txt index ef1be9525..6d5628dcd 100644 --- a/src/script/lua_api/CMakeLists.txt +++ b/src/script/lua_api/CMakeLists.txt @@ -24,6 +24,8 @@ set(common_SCRIPT_LUA_API_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/l_storage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_util.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_vmanip.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/l_rotation.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/l_matrix4.cpp PARENT_SCOPE) set(client_SCRIPT_LUA_API_SRCS diff --git a/src/script/lua_api/l_matrix4.cpp b/src/script/lua_api/l_matrix4.cpp new file mode 100644 index 000000000..1a3d5449e --- /dev/null +++ b/src/script/lua_api/l_matrix4.cpp @@ -0,0 +1,458 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Lars Müller + +#include "common/c_converter.h" +#include "irrTypes.h" +#include "irr_v3d.h" +#include "lua_api/l_rotation.h" +#include "matrix4.h" + +#include "lua_api/l_matrix4.h" +#include "common/c_packer.h" +#include "lua_api/l_internal.h" +#include "quaternion.h" + +#include +#include +#include +#include +#include +#include + +template +static int read_index(lua_State *L, int index) +{ + f64 value = luaL_checknumber(L, index); + if (std::floor(value) != value) + luaL_argerror(L, index, "index must be integer"); + if (value < 1 || value > max) + luaL_argerror(L, index, "index out of range"); + return static_cast(value) - 1; +} + +core::matrix4 &LuaMatrix4::check(lua_State *L, int index) +{ + return static_cast(luaL_checkudata(L, index, LuaMatrix4::className))->matrix; +} + +inline core::matrix4 &LuaMatrix4::create(lua_State *L) +{ + auto *mat = static_cast(lua_newuserdata(L, sizeof(LuaMatrix4))); + luaL_getmetatable(L, LuaMatrix4::className); + lua_setmetatable(L, -2); + return mat->matrix; +} + +int LuaMatrix4::l_identity(lua_State *L) +{ + create(L) = core::IdentityMatrix; + return 1; +} + +int LuaMatrix4::l_all(lua_State *L) +{ + f32 v = luaL_checknumber(L, 1); + create(L) = v; + return 1; +} + +int LuaMatrix4::l_new(lua_State *L) +{ + if (lua_gettop(L) != 16) + luaL_error(L, "expected 16 arguments"); + core::matrix4 &matrix = create(L); + for (int i = 0; i < 16; ++i) + matrix[i] = luaL_checknumber(L, 1 + i); + return 1; +} + +int LuaMatrix4::l_translation(lua_State *L) +{ + v3f translation = readParam(L, 1); + core::matrix4 &matrix = create(L); + matrix = core::IdentityMatrix; + matrix.setTranslation(translation); + return 1; +} + +int LuaMatrix4::l_rotation(lua_State *L) +{ + const core::quaternion &rotation = LuaRotation::check(L, 1); + core::matrix4 &matrix = create(L); + rotation.getMatrix_transposed(matrix); + return 1; +} + +int LuaMatrix4::l_scale(lua_State *L) +{ + v3f scale = readParam(L, 1); + core::matrix4 &matrix = create(L); + matrix = core::IdentityMatrix; + matrix.setScale(scale); + return 1; +} + +int LuaMatrix4::l_reflection(lua_State *L) +{ + v3f normal = readParam(L, 1); + normal.normalize(); + core::matrix4 &matrix = create(L); + matrix = core::IdentityMatrix; + // TODO move to CMatrix4 + f32 factor = 2.0f / normal.getLengthSQ(); + auto subtract_scaled_row = [&](int i, f32 scalar) { + v3f scaled = (factor * scalar) * normal; + matrix(i, 0) -= scaled.X; + matrix(i, 1) -= scaled.Y; + matrix(i, 2) -= scaled.Z; + }; + subtract_scaled_row(0, normal.X); + subtract_scaled_row(1, normal.Y); + subtract_scaled_row(2, normal.Z); + return 1; +} + +// Container utils + +int LuaMatrix4::l_get(lua_State *L) +{ + const auto &matrix = check(L, 1); + int row = read_index(L, 2); + int col = read_index(L, 3); + lua_pushnumber(L, matrix(row, col)); + return 1; +} + +int LuaMatrix4::l_set(lua_State *L) +{ + auto &matrix = check(L, 1); + int row = read_index(L, 2); + int col = read_index(L, 3); + f64 value = luaL_checknumber(L, 4); + matrix(row, col) = value; + return 0; +} + +int LuaMatrix4::l_get_row(lua_State *L) +{ + const auto &matrix = check(L, 1); + int row = read_index(L, 2); + for (int col = 0; col < 4; ++col) + lua_pushnumber(L, matrix(row, col)); + return 4; +} + +int LuaMatrix4::l_set_row(lua_State *L) +{ + auto &matrix = check(L, 1); + int row = read_index(L, 2); + f32 x = luaL_checknumber(L, 3); + f32 y = luaL_checknumber(L, 4); + f32 z = luaL_checknumber(L, 5); + f32 w = luaL_checknumber(L, 6); + matrix(row, 0) = x; + matrix(row, 1) = y; + matrix(row, 2) = z; + matrix(row, 3) = w; + return 0; +} + +int LuaMatrix4::l_get_column(lua_State *L) +{ + const auto &matrix = check(L, 1); + int col = read_index(L, 2); + for (int row = 0; row < 4; ++row) + lua_pushnumber(L, matrix(row, col)); + return 4; +} + +int LuaMatrix4::l_set_column(lua_State *L) +{ + auto &matrix = check(L, 1); + int col = read_index(L, 2); + f32 x = luaL_checknumber(L, 3); + f32 y = luaL_checknumber(L, 4); + f32 z = luaL_checknumber(L, 5); + f32 w = luaL_checknumber(L, 6); + matrix(0, col) = x; + matrix(1, col) = y; + matrix(2, col) = z; + matrix(3, col) = w; + return 0; +} + +int LuaMatrix4::l_copy(lua_State *L) +{ + const auto &matrix = check(L, 1); + create(L) = matrix; + return 1; +} + +int LuaMatrix4::l_unpack(lua_State *L) +{ + const auto &matrix = check(L, 1); + lua_createtable(L, 16, 0); + for (int i = 0; i < 16; ++i) + lua_pushnumber(L, matrix[i]); + return 16; +} + +// Linear algebra + +int LuaMatrix4::l_transform_4d(lua_State *L) +{ + const auto &matrix = check(L, 1); + f32 vec4[4]; + for (int i = 0; i < 4; ++i) + vec4[i] = luaL_checknumber(L, i + 2); + f32 res[4]; + matrix.transformVec4(res, vec4); + for (int i = 0; i < 4; ++i) + lua_pushnumber(L, res[i]); + return 4; +} + +int LuaMatrix4::l_transform_position(lua_State *L) +{ + const auto &matrix = check(L, 1); + v3f vec = readParam(L, 2); + matrix.transformVect(vec); + push_v3f(L, vec); + return 1; +} + +int LuaMatrix4::l_transform_direction(lua_State *L) +{ + const auto &matrix = check(L, 1); + v3f vec = readParam(L, 2); + v3f res = matrix.rotateAndScaleVect(vec); + push_v3f(L, res); + return 1; +} + +int LuaMatrix4::l_compose(lua_State *L) +{ + int n_args = lua_gettop(L); + if (n_args == 0) + return LuaMatrix4::l_identity(L); + const auto &first = check(L, 1); + auto &product = create(L); + product = first; + for (int i = 2; i <= n_args; ++i) { + product *= check(L, i); + } + return 1; +} + +int LuaMatrix4::l_transpose(lua_State *L) +{ + const auto &matrix = check(L, 1); + create(L) = matrix.getTransposed(); + return 1; +} + +int LuaMatrix4::l_determinant(lua_State *L) +{ + const auto &matrix = check(L, 1); + lua_pushnumber(L, matrix.determinant()); + return 1; +} + +int LuaMatrix4::l_invert(lua_State *L) +{ + const auto &matrix = check(L, 1); + core::matrix4 inverse; + if (!matrix.getInverse(inverse)) { + lua_pushnil(L); + return 1; + } + create(L) = inverse; + return 1; +} + +int LuaMatrix4::l_equals(lua_State *L) +{ + const auto &a = check(L, 1); + const auto &b = check(L, 2); + f32 tol = luaL_optnumber(L, 3, 0.0); + lua_pushboolean(L, a.equals(b, tol)); + return 1; +} + +int LuaMatrix4::l_is_affine_transform(lua_State *L) +{ + const auto &matrix = check(L, 1); + f32 tol = luaL_optnumber(L, 3, 0.0); + lua_pushboolean(L, matrix.isAffine(tol)); + return 1; +} + +// Affine transform helpers + +int LuaMatrix4::l_get_translation(lua_State *L) +{ + auto matrix = check(L, 1); + push_v3f(L, matrix.getTranslation()); + return 1; +} + +int LuaMatrix4::l_get_rs(lua_State *L) +{ + // TODO ? should check that it is, in fact, a rotation matrix; + // not a fake rotation (axis flip) or a shear matrix + auto matrix = check(L, 1); + v3f scale = matrix.getScale(); + if (scale.X == 0.0f || scale.Y == 0.0f || scale.Z == 0.0f) { + LuaRotation::create(L, core::quaternion()); + push_v3f(L, scale); + return 2; + } + matrix.scaleAxes(v3f(1.0f) / scale); + LuaRotation::create(L, core::quaternion(matrix)); + push_v3f(L, scale); + return 2; +} + +int LuaMatrix4::l_set_translation(lua_State *L) +{ + auto &matrix = check(L, 1); + v3f translation = readParam(L, 2); + matrix.setTranslation(translation); + return 0; +} + +int LuaMatrix4::mt_add(lua_State *L) +{ + const auto &a = check(L, 1); + const auto &b = check(L, 2); + auto &res = create(L); + res = a; + res += b; + return 1; +} + +int LuaMatrix4::mt_sub(lua_State *L) +{ + const auto &a = check(L, 1); + const auto &b = check(L, 2); + auto &res = create(L); + res = a; + res -= b; + return 1; +} + +int LuaMatrix4::mt_unm(lua_State *L) +{ + const auto &matrix = check(L, 1); + auto &res = create(L); + res = matrix; + res *= -1.0f; + return 1; +} + +int LuaMatrix4::mt_mul(lua_State *L) +{ + if (lua_isnumber(L, 1)) { + f32 scalar = luaL_checknumber(L, 1); + const auto &matrix = check(L, 2); + create(L) = scalar * matrix; + } else { + const auto &matrix = check(L, 1); + f32 scalar = luaL_checknumber(L, 2); + create(L) = matrix * scalar; + } + return 1; +} + +int LuaMatrix4::mt_eq(lua_State *L) +{ + const auto &a = check(L, 1); + const auto &b = check(L, 2); + lua_pushboolean(L, a == b); + return 1; +} + +int LuaMatrix4::mt_tostring(lua_State *L) +{ + const auto &matrix = check(L, 1); + std::ostringstream ss; + ss << matrix; + std::string str = ss.str(); + lua_pushlstring(L, str.c_str(), str.size()); + return 1; +} + +void *LuaMatrix4::packIn(lua_State *L, int idx) +{ + return new core::matrix4(check(L, idx)); +} + +void LuaMatrix4::packOut(lua_State *L, void *ptr) +{ + auto *matrix = static_cast(ptr); + if (L) + create(L) = *matrix; + delete matrix; +} + +void LuaMatrix4::Register(lua_State *L) +{ + static const luaL_Reg metamethods[] = { + {"__tostring", mt_tostring}, + {"__add", mt_add}, + {"__sub", mt_sub}, + {"__unm", mt_unm}, + {"__mul", mt_mul}, + {"__eq", mt_eq}, + {0, 0} + }; + registerClass(L, methods, metamethods); + + lua_createtable(L, 0, 0); + int Matrix4 = lua_gettop(L); +#define CONSTRUCTOR(name) \ + lua_pushcfunction(L, l_##name); \ + lua_setfield(L, Matrix4, #name); \ + + CONSTRUCTOR(new) + CONSTRUCTOR(identity) + CONSTRUCTOR(all) + + CONSTRUCTOR(translation) + CONSTRUCTOR(rotation) + CONSTRUCTOR(scale) + CONSTRUCTOR(reflection) + CONSTRUCTOR(compose) +#undef CONSTRUCTOR + lua_setglobal(L, className); + + script_register_packer(L, className, packIn, packOut); +} + +const char LuaMatrix4::className[] = "Matrix4"; +#define METHOD(name) luamethod(LuaMatrix4, name) +const luaL_Reg LuaMatrix4::methods[] = { + METHOD(get), + METHOD(set), + METHOD(get_row), + METHOD(set_row), + METHOD(get_column), + METHOD(set_column), + METHOD(copy), + METHOD(unpack), + METHOD(transform_4d), + METHOD(transform_position), + METHOD(transform_direction), + METHOD(compose), + METHOD(determinant), + METHOD(invert), + METHOD(transpose), + METHOD(equals), + METHOD(is_affine_transform), + METHOD(get_translation), + METHOD(get_rs), + METHOD(set_translation), + {0,0} +}; +#undef METHOD diff --git a/src/script/lua_api/l_matrix4.h b/src/script/lua_api/l_matrix4.h new file mode 100644 index 000000000..1fcd273c5 --- /dev/null +++ b/src/script/lua_api/l_matrix4.h @@ -0,0 +1,114 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Lars Müller + +#pragma once + +#include "lua_api/l_base.h" +#include "matrix4.h" +#include + +class LuaMatrix4 : public ModApiBase +{ +private: + core::matrix4 matrix; + + static const luaL_Reg methods[]; + + // Exported functions + + // Constructors + + // identity() + static int l_identity(lua_State *L); + // all(number) + static int l_all(lua_State *L); + // translation(vec) + static int l_translation(lua_State *L); + // rotation(rot) + static int l_rotation(lua_State *L); + // reflection(normal) + static int l_reflection(lua_State *L); + // scale(vec) + static int l_scale(lua_State *L); + // new(a11, a12, ..., a44) + static int l_new(lua_State *L); + + // Misc. container utils + + // get(self, row, column) + static int l_get(lua_State *L); + // set(self, row, column, value) + static int l_set(lua_State *L); + // x, y, z, w = get_row(self, row) + static int l_get_row(lua_State *L); + // set_row(self, row, x, y, z, w) + static int l_set_row(lua_State *L); + // x, y, z, w = get_column(self, column) + static int l_get_column(lua_State *L); + // set_column(self, column, x, y, z, w) + static int l_set_column(lua_State *L); + // copy(self) + static int l_copy(lua_State *L); + // unpack(self) + static int l_unpack(lua_State *L); + + // x, y, z, w = transform_4d(self, x, y, z, w) + static int l_transform_4d(lua_State *L); + // transform_position(self, vector) + static int l_transform_position(lua_State *L); + // transform_direction(self, vector) + static int l_transform_direction(lua_State *L); + // compose(self, other) + static int l_compose(lua_State *L); + // determinant(self) + static int l_determinant(lua_State *L); + // invert(self) + static int l_invert(lua_State *L); + // transpose(self) + static int l_transpose(lua_State *L); + // equals(self, other, [tolerance]) + static int l_equals(lua_State *L); + // is_affine_transform(self) + static int l_is_affine_transform(lua_State *L); + + // get_translation(self) + static int l_get_translation(lua_State *L); + // rotation, scale = get_rs(self) + static int l_get_rs(lua_State *L); + + // set_translation(self, translation) + static int l_set_translation(lua_State *L); + + // set_rotation and set_scale are deliberately omitted + // to nudge users not to decompose and recompose matrices. + // Instead they should store TRS transforms and construct matrices from them. + + // m1 + m2 + static int mt_add(lua_State *L); + // m1 - m2 + static int mt_sub(lua_State *L); + // -m + static int mt_unm(lua_State *L); + // scalar * m; m * scalar + static int mt_mul(lua_State *L); + // m1 == m2 + static int mt_eq(lua_State *L); + // tostring(m) + static int mt_tostring(lua_State *L); + + static void *packIn(lua_State *L, int idx); + static void packOut(lua_State *L, void *ptr); + +public: + + // Constructor. Leaves the value on top of the stack. + // Returns a reference that *must* be overwritten. + [[nodiscard]] static inline core::matrix4 &create(lua_State *L); + + [[nodiscard]] static core::matrix4 &check(lua_State *L, int index); + + static void Register(lua_State *L); + + static const char className[]; +}; diff --git a/src/script/lua_api/l_rotation.cpp b/src/script/lua_api/l_rotation.cpp new file mode 100644 index 000000000..c2f534d42 --- /dev/null +++ b/src/script/lua_api/l_rotation.cpp @@ -0,0 +1,239 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Lars Müller + +#include "lua_api/l_rotation.h" +#include "common/c_packer.h" +#include "lua_api/l_internal.h" + +#include "common/c_converter.h" +#include "irr_v3d.h" +#include "quaternion.h" + +#include +#include +#include +#include + +core::quaternion &LuaRotation::check(lua_State *L, int index) +{ + return static_cast(luaL_checkudata(L, index, className))->quaternion; +} + +void LuaRotation::create(lua_State *L, const core::quaternion &quaternion) +{ + auto *rot = static_cast(lua_newuserdata(L, sizeof(LuaRotation))); + rot->quaternion = quaternion; + luaL_getmetatable(L, LuaRotation::className); + lua_setmetatable(L, -2); +} + +// Constructors + +int LuaRotation::l_identity(lua_State *L) +{ + create(L, core::quaternion()); + return 1; +} + +int LuaRotation::l_quaternion(lua_State *L) +{ + // TODO be more strict. + f64 x = luaL_checknumber(L, 1); + f64 y = luaL_checknumber(L, 2); + f64 z = luaL_checknumber(L, 3); + f64 w = luaL_checknumber(L, 4); + // Note: Converted to f32 + core::quaternion q(x, y, z, w); + q.normalize(); + create(L, q); + return 1; +} + +int LuaRotation::l_axis_angle(lua_State *L) +{ + v3f axis = readParam(L, 1); + f64 angle = luaL_checknumber(L, 2); + core::quaternion quaternion; + // Note: Axis converted to f32 + axis.normalize(); + quaternion.fromAngleAxis(angle, axis); + create(L, quaternion); + return 1; +} + +template +int LuaRotation::l_fixed_axis_angle(lua_State *L) +{ + f64 angle = luaL_checknumber(L, 1); + // Note: Angle converted to f32 + v3f axis; + axis.*C = 1.0f; + create(L, core::quaternion().fromAngleAxis(angle, axis)); + return 1; +} + +int LuaRotation::l_euler_angles(lua_State *L) +{ + v3f euler = readParam(L, 1); + core::quaternion quaternion; + // Note: Euler angles converted to f32 + quaternion.set(euler.X, euler.Y, euler.Z); + create(L, quaternion); + return 1; +} + +int LuaRotation::l_to_quaternion(lua_State *L) +{ + const auto &q = check(L, 1); + lua_pushnumber(L, q.X); + lua_pushnumber(L, q.Y); + lua_pushnumber(L, q.Z); + lua_pushnumber(L, q.W); + return 4; +} + +int LuaRotation::l_to_axis_angle(lua_State *L) +{ + const auto q = check(L, 1); + core::vector3df axis; + f32 angle; + q.toAngleAxis(angle, axis); + push_v3f(L, axis); + lua_pushnumber(L, angle); + return 2; +} + +int LuaRotation::l_to_euler_angles(lua_State *L) +{ + const auto &q = check(L, 1); + core::vector3df euler; + q.toEuler(euler); + lua_pushnumber(L, euler.X); + lua_pushnumber(L, euler.Y); + lua_pushnumber(L, euler.Z); + return 3; +} + +// Math + +int LuaRotation::l_apply(lua_State *L) +{ + const auto &q = check(L, 1); + v3f vec = readParam(L, 2); + push_v3f(L, q * vec); + return 1; +} + +int LuaRotation::l_compose(lua_State *L) +{ + int n_args = lua_gettop(L); + if (n_args == 0) + return LuaRotation::l_identity(L); + auto product = check(L, 1); + for (int i = 2; i <= n_args; ++i) { + product *= check(L, i); + } + create(L, product); + return 1; +} + +int LuaRotation::l_invert(lua_State *L) +{ + create(L, check(L, 1).makeInverse()); + return 1; +} + +int LuaRotation::l_slerp(lua_State *L) +{ + const auto &from = check(L, 1); + const auto &to = check(L, 2); + f32 time = readParam>(L, 3).value; + core::quaternion result; + result.slerp(from, to, time); + create(L, result); + return 1; +} + +int LuaRotation::l_angle_to(lua_State *L) +{ + const auto &from = check(L, 1); + const auto &to = check(L, 2); + f32 angle = from.angleTo(to); + lua_pushnumber(L, angle); + return 1; +} + +// Serialization + +int LuaRotation::mt_tostring(lua_State *L) +{ + const auto &q = check(L, 1); + lua_pushfstring(L, "(%f\t%f\t%f\t%f)", q.X, q.Y, q.Z, q.W); + return 1; +} + +void *LuaRotation::packIn(lua_State *L, int idx) +{ + return new core::quaternion(check(L, idx)); +} + +void LuaRotation::packOut(lua_State *L, void *ptr) +{ + auto *quat = static_cast(ptr); + if (L) + create(L, *quat); + delete quat; +} + +void LuaRotation::Register(lua_State *L) +{ + static const luaL_Reg metamethods[] = { + {"__tostring", mt_tostring}, + {0, 0} + }; + registerClass(L, methods, metamethods); + + lua_createtable(L, 0, 0); + int Matrix4 = lua_gettop(L); + +#define SET_CONSTRUCTOR(name, method) \ + lua_pushcfunction(L, method); \ + lua_setfield(L, Matrix4, name); \ + +#define CONSTRUCTOR(name) SET_CONSTRUCTOR(#name, l_##name) + + CONSTRUCTOR(identity) + CONSTRUCTOR(quaternion) + CONSTRUCTOR(axis_angle) + CONSTRUCTOR(euler_angles) + CONSTRUCTOR(compose) + +#undef CONSTRUCTOR + + SET_CONSTRUCTOR("x", l_fixed_axis_angle<&v3f::X>) + SET_CONSTRUCTOR("y", l_fixed_axis_angle<&v3f::Y>) + SET_CONSTRUCTOR("z", l_fixed_axis_angle<&v3f::Z>) + +#undef SET_CONSTRUCTOR + + lua_setglobal(L, className); + + script_register_packer(L, className, packIn, packOut); +} + +const char LuaRotation::className[] = "Rotation"; + +#define METHOD(name) luamethod(LuaRotation, name) +const luaL_Reg LuaRotation::methods[] = { + METHOD(to_quaternion), + METHOD(to_axis_angle), + METHOD(to_euler_angles), + METHOD(apply), + METHOD(compose), + METHOD(invert), + METHOD(slerp), + METHOD(angle_to), + {0,0} +}; +#undef METHOD diff --git a/src/script/lua_api/l_rotation.h b/src/script/lua_api/l_rotation.h new file mode 100644 index 000000000..458bbd78b --- /dev/null +++ b/src/script/lua_api/l_rotation.h @@ -0,0 +1,69 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2025 Lars Müller + +#pragma once + +#include "irr_v3d.h" +#include "lua_api/l_base.h" +#include "quaternion.h" + +class LuaRotation : public ModApiBase +{ +private: + core::quaternion quaternion; + + static const luaL_Reg methods[]; + + // Conversions + // Note: Matrix conversions are in l_matrix.h + + // self = identity() + static int l_identity(lua_State *L); + // self = quaternion(x, y, z, w) + static int l_quaternion(lua_State *L); + // self = axis_angle(axis, angle) + static int l_axis_angle(lua_State *L); + // self = x(angle); self = y(angle); self = z(angle) + template + static int l_fixed_axis_angle(lua_State *L); + // self = euler_angles(pitch, yaw, roll) + static int l_euler_angles(lua_State *L); + + // x, y, z, w = to_quaternion(self) + static int l_to_quaternion(lua_State *L); + // axis, angle = to_axis_angle(self) + static int l_to_axis_angle(lua_State *L); + // pitch, yaw, roll = to_euler_angles(self) + static int l_to_euler_angles(lua_State *L); + + // rotated_vector = apply(self, vector) + static int l_apply(lua_State *L); + // composition = compose(self, other) + static int l_compose(lua_State *L); + // inverse = invert(self) + static int l_invert(lua_State *L); + + // slerped = slerp(from, to, time) + static int l_slerp(lua_State *L); + // angle = angle_to(to) + static int l_angle_to(lua_State *L); + + // tostring(self) + static int mt_tostring(lua_State *L); + + static void *packIn(lua_State *L, int idx); + static void packOut(lua_State *L, void *ptr); + + +public: + + /// Constructor. Leaves the value on top of the stack. + static void create(lua_State *L, const core::quaternion &quaternion); + + [[nodiscard]] static core::quaternion &check(lua_State *L, int index); + + static void Register(lua_State *L); + + static const char className[]; +}; diff --git a/src/script/scripting_server.cpp b/src/script/scripting_server.cpp index f30def03d..7e2c0de56 100644 --- a/src/script/scripting_server.cpp +++ b/src/script/scripting_server.cpp @@ -3,6 +3,8 @@ // Copyright (C) 2013 celeron55, Perttu Ahola #include "scripting_server.h" +#include "lua_api/l_rotation.h" +#include "lua_api/l_matrix4.h" #include "server.h" #include "log.h" #include "settings.h" @@ -141,6 +143,8 @@ void ServerScripting::InitializeModApi(lua_State *L, int top) LuaRaycast::Register(L); LuaSecureRandom::Register(L); LuaVoxelManip::Register(L); + LuaRotation::Register(L); + LuaMatrix4::Register(L); NodeMetaRef::Register(L); NodeTimerRef::Register(L); ObjectRef::Register(L); @@ -179,6 +183,8 @@ void ServerScripting::InitializeAsync(lua_State *L, int top) LuaSecureRandom::Register(L); LuaVoxelManip::Register(L); LuaSettings::Register(L); + LuaRotation::Register(L); + LuaMatrix4::Register(L); // globals data auto *data = ModApiBase::getServer(L)->m_lua_globals_data.get(); diff --git a/src/unittest/test_irr_rotation.cpp b/src/unittest/test_irr_rotation.cpp index 0aa9c9573..0686938bd 100644 --- a/src/unittest/test_irr_rotation.cpp +++ b/src/unittest/test_irr_rotation.cpp @@ -96,6 +96,22 @@ SECTION("matrix-quaternion roundtrip") { }); } +SECTION("matrix-quaternion roundtrip") { + v3f rad(0, 0, irr::core::PI / 2); + // test_euler_angles_rad([](v3f rad) { + quaternion q; + q.set(rad); + matrix4 mat; + q.getMatrix(mat); + quaternion q2(mat); + // q2.makeInverse(); + matrix4 mat2; + q2.getMatrix(mat2); + CHECK(matrix_equals(mat, mat2)); + // CHECK(q.angleTo(q2) < 1e-4); + // }); +} + SECTION("matrix-euler roundtrip") { test_euler_angles_rad([](v3f rad) { matrix4 mat, mat2;