Blender's linked Objects

OK, I have made an attempt to add support for list, array nad map to user data.
Please take a look and give me some feedback.

Inside JME you’re right - it won’t be useful. But you never know if any user out there, using the library, will not find a use case for such classes.
But it doesn’t matter now since they won’t be added anyway :wink:

Here is the code of UserData.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;

 * <code>UserData</code> is used to contain user data objects
 * set on spatials (primarily primitives) that do not implement
 * the {@link Savable} interface. Note that attempting
 * to export any models which have non-savable objects
 * attached to them will fail.
public final class UserData implements Savable {

     * Boolean type on Geometries to indicate that physics collision
     * shape generation should ignore them.
    public static final String JME_PHYSICSIGNORE = "JmePhysicsIgnore";

     * For geometries using shared mesh, this will specify the shared
     * mesh reference.
    public static final String JME_SHAREDMESH    = "JmeSharedMesh";

    private static final int   TYPE_INTEGER      = 0;
    private static final int   TYPE_FLOAT        = 1;
    private static final int   TYPE_BOOLEAN      = 2;
    private static final int   TYPE_STRING       = 3;
    private static final int   TYPE_LONG         = 4;
    private static final int   TYPE_SAVABLE      = 5;
    private static final int   TYPE_LIST         = 6;
    private static final int   TYPE_MAP          = 7;
    private static final int   TYPE_ARRAY        = 8;

    protected byte             type;
    protected Object           value;

