Skip to content

Add matrix & rotation Lua APIs (2nd try)#16870

Open
appgurueu wants to merge 37 commits intoluanti-org:masterfrom
appgurueu:feat/matrix4
Open

Add matrix & rotation Lua APIs (2nd try)#16870
appgurueu wants to merge 37 commits intoluanti-org:masterfrom
appgurueu:feat/matrix4

Conversation

@appgurueu
Copy link
Contributor

This PR exposes userdata wrappers around Irrlicht 4x4 matrices and rotation quaternions. This is already useful as-is; I expect it to be even more useful once we eventually give proper control over client rendering to modders via SSCSM.

Closes #7478, closes #15985.

How to test

Unit tests are included; the bindings should work as expected. Testing more error cases (e.g. wrong types) could be considered.

Follow up

After this is merged, functions that take rotations (e.g. bone overrides, entity rotations) should be updated to accept Rotation objects. There should perhaps be new getters that produce Rotation objects.

To discuss

This PR is Ready for Review.

I am grateful for @grorp's and everyone else's comments on the last PR. I think I have dealt with everything decently here.


Supersedes #16212. (I wanted to reopen that PR, but it turns out funny things happen once you rebase and force push.)

Full disclosure: I did experiment with GitHub copilot in the making of this PR since it is quite a chore-y job of writing bindings. It was quite underwhelming though, so I ended up writing most of it by hand still.

@appgurueu appgurueu added @ Script API Feature ✨ PRs that add or enhance a feature labels Jan 21, 2026
@appgurueu appgurueu added the Action / change needed Code still needs changes (PR) / more information requested (Issues) label Feb 2, 2026
@appgurueu
Copy link
Contributor Author

appgurueu commented Feb 2, 2026

Thanks for the review! I'll address it soon.


Another thing that came to my mind, I'll want a rotation constructor similar to vector.dir_to_rotation(direction[, up]), aka "look at".

Currently considering a Rotation.mapsto(dir_in, dir_out) that would give you a rotation that maps dir_in to dir_out, rotating only vectors in the plane spanned by the two directions.


I should probably also add some devtest test entities that make use of these APIs to prove visually that everything works out in the end.

@appgurueu appgurueu removed the Action / change needed Code still needs changes (PR) / more information requested (Issues) label Feb 4, 2026
@appgurueu
Copy link
Contributor Author

I've now addressed the review, so now I'm waiting on further replies / comments, and maybe will implement the additional Rotation.mapsto and visual devtest tests in the meantime (though they are not strictly necessary for this PR).

@appgurueu
Copy link
Contributor Author

Okay, about non-finite floats:

For rotations, it's clear that we don't want them. We effectively have as invariant that we want a normalized quaternion (a well-defined rotation), and that only works with finite floats. So I don't permit them there.

For matrices, we can allow them, and most operations (e.g. applying matrices to vectors, matrix multiplication, etc.) are still well-defined. In fact it is possible to get matrices with infinities through matrix multiplication if things go out of range. It's also possible to put in large doubles which get cast to inf floats.
So I would prefer not to outright ban them.

Thoughts? Scream if you think this is a problem.

@appgurueu
Copy link
Contributor Author

Two meta-thoughts:

  • Do we mark this experimental?
  • Do we put it on the 5.17 milestone?

Copy link
Member

@Desour Desour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, idk why I haven't left any comments on this yet, for 8? months. Must have forgotten.

Only some inputs on doc for now (hopefully not too annoying).

Comment on lines 4099 to +4102
* `vector.cross(v1, v2)`:
* Returns the cross product of `v1` and `v2`.
* Returns the *right-handed* cross product of `v1` and `v2`.
* To get the left-handed cross product
(e.g. for use with rotations), swap `v1` and `v2`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cross((1,0,0), (0,1,0)) is (0,0,1).
In a right-handed coord system this is right-handed.
But luanti worlds use a left-handed coord system, so it's left-handed.

Doc needs to be clear. Just saying "right-handed" is ambiguous.

The precision of the implementation may change (improve) in the future.
Rotations currently use 32-bit floats (*less* precision than the Lua number type).

Rotations use **left-handed** conventions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here.

Rotations currently use 32-bit floats (*less* precision than the Lua number type).

Rotations use **left-handed** conventions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In the current implementation, Rotations are just an abstraction over quaternions.

Would be helpful info (and grep endpoint) imo for users who want to use quaternions.

* This is consistent with the Euler angles that can be used for entities.
You can do `Rotation.euler_zxy((-rotation):unpack())`
to convert an entity rotation vector (note the handedness conversion).
* `Rotation.mapsto(dir_from, dir_to)`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sound like a function that returns a bool.
Would prefer mappingto or similar.

Also, why no snake case? maps_to

* `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)`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(

Also here, sounds like it's measuring the axis of an angle (yes this makes no sense, but that's how I read x), the thinking comes afterwards).
from_axis_angle would be more clear.

Similar for other ctors.

Well, I guess one can get accustomed to this kind of naming.

)

========

Luanti uses 4x4 matrices to represent linear transformations of 3D vectors.
For this, 3D vectors are embedded into 4D space.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

into 3D projective space using homogeneous coordinates.

* `mat:compose(...)`: Shorthand for `Matrix4.compose(mat, ...)`.
* `mat:determinant()`: Returns the determinant.
* `mat:invert()`: Returns a newly created inverse, or `nil` if the matrix is (close to being) singular.
* `mat:transpose()`: Returns a transposed copy of the matrix.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(

Would like to also have the adjucate (adj) (essentially a cheaper but scaled inverse). (And combination of adj and transposed.)
(Probably will only get relevant once we have 4d vectors though.)
(Could also have even more ctors then, i.e. for projection matrices.)

)

* `mat: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.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(This is the case if the last column is approximately 0, 0, 0, 1.)
(This is the case if the last row is approximately 0, 0, 0, 1.)

(Also, technically you can also have affine transformations with other last rows (0,0,0,a), but you have to divide by the w component (by a) when dehomogenizing (equivalent: divide the whole matrix by a). But idk if doc formulation could include this easily, or should.)

let `mat = Matrix4.compose(Matrix4.translation(t), Matrix4.rotation(r), Matrix4.scale(s))`.
Then we can decompose `mat` further. Note that `mat` must not shear or reflect.

* `rotation, scale = mat:get_rs()`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading mat:get_rs() somewhere, I wouldn't know what it means. Is it that common that it needs such a short name?
get_rot_scale?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TRS is a common thing, but I'm not sure if RS is clear to people

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will give this a longer name.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if not, methods will still exist, right? UB? Or unspecified result? (I.e. may it crash?)

@appgurueu
Copy link
Contributor Author

Sorry, idk why I haven't left any comments on this yet, for 8? months. Must have forgotten.
Only some inputs on doc for now (hopefully not too annoying).

No worries, I appreciate the review; there's no rush. I'll address it in a bit.

Comment on lines +4358 to +4360
* `mat:transform_pos(pos)`:
* Apply the matrix to a vector representing a position.
* Applies the transformation as if w = 1 and discards the resulting w component.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ... does this effectively do M * T(v.append(1)) where T means transposing?

Copy link
Contributor Author

@appgurueu appgurueu Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take v.append(1) to be the row vector (x, y, z, 1), and then discard the w component: Yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature ✨ PRs that add or enhance a feature @ Script API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lua API: Add rotation class Lua API: Add matrix math facilities

4 participants