[SOLVED] Import FBX

Note that importing .fbx the way you did it used the SDKs fbx importer, which means opening the file in blender and using the blender importer to open the generated file. Unfortunately the blender importer has got it’s problems, so the best option always is xbuf/gltf from blender [for xbuf I can’t tell if it supports morph and what you need]

Thanks, I imported my FBX into blender and exported it as a GLTF. I am fiddling with the code right now, but once I have a working test I’ll post the code here.

Note, there are two gltf addons for blender.

1- Old one ( Not being developed any more but stable )

2- New one (In beta state and had some issues with animations last time I tested it few months ago)

So suggest first try with 2nd one if not worked use 1st one.

And make sure before importing to JME check your gltf model in an online gltf validator/viewer .
Here are a few of them :
https://github.khronos.org/glTF-Validator/
https://gltf-viewer.donmccurdy.com/
https://sandbox.babylonjs.com/

1 Like

Thank you for the tips! I was using the first one, and was not seeing the animations (probably something I am doing wrong though) I will check on those websites.

Thanks again!

OK, so I have learned a couple things.

  1. I could not get a working gltf file from blender 2.79b. Moving over to beta and now I can get a working gltf file no problem. It loads in to jme fine and I can see all the bones.

  2. According to babylonjs my gltf file has morph targets, and no animations. Seeing as I do not have any animations, but I have shape keys, I am assuming this is correct.

I have a working test project that I can see the model in jme with, but now I need to figure out how to access the shape keys.

Current code:

package io.tlf.outside.client.test;

import com.jme3.anim.*;
import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.math.*;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.custom.ArmatureDebugAppState;
import com.jme3.system.JmeSystem;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

/**
 *
 * @author Trevor, based from @Nehon  https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java
 * 
 */
public class CCTest extends SimpleApplication {

    ArmatureDebugAppState debugAppState;
    AnimComposer composer;
    Queue<String> anims = new LinkedList<>();
    boolean playAnim = true;
    File file;

    public static void main(String... argv) {
        CCTest app = new CCTest();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        setTimer(new EraseTimer());
        //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f);
        viewPort.setBackgroundColor(ColorRGBA.LightGray);
        //rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal()));
        rootNode.addLight(new AmbientLight(ColorRGBA.White));
        //Node probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
        //rootNode.attachChild(probeNode);
        Spatial model = assetManager.loadModel("Models/G8M/g8m.gltf");

        File storageFolder = JmeSystem.getStorageFolder();
        file = new File(storageFolder.getPath() + File.separator + "g8m.j3o");
        BinaryExporter be = new BinaryExporter();
        try {
            be.save(model, file);
        } catch (IOException e) {
            e.printStackTrace();
        }

        assetManager.registerLocator(storageFolder.getPath(), FileLocator.class);
        Spatial model2 = assetManager.loadModel("g8m.j3o");

        //model2.setLocalScale(1f);
        rootNode.attachChild(model2);

        debugAppState = new ArmatureDebugAppState();
        stateManager.attach(debugAppState);

        listAnimations(model2);

        setupModel(model2);

        flyCam.setEnabled(false);

        Node target = new Node("CamTarget");
        //target.setLocalTransform(model.getLocalTransform());
        target.move(0, 0, 0);
        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
        chaseCam.setTarget(target);
        getStateManager().attach(chaseCam);
        chaseCam.setInvertHorizontalAxis(true);
        chaseCam.setInvertVerticalAxis(true);
        chaseCam.setZoomSpeed(0.5f);
        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
        chaseCam.setRotationSpeed(3);
        chaseCam.setDefaultDistance(3);
        chaseCam.setMinDistance(0.01f);
        chaseCam.setZoomSpeed(0.01f);
        chaseCam.setDefaultVerticalRotation(0.3f);

        initInputs();
    }

