For anyone reading this thread because you are trying to make a game by importing 3D models from Blender using the Babylon exporter, here is the code you need to fix all the animation issues:
const bredthFirst = function (root, fn) {
if (!root) return;
fn(root);
const children = root.getChildren();
if (children) children.forEach((child) => bredthFirst(child, fn));
}
const meshByName = function (root, name) {
name = name.toLowerCase();
let mesh;
bredthFirst(root, (child) => {
if (child.name.toLowerCase().endsWith(name))
mesh = child;
});
return mesh;
}
// Creates animation groups for mesh animations. Uses a naming convention
// that animation ranges will be a concatenation of the mesh name and the group
// name. For example if the mesh is called 'ee1' then the animation ranges can be
// called 'ee1Working', 'ee1Firing', 'ee1Moving' etc. which will produce animation
// groups called 'Working', 'Firing' and 'Moving'. The mesh parent/child tree is
// traversed and animations added to the matching animation groups. For example if
// the model has 4 wheel meshes called 'wheel1', 'wheel2' etc and there are animation
// ranges 'wheel1Moving', 'wheel2Moving' etc, then the 'Moving' animation group will contain
// the 'Moving' animations for each wheel, and starting this group will animate all of the wheels.
const groupAnimations = function(container){
let model;
container.meshes.forEach(function (mesh) {
if (!mesh.parent) {
if (model) console.log('Model ' + model.name + ' has multiple root meshes ' + mesh.name);
else model = mesh;
}
});
if (!model) return;
const animationGroups = [];
bredthFirst(model, function (child) {
const meshName = child.name.toLowerCase();
const ranges = child.getAnimationRanges();
let range;
if (ranges) {
ranges.forEach((r) => {
if (r.name.toLowerCase().startsWith(meshName))
range = r;
});
}
if (!range) return;
const actionName = range.name.substr(meshName.length).toLowerCase();
const groupName = model.name + '.' + actionName;
let animationGroup = animationGroups.find(g => g.groupName === groupName);
if (!animationGroup) {
animationGroup = { groupName, actionName, targets: [] };
animationGroups.push(animationGroup);
}
if (child.animations) {
const unpackedAnimations = [];
child.animations.forEach(anim => {
const keys = anim.getKeys();
if (keys) {
let hasChanged = (a, b) => a != b;
if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_VECTOR2) {
hasChanged = (a, b) => a.x != b.x || a.y != b.y;
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_VECTOR3) {
hasChanged = (a, b) => a.x != b.x || a.y != b.y || a.z != b.z;
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_COLOR3) {
throw 'Not yet implementted';
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_COLOR4) {
throw 'Not yet implementted';
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_FLOAT) {
throw 'Not yet implementted';
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_SIZE) {
throw 'Not yet implementted';
} else if (anim.dataType === BABYLON.Animation.ANIMATIONTYPE_QUATERNION) {
throw 'Not yet implementted';
}
const unpackedKeys = [];
let prior;
let hasAnimation = false;
keys.forEach((k) => {
if (k.frame >= range.from && k.frame <= range.to) {
if (prior === undefined) prior = k.value;
else if (!hasAnimation) {
hasAnimation = hasChanged(prior, k.value);
prior = k.value;
}
unpackedKeys.push({
frame: k.frame - range.from,
inTangent: k.inTangent,
outTangent: k.outTangent,
interpolation: k.interpolation,
value: k.value
});
}
});
if (hasAnimation) {
const animationName = meshName + '.' + anim.targetProperty;
const unpacked = new BABYLON.Animation(animationName, anim.targetProperty, anim.framePerSecond, anim.dataType, anim.loopMode);
unpacked.setKeys(unpackedKeys);
unpackedAnimations.push(unpacked);
}
}
});
if (unpackedAnimations.length) animationGroup.targets.push({ mesh: child.name, animations: unpackedAnimations });
}
});
model.groupedAnimations = animationGroups;
}
const instantiateModel = function (container, namePrefix, mapLocation, mapOffset, width, length, height) {
let model;
let instance;
container.meshes.forEach(function (mesh) {
if (!mesh.parent) {
model = mesh;
instance = mesh.clone(namePrefix + ':' + mesh.name, null, false, false);
}
});
instance.scaling.x = width;
instance.scaling.y = height;
instance.scaling.z = length;
mapLocation.position(instance, mapOffset);
bredthFirst(instance, (mesh) => {
if (mesh.material) {
const materialName = mesh.material.name;
if (materialName) {
const sceneMaterial = container.scene.getMaterialByName(materialName);
if (sceneMaterial) mesh.material = sceneMaterial;
}
}
});
container.scene.addMesh(instance);
if (model.groupedAnimations) {
instance.actions = {};
model.groupedAnimations.forEach(g => {
const animationGroup = new BABYLON.AnimationGroup(namePrefix + ':' + g.groupName, container.scene);
instance.actions[g.actionName] = animationGroup;
g.targets.forEach(t => {
const mesh = meshByName(instance, t.mesh);
if (mesh) {
t.animations.forEach(animation => {
animationGroup.addTargetedAnimation(animation, mesh);
});
}
});
});
}
if (instance.actions.working) instance.actions.working.play(true);
return instance;
}
const loadBuildingModel = function (scene, buildingStats, onLoaded) {
loadModel(scene,
'b_' + buildingStats.type + '_' + buildingStats.size,
'/assets/models/building/', buildingStats.type + '/' + buildingStats.size,
onLoaded,
function (container) {
container.materials.forEach(function (material) { material.ambientColor = scene.ambientColor });
groupAnimations(container);
});
}
let building;
loadBuildingModel(scene, stats, (container) => {
building = instantiateModel(container, name, mapTile.center, new MapLocation({ r: 0.2 }), radius, radius, height);
});
The last function is specific to my game, but the rest is generic and should work for you. The only reason I included loadBuildingModel
is because it demonstrates how to use the rest.
I also included a line here that says
if (instance.actions.working) instance.actions.working.play(true);
because this demonstrates how to play the animations on your models.
To use this code you need to stick to some naming conventions. Lets say for example we are modelling a car, the car has 4 wheels (as 4 child meshes) and you want a moving animation that animates all 4 wheels.
In Blender, create a car mesh and call it ‘Car’. Create 4 meshes for the wheels and call them ‘Wheel1’, ‘Wheel2’, ‘Wheel3’ and ‘Wheel4’. Make sure to set the ‘Car’ mesh as the parent of the ‘Wheel’ meshes.
Next, in Blender create some Actions in the Action Editor called ‘Wheel1Moving’, ‘Wheel2Moving’, ‘Wheel3Moving’ and ‘Wheel4Moving’ that are specific to each wheel mesh, and perform with whatever animations you want. Make sure that the first and last key frame is the same frame number in each Action. If necessary duplicate the end keyframes and move them to the first or last frame number.
Now export from Blender using the Babylon exporter, and load it into Babylon using the code I pasted above. Now when you call instantiateModel
is will return a new instance of the car model, and calling actions.moving.play()
will run the ‘moving’ animations for all 4 wheels, making this car look like it’s moving. Most importantly, if you instantiate multiple cars, they can have their moving animations started, stopped and paused independently.
The car can also have other kinds of animation. For example you can add a weapon. Lets assume you called the weapon mesh ‘Weapon’ and parented it to ‘Car’. You can create an Action in Blender called ‘WeaponFiring’ which will result in an actions.firing
method on every car that is an AnimationGroup
.
Note that this workflow only allows 1 animation per mesh.