Skip to content

Commit 23bf50a

Browse files
authored
Document & extend testing for rotation conventions (#16200)
* Document Luanti rotation conventions * Add test for setPitchYawRollRad (entity) rotation conventions * Test and document that `vector.rotate` uses (extrinsic) Z-X-Y rotation order
1 parent 3394002 commit 23bf50a

File tree

7 files changed

+123
-38
lines changed

7 files changed

+123
-38
lines changed

builtin/common/tests/vector_spec.lua

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,32 @@ describe("vector", function()
432432
assert.True(almost_equal({x = 1, y = 0, z = 0},
433433
vector.rotate({x = 1, y = 0, z = 0}, {x = math.pi / 123, y = 0, z = 0})))
434434
end)
435-
it("is counterclockwise", function()
435+
it("rotation order is Z-X-Y", function()
436+
local r = vector.new(1, 2, 3)
437+
for _, v in ipairs({
438+
vector.new(1, 0, 0),
439+
vector.new(0, 1, 0),
440+
vector.new(0, 0, 1),
441+
}) do
442+
local expected = v:rotate(r)
443+
local function try(order)
444+
local rotated = v
445+
for axis in order:gmatch(".") do
446+
local r_axis = vector.zero()
447+
r_axis[axis] = r[axis]
448+
rotated = vector.rotate(rotated, r_axis)
449+
end
450+
return almost_equal(rotated, expected)
451+
end
452+
assert.False(try("xyz"))
453+
assert.False(try("xzy"))
454+
assert.False(try("yxz"))
455+
assert.False(try("yzx"))
456+
assert.True(try("zxy"))
457+
assert.False(try("zyx"))
458+
end
459+
end)
460+
it("is right handed", function()
436461
local v_before1 = {x = 0, y = 1, z = -1}
437462
local v_after1 = vector.rotate(v_before1, {x = math.pi / 4, y = 0, z = 0})
438463
assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0}))

doc/lua_api.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3935,6 +3935,32 @@ The following functions provide escape sequences:
39353935
* Removes all color escape sequences.
39363936

39373937