    public void initInputs() {
        inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN));

        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed) {
                    playAnim = !playAnim;
                    if (playAnim) {
                        String anim = anims.poll();
                        anims.add(anim);
                        composer.setCurrentAction(anim);
                        System.err.println(anim);
                    } else {
                        composer.reset();
                    }
                }
            }
        }, "toggleAnim");
        inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed && composer != null) {
                    String anim = anims.poll();
                    anims.add(anim);
                    composer.setCurrentAction(anim);
                    System.err.println(anim);
                }
            }
        }, "nextAnim");
    }

    private void setupModel(Spatial model) {
        if (composer != null) {
            return;
        }
        System.out.println("Setting up model");
        composer = model.getControl(AnimComposer.class);
        if (composer != null) {
//            model.getControl(SkinningControl.class).setEnabled(false);
//            model.getControl(MorphControl.class).setEnabled(false);
//            composer.setEnabled(false);
            System.out.println("Got animator");

            SkinningControl sc = model.getControl(SkinningControl.class);
            debugAppState.addArmatureFrom(sc);

            anims.clear();
            for (String name : composer.getAnimClipsNames()) {
                anims.add(name);
                System.out.println("Animation " + name);
            }
            if (anims.isEmpty()) {
                return;
            }
            if (playAnim) {
                String anim = anims.poll();
                anims.add(anim);
                composer.setCurrentAction(anim);
                System.out.println("Playing animation: " + anim);
            }

        } else {
            System.out.println("No animator");
            if (model instanceof Node) {
                Node n = (Node) model;
                for (Spatial child : n.getChildren()) {
                    setupModel(child);
                }
            }
        }
        System.out.println("Animator has: " + String.join(", ", composer.getAnimClipsNames()));

    }

    private void listAnimations(Spatial model) {
        for (int i = 0; i < model.getNumControls(); i++) {
            System.out.println(model.getName() + " has control: " + model.getControl(i));
        }
        AnimComposer anim = model.getControl(AnimComposer.class);
        if (anim != null) {
            System.out.println(model.getName() + " has animations: " + String.join(", ", anim.getAnimClipsNames()));
        }
        SkinningControl sc = model.getControl(SkinningControl.class);
        if (sc != null) {
            //System.out.println();
        }
        if (model instanceof Node) {
            Node n = (Node) model;
            for (Spatial child : n.getChildren()) {
                listAnimations(child);
            }
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        file.delete();
    }
}

PS: I guess I will take the solved tag off of the title as we are still discussing things.

@Ali_RS maybe you have some insight into this, but is a shape key in the mesh not the same as a shape key animation? I have been looking through the jme gltf loader, and I see where mesh morph animations are loaded in, but my model does not have any animations. It does have mesh morphs, but I do not see where the loader loads those. I do think that the existing MorphControl would work if the loader supported mesh morphs outside of animations.

Maybe I am just confused and do not understand how to export my shape keys from blender.

I have not tested Morph animation yet so not sure how we are supposed to use it.

It seems morph data is being loaded in to MorphTrack. And ClipAction will detected it and will play it.

I guess you should not directly deal with MorphControl and I think you should be able to play your morph animation just like others animation using AnimComposer.
animComposer.setCurrentAction("morphAnimName");
I might be wrong because I have not tried it myself.

Maybe better to take one of these models from sketchfab and load them in JME and try to figure out on them.

Mannequin: Anatomy Aid (Free download) by Rob Allen on Sketchfab

You can use animComposer.getAnimClipsNames() to findout all animation names are loaded in your model.

The Mannequin for example, the mesh morphs have been recorded as part of an animation, the animation is using the shape keys. I can play the animation, and the mesh morphs as part of that. But I do not see any way to access the shape keys myself to apply them outside of the animation.

You can try this :
AnimClip clip = animComoser.getAnimClip(name);
then get tracks from it :
AnimTrack [ ] tracks = clip.getTracks();
iterate over tacks array and get MorphTrack out of them.

for(AnimTrack tarck : tarcks){
      if(track instanceof MorphTrack){
         // you can do whatever you want with this track
       }
}

You can create a new AnimClip (with any name you want) and set your MorphTacks to it and add it to AnimComposer.
animcomposer.addAnimClip( animClip);
now you can play it with anim composer. It will play just the morph animations.

