Skip to content

Conversation

@donmccurdy
Copy link
Collaborator

@donmccurdy donmccurdy commented Jul 18, 2018

Fixes #5878, fixes #11574.

Adds a helper method, SkeletonUtils.clone( object ) that preserves SkinnedMesh/Bone relationships. Ideally this would "just work" with the usual object.clone(), but I can't see any clean way of doing that — bones are not necessarily children of the SkinnedMesh(es?) they're associated with, and trying to connect them requires logic that would need to live somewhere outside the recursive callback series. This helper method provides an easy way to work around the problem.

Based on https://gist.github.com/zellski/be4e9207ab8e70c4e89062d48ce345ba#file-gltf_utils-js-L19 by @zellski and https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87 by @cdata.

@Mugen87
Copy link
Collaborator

Mugen87 commented Jul 18, 2018

I think it's good to have such a function as a work around until we solve the real problem and have a consistent approach for skeleton animation in three.js.

@takahirox
Copy link
Collaborator

takahirox commented Jul 18, 2018

I haven't followed up the discussion yet, but why is cloning method for SkinnedMesh in AnimationUtils?


source.traverse( function ( node ) {

sourceNodes[ node.name ] = node;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you assume node has an unique name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unique within the subtree being cloned, yes.

Copy link
Collaborator

@takahirox takahirox Jul 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it may be expecting too much... As you know, name is optional in Three.js and two or more nodes have the same name. In many cases, most of non-bone nodes have an empty strings name.

If I were you, I'd write like the following by making the use of that source and clone have the same tree structure.

var clone = source.clone();

var sourceToClone = new Map();

function makeMapping( object1, object2 ) {

    sourceToClone( object1, object2 );

    for ( var i = 0, il = object1.chilldren.length; i < il; i ++ ) {

        traverse( object1.children[ i ], object2.children[ i ] );

    }

}

makeMapping( source, clone );

source.traverse( function ( node ) {

    if ( ! node.isSkinnedMesh ) return;

    var sourceMesh = node;
    var clonedMesh = sourceToClone.get( node );
    var sourceBones = sourceMesh.skeleton.bones;

    clonedMesh.skeleton = sourceMesh.skeleton.clone();

    clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {

        return sourceToClone.get( bone );

    } );

    clonedMesh.bind( clonedMesh.skeleton, sourceMesh.bindMatrix );

} );

return clone;

EDIT: Updated the code.

@donmccurdy
Copy link
Collaborator Author

I haven't followed up the discussion yet, but why is cloning method for SkinnedMesh in AnimationUtils?

I'd rather it just work via mesh.clone() of course, but bones may not be children of the mesh. Consider this subtree:

  • object
  • object
  • object
    • bone
      • bone
        • bone
  • object
    • skinned mesh

There's no guarantee the new bones will exist at the time the skinned mesh is recursively cloned. It seems like we need somewhere else to put that logic, and so a Utils class seems as good as any. This solution is another, but relies on cloning happening at the Scene level, and not all loaders return Scene objects.

@takahirox
Copy link
Collaborator

Sorry I wrote poorly. I think it's good to have a workaround in Utils for now. My question is why is it in "Animation"Utils tho cloning SkinnedMesh isn't animation special.


clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {

return clonedNodes[ bone.name ];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly do you assume bones are in a subtree?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes the bones are within the subtree of the object being cloned, but not necessarily descendants of the SkinnedMesh they're associated with.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to check if bones are in a subtree and halt with warn/error log if they aren't. Because it isn't guaranteed, for example users might call Utils.clone( object2 ) for the following graph. Sometimes even bones wouldn't be in the scene graph.

  • object1
    • bone1
      • bone2
  • object2
    • skinned mesh

@donmccurdy
Copy link
Collaborator Author

donmccurdy commented Jul 18, 2018

My question is why is it in "Animation"Utils tho cloning SkinnedMesh isn't animation special.

Because I can't think of a better place to put it, and skinning is at least somewhat related to animation. Suggestions welcome. 😉

EDIT: Maybe SceneUtils.cloneWithSkinning( ... )?

@takahirox
Copy link
Collaborator

Hm, SceneUtils or (Skinned)MeshUtils?


} else {

node.name = 'gltfNode' + ( nextNodeIndex++ );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for naming an unique name as much as possible?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. 👍

var sourceBones = sourceMesh.skeleton.bones;

clonedMesh.skeleton = sourceMesh.skeleton.clone();
clonedMesh.bindMatrix = sourceMesh.bindMatrix;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should copy the matrix, rather than we share?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

@donmccurdy
Copy link
Collaborator Author

Updated to ensure cloning does not depend on names. I removed the part adding unique names to glTF nodes for now, but will probably try another PR for that in the future since animation tracks still require it.


} );

clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copying matrix to .bindMatrix is done in SkinnedMesh.bind().

https://github.com/mrdoob/three.js/blob/dev/src/objects/SkinnedMesh.js#L112

So I think you can replace this line with

clonedMesh.bind( clonedMesh.skeleton, sourceMesh.bindMatrix );

and remove the line above clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to pass "source"Mesh.bindMatrix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ✅

@donmccurdy donmccurdy force-pushed the feat-animationutils-clone branch from 3a0e2b8 to a9cde37 Compare July 23, 2018 14:33
} );

return clone;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we remove sourceLookup?

source.traverse( function ( node ) {

    if ( ! node.isSkinnedMesh ) return;

    var sourceMesh = node;
    var sourceBones = sourceMesh.skeleton.bones;
    var clonedMesh = cloneLookup.get( node );

    clonedMesh.skeleton = sourceMesh.skeleton.clone();

    clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {

        return cloneLookup.get( bone );

    } );

    clonedMesh.bind( clonedMesh.skeleton, sourceMesh.bindMatrix );

} );

