Instance skinned geometry

Hi there,

I’ve made a little animation of an algae floating in the ocean soil.

The algae is skinned.

Now, I need lots of algaes and my plan was to instance that skinned algae lots of times in order to save memmory and file size.

But the skinned geometry only works correctly when it’s placed in the world coordinates center. When moved to another place, the geometry explodes.

The algae geometry is grouped with its joints in a group.

Is this an unsupported feature (to instance and scatter skinned geometry) or it’s my fault and I’m trying to it the wrong way?

Please help, thanks!

cc @Evgeni_Popov

Hi!

Hard to tell from the description alone whether this is an unsupported case, a setup issue with the skeleton/transforms, or a bug.

Could you please share a minimal Playground showing one skinned algae and one moved instance that “explodes”? If the real asset can’t be shared, a simplified skinned mesh reproducing the same hierarchy/issue is enough.

Once we have a repro, we can check whether instancing this skinned setup is expected to work or if something needs fixing.

Thanks for your response. I don’t know how to code, but I guess it’s also OK to just give you the URL for the asset in my Dropbox.

As you can see, the middle algae is instanced and moved away. At first frame, it seems to be OK, but then you play the animation and starts to explode.

Thanks, no worries about not coding — the .glb is useful.

From the asset, this looks like a skinned-mesh instancing limitation/setup issue: the moved algae is sharing the same skeleton/joints as another algae, but it needs its own rig/bind pose once animated. That’s why it can look OK on the first frame, then explode when the bones start moving.

For skinned meshes, don’t use mesh instances for independently placed copies. Clone/duplicate the whole rig instead — mesh + skeleton + animation group — and move the cloned root. In Babylon.js, AssetContainer.instantiateModelsToScene() is the recommended path, because it avoids instancing skinned meshes and clones the skeletons/animation groups. Materials/geometry can still be shared, so it should be much cheaper than fully re-exporting everything.

I’m asuming there is no way to export that way from my DCC (MAYA) and the cloning has to be done by code.

Is there any other way to do what I need?

For example, Is there a way to bake the deformation animation as vertex animation for example?

That way may work and be able to be instanced and moved?

I’m adding my colleage @paleRider, who knows exactly the technical limitations of our current project.

Yes, baked vertex animation / VAT is probably the right direction for this use case.

Instead of evaluating the skeleton for every algae, you bake the skinned deformation into a texture, then render many copies as instances or thin instances. That should avoid the “shared skeleton” problem, and each algae can be moved/scattered independently.

The trade-off is that it is still a pipeline/code solution: the animation has to be baked first, then the scene needs to load/use the baked data with BakedVertexAnimationManager. So it’s not something the glTF/Maya export alone will automatically solve unless you have a custom export/baking step.

Some useful references:

So the options are roughly:

  1. Clone the full rig per algae with code, using AssetContainer.instantiateModelsToScene().
  2. Bake the deformation to VAT, then instance/thin-instance the animated mesh.
  3. Duplicate everything in Maya before export, which needs less runtime code but costs more file size/memory.

For a large number of animated algae, I’d investigate option 2.

I have continue investigating the way to create what I need, but I always face a wall.

I’ve tried the method mentioned in this post and I thought it worked because in the Windows 10 3d viewer it showed all instanced objects sharing source blendshape animation. But then I’ve tested it in Snadbox and even and only first instance has the blendshape weights animated.

I thought all instances would have the blenshape animated because, in MAYA at least, deformations happen in the shape node, not in the transform one, so if something happens to the shape, all share the same.

So, the instances have the blendshape, but only the first one is animated.

Is that a bug or something?

It’s not really a bug, it’s by design — and it’s actually the same root cause as with the skeleton.

In Babylon.js, a Mesh and its instances (created via mesh.createInstance() / glTF nodes that reference the same mesh) share the geometry, the material, the skeleton, and the MorphTargetManager. The morph target weights live on the MorphTargetManager, not on each instance node, so there is only one set of weights for the source mesh + all its instances. When the glTF loader binds a morph weight animation to a specific node, it ends up animating that single shared manager — and visually only the first one looks “right” (and, depending on the file, the others may not even update at all because the animation targets a node whose weights are ignored).

Your Maya mental model (“deformation happens on the shape node, so all transform instances inherit it”) matches what the Windows 3D viewer does, but it does not match how Babylon’s InstancedMesh works: instances are intentionally lightweight and cannot have their own per-instance skeleton/morph state.

