diff --git a/doc/lua_api.md b/doc/lua_api.md index 1b7bafe98..34311d33c 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -4154,19 +4154,22 @@ For example: Rotations ========= -As abusing vectors of euler angles is discouraged as error-prone, Luanti provides a proper helper class for working with 3d rotations. +Using vectors of euler angles instead is discouraged as it is error-prone. -You must not rely on the specific type or imprecision of the current implementation. +The precision of the implementation may change (improve) in the future. + +Adhering to Luanti and Irrlicht conventions, rotations use **left-handed** conventions +with a rotation order of **XYZ** (X first, then Y, then Z). 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) + 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 + 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: @@ -4175,16 +4178,15 @@ Constructors * `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. + * Mathematically equivalent to `Rotation.compose(Rotation.z(roll), Rotation.y(yaw), Rotation.x(pitch))`. + * Consistent with the euler angles that can be used for bones or attachments. Conversions ----------- -Corresponding to the constructors, quaternions can be converted +Corresponding to the constructors, rotations 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: +you merely get values that produce a (roughly) equivalent rotation when passed to the corresponding constructor: * `x, y, z, w = Rotation:to_quaternion()` * Returns the normalized quaternion representation. @@ -4194,8 +4196,8 @@ you merely get values that produce the same rotation when passed to the correspo * `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 + +Rotations can also be converted to matrices using `Matrix4.rotation(rot)`. Methods ------- @@ -4212,11 +4214,14 @@ Methods * `Rotation:angle_to(other)`: Returns the absolute angle between two quaternions. * Useful to measure similarity. +Rotations implement `__tostring`. The format is only intended for human-readability, +not serialization, and may thus change. + Matrices ======== -Luanti uses 4x4 matrices to represent transformations of 3d vectors. +Luanti uses 4x4 matrices to represent transformations of 3d vectors (embedded into 4d space). 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. @@ -4231,6 +4236,8 @@ 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. +Row and column indices range from `1` to `4`. + Constructors ------------ @@ -4250,31 +4257,34 @@ 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 +* `Matrix4:get(row, col)` +* `Matrix4:set(row, col, number)` * `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. +* `Matrix4:copy()` +* `... = Matrix4:unpack()`: Get the 16 numbers in the matrix in row-major order + (inverse of `Matrix4.new`). Linear algebra: +* Vector transformations: + * `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.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). + If `...` is empty, this is just the identity. +* `Matrix4:determinant()`: Returns the determinant. +* `Matrix4:invert()`: Returns a newly created inverse, or `nil` if the matrix is (close to being) singular. +* `Matrix4:transpose()`: Returns a transposed copy of the matrix. * `Matrix4:equals(other, [tolerance = 0])`: Returns whether all components differ in absolute value at most by the given tolerance. + * `m1 == m2`: Returns whether `m1` and `m2` are identical (`tolerance = 0`). * `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. @@ -4282,36 +4292,33 @@ Linear algebra: For working with affine transforms, the following methods are available: -* `Matrix4:get_translation()`: - Returns the translation as a vector. -* `Matrix4:set_translation(vec)` +* `Matrix4:get_translation()`: Returns the translation as a vector. +* `Matrix4:set_translation(vec)`: Sets (overwrites) the translation in the last row. 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`. + 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: +Similar to vectors, matrices define some element-wise 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`: Returns the additive inverse. * `m * s` or `s * m`: Returns the matrix `m` scaled by the scalar `s`. - * Note: *All* entries are scaled, including the last row. + * Note: *All* entries are scaled, including the last column: + The matrix may not be an affine transform afterwards. Matrices also define a `__tostring` metamethod. This is only intended for human readability and not for serialization. - Helper functions ================ diff --git a/irr/include/matrix4.h b/irr/include/matrix4.h index 65927a141..c25a3b5d6 100644 --- a/irr/include/matrix4.h +++ b/irr/include/matrix4.h @@ -1663,14 +1663,16 @@ inline void CMatrix4::getTransposed(CMatrix4 &o) const template std::ostream& operator<<(std::ostream& os, const CMatrix4& matrix) { - for (int row = 0; row < 4; ++row) { + os << "(\n"; + for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { os << "\t"; os << matrix(row, col); } os << "\n"; } - return os; + os << ")"; + return os; } // used to scale <-1,-1><1,1> to viewport diff --git a/irr/include/quaternion.h b/irr/include/quaternion.h index 00ec8a16d..f3ddc5bd6 100644 --- a/irr/include/quaternion.h +++ b/irr/include/quaternion.h @@ -9,6 +9,8 @@ #include "matrix4.h" #include "vector3d.h" +#include + // NOTE: You *only* need this when updating an application from Irrlicht before 1.8 to Irrlicht 1.8 or later. // Between Irrlicht 1.7 and Irrlicht 1.8 the quaternion-matrix conversions changed. // Before the fix they had mixed left- and right-handed rotations. @@ -215,6 +217,12 @@ public: f32 W; // real part }; +std::ostream& operator<<(std::ostream& os, const quaternion& q) +{ + os << q.X << "\t" << q.Y << "\t" << q.Z << "\t" << q.W; + return os; +} + // Constructor which converts Euler angles to a quaternion inline quaternion::quaternion(f32 x, f32 y, f32 z) { diff --git a/src/script/common/helper.cpp b/src/script/common/helper.cpp index 7c5e633d5..902d9c9eb 100644 --- a/src/script/common/helper.cpp +++ b/src/script/common/helper.cpp @@ -13,7 +13,6 @@ extern "C" { #include #include #include "c_converter.h" -#include "c_types.h" /* * Read template functions @@ -50,12 +49,12 @@ f64 LuaHelper::readParam(lua_State *L, int index) } template <> -LuaHelper::Finite LuaHelper::readParam(lua_State *L, int index) +f32 LuaHelper::readFiniteParam(lua_State *L, int index) { f64 original_value = luaL_checknumber(L, index); f32 v = static_cast(original_value); if (std::isfinite(v)) - return {v}; + return v; if (std::isnan(original_value)) luaL_argerror(L, index, "number is NaN"); if (!std::isfinite(original_value)) @@ -66,11 +65,11 @@ LuaHelper::Finite LuaHelper::readParam(lua_State *L, int index) } template <> -LuaHelper::Finite LuaHelper::readParam(lua_State *L, int index) +f64 LuaHelper::readFiniteParam(lua_State *L, int index) { f64 v = luaL_checknumber(L, index); if (std::isfinite(v)) - return {v}; + return v; if (std::isnan(v)) luaL_argerror(L, index, "number is NaN"); luaL_argerror(L, index, "number is not finite"); diff --git a/src/script/common/helper.h b/src/script/common/helper.h index 0932029e1..df7ef6193 100644 --- a/src/script/common/helper.h +++ b/src/script/common/helper.h @@ -4,7 +4,6 @@ #pragma once -#include #include extern "C" { @@ -25,11 +24,12 @@ protected: template static T readParam(lua_State *L, int index); - /// Type to represent a restriction to finite floats - template - struct Finite { - T value; - }; + /** + * @brief Read a value, but restrict to finite floats. + * @see readParam + */ + template + static T readFiniteParam(lua_State *L, int index); /** * Read a value using a template type T from Lua state L at index diff --git a/src/script/lua_api/l_matrix4.cpp b/src/script/lua_api/l_matrix4.cpp index 1a3d5449e..56c447aba 100644 --- a/src/script/lua_api/l_matrix4.cpp +++ b/src/script/lua_api/l_matrix4.cpp @@ -14,19 +14,18 @@ #include "quaternion.h" #include -#include #include #include #include #include -template -static int read_index(lua_State *L, int index) +template +int LuaMatrix4::readIndex(lua_State *L, int index) { - f64 value = luaL_checknumber(L, index); + f64 value = readParam(L, index); if (std::floor(value) != value) luaL_argerror(L, index, "index must be integer"); - if (value < 1 || value > max) + if (value < 1 || value > MAX) luaL_argerror(L, index, "index out of range"); return static_cast(value) - 1; } @@ -52,7 +51,7 @@ int LuaMatrix4::l_identity(lua_State *L) int LuaMatrix4::l_all(lua_State *L) { - f32 v = luaL_checknumber(L, 1); + f32 v = readParam(L, 1); create(L) = v; return 1; } @@ -63,7 +62,7 @@ int LuaMatrix4::l_new(lua_State *L) 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); + matrix[i] = readParam(L, 1 + i); return 1; } @@ -118,8 +117,8 @@ int LuaMatrix4::l_reflection(lua_State *L) 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); + int row = readIndex(L, 2); + int col = readIndex(L, 3); lua_pushnumber(L, matrix(row, col)); return 1; } @@ -127,9 +126,9 @@ int LuaMatrix4::l_get(lua_State *L) 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); + int row = readIndex(L, 2); + int col = readIndex(L, 3); + f64 value = readParam(L, 4); matrix(row, col) = value; return 0; } @@ -137,7 +136,7 @@ int LuaMatrix4::l_set(lua_State *L) int LuaMatrix4::l_get_row(lua_State *L) { const auto &matrix = check(L, 1); - int row = read_index(L, 2); + int row = readIndex(L, 2); for (int col = 0; col < 4; ++col) lua_pushnumber(L, matrix(row, col)); return 4; @@ -146,11 +145,11 @@ int LuaMatrix4::l_get_row(lua_State *L) 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); + int row = readIndex(L, 2); + f32 x = readParam(L, 3); + f32 y = readParam(L, 4); + f32 z = readParam(L, 5); + f32 w = readParam(L, 6); matrix(row, 0) = x; matrix(row, 1) = y; matrix(row, 2) = z; @@ -161,7 +160,7 @@ int LuaMatrix4::l_set_row(lua_State *L) int LuaMatrix4::l_get_column(lua_State *L) { const auto &matrix = check(L, 1); - int col = read_index(L, 2); + int col = readIndex(L, 2); for (int row = 0; row < 4; ++row) lua_pushnumber(L, matrix(row, col)); return 4; @@ -170,11 +169,11 @@ int LuaMatrix4::l_get_column(lua_State *L) 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); + int col = readIndex(L, 2); + f32 x = readParam(L, 3); + f32 y = readParam(L, 4); + f32 z = readParam(L, 5); + f32 w = readParam(L, 6); matrix(0, col) = x; matrix(1, col) = y; matrix(2, col) = z; @@ -205,7 +204,7 @@ 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); + vec4[i] = readParam(L, i + 2); f32 res[4]; matrix.transformVec4(res, vec4); for (int i = 0; i < 4; ++i) @@ -354,12 +353,12 @@ int LuaMatrix4::mt_unm(lua_State *L) int LuaMatrix4::mt_mul(lua_State *L) { if (lua_isnumber(L, 1)) { - f32 scalar = luaL_checknumber(L, 1); + f32 scalar = readParam(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); + f32 scalar = readParam(L, 2); create(L) = matrix * scalar; } return 1; diff --git a/src/script/lua_api/l_matrix4.h b/src/script/lua_api/l_matrix4.h index 1fcd273c5..3f44f8371 100644 --- a/src/script/lua_api/l_matrix4.h +++ b/src/script/lua_api/l_matrix4.h @@ -100,6 +100,9 @@ private: static void *packIn(lua_State *L, int idx); static void packOut(lua_State *L, void *ptr); + template + static int readIndex(lua_State *L, int index); + public: // Constructor. Leaves the value on top of the stack. diff --git a/src/script/lua_api/l_rotation.cpp b/src/script/lua_api/l_rotation.cpp index c2f534d42..5c01a2f0f 100644 --- a/src/script/lua_api/l_rotation.cpp +++ b/src/script/lua_api/l_rotation.cpp @@ -10,10 +10,9 @@ #include "irr_v3d.h" #include "quaternion.h" -#include -#include #include #include +#include core::quaternion &LuaRotation::check(lua_State *L, int index) { @@ -38,12 +37,10 @@ int LuaRotation::l_identity(lua_State *L) 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 + f32 x = readFiniteParam(L, 1); + f32 y = readFiniteParam(L, 2); + f32 z = readFiniteParam(L, 3); + f32 w = readFiniteParam(L, 4); core::quaternion q(x, y, z, w); q.normalize(); create(L, q); @@ -53,9 +50,8 @@ int LuaRotation::l_quaternion(lua_State *L) int LuaRotation::l_axis_angle(lua_State *L) { v3f axis = readParam(L, 1); - f64 angle = luaL_checknumber(L, 2); + f32 angle = readFiniteParam(L, 2); core::quaternion quaternion; - // Note: Axis converted to f32 axis.normalize(); quaternion.fromAngleAxis(angle, axis); create(L, quaternion); @@ -65,8 +61,7 @@ int LuaRotation::l_axis_angle(lua_State *L) template int LuaRotation::l_fixed_axis_angle(lua_State *L) { - f64 angle = luaL_checknumber(L, 1); - // Note: Angle converted to f32 + f32 angle = readFiniteParam(L, 1); v3f axis; axis.*C = 1.0f; create(L, core::quaternion().fromAngleAxis(angle, axis)); @@ -77,7 +72,6 @@ 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; @@ -148,7 +142,7 @@ 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; + f32 time = readFiniteParam(L, 3); core::quaternion result; result.slerp(from, to, time); create(L, result); @@ -169,7 +163,10 @@ int LuaRotation::l_angle_to(lua_State *L) 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); + std::stringstream ss; + ss << q; + std::string str = ss.str(); + lua_pushlstring(L, str.c_str(), str.size()); return 1; }