I finally figured it out after deconstructing the GltfLoader.
For anyone who runs into this in the future:

To access mesh morphs that are not part of an animation, there will not be a MorphControl on the spatial, instead you need to get the mesh from the geometry and check if it has morph targets, if it dies, then those are the things you need;

        if (model instanceof Geometry) {
            if (((Geometry) model).getMesh().hasMorphTargets()) {
                System.out.println("Found morphs: " + ((Geometry) model).getMesh().getMorphTargets().length);
            }
        }

Here is the full sample code for the net person who comes along

import com.jme3.anim.*;
import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.math.*;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.custom.ArmatureDebugAppState;
import com.jme3.system.JmeSystem;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Trevor, based from @Nehon
 * https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMorphSerialization.java
 *
 */
public class CCTest extends SimpleApplication {
    ArmatureDebugAppState debugAppState;
    AnimComposer composer;
    Queue<String> anims = new LinkedList<>();
    boolean playAnim = true;
    File file;

    public static void main(String... argv) {
        Logger.getLogger("com.jme").setLevel(Level.FINE);
        CCTest app = new CCTest();
        //AppSettings settings = new AppSettings(true);
        
        app.start();
    }

    @Override
    public void simpleInitApp() {
        setTimer(new EraseTimer());
        //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f);
        viewPort.setBackgroundColor(ColorRGBA.LightGray);
        //rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal()));
        rootNode.addLight(new AmbientLight(ColorRGBA.White));
        //Node probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
        //rootNode.attachChild(probeNode);
        Spatial model = assetManager.loadModel("Models/G8M/g8m.gltf");

        File storageFolder = JmeSystem.getStorageFolder();
        file = new File(storageFolder.getPath() + File.separator + "g8m.j3o");
        BinaryExporter be = new BinaryExporter();
        try {
            be.save(model, file);
        } catch (IOException e) {
            e.printStackTrace();
        }

        assetManager.registerLocator(storageFolder.getPath(), FileLocator.class);
        Spatial model2 = assetManager.loadModel("g8m.j3o");

        //model2.setLocalScale(1f);
        rootNode.attachChild(model2);
        debugAppState = new ArmatureDebugAppState();
        stateManager.attach(debugAppState);

        listAnimations(model2);

        setupModel(model2);

        flyCam.setEnabled(false);

        Node target = new Node("CamTarget");
        //target.setLocalTransform(model.getLocalTransform());
        target.move(0, 0, 0);
        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
        chaseCam.setTarget(target);
        getStateManager().attach(chaseCam);
        chaseCam.setInvertHorizontalAxis(true);
        chaseCam.setInvertVerticalAxis(true);
        chaseCam.setZoomSpeed(0.5f);
        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
        chaseCam.setRotationSpeed(3);
        chaseCam.setDefaultDistance(3);
        chaseCam.setMinDistance(0.01f);
        chaseCam.setZoomSpeed(0.01f);
        chaseCam.setDefaultVerticalRotation(0.3f);

        initInputs();
    }