So for your algae you have basically the same set of options as before:

  1. Clone the rig per algae with AssetContainer.instantiateModelsToScene(). That clones the MorphTargetManager / skeleton / animation groups per copy, while still sharing geometry and material. It’s the cleanest “many independently animated copies” path when you stay below a few hundred copies.
  2. Bake to VAT (VertexAnimationBaker + BakedVertexAnimationManager) and then thin-instance / instance the baked mesh. VAT moves the deformation into a texture sampled in the vertex shader, so per-instance weights are no longer needed and you can scale to thousands of algae cheaply. This is still the path I’d recommend for your use case.
  3. Duplicate the geometry+rig in Maya before export (heavy on file size / memory, light on runtime code).

Mesh instances + animated morph targets / blendshapes is not a workable combination on its own in Babylon — that part is expected behavior, not a bug.

Understood.

Just asking (and forget it if it is a dumb question) though…, could it be a fourth way of doing that, by duplicating the anim curves connections of the source instances and connect them to the other instances in the glTF code?

Obviously that can’t be done in MAYA because there is only one shape in reality, but I guess the animation curves are connected to each weight in the glTF code, so it “may” be connected in the text code afterwards?

Not a dumb question at all — and actually, looking at it more carefully, I owe you a correction on my previous post.

For morph targets / blendshapes specifically, Babylon’s glTF loader does not use InstancedMesh when a mesh has morph targets. The relevant check in glTFLoader.ts is roughly:

const shouldInstance =
    this._disableInstancedMesh === 0 &&
    this._parent.createInstances &&
    node.skin == undefined &&
    !mesh.primitives[0].targets;

So if a mesh has morph targets, every node that references it gets a full Mesh with its own MorphTargetManager. The morph target data (positions/normals/…) is reloaded per copy, but the weights are independent per node.

That means your idea is right: adding extra animation channels in the glTF JSON, each targeting a different node’s weights path, would animate each algae independently. The reason only the first one animated in the Sandbox is almost certainly that your exporter only produced one channel (targeting the original node) — the other nodes have their own MorphTargetManager sitting there with weights stuck at 0.

Concretely, in the glTF you’d want something like:

"animations": [{
  "channels": [
    { "sampler": 0, "target": { "node": 5, "path": "weights" } },
    { "sampler": 0, "target": { "node": 6, "path": "weights" } },
    { "sampler": 0, "target": { "node": 7, "path": "weights" } }
  ],
  "samplers": [ { "input": ..., "output": ..., "interpolation": "LINEAR" } ]
}]

All three channels can share the same sampler (same keyframes / output buffer), so it costs almost nothing in file size — just three extra channel entries per algae. You can script that as a small post-process on the exported .gltf (or .glb after unpacking).

Two caveats so you don’t get bitten:

  • This trick only works for morph targets, not for the skinned case. For nodes with a skin, the loader still creates separate Mesh objects, but they all share the same Skeleton (because the skin is shared by index). Animating bones still moves them once, and every copy that uses that skeleton deforms identically. So for the skinned algae you still need either instantiateModelsToScene() (which clones the skeleton) or VAT, or duplicating the skin in the glTF.
  • If you ever switch the algae to a skinned + morphed mesh, only the morph part will be independent via this trick; the skin part will still be shared.

So a more accurate option list, just for the morph-only case, would be:

  1. AssetContainer.instantiateModelsToScene() — clones the rig + morph manager per copy.
  2. VAT — fully bakes the deformation, scales to thousands of copies.
  3. Duplicate the mesh/skin in Maya — heaviest in size, lightest in code.
  4. Your idea: keep one shared mesh + multiple nodes, but add per-node weights animation channels in the glTF JSON. Cheap on size, no runtime code, works because the loader gives each morphed-mesh node its own MorphTargetManager.

Sorry for the confusion in the previous answer — I’d conflated the skinned case (where the original “explode” symptom came from) with the morph-target case, but Babylon handles those two differently inside the loader.

Thanks for your explanation. I didn’t want to start another thread just because of that, sorry.

Good news is that I gave all this information to Gemini and it made a webapp that takes the offending glTF with all animation data but disconnected from the instances, fixes it and save it back.

And it works like a charm.