Help needed with Blender export to Babylon

I have been struggling all day with trying to get the result I want in Babylon by editing my model in Blender, and I am out of ideas about what to try.

This is what I currently have in Blender - it looks the way I want.

This is what the model looks like in Babylon

How can I get predictable results with this workflow. My trial and error approach is taking forever.
Thanks.

After another day pf fiddling with settings, I have improved my situation, but this is still an incredible pain. The biggest breakthrough was turning off the PBR materials in the Exporter. The other thing that helped a lot was turning on only one light at a lime in Blender and Babylon and adjusting each light individually.

Although I made progress, you can see from these screenshots that the output is very far from predictable.

pinging @PirateJC

Hey @Bikeman868 - the Blender to Babylon workflow can be pretty straight forward once you know what to look for and what to do.

Some more info would be helpful on your current workflow.

Are you exporting to .gltf/.glb? What exporter are you using? The one that’s built into Blender or the Babylon exporter?

Biggest tip I can offer you without knowing more is to make sure you’re using the PrincipledBSDF shader for your materials. That ends up hanging people up quite a bit.

It might be time for us to write up a documentation page about authoring assets in Blender and getting them into Babylon. :slight_smile:

2 Likes

I am capturing my learnings here. Maybe someone can turn it into documentation later.
My learnings from today are:

  1. Don’t try to build your lights in Babylon and match them up in Blender. Instead create a Blender file that contains your lights and all of your materials. This blend file can be linked to all of your other models so that they can be developed using a set of shared materials and using a consistent lighting scheme.

  2. After loading a .babylon file you enumerate all the materials and set the ambientColor properties otherwise you have no ambient light and your scene will be much darker then in Blender…

I wrote this model loading function:

    const models = {};

    const loadModel = function(scene, key, root, asset, onLoaded){
        var model = models[key];
        if (model) {
            if (model.container) onLoaded(model.container);
            else model.loaders.push(onLoaded);
            return;
        }
        model = {
            loaders: [onLoaded],
            container: null
        };
        models[key] = model;
        BABYLON.SceneLoader.LoadAssetContainer(root, asset + '.babylon', scene, function (container) {
            container.materials.forEach(function (material) { material.ambientColor = scene.ambientColor });
            model.container = container;
            model.loaders.forEach(l => l(container));
            model.loaders = [];
        });
    }

It will allow you to reuse the same model multiple times without duplicating the mesh vertexes.

This is how you would use this function to load your lights and shared materials from a scene.babylon file:

loadModel(scene, 'scene', '/assets/models/', 'scene', function (container) { container.addAllToScene(); });

My next step is figuring out how to create a Blender file for each model that is linked to shared materials in my scene.babylon file, and have these materials hooked up correctly in Babylon.

To answer your questions, I am not set on any particular workflow, I am happy to use anything that works smoothly. My first attempt was Sketchup exporting .stl files until I discovered that it only saves the mesh and none of the materials. Mt secord attempt was to import the .stl files into Blender, redo the materials then export as a .glb file. This works reasonably well, but creates rotated models that are a pain to work with, so I searched around and discovered a Babylon exporter plugin for Blender that supports things like setting isPickable on meshes etc, so this is what I am playing with right now.

It’s quite a journey, and anyone wanting to make a reasonably complex game would need to find a path through this maze. There are so many 3D file formats, converters, editors, exporters and importers, and everything has a bazillion settings, making the possible permutations astronomical.

My advice at this point to anyone else starting out on this journey would be:

  1. Create a Blender scene with your lighting and shared materials.

  2. Use the Babylon exporter ad-on for Blender to export your scene as a .babylon file (note that .glb files are smaller).

  3. Create additional .blend files for each model (building, vehicle, character, item etc). These files can link to your main scene.blend and reference the lights and shared materials in there. This way you develop your models for consistent lighting.

  4. Export all of your blender models to .babylon files (on .glb if you want to squeeze out the last few bytes). This can be done using a command line script so that if you change a shared material, you can re-run all of the .blend to .babylon conversions to regenerate all of your models.

  5. Load .babylon files into containers in Babylon so that you can easily add/remove to the scene and create multiple instances without duplicating the mesh vertexes. After loading each file, loop through all of the materials setting their ambientColor property.

I haven’t tried loading models into Babylon that have links to shared materials yet, so this workflow may not work as I am hoping - hopefully there will be workarounds for any issues I encounter.

1 Like

Just to close the loop on this one, if you create a scene.blend file with shared materials, then ‘Link’ that file to each Blender file that defines a model, then export these models to .babylon files and load them into Babylon, what you find is:

  • It works, in the sense that everything looks the way it should.
  • The shared materials will be repeated in every .babylon file which makes them larger than necessary.
  • If you look in the Babylon Scene Explorer, you can see that the shared materials are duplicated in the scene, so that your game will consume more memory than necessary.

I don’t think there is much that I can do about the size of the .babylon files other than write some code to parse the file and remove the materials from it.

I think I should be able to get around the runtime issues by implementing my own version of container.instantiateModelsToScene that hooks up the meshes to the materials that already exist in the scene rather than creating new ones.

The final step was the animations, and here things didn’t go so well.
Most of my models have multiple animations. For example vehicles have at least a moving animation, a weapon firing animation and a destruction animation. Characters have more complex armature based animations. To have multiple animations for one mesh in Blender, you must use NLA.

Unfortunately the Babylon exporter for Blender does not export NLA animations at all. As an experiment I added some non-NLA animations and they are exported, but you can only have one animation per mesh if you do this and the whole model has a single timeline.

