[SOLVED] Import FBX

Hello,
So I am very ignorant on this topic, I am not a 3d modeler. Disclaimer done.

When prototyping, I like to use Daz models (mostly for the sake that they look good and I have access to a large daz library). Those will later get swapped out for custom models for the project.

But right now I am playing with JME, and decided to see if I could get a genesis character working. I exported it as FBX, re-rigged and redid the materials in blender, then re-exported it back out as a FBX.

I have never used FBX with JME and as I have been reading it sounds like there is some support, but it is not documented as to what support there is (at least that I could find).

So I imported it in to JME and I get this wonderful error when opening it:
EDIT: To clarify opening it, when I double click it

Running blender as converter for file C:/Users/Trevor/Desktop/test_projects/CharacterCreatorTest/assets/Models/G8M/jme.fbx
found bundled python: C:\Program Files\jmonkeyplatform\blender\2.79\python
Info: Total files 7 | Changed 7 | Failed 0
FBX Import: start importing C:\Users\Trevor\Desktop\test_projects\CharacterCreatorTest\assets\Models\G8M\jme.fbx
FBX version: 7400
	FBX import: Prepare...
		Done (0.000000 sec)
	FBX import: Templates...
		Done (0.000000 sec)
	FBX import: Nodes...
		Done (0.000000 sec)
	FBX import: Connections...
		Done (0.015625 sec)
	FBX import: Meshes...
		Done (0.562500 sec)
	FBX import: Materials & Textures...
		Done (0.015625 sec)
	FBX import: Cameras & Lamps...
		Done (0.000000 sec)
	FBX import: Objects & Armatures...
		Done (0.093750 sec)
	FBX import: ShapeKeys...
		Done (0.375000 sec)
	FBX import: Animations...
		Done (0.000000 sec)
	FBX import: Assign materials...
		Done (0.000000 sec)
	FBX import: Assign textures...
WARNING: material link b'TransparencyFactor' ignored
		Done (0.000000 sec)
	FBX import: Cycles z-offset workaround...
		Done (0.000000 sec)
	Done (1.328125 sec)
Import finished.
batch job finished, exiting
Blender quit
AL lib: (EE) UpdateDeviceParams: Failed to set 44100hz, got 48000hz instead
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Unknown block header: KE00
Loading animations that will be later applied to scene features.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.006
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.004
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8MaleEyelashes.001
	Unable to find two closest vertices while triangulating face.
There were problems with triangulating the faces of a mesh: Genesis8Male.005
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8MaleEyelashes
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.002
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.003
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.007
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
There were problems with triangulating the faces of a mesh: Genesis8Male.001
	Unable to find two closest vertices while triangulating face.
There were problems with triangulating the faces of a mesh: Genesis8Male
	Unable to find two closest vertices while triangulating face.
Loading model's textures.
Loaded asset jme

I would like to bring the shape keys (morphs) in if possible. I have read in a couple old topics here that JME does not support shape keys, is that still true?

Anyways, I would love some help or guidance here.
Thank you,
Trevor

EDIT 2: So I discovered if I just use the blend file, I can open and convert to a j3o no issue.
SO I guess my question is, how can I get/access the shape keys?

jME does not directly support shape keys. However, I don’t think it would be that difficult to develop that yourself (depending on how shape keys are represented - never worked with that so I don’t know).

If by shape key OP means morph animation (vertex animation) then
It does with new gltf importer @nehon wrote. You should use jme master branch for it.
I have not tested it my self yet but you can take a look here

and

Regarding FBX, there is two FBX importer for JME, the old one not support animation at all, and new one developed by @Eirenliel does support animation but there is problem if you export it from blender. Take a look here

2 Likes

I stand corrected then… I hadn’t heard that the gLTF pipeline supports morph.

That is awesome. Thank you for the quick replay. I will look into the gltf importer. It looks like it is exactly what I need. I did not see any documentation for how to use it though. Do you know where that might be, or should I send a message over to @nehon? I found the test gltf project but it is from two years ago and it no up-to-date with these new changes as far as I can tell.

EDIT: I will look through, I might see how to use it already.

You can find examples here

1 Like

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.