-
-
Notifications
You must be signed in to change notification settings - Fork 21.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Transform2D and Transform3D scaled and rotated methods #48769
Conversation
Will it change results/behavior for people's existing projects or is this just a mathematical fix that won't affect gameplays? |
This change If you're trying to rotate or scale a |
These methods (plus
So since multiplying transforms behaves the same as in math (factor transformations are applied from right to left):
Thus I find left multiplication being way more intuitive. Besides that I think left vs right multiplication is not a proper discussion. I do think the proper solution is providing all 6 methods as proposed in godotengine/godot-proposals#1336. Then whether left or right multiplication would be the default one (used by |
This shouldn't be merged. The premise is based on some misconceptions, which I'll try to clarify below. But the solution is very simple (the last item at the bottom). 1. Godot uses column vectors.As such, matrices act on vectors as The action of a matrix on a column vector is The way the data is stored is different in Basis and Transform2D (row vs column major), but this only affects the internal implementation details, and is transparent to the user and is irrelevant in this context (By the way, I offered to change the internals in the past, but reduz wanted to keep it this way for the convenience of easily getting the columns, so instead, this point lives on as a comment in transform2d.h). This is why we type var v = Vector2(1,2)
var T = Transform2D(PI/3, Vector2(0,0))
print(T*v) returns (-1.232051, 1.866025), and not (2.23..., 0.13...). 2. The operation order on column vectors is read from right-to-leftThere is only one ordering of operations that act on column vectors:
There is nothing counter-intuitive about this, this is just how operators (whether they are functions or matrices) work. When you type g(f(v)), it means f acts first, g acts second. 3. What is a transform?A transform represents Moreover, even though Internally, Godot uses a matrix to store the You can "chain" arbitrary number of T, R and S operations in arbitrary order, but that results in garbage data and things will break. Only The following code from the original post var M = Transform.IDENTITY\
.scaled(... S_1 ...)\
.translated(... T_1 ...)\
.scaled(... S_2 ...)\
.rotated(... R_1 ...)\
.translated(... T_2 ...)\
.rotated(... R_2 ...) would produce an invalid transform in general, so there is no real use case for operations like that (regardless of the syntax, be it "chaining" or nested function calls). 4. Local and global transforms, not right or left multiplicationsWhen we type a matrix or a vector, the numbers that go inside are coordinates with respect to some frame. If we have do different frames, and know how these two are related to each other (for example "frame F is related to frame G by 60 degrees rotation and then 2x scaling" etc, which can be written as a Basis B), we can convert matrices and vectors between frames. Vectors transform as In Godot, a Transform is written in the global frame (or if the node has a parent, the local frame of the parent). In 3D, it sometimes makes sense to work in node's local frame (for example, when you want to tilt the characters head up/down in a FPS, that's a rotation around the "left-right" axis of the character). This can be done as follows. Suppose that the node has the basis The rotation in the local frame is given by
Although the end result effectively looks like we are doing a "right multiplication", that's not what we did at all. We applied a rotation on top (=multiplication from left) the existing basis, as usual. There is no such thing as "more intuitive right multiplication". What we have is global or local transformations, and they are two different things. Basis has both global and local versions of the operations (the latter has _local suffix). When I added those functions a few years ago, I also considered doing the same for Transform2D but ended up not doing it because all rotations in 2D commute. However, it can still be relevant in situations which involve nonuniform scales in conjunction with rotations. 5. What should be done?The root of the real issue is the confusion caused by the following inconsistency: rotated and scaled functions are in global frame, whereas translated is in local frame. To make the API consistent, _local versions of these functions should be added (similar to the way Basis works). In Transform2D and Transform, translated should be renamed as translated_local, and a global version (which does t.origin += v) should be added. There should also be a piece of documentation which explains what global and local rotations are for those aren't familiar with those. (Once upon a time, I started a documentation, which included a section on this, but it was deemed to be too technical and removed. That might be in the git history of the docs repo somewhere and could be used as a starting point) |
What you are seeing is exactly what a transform followed by a global rotate/scale should do. What you want is object-local versions of them, which can be implemented in the same way as Basis::rotate_local. The only issue here is, both global and local operations should be provided, they are both valid and different things. And BTW, again, you're not supposed to do do transform followed by global rotate/scale, because it results in an invalid Transform which doesn't have the form TRS; various code-paths under core/math and physics rely on the definition that Transform is TSR matrix.. |
It is still a valid affine transformation, right? Invertible affine transformation form a group, and an affine transformation does not require orthogonality. The whole point of using affine transformations / homogeneous coordinates in computer graphics is to have a unified framework for scale, rotation, translations and perspective transformations (which you'd have to call "invalid" as well?) to easily transition from model space <=> world space <=> camera space. Non-TRS transforms in user space may not be super common, but there are certainly valid use cases for sheared transforms.
Since affine transformation are in no way limited to TSR, these code paths should handle this implicit assumption pro-actively by checking if their pre-condition is satisfied.
From all my experience with other libraries and working with students/teams in physics/robotics, I have the impression that our brains can much easier think in left/right matrix multiplication (or rather after/before the current transform) to put a chain of transforms together. Perhaps because most people first touch the topic of transform chaining by multiplying matrices in linear algebra courses. Conversely when discussing transform topics, I've seen many people get confused by speaking of local/global, perhaps because what "global" really means is vague when you are only partly through a transform chain. In other words: If you are thinking in camera space, world space, model space, submodel space, ... thinking in local/global is ambiguous. I'd simply call it a day and offer all 6 variants like other libraries do, keeping the entry barrier low for people from other ecosystems. |
Godot isn't a general-purpose geometry or linear algebra library, it's a game engine. Godot's Transform (or Unity/Blender/Maya/Bullet/etc) doesn't implement general affine transforms, and valid elements of Transform are not closed under multiplication. As I explained above, it is designed to implement a TRS operation only. In fact, Bullet has additional restrictions on S (as well as some other code paths in Godot). This is a working trade-off for modeling and game physics, and allows various optimizations. As I made it very clear above, "global" meant the local frame of the parent node, and if there is no parent, it's the global frame. In the docs, I referred to it as the parent-local frame. In Basis, this is default so is no suffix for it. Local means object-local. They are very well defined frames without ambiguity. 3D->2D camera projection (which is not even invertible) is different and not a part of Transform. There are functions for that in Camera nodes. True global transformations aren't part in the API because they will often result in something which cannot be represented as TRS (unless the intermediate nodes up in the chain are uniform scaling matrices and/or the rotations are multiples of 180 degrees). Internally, such it is nonetheless used is various places internally in the form of manual transformations (skeleton code on top of my head) under various assumptions (that can be easily broken by the user), so maybe they can be added provided that the constraints are well documented. |
They are according to the type system. |
For the simplification, below I'm refering just to 2D.
@madmiraal Seems like there's a misconception in your argument. What you see in the editor inspector are
Besides that this PR introduces new inconsistency (on the C++ side): now |
The valid elements of Transform are not closed under multiplication, obviously, gosh what a nitpicker. As I repeatedly explained, yes, you can generate garbage Transform data that will break things. Look at Basis docs, I documented it there that B = RS. Somebody else can do it for 2D. In any case, game engines work with a specific subset of affine operations which don't form a group. It has worked fine for decades. You can complain all you want about it, but unless you submit an efficient replacement of core/math that does general affine transformations AND an efficient physics engine replacement that can handle those transformations, I doubt reduz or anyone will take it seriously. Talk is cheap, show me the code. |
@madmiraal After thinking about it some more, only switching to right-multiplication semantics for @tagcup2 After re-reading your proposal, I think it is exactly the same as my original proposal expect for the function naming convention. And after having a look at the existing code in |
Even though I understand why we decided to go this way, I am feeling inclined to convalidate this PR just for the fact that the way proposed is more common in graphic APIs, from Logo back in the day to OpenGL functions like glRotate/glScale and glTranslate (and by the way OpenGL transrforms are also column based), all used local transforms by default, and rename the existing ones to pre_rotate, pre_scale and pre_translate. In most cases I can think of, local transforms are preferred for ease of use. |
- Performs translated, scaled and rotated methods in the local reference frame. - Adds pre_translated, pre_scaled and pre_rotated methods that perform transformations in the parent's reference frame.
acd6307
to
34f328a
Compare
Updated to include |
With the current state of the changes I find the For example in See also godotengine/godot-proposals#1336 (comment) comparing this PR and #55923. |
@kleonc After discussing with many other contributors, opinions are pretty torn on this. I never expected it to be so divisive. |
@reduz Everyone in the discussions seems to agree that we want to fix the inconsistency between In the long term, as long as we offer all 6 variants in a consistent manner and document their semantic difference well, it doesn't matter much which one gets the "short" name in my opinion, but we might as well minimizing the amount of breakage. |
@kleonc, with this PR, Another way to think about it, and why I've explicitly stated it in the documentation, is |
@madmiraal Thanks. I see how my confusion was a result of me mixing up the conventions. Even though this PR changes the convention to "think in the local reference frame", when I saw Of course, such naming will be confusing for people fixed up to thinking in the right-to-left manner (like me) but probably any naming would be confusing here as long as someone will be thinking in the convention opposite to the one being followed. Personally, I think I've already found "a switch in my head" so it's probably a matter of realizing what perspective to look from, and getting used to it. I see how, comparing to #55923, this PR might be more average-user-friendly approach. Both of these PRs/approaches have upsides/downsides, both make sense and I don't have a strong opinion on which one I favor. At this moment #55923 seems way more intuitive for me but I see how this PR could get intuitive for me too. But there's also an average-user to consider... To sum up: now I'm pretty torn between these two approaches myself. 😄 @madmiraal BTW seems like this PR still have this issue I've mentioned earlier:
Meaning |
This proposal is totally based on misconceptions. Not that it is right or wrong. What shall be discussed here is the meaning of "rotating" a From the point of view of a If someone that lives inside Now, if you are treating, not the So, when one calls Important question 1Suppose that Again, we are taken to This has nothing to do with "doing on the left". It is a shame we keeping discussing on left and right. Exercise (read this!!!)Suppose you have a stretched axis with origin at
(Edit: These two, above were wrong. They were Inside this system of coordinates, you have a
Number 1 is what you get if you "multiply on the left". Number 2 is what you get if you "multiply on the right". Shall you rotate the house and then apply the stretching (right multiplication), getting a fat house? Or shall you apply the stretching and then rotate the house (left multiplication), getting a tall house? |
@andre-caldas I agree with you completely. With this PR, To help visualise the combinations, I've created a test project: 2DTransforms.zip Row 1 contains the rotated first combinations:
Row 2 contains the scaled first combinations:
Row 3 contains the translated first combinations:
These are the results with this PR: |
I am very sorry, the post you are referring to had wrong Second row, second column:
This should, IMO, first scale. That is, the mascot should become wide ( |
This was my first gut reaction too. However, it is correct as it is. It's easier if you first consider that it needs to be the opposite of Therefore, Note: This is the same result seen when (in the Inspector) you scale the parent and rotate the child. |
I will list what I think methods should do... Let me define things, beforehand. The
Another way to look at The linear transform takes [ x1 y1 ] [ ] [ x2 y2 ] The RotationRotate just
There is no left, there is no right!!! You rotate Although, I do not think talking about matrices very productive... in terms of matrix, this is done from the left: [cos(a) -sin(a)] [ x1 y1 ] [ ] [ ] [sin(a) cos(a)] [ x2 y2 ] Notice that those two operations above comute when scaling is uniform. That is, when The problem is when you want to represent it using "augumented matrices"!!! If you multiply augumented matrices on the left, then the origin also gets rotated. This is another reason why I find it so counterintuitive to talk about matrices in this case. Because you would have to rotate and then translate the origin back to its original position. ScalingI imagine that scaling means stretching the
Again, there is no left or right! But, of course, if you want to use matrix multiplication, then you would do it from the right: [ x1 y1 ] [ a 0 ] [ ] [ ] [ x2 y2 ] [ 0 b ] TranslationTranslation shall not affect Gobal coordinates:
Local coordinates:
Here,
Translation, rotation and scaling commute!!!By the way, the way I describe, rotation and translatio commute! And this is the expected behaviour if you want rotation to keep positition fixed, and translation to keep rotation fixed. Rotation and scaling also commute!!! And so does translation and scalling!!! They all commute. I think this is the expected behaviour!!! They all commute because translation only affects Scalling globally, but keeping origin fixedOne can also think of a global scalling: [ a 0 ] [ x1 y1 ] [ ] [ ] [ 0 b ] [ x2 y2 ] But this would generate tilted axis. It is also very legitimate! But Godot does not cope well with tilted axis. So I will not even get started with it. SummaryJust to summarize, in terms of rotation, translation and scaling, I only see the need of:
|
What is supposed to happen with:
Should it become "tilted"? Godot does not cope well with "tilted"... Godot math library has problems when |
It should do whatever a parent, child combination would do with the parent taking the first transformation and the child the second. I've created a simple demo project for the two I've animated the rotation in each case. This is the output with this PR: |
I barely see the uneven scaling on the second column.
Even if it did, the order is swapped. If you do:
It is the same as:
Rotation should be done first, and scaling, second. This is what happens if you, for example, rotate the child and scale the parent. Anyway, I think it should simply be as At the end, what you have is an origin and two axis. Forgetting about matrices, natural operations to do with this is:
This is, if I am not missing anything, what |
I would like to suggest that we solve this (and others) issue once and for all. :-) We could create one or two (or three) very precise proposals on exactly how
Also, we could have a document listing "problems" and "use cases". Each proposal must address each listed problem and use case, one by one, in a separate section. Then, some how, we could choose one. Or someone could chose one... I don't know how democratic we are. :-) Right now, the specifications for how With this in hands, it would be very easy, IMO, to implement a very nice Can anyone suggest/setup/point out the necessary infrastructure? We need a place to discuss AND a place to produce a wiki-like document. |
I've spend many many hours on this topic, and the result is exactly #55923. The public interface is complete and consistent and covers all use cases. It includes a thorough test suite covering the full API. It is also consistent with existing code of I've also spend several days alone thinking about how to cover it in the documentation. I have explained it in the docs from different perspective so that people of various backgrounds have a chance of interpreting the semantics in their way, i.e., explaining it both in terms of (1) global vs local transformation, (2) in terms of mathematical equations, and (3) in terms of transformation order, because everyone has a preferred way of approaching the topic. The goal of all these hours was exactly to provide this "very very precise" specification / documentation... |
Superseded by #55923 (for now at least). |
Currently,
Transform
andTransform2D
'sscaled()
androtated()
methods are performed in the parent reference frame (i.e. left matrix multiplication). This may make sense from a mathematical point of view, but it's not intuitive from the user's point of view, who expects the transform to take place in the local reference frame. Furthermore, it prevents these methods from being chained. To make it even more confusing, thetranslated()
method does the intuitive transform in the local reference frame (i.e. right matrix multiplication). The result is, there is currently no simply way to recreate an editor'sTransfrom
'sTranslation
,Rotation
andScale
using a series oftranslated()
,rotated()
andscaled()
methods.This PR makes the
scaled()
androtated()
perform a local transformation (i.e. right matrix multiplication) so they behave the same way astranslated()
. This not only makes them more intuitive, but it allows them to be chained.Below are 2D and 3D demo projects showing how, with this PR, an object's
Translation
,Rotation
andScale
can be easily recreated using chainedtranslated()
,rotated()
andscaled()
methods e.g.:2DTransforms.zip
3DTransforms.zip
Properly fixes #34329
Closes godotengine/godot-proposals#1336