3938+
Coordinate System
3939+
=================
3940+
3941+
Luanti uses a **left-handed** coordinate system: Y is "up", X is "right", Z is "forward".
3942+
This is the convention used by Unity, DirectX and Irrlicht.
3943+
It means that when you're pointing in +Z direction in-game ("forward"), +X is to your right; +Y is up.
3944+
3945+
Consistently, rotation is [**left-handed**](https://en.wikipedia.org/w/index.php?title=Right-hand_rule) as well.
3946+
Luanti uses [Tait-Bryan angles](https://en.wikipedia.org/wiki/Euler_angles#Tait%E2%80%93Bryan_angles) for rotations,
3947+
often referred to simply as "euler angles" (even though they are not "proper" euler angles).
3948+
The rotation order is extrinsic X-Y-Z:
3949+
First rotation around the (unrotated) X-axis is applied,
3950+
then rotation around the (unrotated) Y-axis follows,
3951+
and finally rotation around the (unrotated) Z-axis is applied.
3952+
(Note: As a product of rotation matrices, this will be written in reverse, so `Z*Y*X`.)
3953+
3954+
Attachment and bone override rotations both use these conventions.
3955+
3956+
There is an exception, however: Object rotation (`ObjectRef:set_rotation`, `ObjectRef:get_rotation`, `automatic_rotate`)
3957+
**does not** use left-handed (extrinsic) X-Y-Z rotations.
3958+
Instead, it uses **right-handed (extrinsic) Z-X-Y** rotations:
3959+
First roll (Z) is applied, then pitch (X); yaw (Y) is applied last.
3960+
3961+
See [Scratchapixel](https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/coordinate-systems.html)
3962+
or [Wikipedia](https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Orientation_and_handedness)
3963+
for a more detailed and pictorial explanation of these terms.
39383964

39393965

39403966
Spatial Vectors
@@ -4134,6 +4160,7 @@ angles in radians.
41344160

41354161
* `vector.rotate(v, r)`:
41364162
* Applies the rotation `r` to `v` and returns the result.
4163+
* Uses (extrinsic) Z-X-Y rotation order and is right-handed, consistent with `ObjectRef:set_rotation`.
41374164
* `vector.rotate(vector.new(0, 0, 1), r)` and
41384165
`vector.rotate(vector.new(0, 1, 0), r)` return vectors pointing
41394166
forward and up relative to an entity's rotation `r`.
@@ -8506,9 +8533,9 @@ child will follow movement and rotation of that bone.
85068533
* `interpolation`: The old and new overrides are interpolated over this timeframe (in seconds).
85078534
* `absolute`: If set to `false` (which is the default),
85088535
the override will be relative to the animated property:
8509-
* Translation in the case of `position`;
8510-
* Composition in the case of `rotation`;
8511-
* Per-axis multiplication in the case of `scale`
8536+
* Translation in the case of `position`;
8537+
* Composition in the case of `rotation`;
8538+
* Per-axis multiplication in the case of `scale`
85128539
* `property = nil` is equivalent to no override on that property
85138540
* **Note:** Unlike `set_bone_position`, the rotation is in radians, not degrees.
85148541
* Compatibility note: Clients prior to 5.9.0 only support absolute position and rotation.
@@ -8589,9 +8616,10 @@ child will follow movement and rotation of that bone.
85898616
* `acc` is a vector
85908617
* `get_acceleration()`: returns the acceleration, a vector
85918618
* `set_rotation(rot)`
8592-
* Sets the rotation
85938619
* `rot` is a vector (radians). X is pitch (elevation), Y is yaw (heading)
85948620
and Z is roll (bank).
8621+
* Sets the **right-handed Z-X-Y** rotation:
8622+
First roll (Z) is applied, then pitch (X); yaw (Y) is applied last.
85958623
* Does not reset rotation incurred through `automatic_rotate`.
85968624
Remove & re-add your objects to force a certain rotation.
85978625
* `get_rotation()`: returns the rotation, a vector (radians)
@@ -9506,7 +9534,7 @@ Player properties need to be saved manually.
95069534
-- (see node sound definition for details).
95079535

95089536
automatic_rotate = 0,
9509-
-- Set constant rotation in radians per second, positive or negative.
9537+
-- Set constant right-handed rotation in radians per second, positive or negative.
95109538
-- Object rotates along the local Y-axis, and works with set_rotation.
95119539
-- Set to 0 to disable constant rotation.
95129540

irr/include/matrix4.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,9 @@ class CMatrix4
167167
vector3d<T> getTranslation() const;
168168

169169
//! Make a rotation matrix from Euler angles. The 4th row and column are unmodified.
170-
//! NOTE: Rotation order is ZYX. This means that vectors are
171-
//! first rotated around the X, then the Y, and finally the Z axis.
170+
//! NOTE: Rotation order is (extrinsic) X-Y-Z.
171+
//! This means that vectors are first rotated around the X,
172+
//! then the (unrotated) Y, and finally the (unrotated) Z axis.
172173
//! NOTE: The rotation is done as per the right-hand rule.
173174
//! See test_irr_matrix4.cpp if you're still unsure about the conventions used here.
174175
inline CMatrix4<T> &setRotationRadians(const vector3d<T> &rotation);

src/script/lua_api/l_object.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,8 @@ int ObjectRef::l_set_rotation(lua_State *L)
11581158

11591159
v3f rotation = check_v3f(L, 2) * core::RADTODEG;
11601160

1161+
// Note: These angles are inverted before being applied using setPitchYawRoll,
1162+
// hence we end up with a right-handed rotation
11611163
entitysao->setRotation(rotation);
11621164
return 0;
11631165
}

src/unittest/test_irr_matrix4.cpp

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "irrMath.h"
77
#include "matrix4.h"
88
#include "irr_v3d.h"
9+
#include "util/numeric.h"
10+
#include <functional>
911

1012
using matrix4 = core::matrix4;
1113

@@ -17,10 +19,60 @@ constexpr v3f x{1, 0, 0};
1719
constexpr v3f y{0, 1, 0};
1820
constexpr v3f z{0, 0, 1};
1921

22+
constexpr f32 QUARTER_TURN = core::PI / 2;
23+
24+
static void LEFT_HANDED(const std::function<void(core::matrix4 &m, const v3f &rot_rad)> &f) {
25+
SECTION("rotation is left-handed") {
26+
SECTION("around the X-axis") {
27+
matrix4 X;
28+
f(X, {QUARTER_TURN, 0 , 0});
29+
CHECK(X.transformVect(x).equals(x));
30+
CHECK(X.transformVect(y).equals(z));
31+
CHECK(X.transformVect(z).equals(-y));
32+
}
33+
34+
SECTION("around the Y-axis") {
35+
matrix4 Y;
36+
f(Y, {0, QUARTER_TURN, 0});
37+
CHECK(Y.transformVect(y).equals(y));
38+
CHECK(Y.transformVect(x).equals(-z));
39+
CHECK(Y.transformVect(z).equals(x));
40+
}
41+
42+
SECTION("around the Z-axis") {
43+
matrix4 Z;
44+
f(Z, {0, 0, QUARTER_TURN});
45+
CHECK(Z.transformVect(z).equals(z));
46+
CHECK(Z.transformVect(x).equals(y));
47+
CHECK(Z.transformVect(y).equals(-x));
48+
}
49+
}
50+
}
51+
2052
TEST_CASE("matrix4") {
2153

54+
// This is in numeric.h rather than matrix4.h, but is conceptually a matrix4 method as well
55+
SECTION("setPitchYawRollRad") {
56+
SECTION("rotation order is Y*X*Z (matrix notation)") {
57+
v3f rot{1, 2, 3};
58+
matrix4 X, Y, Z, YXZ;
59+
setPitchYawRollRad(X, {rot.X, 0, 0});
60+
setPitchYawRollRad(Y, {0, rot.Y, 0});
61+
setPitchYawRollRad(Z, {0, 0, rot.Z});
62+
setPitchYawRollRad(YXZ, rot);
63+
CHECK(!matrix_equals(X * Y * Z, YXZ));
64+
CHECK(!matrix_equals(X * Z * Y, YXZ));
65+
CHECK(matrix_equals(Y * X * Z, YXZ));
66+
CHECK(!matrix_equals(Y * Z * X, YXZ));
67+
CHECK(!matrix_equals(Z * X * Y, YXZ));
68+
CHECK(!matrix_equals(Z * Y * X, YXZ));
69+
}
70+
71+
LEFT_HANDED(setPitchYawRollRad);
72+
}
73+
2274
SECTION("setRotationRadians") {
23-
SECTION("rotation order is ZYX (matrix notation)") {
75+
SECTION("rotation order is Z*Y*X (matrix notation)") {
2476
v3f rot{1, 2, 3};
2577
matrix4 X, Y, Z, ZYX;
2678
X.setRotationRadians({rot.X, 0, 0});
@@ -35,36 +87,12 @@ SECTION("setRotationRadians") {
3587
CHECK(matrix_equals(Z * Y * X, ZYX));
3688
}
3789

38-
const f32 quarter_turn = core::PI / 2;
39-
4090
// See https://en.wikipedia.org/wiki/Right-hand_rule#/media/File:Cartesian_coordinate_system_handedness.svg
4191
// for a visualization of what handedness means for rotations
4292

43-
SECTION("rotation is right-handed") {
44-
SECTION("rotation around the X-axis is Z-up, counter-clockwise") {
45-
matrix4 X;
46-
X.setRotationRadians({quarter_turn, 0, 0});
47-
CHECK(X.transformVect(x).equals(x));
48-
CHECK(X.transformVect(y).equals(z));
49-
CHECK(X.transformVect(z).equals(-y));
50-
}
51-
52-
SECTION("rotation around the Y-axis is Z-up, clockwise") {
53-
matrix4 Y;
54-
Y.setRotationRadians({0, quarter_turn, 0});
55-
CHECK(Y.transformVect(y).equals(y));
56-
CHECK(Y.transformVect(x).equals(-z));
57-
CHECK(Y.transformVect(z).equals(x));
58-
}
59-
60-
SECTION("rotation around the Z-axis is Y-up, counter-clockwise") {
61-
matrix4 Z;
62-
Z.setRotationRadians({0, 0, quarter_turn});
63-
CHECK(Z.transformVect(z).equals(z));
64-
CHECK(Z.transformVect(x).equals(y));
65-
CHECK(Z.transformVect(y).equals(-x));
66-
}
67-
}
93+
LEFT_HANDED([](core::matrix4 &m, const v3f &rot_rad) {
94+
m.setRotationRadians(rot_rad);
95+
});
6896
}
6997

7098
SECTION("getScale") {

src/unittest/test_irr_rotation.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
#include "catch_amalgamated.hpp"
66
#include "irrMath.h"
77
#include "matrix4.h"
8-
#include "irrMath.h"
9-
#include "matrix4.h"
108
#include "irr_v3d.h"
119
#include "quaternion.h"
1210
#include <functional>

src/util/numeric.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,16 @@ inline void wrappedApproachShortest(T &current, const T target, const T stepsize
478478
}
479479
}
480480

481+
/// @note Uses (extrinsic) Z-X-Y rotation order, left-handed rotation
482+
/// @note This is not consistent with matrix4::setRotationRadians
481483
void setPitchYawRollRad(core::matrix4 &m, v3f rot);
482484

483485
inline void setPitchYawRoll(core::matrix4 &m, v3f rot)
484486
{
485487
setPitchYawRollRad(m, rot * core::DEGTORAD);
486488
}
487489

490+
/// @see setPitchYawRollRad
488491
v3f getPitchYawRollRad(const core::matrix4 &m);
489492

490493
inline v3f getPitchYawRoll(const core::matrix4 &m)

0 commit comments

Comments
 (0)