Add reparented_to method to GlobalTransform#4891
Add reparented_to method to GlobalTransform#4891nicopap wants to merge 2 commits intobevyengine:mainfrom
reparented_to method to GlobalTransform#4891Conversation
af699e1 to
a5b1453
Compare
alice-i-cecile
left a comment
There was a problem hiding this comment.
Excellent docs that really help motivate the use case.
a5b1453 to
751a195
Compare
|
Looks like the check-doc CI check failed spuriously. I'd suggest relaunching the CI. |
|
bors try |
|
Dividing a transform is the same as multiplying by the inverse. I find this intuitive, because an inverse "undoes" a transform, e.g. if a transform converts model space coordinates to world space, than its inverse converts world space coordinates to model space. Furthermore, it's easy to understand where the inverted transform needs to be applied (left or right), because if you can put a transform next to its inverse, they cancel out (i.e. Unfortunately, the TRS representation cannot be inverted if the scale is not uniform. |
|
|
||
| /// Divide `self` with `transform`, this is the reciprocal of [`Self::mul_transform`]. | ||
| /// | ||
| /// `t2 * t1 / t2 == t1` Note that transforms are not commutative, meaning that |
There was a problem hiding this comment.
This is confusing because the divisor is on the left, but it applies the inverse on the left. This will cause a lot of confusion. Furthermore, there should be two division operators, but Div can't represent this.
There was a problem hiding this comment.
I'll be honest and admit I'm a bit confused. I've tried making sense of it on paper and my understanding is that if an inverse existed on the domain of transforms then it would be identical to (t2 * t1) * t2^-1. Am I wrong? How is that confusing? It does mirror the real numbers division (ie: a / b == a * b^-1)
Also why should there be two division operations?
There was a problem hiding this comment.
Personally (not speaking for @HackerFoo) I would not define an inverse like this.
I'd instead define it given an operation (*, 1), then an inverse for a given x, y would be such that x * y = 1. Then / can be defined as * by the inverse. Hence I'd expect a * (b / b) = a * 1 = a.
For a well-formed (*, 1), of course a * (b / b) = (a * b) / b = (b * a) / b discontinuities non-withstanding which is the relationship that you have put.
However, the substitution steps steps above rely on the fact that (*, 1) is both commutative and associative and the precedence rules between * and / don't matter. Even without commutativity and associativity I'd very much expect that t1 * (t2 / t2) == t1 * 1 should hold (where 1 is the identity matrix).
Finally, I'd expect a second division operation for scalar division of matrices.
There was a problem hiding this comment.
Thank you for the explanation. I see. So maybe it would be better to have a different name, for example "difference" or "unparent"? This way, not only it doesn't lead to confusion, but it hints at potential usage.
There was a problem hiding this comment.
I think a different name would be good but I'm at a loss at what it should be called.
Of the names you've suggested "difference" feels more correct to me, but I think I'd have to formalize why I think that.
Regardless, I don't think that it is necessarily a problem that the name is not perfect, and I would rather that it's merged for use (with an imperfect name) and then the name finalized before an actual release.
751a195 to
0d1449f
Compare
div_transform method to GlobalTransformunparent_from method to GlobalTransform
|
@yilinwei @HackerFoo I've changed the name of the "unparenting" method from |
dfd16fc to
7978ecb
Compare
|
I was going to ask how this improves on multiplying by the inverse, then I realized that If #4379 is merged, then this would be solved, and we can just use |
It solves it in the sense it stops pretending to be an inverse or a division. Since you rightly pointed out yourself, with the current transform implementation, there is no such thing. It is misleading and confusing to call it And now I realize the name doesn't convey that at all, in fact it conveys the opposite! We could wait until #4379, in which transform has a natural inverse and therefore division. But until then, we would be missing an important and useful functionality. And as you pointed out, division is a direct and simple replacement to the |
Finding the proper transform to make an entity a child of another without its global transform changing is tricky and error prone. It requires some knowledge of how bevy computes and propagates `GlobalTransform` through the hierarchy. This introduces a way to find such a transform. The method is only implemented on `GlobalTransform` since we expect it to be the most common use-case.
7978ecb to
cce1f3c
Compare
unparent_from method to GlobalTransformreparented_to method to GlobalTransform
alice-i-cecile
left a comment
There was a problem hiding this comment.
I think this is useful and clear. Much happier with a more specific name.
HackerFoo
left a comment
There was a problem hiding this comment.
There needs to be a test that shows that this operation can work with nonuniform scale, but I know that there is no way to implement this without changing the representation of Transform.
A simple example is a scale of Vec3::new(1., 2., 1.) rotated around X by pi/4 radians. There is simply no way to represent this in SRT, and so multiplication, division (which would be rotation in the opposite direction), and inversion (which must change the order of scale and rotation, hence rotating the scale) are all incomplete.
|
If this operation is useful, I'd be okay with it if it asserts that the scale of both inputs are uniform, which makes them independent of rotation, and documentation that explains why. |
|
I can see why it should seem possible that you can "undo" each transformation in the reverse order they have been applied, but the problem is that we can't even safely apply a chain of transformations if there is nonuniform scale in any of the transforms applied. I believe |
58cd7fd to
f277b46
Compare
| fn transform_equal(left: GlobalTransform, right: Transform) -> bool { | ||
| left.scale.abs_diff_eq(right.scale, 0.001) | ||
| && left.translation.abs_diff_eq(right.translation, 0.001) | ||
| && left.rotation.angle_between(right.rotation) < 0.0001 | ||
| } | ||
|
|
||
| #[test] | ||
| fn reparented_to_transform_identity() { | ||
| fn reparent_to_same(t1: GlobalTransform, t2: GlobalTransform) -> Transform { | ||
| t2.mul_transform(t1.into()).reparented_to(t2) | ||
| } | ||
| let t1 = GlobalTransform { | ||
| translation: Vec3::new(1034.0, 34.0, -1324.34), | ||
| rotation: Quat::from_euler(XYZ, 1.0, 0.9, 2.1), | ||
| scale: Vec3::new(1.0, 2.345, 0.0), | ||
| }; | ||
| let t2 = GlobalTransform { | ||
| translation: Vec3::new(0.0, -54.493, 324.34), | ||
| rotation: Quat::from_euler(XYZ, 1.9, 0.3, 3.0), | ||
| scale: Vec3::new(3.0, 1.345, 0.9), | ||
| }; | ||
| let retransformed = reparent_to_same(t1, t2); | ||
| assert!( | ||
| transform_equal(t1, retransformed), | ||
| "t1:{t1:#?} retransformed:{retransformed:#?}" | ||
| ); | ||
| } | ||
| #[test] | ||
| fn reparented_usecase() { | ||
| let t1 = GlobalTransform { | ||
| translation: Vec3::new(1034.0, 34.0, -1324.34), | ||
| rotation: Quat::from_euler(XYZ, 0.8, 1.9, 2.1), | ||
| scale: Vec3::new(-1.0, -2.3, 10.9), | ||
| }; | ||
| let t2 = GlobalTransform { | ||
| translation: Vec3::new(28.0, -54.493, 324.34), | ||
| rotation: Quat::from_euler(XYZ, 0.0, 3.1, 0.1), | ||
| scale: Vec3::new(3.0, -1.345, 0.9), | ||
| }; | ||
| // goal: find `X` such as `t2 * X = t1` | ||
| let reparented = t1.reparented_to(t2); | ||
| let t1_prime = t2 * reparented; | ||
| assert!( | ||
| transform_equal(t1, t1_prime.into()), | ||
| "t1:{t1:#?} t1_prime:{t1_prime:#?}" | ||
| ); | ||
| } |
There was a problem hiding this comment.
@HackerFoo Do you think those new tests are sufficient? Seems the transform_equal does test equality correctly now. I'll admit, the numerical accuracy sucks (only precision to the thousandth) and we might be able to get better accuracy if I dig up my numerical analysis books, but it's fine for the job now I think. I even used negative values and 0 for scale in reparented_usecase to check that the reparenting works even for admittedly silly and probably invalid values of scale.
|
Superseded by #4379. Though I still think the example code in this PR is a good hint for the user. However, it might be better suited for a project like the cheat book. I'll close this for now. |
Objective
Finding the proper transform to make an entity a child of another
without its global transform changing is tricky and error prone. It
requires some knowledge of how bevy computes and propagates
GlobalTransformthrough the hierarchy.Solution
This introduces a way to find such a transform. The method is only
implemented on
GlobalTransformsince we expect it to be the mostcommon use-case.
Changelog
reparented_tomethod toGlobalTransform