-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
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
AnimationUtils: Add .subclip(). #13430
Conversation
Wow, you are fast! 😁 This looks great, although I would rather a signature like: THREE.AnimationUtils.splitClip( clip, name, from, to ); This would allow greater flexibility so, for example, if const walkClip = THREE.AnimationUtils.splitClip( clip, 'walk', 0, 1 );
// Note: decided not to use this as it looks silly
// const goofyWalk= THREE.AnimationUtils.splitClip( clip, 'goofyWalk', 1, 2 );
const idleClip = THREE.AnimationUtils.splitClip( clip, 'idle', 2, 3 ); Since you are using the |
Here is a combined animation file in JSON format: It will work with the var loader = new THREE.FBXLoader();
loader.load( 'models/fbx/Samba Dancing.fbx', function ( object ) {
object.mixer = new THREE.AnimationMixer( object );
mixers.push( object.mixer );
var action;
var animLoader = new THREE.FileLoader();
animLoader.setResponseType( 'json' );
animLoader.load( 'anims.json', function( json ) {
var clip = THREE.AnimationClip.parse( json );
action = object.mixer.clipAction( clip );
action.play();
} );
scene.add( object );
} ); Animations are at 60fps and are: Idle: frames 0 to 198 At least, that's the frames in 3DS Max, there seem to be a couple of garbage frames added at the end when I apply it to the model - these may be artefacts of my converting the animation from FBX to JSON. |
Awesome!!! This will be a huge contribution to threejs and easy for the users!! |
Thanks @looeee! I think I like that syntax structure better, yeah. Perhaps renaming to The model is "working" (I can split the clip fine) but it seems like there are some issues: (1) garbage frames as you mention, and (2) the exported frames have been optimized I think, and there are clips that do not "contain" some of the frames that still apply to them:
Not exactly this, but gets the point across... do we consider this "bad input" and expect users to export all necessary frames for each range? Or try to preserve the last keyframe from the prior track, if the track has no keyframe at its own beginning? I've already commented out the I'm confident I could create a working example in Blender, but then the point is to have a viable workflow from DCC tools that can't export multiple actions to three.js... 😕 |
I've tested the FBX file that I used to generate the JSON anims - it has the same issue with garbage frames at the end, so that wasn't an artefact of conversion to JSON, however exporting the file with baked animation does remove them. I've tested it with a couple of other tools (FBX review, Motion builder) and the unbaked animations play fine there, so I think that we should consider them "good input". HOWEVER.... This is all probably an issue with the way that FBX stores animations (sparse by default), and is probably a bug in the FBXLoader rather than something you need to worry about here. So here's the FBX file exported with baked animations. I would say if you can get it to work with this, that's good enough for now and I'll work on getting the FBXLoader to load the unbaked animation correctly. |
Off the topic, but how can users/viewers know the ranges in animation data? For example, Ams - Bms for walk, Cms - Dms for run. Externally documented? This packing animation data style is new to me and I'm curious to know. |
Thanks @looeee I'll give that a try soon! @takahirox — if all you have is the model, you really can't tell. Or at least not well enough to make a walk cycle loop cleanly. But in the modeling tool your artist could make a 60-frame walk cycle, 60-frame run cycle, and you'd just ask for that information when importing to Unity or something. Blender has Markers you could use to keep track of things like that, although they aren't exported I don't think. Related: is it better to have the method take start/stop as a number of frames, rather than a time? not sure how this workflow typically is done.. |
I would say splitting by frames makes more sense - that's the way animations are usually specified in asset creation tools. |
13a20f7
to
258b462
Compare
Updated to split by frames. Added a demo (probably not intended to submit) showing progress: ^Note the demo resets at 5s, but each clip loops at least once in that time range. Some notes:
So, getting better, but this still feels hard to use and I'm not really sure where the problem is yet. |
So something strange was happening - I exported the animations from Mixamo at 60fps, but when I loaded the files in 3DS Max they imported at 30fps. For the previous JSON example I had rescaled them back to 60fps, but perhaps I neglected to save after doing that. Sorry for confusion!
Again, probably an artefact of the conversion process from Mixamo - or perhaps even an issue with the clip itself. Otherwise, looks great! If we decide to go with this model and setup for the example then I'll redo everything, get rid of the jank and make sure it's exported at 60fps. |
Another thought - the model from the example I linked above (webgl_animation_skinning_blending) is really showing its age. Perhaps we could replace it with this one, or even combine the two examples into webgl_animation_skinning_subclip_blending? |
Ok thanks, that's more promising then 🙂 About the THREE.KeyframeTrack.autoOptimize = false;
// ...load clip from file...
var idleClip = THREE.AnimationUtils.subclip( clip, 'idle', 0, 60 ).optimize();
var walkClip = THREE.AnimationUtils.subclip( clip, 'walk', 0, 60 ).optimize(); |
Yeah, I think that would work. Then inside the subclip: function ( sourceClip, name, startFrame, endFrame ) {
var autoOptimiseValue = THREE.KeyframeTrack.autoOptimize;
THREE.KeyframeTrack.autoOptimize = false;
var clip = sourceClip.clone();
...
clip.resetDuration();
THREE.KeyframeTrack.autoOptimize = autoOptimiseValue;
return clip;
}
|
I've tried here, but AnimationUtils.subclip is not a function. |
@DenisKen note that this PR has not been merged. |
Perhaps the issue with this one is that it includes the example? @donmccurdy could you make the example a seperate PR? It would be nice to get these added. |
The main problem, I think, is that I don’t have a workflow where I can confirm this works well and solves a useful problem... sample models I’ve found that cycle through different animations are often “optimized” such that they can’t trivially be split and loop cleanly. If someone can produce such a workflow and model(s) to test I would be glad to clean this up. Otherwise it may be better to spend time on things like KhronosGroup/glTF-Blender-Exporter#166. |
Not everyone is using Blender. For people that use pretty much any commercial 3D software, I'm not aware of any exporters in the works. Just converters, which won't solve this issue. |
Do you mean for glTF? Or that support multiple animation export generally? There are a couple third-party plugins for 3DS Max and Maya, although I don't know what level of quality they're at today. In any case, I don't think we can justify merging this PR without finding at least one workflow where it works correctly with a clean loop on each clip. |
Yes.
The only glTF exporter I know of the 3DS Max is the Babylon.js exporter, which has completely failed for anything that I've tried exporting. Then again, things are changing so fast here that there could be ten more by now! |
i just want to say HUGE THANKS, and confirm this commit donmccurdy@2538285 as working (tested with cutting big clip to subclips and play them separatelly). |
@abrakadobr good to hear this was helpful! Would you be willing to share a bit about your workflow for exporting animated models? The reason I haven't updated this PR lately is that I don't really have a good workflow to create animated models with animation on a single timeline... I think this PR needs at least one example model that can be split and will loop cleanly. I've been using glTF, which lets you split clips at export time, and so haven't had much need to work on this PR lately. But still happy to update it if it's useful to others. |
happy to share small peaces of code right now (peaces about animation clips), cuz it's a bit messy to see all together and code is still "dev-dirty", sorry =) and all project I hope to publish in few days, so whole code can be reviewed later =) helper class methods: loadFbxAnimations(path)
{
return new Promise(acc=>{
let loader = new THREE.FBXLoader()
loader.load(path+'.fbx',(model)=>{
acc(model.animations)
},(progress)=>{},(err)=>{
console.log(['FBXANIM ERR',err])
})
})
}
loadFbx(path,sc=1,shadows=true)
{
return new Promise(acc=>{
let loader = new THREE.FBXLoader()
loader.load(path+'.fbx',(model)=>{
model.scale.set(sc,sc,sc)
if (shadows)
model.traverse( function ( child ) {
if ( child.isMesh ) {
child.castShadow = true
child.receiveShadow = true
}
})
model.mixer = new THREE.AnimationMixer( model )
//model.a0 = model.mixer.clipAction( model.animations[0] )
acc(model)
},(progress)=>{},(err)=>{
console.log(['FBX ERR',err])
})
})
}
cloneFbx(fbx)
{
const clone = fbx.clone(true)
clone.animations = fbx.animations
clone.skeleton = { bones: [], getBoneByName:(n)=>{} }
const skinnedMeshes = {}
fbx.traverse(node => {
if (node.isSkinnedMesh)
skinnedMeshes[node.name] = node
})
const cloneBones = {}
const cloneSkinnedMeshes = {}
clone.traverse(node => {
if (node.isBone)
cloneBones[node.name] = node
if (node.isSkinnedMesh)
cloneSkinnedMeshes[node.name] = node
})
for (let name in skinnedMeshes) {
const skinnedMesh = skinnedMeshes[name]
const skeleton = skinnedMesh.skeleton
const cloneSkinnedMesh = cloneSkinnedMeshes[name]
const orderedCloneBones = []
for (let i=0; i<skeleton.bones.length; i++) {
const cloneBone = cloneBones[skeleton.bones[i].name]
orderedCloneBones.push(cloneBone)
}
cloneSkinnedMesh.bind(new THREE.Skeleton(orderedCloneBones, skeleton.boneInverses),cloneSkinnedMesh.matrixWorld)
// For animation to work correctly:
clone.skeleton.bones.push(cloneSkinnedMesh)
clone.skeleton.bones.push(...orderedCloneBones)
}
return clone
} mixmao character class also has method to add separate loaded animation from separate files //add separate loaded animations for mixmao characters
addSkinAnimations(animations)
{
if(!this.skin)
return
for(let clipName in animations)
{
let ai = this.skin.animations.length
this.skin.animations.push(animations[clipName])
this.actions[clipName] = this.skin.mixer.clipAction(this.skin.animations[ai])
}
} i'm using this animateted model: https://www.turbosquid.com/FullPreview/Index.cfm/ID/1282563 and it has all animations together, and has txt file with description of subclips frames somewhere, in loader object, model downloading: this.models.ship = await this.helper.loadFbx('3d/ship/sci_fi_aircraft',1,true) , so, here is code of clips cutting in spaceship class: this.model = this.loader.helper.cloneFbx(this.loader.models.ship)
this.mixer = new THREE.AnimationMixer( this.model )
//.............
//prepare spaceship animations
let all = this.mixer.clipAction( this.model.animations[ 0 ] )
this.subclips = {
idle1: THREE.AnimationUtils.subclip( all._clip, 'idle1', 0, 140 ),
idle2: THREE.AnimationUtils.subclip( all._clip, 'idle2', 145, 240 ),
walk: THREE.AnimationUtils.subclip( all._clip, 'walk', 250, 285 ),
//run: THREE.AnimationUtils.subclip( all._clip, 'run', 290, 305 ),
//skillstart: THREE.AnimationUtils.subclip( all._clip, 'skillstart', 310, 320 ),
}
this.actions = {}
for(let clip in this.subclips)
this.actions[clip] = this.mixer.clipAction( this.subclips[clip] ) and small method of spaceship class to play clips play(action)
{
if (this.actions[action])
this.actions[action].play()
} so how, i play clips just calling object method like //.... let ship = new ....
ship.play('idle1') everithing works fine, possible to load clips in searate files and push them to skinned model, and possible to load single big clip and cut it to peaces... |
Thanks! I'm wondering about the part where you're splitting imported animations: {
idle1: THREE.AnimationUtils.subclip( all._clip, 'idle1', 0, 140 ),
idle2: THREE.AnimationUtils.subclip( all._clip, 'idle2', 145, 240 ),
walk: THREE.AnimationUtils.subclip( all._clip, 'walk', 250, 285 ),
...
} Do you export FBX from Maya, 3DS Max, or another tool? With my workflow in Blender, animations often have to be "baked" to preserve IK constraints, and this inserts additional keyframes at export time. Which, now that I'm thinking it through, is a basic flaw in this PR — the I will update |
/cc @zellski does FBX2GLTF do anything particular with LocalStart / LocalStop, if you've run into them? |
I'd be happy to write up some best practices/tutorial for exporting from Maya FBX to three.js (multiple clips, single clip, using time editor to create clips, etc).
Seems to be exactly what I'm looking for. I can think of a few use cases where the shift shouldn't be applied to every clip coming in on load. It makes more sense to me if the shift is applied on a per-clip basis. Whether or not the shift should be applied by default is debatable though. |
Hi folks. I will add try to comment, with the caveats that I didn't have time to read through the above in great detail, that I'm a little vague on the finer points of animation support, and that FBX2glTF takes some liberties in this area. With that out of the way, briefly, first, yes, those are precisely the parameters we use. For each FbxAnimStack, extract its associate FbxTakeInfo (if null, skip this animation). Then:
However, because we bake animations at a fixed rate, we immediately lose the precision of that interval:
where eMode is hardcoded to a fairly arbitrary FbxTime::eFrames24. This is a likely source of some animation errors I've seen, where the animation's beginning and end doesn't fall exactly on one of these new keyframes we're introducing for our own benefit. Then we iterate through each frame, and for each frame, traverse the entire scene graph, letting the FBX SDK do all the heavy lifting to compute the local transform of each node at each moment in time. That local transform is what we append to the glTF animation associated with the current FbxAnimStack. We only create one animation within the exported glTF for each such stack. For what it's worth, I found this is a pretty decent summary of FBX animation internals. As for practical workflows and the like, I do get the sense that the game exporter approach is common, as is separate FBX files for animations. I apologise if this is a non-answer; I am happy to follow up tomorrow when I will also have more time to read through the in-depth digging you folks have been doing here. |
Currently in the
We're doing something similar - that is, computing the local transform for each animated node for each defined time and then building up an animation track from that.
Yeah, this is what I'm thinking the issue is here - currently, the way that the second clip is created in the loader is to set the times as the are found in the FBX file. This means that the second clip starts at We previously did try shifting track start times to 0 in #13743, however it broke certain models so I reverted it in #13861. @mikebourbeauart here's a version of the loader with that PR added back in, could you test it and see if that works for you? |
@looeee yep, that's just what I was looking for! |
@mikebourbeauart great! Could you open a seperate issue for this, so that we can track it there? I'll investigate what if there's something I can do to make the loader work with your model and the other models that were broken by this fix. |
@donmccurdy Nice work! are you planning on getting that PR merged? I'd be really interested in using it 🙂 |
@mvilledieu I'm not sure, there's another PR doing the same thing actually: #15615. I'm happy with either of those PRs, but what I don't have at the moment is a good sample model that demonstrates either one is working correctly. If you have a model with multiple animations designed to be split at certain keyframes, that might be helpful! I've been using Blender, which can export each animation separately anyway, so I'm not currently using this. Still seems useful for 3DS Max / FBX users though. |
I forgot about this! @donmccurdy I'll package up something and send it your way. @mvilledieu if you have access to the source 3D files and can utilize another format it might be worth checking out Maya2glTF. I had the best results from that workflow (blend shapes worked well with clips and much better file size than FBX). Make sure to use V0.9.9 or older as V0.9.10 has a few mesh breaking bugs. |
@donmccurdy yes the Idea was using one animated FBX exported from Maya (Or animation tool of the 3D artist choice) re-export it to glTF using blender and Draco compress the mesh then get the possibility to play clips at different frames. Exactly what this PR is about. I can try to get you a demo file except if @mikebourbeauart is already taking care of that. I tried your PR on my project and I'm losing the model (the model is being replaced by a random plane) like if a reference to the model name was missing or something, I'm digging a little more into that and will let you know. @mikebourbeauart Hey Mike nice, I'll try that out thanks for the info. |
@donmccurdy @mikebourbeauart ok everything is working nicely on my end! Nice work. Thanks again for that PR. For now, I'm just going to import the modified version of the files you've specifically updated. Hopefully that PR gets merged! |
9d9b1bc
to
2321a4b
Compare
I've rebased this PR and removed the "WIP" label. I could still use an example model with an animation that loops cleanly after being split. If you've got something working, can you confirm that the frame offsets passed into |
Nice! Yes, In my case Maya's frames match with the one I specified in |
Hi, everyone.
And update by time
Because Look inside At first, I want to control the tracks like Finally, I found that the First, Add one Array in the obj.
Then add the following code after
Therefore, the animation will play the first slice in the beginning. If you want to change the slice, just change the obj.animationSlices[0].index directly. Hope this method can resolve other problem. |
Thanks! |
I am so glad that I can make some contributions for the Three.js
Actually, I work for one project for webAR and webVR with three.js and
aframe, I will share our project as soon as possible!
Mr.doob <notifications@github.com>於 2019年10月19日 週六,19:42寫道:
… Thanks!
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#13430?email_source=notifications&email_token=AGAOJJTIQD2PE53PHJZYV2LQPLXCHA5CNFSM4ESIXOOKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBXNGUQ#issuecomment-544133970>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGAOJJQBPBBISKEM5SXDIXLQPLXCHANCNFSM4ESIXOOA>
.
|
See discussion in the forums, ClipAction select range to play. The original suggestion was to allow clamping a clip to a particular time span, but I think an API for splitting clips would be best — this makes it possible to play each section of the clip independently, or crossfade between them.
Usage:
Previous syntax
Updated syntax:
If this API seems OK, I'm glad to add docs and some unit tests. A few questions first:
.splitClip()
portion be kept out of core, e.g.AnimationClipCreator.splitClips()
, instead?IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
. I'm not sure what this means, or if it's relevant here. @bhouston do you recall?/cc @looeee