    public UserData() {

     * Creates a new <code>UserData</code> with the given
     * type and value.
     * @param type
     *            Type of data, should be between 0 and 8.
     * @param value
     *            Value of the data
    public UserData(byte type, Object value) {
        assert type >= 0 && type <= 8;
        this.type = type;
        this.value = value;

    public Object getValue() {
        return value;

    public String toString() {
        return value.toString();

    public static byte getObjectType(Object type) {
        if (type instanceof Integer) {
            return TYPE_INTEGER;
        } else if (type instanceof Float) {
            return TYPE_FLOAT;
        } else if (type instanceof Boolean) {
            return TYPE_BOOLEAN;
        } else if (type instanceof String) {
            return TYPE_STRING;
        } else if (type instanceof Long) {
            return TYPE_LONG;
        } else if (type instanceof Savable) {
            return TYPE_SAVABLE;
        } else if (type instanceof List) {
            return TYPE_LIST;
        } else if (type instanceof Map) {
            return TYPE_MAP;
        } else if (type instanceof Object[]) {
            return TYPE_ARRAY;
        } else {
            throw new IllegalArgumentException("Unsupported type: " + type.getClass().getName());

    public void write(JmeExporter ex) throws IOException {
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(type, "type", (byte) 0);

        switch (type) {
            case TYPE_INTEGER:
                int i = (Integer) value;
                oc.write(i, "intVal", 0);
            case TYPE_FLOAT:
                float f = (Float) value;
                oc.write(f, "floatVal", 0f);
            case TYPE_BOOLEAN:
                boolean b = (Boolean) value;
                oc.write(b, "boolVal", false);
            case TYPE_STRING:
                String s = (String) value;
                oc.write(s, "strVal", null);
            case TYPE_LONG:
                Long l = (Long) value;
                oc.write(l, "longVal", 0l);
            case TYPE_SAVABLE:
                Savable sav = (Savable) value;
                oc.write(sav, "savableVal", null);
            case TYPE_LIST:
                this.writeList(oc, (List<?>) value);
            case TYPE_MAP:
                Map<?, ?> map = (Map<?, ?>) value;
                this.writeList(oc, Arrays.asList(map.keySet()));
                this.writeList(oc, Arrays.asList(map.values()));
            case TYPE_ARRAY:
                this.writeList(oc, Arrays.asList((Object[]) value));
                throw new UnsupportedOperationException("Unsupported value type: " + value.getClass());

    public void read(JmeImporter im) throws IOException {
        InputCapsule ic = im.getCapsule(this);
        type = ic.readByte("type", (byte) 0);

        switch (type) {
            case TYPE_INTEGER:
                value = ic.readInt("intVal", 0);
            case TYPE_FLOAT:
                value = ic.readFloat("floatVal", 0f);
            case TYPE_BOOLEAN:
                value = ic.readBoolean("boolVal", false);
            case TYPE_STRING:
                value = ic.readString("strVal", null);
            case TYPE_LONG:
                value = ic.readLong("longVal", 0l);
            case TYPE_SAVABLE:
                value = ic.readSavable("savableVal", null);
            case TYPE_LIST:
                value = this.readList(ic);
            case TYPE_MAP:
                Map<Object, Object> map = new HashMap<Object, Object>();
                List<?> keys = this.readList(ic);
                List<?> values = this.readList(ic);
                for (int i = 0; i < keys.size(); ++i) {
                    map.put(keys.get(i), values.get(i));
                value = map;
            case TYPE_ARRAY:
                value = this.readList(ic).toArray();
                throw new UnsupportedOperationException("Unknown type of stored data: " + type);

     * The method stores a list in the capsule.
     * @param oc
     *            output capsule
     * @param list
     *            the list to be stored
     * @throws IOException
    private void writeList(OutputCapsule oc, List<?> list) throws IOException {
        if (list != null) {
            oc.write(list.size(), "size", 0);
            int counter = 0;
            for (Object o : list) {
                // t is for 'type'; v is for 'value'
                if (o instanceof Integer) {
                    oc.write(TYPE_INTEGER, "t" + counter, 0);
                    oc.write((Integer) o, "v" + counter, 0);
                } else if (o instanceof Float) {
                    oc.write(TYPE_FLOAT, "t" + counter, 0);
                    oc.write((Float) o, "v" + counter, 0f);
                } else if (o instanceof Boolean) {
                    oc.write(TYPE_BOOLEAN, "t" + counter, 0);
                    oc.write((Boolean) o, "v" + counter, false);
                } else if (o instanceof String || o == null) {// treat null's like Strings just to store them and keep the List like the user intended
                    oc.write(TYPE_STRING, "t" + counter, 0);
                    oc.write((String) o, "v" + counter, null);
                } else if (o instanceof Long) {
                    oc.write(TYPE_LONG, "t" + counter, 0);
                    oc.write((Long) o, "v" + counter, 0L);
                } else if (o instanceof Savable) {
                    oc.write(TYPE_SAVABLE, "t" + counter, 0);
                    oc.write((Long) o, "v" + counter, 0L);
                } else {
                    throw new UnsupportedOperationException("Unsupported type stored in the list: " + o.getClass());
        } else {
            oc.write(0, "size", 0);

     * The method loads a list from the given input capsule.
     * @param ic
     *            the input capsule
     * @return loaded list (an empty list in case its size is 0)
     * @throws IOException
    private List<?> readList(InputCapsule ic) throws IOException {
        int size = ic.readInt("size", 0);
        List<Object> list = new ArrayList<Object>(size);
        for (int i = 0; i < size; ++i) {
            int type = ic.readInt("t" + i, 0);
            switch (type) {
                case TYPE_INTEGER:
                    list.add(ic.readInt("v" + i, 0));
                case TYPE_FLOAT:
                    list.add(ic.readFloat("v" + i, 0));
                case TYPE_BOOLEAN:
                    list.add(ic.readBoolean("v" + i, false));
                case TYPE_STRING:
                    list.add(ic.readString("v" + i, null));
                case TYPE_LONG:
                    list.add(ic.readLong("v" + i, 0L));
                case TYPE_SAVABLE:
                    list.add(ic.readSavable("v" + i, null));
                    throw new UnsupportedOperationException("Unknown type of stored data in a list: " + type);
        return list;

As I see no objections to this code I assume it is OK :wink:

One more thing. When loading linked content blender recognises it by a pair of belnder path and asset name.
Every class that I load has a name property except: Image.
Do you mind if I add it to this class ?

Textures have names… textures are assets. Not sure why image needs a name since it’s not really an asset.

Because in blender you can link an image from another blender file. An this is different than linking a texture.

Not really, is it? Its either a file or an internal buffer which is later to be mapped as a texture for all thats relevant to jME.

OK, so I guess I will import images as textures then and use their internal image only where needed.

Its also better when the user actually uses the extended data. This way they don’t get an in-memory copy of a texture thats right in their assets folder.

Here is a short summary of changes I intend to commit.

  1. Added support for blender loader to load linked content of the following types:
    1.1 object
    1.2 mesh
    1.3 material
    1.4 image (loaded as texture)
    1.5 texture
    1.6 light

  2. Removed the seting from BlenderKey to decide which features to load. By default everything now is loaded because one feature might depend on another. So in such case we need to have them all, especially when unlinked content is being loaded.

  3. Only one loader is now in place: BlenderLoader. The BlenderModelLoader only extends it and is deprecated.

  4. The class ‘LoadingResults’ that was defined in BlenderLoader is now removed. It was returned as BlenderLoader’s result but since the return value is now different, someone will need to alter his/her code in order to adapt to that change. That is the reason to better delete it than keep it as deprecated.

  5. The class UserData (in jme core) was improved. It now can keep Arrays, Lists and Maps. And it allows recurency, co you can hold a Map in a map or in a list. It works as long as the end leafs of such structure are those originally supported by UserData (integer, float, boolean, string) or savable.

  6. The last data types that are not (at least yet) supported in linking are: animation and world data (fog, background color, sky). But I guess those won’t be commonly linked so I can add the support for them when it is neccessary.

  7. This functionality is vulnerable to recurent linking. Unfortunately it will crash the importer. I will try to add support for this as soon as I can.

And there is one last question before I make a commit.
Do you want to have userData in the result spatial that the importer returns ? This keeps the whole references to the loaded features.

The problem with keeping the user data in a aresult spatial is that there is one class (MaterialContext) that is importer’s internal class. I can make it savable but this will require the user to remove the user data manually if he or she intend to use the model without blender importer plugin (which means most cases :wink: ).

And if anyone have any more suggestions please let me know.
If everything is fine - I can commit the changes tomorrow.

Thing is, I don’t see anybody actually saving this data into a J3O file and then loading it later. If anything, you might want to do something with it after loading a .blend file directly, and then discarding it. In the SDK’s case, it makes even less sense, you just want to load the model itself and none of the other stuff matters.

So I am not really opposed if that data refuses to be written into a J3O file (e.g. throws exception on the write() method).

I have just commited the changes. Hope they won’t break anything :wink:

@Darkchaos please check it out and tell me if there are any errors.

There is one known issue as mentioned before: recurrent linking.
I hope to add a fix to that.

1 Like

Hm one questions wtih this.

I use final Spatial model = this.assetManager.loadModel(toCompilerelative); for loading the blender files, and after this iterate over the geometrys to patch them to use dds instead of tga/whatever in my modelToJ3o application.

However the loaded textures now do not have a key anmore, and thus get saved into the j3o as the exporter assumes they are not avialable external (my guess here).
This results in my models to explode in size as they use often shared texture sets or atlases.

Could this be caused by this change?

I have a second strange behaviour with the blender loader;
This file
works when loaded with the SDK (Thanks @BigBob ) (aka jme 3.0) stored as j3o and loaded by my code. It has a animcontroll with CubeAction animation wich lets it move a bit.

When loading with 3.1 from git directly, it says in the logger INFO: Found animation: CubeAction however in the whole scenegraph there is no animcontroll.

I will take a look at this.
I have already seen there was a change made in TextureHelper, but I did not have yet time to take a closer look.

there is a way to move and rotate a linked object?
Because i tried to make a proxy of it (ctrl + alt + p) but when i import the scene i get this error

This archive contains both the file i’m trying to import (map.blend) and the linked one

Hi @RiccardoBlb
As you can see - your pm worked :wink:

I downloaded your model but it imported fine. I did not get any errors.

Can you describe it here, point by point, hod do you prepare your models in both files?
I have to admit that I am quite unfamiliar with the Proxy feature of blender so maybe I did something differently and that is why no error appeared for me.

Hi, thanks for the reply.
This is what I do:

I guess I know the error :stuck_out_tongue:
You are using the PBRisComing branch, try the master instead.
The PBR Branch is always a bit behind until the changes get merged so you could be missing them (On the other hand, there were 14 days inbetween, I guess it would already be merged.)

It’s worth a try though :slight_smile:

But another thing: Proxys don’t really have something to do with the linked objects as they actually copy the vertices to make them editable and such. So it’s no link anymore but rather a copy.

On 3.0 I could use proxies with success, I only had the error of duplicate Nodes somehow, IIRC.
Try that aswell :slight_smile:

The one you see in the video is the master branch, I cloned it just for the video.
What can I use instead of proxies to be able to move the linked object? (with ALT+D i get the same error)

Hey @RiccardoBlb
I tried to reproduce your error copying the steps you showed on the video.
But I did not get the error. My objects loaded fine.

I will download the SDK today and check it out there. Maybe I will find out what went wrong.

On the other hand you could simply link a mesh to the object and then modify the object’s position.

1 Like


Could it be because i am using an old version of blender (2.72b) and maybe the file is saved differently? Wich version do you use?

What do you mean?