return clone;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ✅

@takahirox
Copy link
Collaborator

Thanks for the update.

So, you still want this method in AnimationUtils? Or do you move it to SceneUtils or somewhere?


clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {

return cloneLookup.get( bone );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you wanna check the existence of bones in the subtree as I mentioned?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

@donmccurdy
Copy link
Collaborator Author

I'm OK with whatever namespace for this, but I don't think I have a better idea than the current name... maybe cloneWithBones or cloneWithSkinning? It is all a less-preferable solution than fixing .clone() directly but I don't see any straightforward way to do that. /cc @mrdoob


throw new Error( 'THREE.AnimationUtils: Required bones are not descendants of the given object.' );

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Using .has() may look more natural?

if ( ! cloneLookup.has( sourceBone ) ) {

    throw new Error( 'THREE.AnimationUtils: Required bones are not descendants of the given object.' );

}

var clonedBone = cloneLookup.get( sourceBone );

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

@takahirox
Copy link
Collaborator

Voting for SceneUtils.cloneWithSkinning.

@donmccurdy
Copy link
Collaborator Author

If #14574 goes in, SkeletonUtils seems like a good place to put this. 🤔

@takahirox
Copy link
Collaborator

Agreed.

@mrdoob
Copy link
Owner

mrdoob commented Aug 29, 2018

If #14574 goes in, SkeletonUtils seems like a good place to put this. 🤔

Merged.

@mrdoob mrdoob added this to the r97 milestone Aug 29, 2018
@donmccurdy
Copy link
Collaborator Author

@takahirox @mrdoob Does SkeletonUtils.clone() seem OK as a name? Or something like more explicit like .cloneWithSkinning()?

@takahirox
Copy link
Collaborator

I prefer the explicit one.

@mrdoob mrdoob modified the milestones: r97, r98 Sep 25, 2018
@mrdoob mrdoob modified the milestones: r99, r100 Nov 29, 2018
@mrdoob mrdoob modified the milestones: r100, r101 Dec 31, 2018
@mrdoob mrdoob modified the milestones: r101, r102 Jan 31, 2019
@Mugen87
Copy link
Collaborator

Mugen87 commented Feb 19, 2019

I have lately used this code in a project and it works great. Would love to see it in the code base 😊

@Eketol
Copy link

Eketol commented Feb 19, 2019

Me too. I've also tested it and it works fine for me. Just one question: the use of Map() is "es2015" (ES6), is that ok?
Also, in case it helps other noobs like me: Materials are not cloned, but referenced. In my case I think it is the right behaviour. If you want to customise some of the cloned object's materials, bear that in mind.

@Mugen87
Copy link
Collaborator

Mugen87 commented Feb 19, 2019

I think using Map in this case is okay since SkeletonUtils is no part of the core. SkeletonUtils.cloneWithSkinning() seems to be a good place for this code.

@donmccurdy Any chances to update the PR? 😇

Don McCurdy and others added 2 commits February 19, 2019 20:42
AnimationUtils: Clean up lint errors.

AnimationUtils: Ensure .clone() does not depend on node names.

GLTFLoader: Revert automatic node naming.
@donmccurdy donmccurdy force-pushed the feat-animationutils-clone branch from 6d20dc2 to 427863f Compare February 20, 2019 05:07
@donmccurdy
Copy link
Collaborator Author

Thanks!

I've moved the clone() method from AnimationUtils to SkeletonUtils, and added documentation for SkeletonUtils. I think this is ready to go. 👍

@donmccurdy donmccurdy changed the title AnimationUtils: Add clone() helper for skinned meshes. SkeletonUtils: Add clone() helper for skinned meshes. Feb 20, 2019
@mrdoob mrdoob modified the milestones: r102, r103 Feb 28, 2019
@donmccurdy
Copy link
Collaborator Author

@mrdoob could this be included with r103?

@titansoftime
Copy link
Contributor

This just solved a huge issue I was having. Thank you.

@Mugen87
Copy link
Collaborator

Mugen87 commented Mar 23, 2019

This just solved a huge issue I was having.

https://discourse.threejs.org/t/solved-issue-with-gltfloader-and-reusing-geometry/6697

@mrdoob mrdoob merged commit 7946bd5 into mrdoob:dev Mar 26, 2019
@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2019

@mrdoob could this be included with r103?

Done! 😇

@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2019

Thanks!

@donmccurdy donmccurdy deleted the feat-animationutils-clone branch March 26, 2019 03:05
@canadaduane
Copy link

Also, in case it helps other noobs like me: Materials are not cloned, but referenced. In my case I think it is the right behaviour. If you want to customise some of the cloned object's materials, bear that in mind.

Is there an option to clone the materials as well? In my case, I'd like some of my skinnedmesh objects to be transparent (but not all), even though they've been cloned from the same original.

@Mugen87
Copy link
Collaborator

Mugen87 commented Apr 3, 2020

SkeletonUtils.clone() does not touch materials. You have to apply the material changes in a subsequent step.

@enricocantori
Copy link

Three.is is frequently used in the world of character animations. I can’t find no functions, no working examples related to the replace of parts of characters. Almost all characters are composed by skinnedMesh children and a lot of time is necessary change a part of the character like clothes, hair, face within characters that have same skeleton without broke animation (ex. Readyplayes characters). Clone and SkeletonUtils.Clone doesn’t solve this problem. Exist one more specific function?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way of cloning a mesh so it works with a skinning material geometry.clone() doesn't copy reference to bones/skinweights/animations etc.

8 participants