    public void initInputs() {
        inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN));

        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed) {
                    playAnim = !playAnim;
                    if (playAnim) {
                        String anim = anims.poll();
                        anims.add(anim);
                        composer.setCurrentAction(anim);
                        System.err.println(anim);
                    } else {
                        composer.reset();
                    }
                }
            }
        }, "toggleAnim");
        inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT));
        inputManager.addListener(new ActionListener() {
            @Override
            public void onAction(String name, boolean isPressed, float tpf) {
                if (isPressed && composer != null) {
                    String anim = anims.poll();
                    anims.add(anim);
                    composer.setCurrentAction(anim);
                    System.err.println(anim);
                }
            }
        }, "nextAnim");
    }

    private void setupModel(Spatial model) {
        if (composer != null) {
            return;
        }
        System.out.println("Setting up model");
        composer = model.getControl(AnimComposer.class);
        if (composer != null) {
//            model.getControl(SkinningControl.class).setEnabled(false);
//            model.getControl(MorphControl.class).setEnabled(false);
//            composer.setEnabled(false);
            System.out.println("Got animator");

            SkinningControl sc = model.getControl(SkinningControl.class);
            debugAppState.addArmatureFrom(sc);

            anims.clear();
            for (String name : composer.getAnimClipsNames()) {
                anims.add(name);
                System.out.println("Animation " + name);
            }
            if (anims.isEmpty()) {
                return;
            }
            if (playAnim) {
                String anim = anims.poll();
                anims.add(anim);
                composer.setCurrentAction(anim);
                System.out.println("Playing animation: " + anim);
            }

        } else {
            System.out.println("No animator");
            if (model instanceof Node) {
                Node n = (Node) model;
                for (Spatial child : n.getChildren()) {
                    setupModel(child);
                }
            }
        }
        System.out.println("Animator has: " + String.join(", ", composer.getAnimClipsNames()));

    }

    private void listAnimations(Spatial model) {
        //
        if (model instanceof Geometry) {
            if (((Geometry) model).getMesh().hasMorphTargets()) {
                System.out.println(model.getName() + " has morphs: " + ((Geometry) model).getMesh().getMorphTargets().length);
            }
            
        }
        //
        for (int i = 0; i < model.getNumControls(); i++) {
            System.out.println(model.getName() + " has control: " + model.getControl(i));
        }
        AnimComposer anim = model.getControl(AnimComposer.class);
        if (anim != null) {
            System.out.println(model.getName() + " has animations: " + String.join(", ", anim.getAnimClipsNames()));
        }
        SkinningControl sc = model.getControl(SkinningControl.class);
        if (sc != null) {
            //System.out.println();
        }
        MorphControl mc = model.getControl(MorphControl.class);
        if (mc != null) {
            System.out.println("Morph control found");
        }
        if (model instanceof Node) {
            Node n = (Node) model;
            System.out.println(n.getName() + " of type " + n.getClass().getName());
            
            for (Spatial child : n.getChildren()) {
                listAnimations(child);
            }
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        file.delete();
    }
}
3 Likes

had a lot problems, i still dont know how to import animations, i had like below, but no animation is imported:
vvvvvdddddd2 vvvvvdddddddd vvvvvvdddddd3 vvvvvvdddddd4

but got it for manual change.
(at least for manual morph change)

i was first trying do anything about:
m.setInt(“NumberOfMorphTargets”, 1);

but nothing like this is required, just throw error for me.

what i needed was just:

morphGeometry.setMorphState(weights);

where weights is:
float[] weights = new float[1]; //1 depends on how many morph targets geometry have from Trevor provided code. (i tested with 1 target - not counting base mesh) in my test it is:

    model.depthFirstTraversal(new SceneGraphVisitor() {
        @Override
        public void visit(Spatial spatial) {
            if (spatial instanceof Geometry) {
                if (((Geometry) spatial).getMesh().hasMorphTargets()) {
                    System.out.println("Found morphs: " + ((Geometry) spatial).getMesh().getMorphTargets().length);
                    morphGeometry = (Geometry)spatial;
                    // initialize weights here based on amount of morph targets here like below
                    weights = new float[((Geometry) spatial).getMesh().getMorphTargets().length]; 
                    morphGeometry.setMorphState(weights);
                }
            }
        }
    });

so now i just use:

    inputManager.addMapping("increaseMorph", new KeyTrigger(KeyInput.KEY_LEFT));
    inputManager.addListener(new AnalogListener() {
        @Override
        public void onAnalog(String name, float value, float tpf) {
            if (composer != null) {
                weights[0] += tpf;
                morphGeometry.setMorphState(weights);
                System.out.println(weights[0]);
            }
        }
    }, "increaseMorph");

Still, i dont know how to IMPORT morph animations, but manual change morph was what i was looking anyway (libsync or face/body customization)

1 Like

Hmm… I have not tried to use morph animations, I have only manually changed morphs. I do not have any morph animations in my models. If you figure it out, please let us know.

This model might help you to figure it out how to use shape keys :slightly_smiling_face:

