-
-
Notifications
You must be signed in to change notification settings - Fork 3.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
Add reparented_to
method to GlobalTransform
#4891
Conversation
af699e1
to
a5b1453
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
@@ -220,6 +224,52 @@ impl GlobalTransform { | |||
} | |||
} | |||
|
|||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 GlobalTransform
unparent_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 GlobalTransform
reparented_to
method to GlobalTransform
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is useful and clear. Much happier with a more specific name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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
GlobalTransform
through the hierarchy.Solution
This introduces a way to find such a transform. The method is only
implemented on
GlobalTransform
since we expect it to be the mostcommon use-case.
Changelog
reparented_to
method toGlobalTransform