I think this means that the .babylon file format can’t be used for most types of games if you want to use Blender to create the models.

I think that what should happen in the Babylon exporter for Blend is that NLA Action Strips with the same name should be combined into a Babylon ActionGroup that can be started and stopped within Bablyon in response to game events (such as firing a weapon).

For now I will have to go back to square one and investigate another file format. I still like Blender, so I think I will do an experiment where I export a model from Blender in every supported file format, then try importing them one at a time into Babylon to see which ones have limitations that can be worked around.

This is quite a surprising outcome for me because:

  • Blender is probably the best free tool available for 3D modelling and animation.
  • Writing games seems like a primary use case for Babylon
  • Most games need animation to be interesting to play
  • .babylon seems like the most logical choice for file format to use with Babylon

@JCPalmer our wonderful Blender expert and the actual .babylon exporter daddy for blender as he is always full of tricks for it.

1 Like

What Blender requires does not matter in this case. An individual mesh / armature has an active Action, which can also be None. For each object where the active Action is not None, all actions in the scene are exported.

Babylon format only allows only one animation per object. The exporter also records the start and stop frames of each action as a BABYLON.AnimationRange with the same name as the Blender Action. This means you call up the animation by name, not frame ranges. See your log file if you want to see the actual ranges.

There are 2 exceptions to handle various requirements:

  • You can request that only the current animations be exported on the custom properties of World. You may have multiple actions in the blend, but not want any of the others be on the export without deleting them.

  • If you have multiple objects which have an active Action, but you which to only perform a certain action on a single object, use an objectName-actionName format for the name of the Action. e.g.: MyMesh-Jump.

Thank you for your reply. Just to make sure I understand you properly. I think you are suggesting that:

  1. Don’t push Actions down onto Action Strips in Blender - I know this makes them fail to export.
  2. If a mash has multiple actions, separate them into different ranges of keyframes. E.g. moving animation is frame 0-50 and the weapon firing animation is frame 50-100 etc.
  3. Creating named animations in the Blender Action Editor causes named AnimationRange objects to be created in Babylon so that I can find the start and end frame number of each animation.

I wasn’t aware that it was possible to create multiple Actions on a single mesh in the Action Editor without pushing them down onto Action Strips, but I will experiment with Blender to see how to do this.

I swear that I read every scrap of documentation and I don’t think this is described anywhere.

Right skip #1
.
Wrong on #2. Each action should start at frame 0. It is the exporter which goes thru all the actions & makes and sequences them into the single animation for an object that the file format supports. It will actually make a 5 frame gap between, just for my own sanity.

Right on 3: Call up the actions by name, using AnimationRange. Running the animation directly when there are multiple actions inside is no longer useful.

Right, you cannot put multiple actions on a mesh, but you can make as many actions as you wish. An object only has a current action. It is the exporter, when an Object is encountered which has a current action, which goes through all the animations, setting each to be current one at a time, then running, & grabbing the result.

That is why I said it does not matter what Blender needs for this.

I made some progress. I deleted al of my action strips, and all of my actions and started over. I created 6 actions in the Blender Action Editor with different names, all between fames 0 and 30. Each animation applies to a different object/mesh.

I saved my blend file, then exported to Babylon, and all of the objects with animations rotated by about 45 degrees. No biggie, Ctrl-Z fixed that issue. I tried this several times and each time I exported the objects were rotated. So I closed Blender and re-opened it just to clean out any garbage from the deleted action strips and this time the export worked without the rotation problem, but when I loaded the model into Babylon one of the animated objects/meshes was displaced quite far from where it should be.

Using the Babylon Scene Explorer and Inspector, I can see 6 animation ranges with the same names as my Blender actions. They don’t seem to be associated with the meshes, and I can start any of these animation ranges on any of my meshes. I guess this means that I need to keep track of which animation ranges relate to which meshes or use some naming convention that allows me to associate them.

There are a few odd things:

  1. I cannot start animations on the mesh that was drawn in the wrong place even though it was animated in Blender.
  2. I can only start animations on meshes that had actions assigned to them in Blender even though I can run any animation on any mesh that was animated, even if the animation wasn’t designed for that mesh. This is not a big problem, but it is very peculiar.
  3. The Babylon inspector says that there are 3 animations on each mesh, but I just did a very simple move and move back on the z-axis to test things out. I don’t seem to be able to access or play these animations.
  4. The timing of the animations is wrong. In Blender they were mostly10 frames long, but in Babylon the animation lengths vary and some animations actually look much slower than others.

The layout of the animation ranges ended up like this:
#1 was 30 frames in Blender -> action range 0…30
#2 was 10 frames -> action range 40…70
#3 was 10 frames -> action range 80…105
#4 was 10 frames -> action range 110…130
#5 was 10 frames -> action range 140-155
#6 was 10 frames -> action range 160-170

I can send you the files if that would be useful.

I found a fix for the animation speed variations. Not sur if this is the right way to fix the problem, but I took the first keyframe of each animation and Shift-D (duplicate) and moved it to keyframe 0, then took the last keyframe and Shift-D move to last keyframe, so that all of the animations are the same length and have the same start and end frame in Blender.
When I export it like this, the animation ranges in Babylon are now correct and all the animations run at the correct speed.

More progress and more snags. My current road block is that AnimationGroup does not seem to support AnimationRange, and the Blender Babylon exporter packs all of the Blender actions into one animation with different frame ranges. This makes the AnimationGroup unusable in this workflow.

I think I could programmatically unpack the animation ranges and create a new set of animations that are zero based but this seems like a lot of hassle. Is there a better way?

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.

2 Likes