Zophrac by branx on Sketchfab

You can download gltf grom Sketchfab and try it in both JME and Blender.

i tried use this model but i had:

SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.ArrayIndexOutOfBoundsException: 19
	at com.jme3.renderer.opengl.GLRenderer.setVertexAttrib(GLRenderer.java:2755)
	at com.jme3.renderer.opengl.GLRenderer.setVertexAttrib(GLRenderer.java:2808)
	at com.jme3.renderer.opengl.GLRenderer.renderMeshDefault(GLRenderer.java:3040)
	at com.jme3.renderer.opengl.GLRenderer.renderMesh(GLRenderer.java:3077)
	at com.jme3.material.logic.DefaultTechniqueDefLogic.renderMeshFromGeometry(DefaultTechniqueDefLogic.java:70)
	at com.jme3.material.logic.SinglePassAndImageBasedLightingLogic.render(SinglePassAndImageBasedLightingLogic.java:255)
	at com.jme3.material.Technique.render(Technique.java:166)
	at com.jme3.material.Material.render(Material.java:1024)
	at com.jme3.renderer.RenderManager.renderGeometry(RenderManager.java:614)
	at com.jme3.renderer.queue.RenderQueue.renderGeometryList(RenderQueue.java:266)
	at com.jme3.renderer.queue.RenderQueue.renderQueue(RenderQueue.java:305)
	at com.jme3.renderer.RenderManager.renderViewPortQueues(RenderManager.java:877)
	at com.jme3.renderer.RenderManager.flushQueue(RenderManager.java:779)
	at com.jme3.renderer.RenderManager.renderViewPort(RenderManager.java:1108)
	at com.jme3.renderer.RenderManager.render(RenderManager.java:1158)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:270)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:151)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:197)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:232)
	at java.lang.Thread.run(Thread.java:748)

Hmm, I do not get ArrayIndexOutOfBoundsException but an assertion error in MorphTrack.java.

It works for me with a trick.

I tested it from the TestGltfLoading.java which Nehon wrote.

but there is an assertion error in

I commented out that line and it works fine.
Not sure if this an issue with the model itself or a bug in Gltf loader.

Edit:
I opened an issue for this:

1 Like

i found some problems, that might need to fix.

Character has animations: Anim1
Morph control found
FOUND MORPH TARGET:Character_0
FOUND MORPH TARGET:Character_1
FOUND MORPH TARGET:Character_2

each of geoms(trully just different materials), even if its same model, they have same keys.
animation is in closest parent node.

using Blender 2.8 and default .gltf exporter

  • Problem 1 is, that after export animation is visible not via NLA tracks, but only from Dope Sheet.(where i can see only edited current action - this mean one action)

  • Problem 2 is, if i got no animation in Dope Sheet, i cant even use manual shape key change. (it dont change if no MorphControl controller is generated and its only generated if there is animation. (i would suggest generate MorphControl even if there are no shape key animations)

  • Animation problem is that even if its visible(only one animation from Dope Sheet, because it dont get animations from NLA tracks) and i use morphHelper.morphElements.get(“Character_0”).composer.setCurrentAction(“Anim1”);
    it just dont work, where this composer says it have animation in its list… (this one is from Character node)
    not sure why this dont work, i tried a lot things and im still not sure, it could be i use new blender and gltf exporter, and @nehon used older so there might be some changes. Or maybe it was not commited into JME code fully.

Currently can use only manual shape key change in code, but anyway need animation visible to get .gltf importer create MorphControl to even have this working.

For what it worth, I already have made an issue for this

1 Like

afaik Nehon did not use blender Gltf addon in his examples. He was using the Gltf exported directly from Sketchfab.

1 Like

right, this model had gltf download version only i think anyway, so yes its possible it could be about gltf exporter.(did you verify its their issue? - because im not sure where to check this in gltf files)

i tried some gltf exporter options, but none helped.

anyway im glad it work somehow for manual change. animations can be done via code, could be nice to have it working, but imo shape keys usually will be used for manual code change purpose.