mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
101.62k stars 35.29k forks source link

feature request: transform origin (or "pivot point") #15965

Open trusktr opened 5 years ago

trusktr commented 5 years ago
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 the Matrix4.compose( position, quaternion, scale ) signature would be changed to Matrix4.compose( position, quaternion, scale[, origin] ), with the origin parameter being optional for backwards compatibility.

An origin property would be added to Object3D, so that we can for example write object.origin.set(1,2,3).

updateMatrixWorld would then call this.matrix.compose( this.position, this.quaternion, this.scale, this.origin ).

Just bike shedding, but pivot could also be another name in place of origin, but I like origin better because "pivot" doesn't seem to align with "scale" (I'm thinking that scale would also happen about the origin).

Three.js version
Browser
OS
Hardware Requirements (graphics card, VR Device, ...)

N/A

WestLangley commented 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.

Mugen87 commented 5 years ago

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.

Mugen87 commented 5 years ago

Related: https://stackoverflow.com/questions/55116131/how-can-we-change-the-rotation-origin-pivot-point-of-a-three-js-object-without/55138754#55138754

I think it might be worth to consider this issue as a valid feature request.

looeee commented 5 years ago

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.

donmccurdy commented 5 years ago

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.

arodic commented 5 years ago

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.

looeee commented 5 years ago

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.

WestLangley commented 5 years ago

@looeee Please stop referring to the existing code as hacks.

WestLangley commented 5 years ago

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.

trusktr commented 5 years ago

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.

Mugen87 commented 5 years ago

Instead of asking the user to monkey-patch library code, I would prefer to include Object3D.pivot into the library.

trusktr commented 5 years ago
      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!

mrdoob commented 5 years ago

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;

};
looeee commented 5 years ago

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

trusktr commented 5 years ago

If it goes into the lib, is it better in Object3D than in Matrix4 and Matrix3 (for 2D)?

WestLangley commented 5 years ago

@trusktr Did you try the patch I provided for you? Is it implementing the feature you requested?

trusktr commented 5 years ago

@WestLangley Seems like it does. Here's a fiddle rotating/scaling a cube (and its child) around its corner.

Mugen87 commented 5 years ago

@trusktr Do you want to prepare a PR with the code from https://github.com/mrdoob/three.js/issues/15965#issuecomment-473109926?

WestLangley commented 5 years ago

@Mugen87 You should be asking me that question.

WestLangley commented 5 years ago

[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?

WestLangley commented 5 years ago

@Mugen87 Have you been able to try this? I am finding it very unintuitive when the pivot is changed...

Mugen87 commented 5 years ago

@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.

WestLangley commented 5 years ago

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.

WestLangley commented 5 years ago

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.)

Mar-15-2019 08-22-15

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.

trusktr commented 5 years ago

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.

WestLangley commented 5 years ago

@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.

mrdoob commented 5 years ago

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?

WestLangley commented 5 years ago

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.

mrdoob commented 5 years ago

Yeah, that's what I meant with "it is not too late" πŸ˜‡

looeee commented 5 years ago

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:

  1. Transform the geometry. Call the equivalent pivot matrix Gt.
  2. Add an extra parent Object3D to the scene graph, then the parent.matrix will be the pivot, call it Pt

If the object has no parent, then the world matrix is different in each case, since the final matrix will be

  1. object.matrix * Gt
  2. Pt * object.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.

mrdoob commented 5 years ago

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...

looeee commented 5 years ago

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.

trusktr commented 5 years ago

@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?

trusktr commented 5 years ago

Also, just a thought, but I bet using a parent node as a pivot is much slower than this pivot property.

looeee commented 5 years ago

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.

gkjohnson commented 5 years ago

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:

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?

Mugen87 commented 5 years ago

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.

RemusMar commented 5 years ago

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.

arodic commented 5 years ago

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.

arodic commented 5 years ago

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.

RemusMar commented 5 years ago

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!

gkjohnson commented 5 years ago

@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.

Mugen87 commented 5 years ago

@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.

The actual pivot matrix would be a private property Object3D._pivotMatrix.

WestLangley commented 5 years ago

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.

Mugen87 commented 5 years ago

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.

trusktr commented 5 years ago

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?

Mugen87 commented 5 years ago

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.

dguayrobotiq commented 4 years ago

Any plan on this issue to be implemented in threejs in coming versions?

donmccurdy commented 4 years ago

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.

WestLangley commented 4 years ago

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.