Open trusktr opened 5 years ago
Can you please provide a simple fiddle demonstrating your use case?
Also, please explain in the context of your demo why the existing three.js capabilities are not adequate.
TBH, I always thought that the existing approaches in three.js
feel more like workarounds. I mean using an instance of THREE.Object3D
that acts as a pivot point or translating the geometry is in some sense the utilization of a practical side effect. But it's not usable in all scenarios. Sometimes, you don't want to change your scene graph or the geometry data. Having a pivotMatrix
that defines the pivot point is a solution without such ramifications.
I think it might be worth to consider this issue as a valid feature request.
I agree with @Mugen87 here. I spent way to long using these hacks to get pivot points from the FBX format working, and I still think that there are edge cases that are not covered since the format has the concept of geometric pivot (i.e. transformed geometry relative to the mesh) and also object level pivot points (what @trusktr means here).
Both currently are shoe-horned into the geometric pivot, but it would have been a lot easier and less error prone if threejs had native pivot points.
Since most other 3D apps seem to have this concept, the only reason I can think of for NOT adding them is if there are major caveats for doing so, such as reduced performance.
Having a pivotMatrix that defines the pivot point is a solution without such ramifications.
There are some ramifications here. The new matrix will have to be recursively updated and updateMatrixWorld
will likely become slower. I think we'd want to measure that effect on a CPU-constrained example before proceeding.
Since most other 3D apps seem to have this concept, ...
Any non-DCC examples? I know all modeling tools have this, but they have a lot of concepts that aren't reasonable to include in a realtime rendering library. I think Unity's editor allows you to set pivot points, but they're baked when you build the project and a pivot point cannot be changed at runtime without using parent nodes.
From the authors of the Godot Engine:
Some 3D DCCs support an extra object Pivot (e.g. 3DS MAX). Collada exports this as an extra node and most likely if a glTF 2.0 exporter [for 3DS MAX] existed, the same would be done. ... I don't think any mainstream 3D engine supports extra object pivots either.
The new matrix will have to be recursively updated and updateMatrixWorld will likely become slower.
Perhaps the new matrix can be calculated only if pivot point is specified, as it is optional.
I think we'd want to measure that effect on a CPU-constrained example before proceeding.
Yes, this seems like the obvious next step. If this can only be achieved by reduced performance, then it's a non-starter and we'll have to continue with the hacks.
In Babylon.js it looks like the pivot is a simple pre-transform matrix. If we implement it in a similar way, such that the pivot matrix is undefined by default, then updateMatrixWorld
should only be slower if the pivot is present.
On the other hand, Babylon.js is the only non-DCC tool I can find that implements pivot points in real time (with 5 minutes of research, anyway).
This would be useful for the FBX loader, and I can imagine other situations such as manually building objects with hinges where since this would be useful. But on the other hand, we've got this far using just the hacks and I don't personally have a strong need for this.
@truskr if you want to champion this idea, could you do some more research and see whether other tools beside Babylon offer real time pivots? Also, if you can identify some use cases and see if other people besides you are requesting this feature then that would help strengthen the case.
object.origin.set(1,2,3)
My preference would be to call this object.pivot
.
@looeee Please stop referring to the existing code as hacks.
I would like the OP to answer my questions above before considering adding such a feature.
Note, a user can do the following at the application level. I would not expect it to have a measurable performance impact.
THREE.Object3D.prototype.updateMatrix = function () {
this.matrix.compose( this.position, this.quaternion, this.scale );
if ( this.pivot && this.pivot.isVector3 ) {
var px = this.pivot.x;
var py = this.pivot.y;
var pz = this.pivot.z;
var te = this.matrix.elements;
te[ 12 ] += px - te[ 0 ] * px - te[ 4 ] * py - te[ 8 ] * pz;
te[ 13 ] += py - te[ 1 ] * px - te[ 5 ] * py - te[ 9 ] * pz;
te[ 14 ] += pz - te[ 2 ] * px - te[ 6 ] * py - te[ 10 ] * pz;
}
this.matrixWorldNeedsUpdate = true;
};
and then define the object pivot only when needed:
mesh.pivot = new THREE.Vector3( 5, 5, 5 );
With this approach, children of objects having a defined pivot are still located relative to the object's origin.
Basic example: Using origin/pivot to define the hinge of a door in a game. Here's a fiddle, using CSS transform-origin, but you get the idea.
Instead of asking the user to monkey-patch library code, I would prefer to include Object3D.pivot
into the library.
te[ 12 ] += px - te[ 0 ] * px - te[ 4 ] * py - te[ 8 ] * pz; te[ 13 ] += py - te[ 1 ] * px - te[ 5 ] * py - te[ 9 ] * pz; te[ 14 ] += pz - te[ 2 ] * px - te[ 6 ] * py - te[ 10 ] * pz;
@WestLangley How does that part work? Can you explain (or share the link if you found one)? Sorry, I'm not the best at the maths!
Sometimes, you don't want to change your scene graph or the geometry data.
I spent way to long using these hacks to get pivot points from the FBX format working
I think these are fair points.
We should measure what's the performance impact of modifying updateMatrix()
so it looks like this.
THREE.Object3D.prototype.updateMatrix = function () {
this.matrix.compose( this.position, this.quaternion, this.scale );
var pivot = this.pivot;
if ( pivot !== null ) {
var px = pivot.x, py = pivot.y, pz = pivot.z;
var te = this.matrix.elements;
te[ 12 ] += px - te[ 0 ] * px - te[ 4 ] * py - te[ 8 ] * pz;
te[ 13 ] += py - te[ 1 ] * px - te[ 5 ] * py - te[ 9 ] * pz;
te[ 14 ] += pz - te[ 2 ] * px - te[ 6 ] * py - te[ 10 ] * pz;
}
this.matrixWorldNeedsUpdate = true;
};
We should measure what's the performance impact of modifying updateMatrix() so it looks like this.
Perhaps we can do this at the start of the next release cycle? That way we'll have a month to test it, and we can roll it back if it causes performance degradation
If it goes into the lib, is it better in Object3D than in Matrix4 and Matrix3 (for 2D)?
@trusktr Did you try the patch I provided for you? Is it implementing the feature you requested?
@WestLangley Seems like it does. Here's a fiddle rotating/scaling a cube (and its child) around its corner.
@trusktr Do you want to prepare a PR with the code from https://github.com/mrdoob/three.js/issues/15965#issuecomment-473109926?
@Mugen87 You should be asking me that question.
[FBXLoader] would have been a lot easier and less error prone if threejs had native pivot points.
@looeee Can you please try this patch in the FBXLoader
? Do animations work when pivots are specified?
@Mugen87 Have you been able to try this? I am finding it very unintuitive when the pivot is changed...
@WestLangley Sorry about that. I was not aware you want to do this π
Have you been able to try this? I am finding it very unintuitive when the pivot is changed...
I'm actually waiting on a PR so I can checkout the respective branch and test it. I know the feature from Babylon.js and I'd like to compare both behaviors.
I was not aware you want to do this
If the feature is supported, I will file the PR.
I do not support the feature yet, because we haven't decided what the behavior should be.
This demo requires changing the pivot in real-time, and unfortunately, doing so is not intuitive at all. (One filp is easy; change the pivot and the object jumps to a different location.)
A better API may be a method that rotates an object around an axis apart from the object -- such as the method I proposed in this SO answer.
Here's a fiddle of that rotating plane, using the above pivot
.
I wanted to verify that it works with arbitrary quaternion applied too: this fiddle shows rotating around the front diagonal of a cube as expected. π This shows that with a pivot, and using quaternion.setFromAxisAngle
, we can make arbitrary local axes of rotation (point + axis + angle).
And this fiddle shows pivot
working regardless of having a transformed (scaled and rotated) parent. π
I like the idea of an option to rotate around an axis, but in the SO answer it doesn't work with a transformed parent. Having to figure out a world axis in order to rotate an object may not be convenient. In the door example, it's simple to specify pivot local to the door, as opposed to calculating a world axis (and making it work with rotated parents), and it makes encapsulation easy: the local component doesn't need to worry about the outside world for the animation to work.
@trusktr You are not changing the pivot in real-time -- that means after the object is rendered.
How about answering my question above, and providing a demo of your use case?
If your use case is a door, you can pivot a door by aligning the door's geometry with the origin.
We added a center
property to Sprite
not too long ago. I guess is not too late to refactor that code so it uses a pivot
to lives in Object3D
instead?
We added a center property to Sprite not too long ago. I guess is not too late to refactor that code so it uses a pivot to lives in Object3D instead?
Object3D.pivot
is a Vector3
in the object's local space; it has world units.
Sprite.center
is a Vector2
in [0, 1]; it is unitless.
Yeah, that's what I meant with "it is not too late" π
This demo requires changing the pivot in real-time, and unfortunately, doing so is not intuitive at all.
@WestLangley could you share the code for you example of changing the pivot in real time? I'd like to judge for myself how awkward it is.
In any case, that's not necessarily a problem since in general the pivot point is a property of the object, such as the hinges in a door or a robot arm and doesn't get changed much, if at all. That does mean it's possible to bake it in, of course. The question we're trying to answer here is whether adding a pivot like this is an improvement over transforming the geometry. It's more intuitive, but if that comes at too much of a performance cost then it's not worth it.
There's two ways currently of simulating the pivot. Both are equivalent to adding an extra "pivot matrix" into the mix, but they are not equivalent to each other, since one is a pre transform and the other is a post transform:
Object3D
to the scene graph, then the parent.matrix
will be the pivot, call it PtIf the object has no parent, then the world matrix is different in each case, since the final matrix will be
object.matrix
* Gtobject.matrix
The pivot we're suggesting here should be a replacement for case 1 - the geometric transform.
@WestLangley , your code is not equivalent to either of these, since you are using a Vector3
for the pivot. It needs to be a transformation matrix to be useful in FBX since there are pivots for T,R,and S.
It needs to be a transformation matrix to be useful in FBX since there are pivots for T,R,and S.
Oh man, that got complicated quickly...
that got complicated quickly
Yeah, thinking about how this would apply to animations in FBX, I'm not sure how useful it would be. Testing this on still models would be simple, but testing it with animations would be quite a bit of work.
I have a feeling that in the end it would just be a case of moving the complexity to another place. Models with complex geometry would probably load faster, but models with complex animations might end up loading slower and performing worse, and I don't have the time to do extensive testing of that at the moment.
With that in mind, I'm going to withdraw FBX pivots as one of the use cases. If this gets added for other reasons, then I'll test it at some point with the FBXLoader
, but for now we should focus on other use cases.
For other uses a Vector3
pivot might be sufficient, but we should confirm that a translation pivot is all we'll ever need before deciding that.
@looeee The concept here that @WestLangley suggested behaves like what I was thinking. What do you mean by T, R, and S
? Translation pivot, rotation pivot, and scale pivot?
Also, just a thought, but I bet using a parent node as a pivot is much slower than this pivot
property.
What do you mean by T, R, and S? Translation pivot, rotation pivot, and scale pivot?
Yes, TRS is a common shorthand for Translation, Rotation, Scale.
I figure I'll throw my two cents in since I've had to deal with similar issues of needing to set an objects TRS relative to a different frame.
In the past I've dealt with this by either creating a parent Object3D or creating utility functions that help with getting rotations or matrices relative to another frame. FWIW I don't really consider these "hacks" because nothing has to change about the core library to make it work. Sometimes what you're doing just requires more complex math than what the library abstracts for you.
I'm not necessarily against the idea of adding a "pivot" field, though. If it does get added here are my thoughts:
Vector3
-- I don't think it changes a whole lot if it's a matrix or just a vector.null
then I wouldn't think it would add any noticeable amount of overhead to the updateMatrixWorld
function, but maybe that's not the case.Maybe it's worth considering adding something like FrameUtils
or TransformUtils
(like SceneUtils
) to help with these types of use cases instead of adding them onto Object3D?
I can only recommend to keep a new feature simple. I mean it's good to consider more complex scenarios and edges cases but please don't forget the simple ones. A small addition like demonstrated here would be already great.
This demo requires changing the pivot in real-time, and unfortunately, doing so is not intuitive at all. (One filp is easy; change the pivot and the object jumps to a different location.)
@WestLangley If you start with the pivot in the right-bottom corner of that square, you don't have to change anything at runtime.
If you start with the pivot in the right-bottom corner of that square, you don't have to change anything at runtime.
That is true only if the square makes those exact 4 flips. Imagine the square flipping over few steps in the same direction.
I remember one situation where having a pivot matrix would have been useful. When implementing TransformControls I realized that world-space scaling in a rotated frame is impossible without additional matrix. If pivot was a matrix like @gkjohnson suggested, it could be used to facilitate this type of transformations.
Anyway, if this gets implemented, someone ping me to update TransformControls.
The new matrix will have to be recursively updated and updateMatrixWorld will likely become slower.
That's right. Changing the pivot for a complex model at runtime is a bad idea (you'll get a glitch on mid-range hardware).
p.s. 3DS Max is a 3D modeler, not a 3D engine!
@Mugen87 I don't want to overcomplicate things, for sure, but I think it's worth discussing a bit instead of jumping on the first solution available.
I actually feel that a pivot matrix is a more simple solution than the provided one that manually rotates and scales a vector and adds it to the matrix elements. Here's what (I think) an implementation with a matrix might look like:
THREE.Object3D.prototype.updateMatrix = function () {
this.matrix.compose( this.position, this.quaternion, this.scale );
var pivotMatrix = this.pivotMatrix ;
if ( pivotMatrix !== null ) {
this.matrix.premultiply( pivotMatrix );
}
this.matrixWorldNeedsUpdate = true;
};
Now using just a translation pivot would look like this
object.pivotMatrix.makeTranslation( 0.25, 0.25, 0 );
And I realize that they're modeling tools but both Maya and 3DS Max call this a "pivot" and allow for changing its rotation as well as position. I admit that a "scale" pivot is a bit unintuitive, though...
Also another use case here is to correct odd offset origins on geometry and move them back to the center or bottom of the geometry's bounding box, which is always a pain.
@arodic Unfortunately I think non-uniform scaling in a rotated frame will always result in jank eventually :( If the pivot is changed the scale won't be able to be retained.
@gkjohnson You are right. I have realized right now that Babylon.js allows both to set a pivot point or a pivot matrix^^. If you set a point, it is decoded into the pivot matrix like so:
this.setPivotMatrix(Matrix.Translation(-point.x, -point.y, -point.z), true);
Instead of using object properties, we could also define a method interface for pivot points e.g.
Object3D.setPivotPoint()
Object3D.getPivotPoint()
Object3D.setPivotMatrix()
Object3D.getPivotMatrix()
The actual pivot matrix would be a private property Object3D._pivotMatrix
.
I have asked the OP repeatedly for his use case so I could first determine the changes required to support his needs, and then determine the consequences of those changes on the library and its users.
I have invested considerable time in this. What I have tried is neither easy nor intuitive.
We already have two approaches that are easy and intuitive: translate the geometry, or add a pivot point.
I think they are sufficient.
I think they are sufficient.
I don't think so. The concept of a pivot point or pivot matrix could also be useful to replace Sprite.center
as mentioned by @mrdoob and thus provide a more universal library feature. I think it's too early to give it up.
I have asked the OP repeatedly for his use case so I could first determine the changes required to support his needs
@WestLangley All my use cases are simple so far, like the rotating door. In my cases, I've never wanted to animate pivot, only set it once.
I've been making things with code only, not a DCC. To implement things with a parent object, it is inconvenient to have to modify the tree to insert the parent, move the child's translation to the parent, then place the pivot translation in the child.
An Object3D.pivot
property would be just super convenient, is what it comes down to, and your implementation idea meets the needs I have, and meets many other's needs too (judging from this feature being a common ask on the web for different engines, with most answers being "use a parent")
@Mugen87 Although a pivotMatrix
would be easy to use to achieve the same as a pivot
point, I don't know what else I'd do with a pivotMatrix
besides apply translation to it. Why would we want to apply anything other than translation
to a pivotMatrix
(because if it is a matrix, I assume that is so we can apply other things besides translation)? Anyone have a concrete example of that?
As Babylon.js
mentioned it in their docs, it's for the case you want to set a translation matrix.
A pivot can be set with a translation matrix or directly with the setPivotPoint method.
So it's just a convenient method. I don't feel strong about how the API should look like. Probably Object3D.pivot
is sufficient.
Any plan on this issue to be implemented in threejs in coming versions?
I think that I would vote against adding an object.pivot
API... the same hacks* that are necessary in FBXLoader to load models with pivots would then need to be implemented in OBJExporter, PLYExporter, and GLTFExporter to support scenes that happen to contain a pivot. The complexity of exporting animated scenes containing a pivot will be significantly worse.
I understand that pivots are a useful concept for working with rotations, especially for those coming from 3D modeling tools... I'm just not sure they belong in a 3D engine. Alternatively, adding helper methods like object.rotateAroundPivot( ... )
would provide some of that benefit without complicating other parts of the library.
*Like @gkjohnson, I don't really think these are hacks. 3D modeling tools often support pivots, 3D engines typically do not. In the latter, use of an empty parent is a completely reasonable way to implement the same thing. I've seen Blender tutorials recommend rotating empties too, even though the tool has pivots.
One can find Object3D.rotateAroundWorldAxis( point, axis, angle )
in this stackoverflow answer.
Previously @mrdoob was not in favor of adding this method. I'm OK with that decision.
Description of the problem
As an example, Babylon has this feature built in, called pivot points. CSS has it in the form of the
transform-origin
property.These features enable rotation about a pivot point with a one-liner.
Here's an implementation idea: https://jsfiddle.net/mmalex/hd8ex0ok/ (thanks @nmalex).
I believe ideally this would be implemented inside
Matrix4
, and theMatrix4.compose( position, quaternion, scale )
signature would be changed toMatrix4.compose( position, quaternion, scale[, origin] )
, with theorigin
parameter being optional for backwards compatibility.An
origin
property would be added toObject3D
, so that we can for example writeobject.origin.set(1,2,3)
.updateMatrixWorld
would then callthis.matrix.compose( this.position, this.quaternion, this.scale, this.origin )
.Just bike shedding, but
pivot
could also be another name in place oforigin
, but I likeorigin
better because "pivot" doesn't seem to align with "scale" (I'm thinking thatscale
would also happen about theorigin
).Three.js version
Browser
OS
Hardware Requirements (graphics card, VR Device, ...)
N/A