浏览代码

Gltf loader can load mesh data and scene structure.

Nehon 8 年之前
父节点
当前提交
7951f5a987

+ 2 - 0
jme3-core/src/main/resources/com/jme3/asset/General.cfg

@@ -23,3 +23,5 @@ LOADER com.jme3.scene.plugins.ogre.SceneLoader : scene
 LOADER com.jme3.scene.plugins.blender.BlenderModelLoader : blend
 LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, geom, tsctrl, tseval, glsl, glsllib
 LOADER com.jme3.scene.plugins.fbx.FbxLoader : fbx
+LOADER com.jme3.scene.plugins.gltf.GltfLoader : gltf
+LOADER com.jme3.scene.plugins.gltf.BinLoader : bin

+ 77 - 0
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package jme3test.model;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.PointLight;
+import com.jme3.math.*;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Sphere;
+
+public class TestGltfLoading extends SimpleApplication {
+
+
+    public static void main(String[] args) {
+        TestGltfLoading app = new TestGltfLoading();
+        app.start();
+    }
+
+    public void simpleInitApp() {
+        flyCam.setMoveSpeed(10f);
+        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
+
+        // sunset light
+//        DirectionalLight dl = new DirectionalLight();
+//        dl.setDirection(new Vector3f(-1f, -1.0f, -1f).normalizeLocal());
+//        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
+//        rootNode.addLight(dl);
+//
+//        DirectionalLight dl2 = new DirectionalLight();
+//        dl2.setDirection(new Vector3f(1f, 1.0f, 1f).normalizeLocal());
+//        dl2.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
+//        rootNode.addLight(dl2);
+
+        PointLight pl = new PointLight(new Vector3f(5.0f, 5.0f, 5.0f), ColorRGBA.White, 30);
+        rootNode.addLight(pl);
+        PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50);
+        rootNode.addLight(pl1);
+
+        //rootNode.attachChild(assetManager.loadModel("Models/gltf/box/box.gltf"));
+        rootNode.attachChild(assetManager.loadModel("Models/gltf/duck/Duck.gltf"));
+
+        //rootNode.attachChild(assetManager.loadModel("Models/gltf/hornet/scene.gltf"));
+    }
+
+
+}

+ 2 - 0
jme3-plugins/build.gradle

@@ -6,6 +6,7 @@ sourceSets {
     main {
         java {
             srcDir 'src/ogre/java'
+            srcDir 'src/gltf/java'
             srcDir 'src/fbx/java'
             srcDir 'src/xml/java'
         }
@@ -14,5 +15,6 @@ sourceSets {
 
 dependencies {
     compile project(':jme3-core')
+    compile 'com.google.code.gson:gson:2.8.1'
     testCompile project(':jme3-desktop')
 }

+ 16 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BinLoader.java

@@ -0,0 +1,16 @@
+package com.jme3.scene.plugins.gltf;
+
+import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetLoader;
+
+import java.io.IOException;
+
+/**
+ * Created by Nehon on 08/08/2017.
+ */
+public class BinLoader implements AssetLoader {
+    @Override
+    public Object load(AssetInfo assetInfo) throws IOException {
+        return assetInfo.openStream();
+    }
+}

+ 399 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -0,0 +1,399 @@
+package com.jme3.scene.plugins.gltf;
+
+import com.google.gson.*;
+import com.google.gson.stream.JsonReader;
+import com.jme3.asset.*;
+import com.jme3.material.Material;
+import com.jme3.math.*;
+import com.jme3.scene.*;
+
+import java.io.*;
+import java.nio.Buffer;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.jme3.scene.plugins.gltf.GltfUtils.*;
+
+/**
+ * GLTF 2.0 loader
+ * Created by Nehon on 07/08/2017.
+ */
+public class GltfLoader implements AssetLoader {
+
+    //Data cache for already parsed JME objects
+    private Map<String, Object[]> dataCache = new HashMap<>();
+    private JsonArray scenes;
+    private JsonArray nodes;
+    private JsonArray meshes;
+    private JsonArray accessors;
+    private JsonArray bufferViews;
+    private JsonArray buffers;
+    private JsonArray materials;
+    private Material defaultMat;
+    private byte[] tmpByteArray;
+    private AssetInfo info;
+
+    @Override
+    public Object load(AssetInfo assetInfo) throws IOException {
+        try {
+            dataCache.clear();
+            info = assetInfo;
+
+            if (defaultMat == null) {
+                defaultMat = new Material(assetInfo.getManager(), "Common/MatDefs/Light/PBRLighting.j3md");
+                defaultMat.setColor("BaseColor", ColorRGBA.White);
+                defaultMat.setFloat("Metallic", 0f);
+                defaultMat.setFloat("Roughness", 1f);
+            }
+
+
+            JsonObject root = new JsonParser().parse(new JsonReader(new InputStreamReader(assetInfo.openStream()))).getAsJsonObject();
+
+            JsonObject asset = root.getAsJsonObject().get("asset").getAsJsonObject();
+            String generator = getAsString(asset, "generator");
+            String version = getAsString(asset, "version");
+            String minVersion = getAsString(asset, "minVersion");
+            if (!isSupported(version, minVersion)) {
+                //TODO maybe just warn. gltf specs claims it will be backward compatible so at worst the user will miss some data.
+                throw new AssetLoadException("Gltf Loader doesn't support this gltf version: " + version + (minVersion != null ? ("/" + minVersion) : ""));
+            }
+
+            scenes = root.getAsJsonArray("scenes");
+            nodes = root.getAsJsonArray("nodes");
+            meshes = root.getAsJsonArray("meshes");
+            accessors = root.getAsJsonArray("accessors");
+            bufferViews = root.getAsJsonArray("bufferViews");
+            buffers = root.getAsJsonArray("buffers");
+            materials = root.getAsJsonArray("materials");
+
+            allocatedTmpByteArray();
+
+            JsonPrimitive defaultScene = root.getAsJsonPrimitive("scene");
+
+            Node n = loadScenes(defaultScene);
+            //only one scene let's not return the root.
+            if (n.getChildren().size() == 1) {
+                n = (Node) n.getChild(0);
+            }
+            //no name for the scene... let's set the file name.
+            if (n.getName() == null) {
+                n.setName(assetInfo.getKey().getName());
+            }
+            return n;
+        } catch (Exception e) {
+            throw new AssetLoadException("An error occurred loading " + assetInfo.getKey().getName(), e);
+        }
+    }
+
+    private void allocatedTmpByteArray() {
+        //Allocate the tmpByteArray to the biggest bufferView
+        if (bufferViews == null) {
+            throw new AssetLoadException("No buffer view defined but one is referenced by an accessor");
+        }
+        int maxLength = 0;
+        for (JsonElement bufferView : bufferViews) {
+            Integer byteLength = getAsInteger(bufferView.getAsJsonObject(), "byteLength");
+            if (byteLength != null && maxLength < byteLength) {
+                maxLength = byteLength;
+            }
+        }
+        tmpByteArray = new byte[maxLength];
+    }
+
+    private boolean isSupported(String version, String minVersion) {
+        return "2.0".equals(version);
+    }
+
+    private Node loadScenes(JsonPrimitive defaultScene) throws IOException {
+        if (scenes == null) {
+            //no scene... lets handle this later...
+            throw new AssetLoadException("Gltf files with no scene is not yet supported");
+        }
+        Node root = new Node();
+        for (JsonElement scene : scenes) {
+            Node sceneNode = new Node();
+            //specs says that only the default scene should be rendered,
+            // if there are several scenes, they are attached to the rootScene, but they are culled
+            sceneNode.setCullHint(Spatial.CullHint.Always);
+
+            sceneNode.setName(getAsString(scene.getAsJsonObject(), "name"));
+            JsonArray sceneNodes = scene.getAsJsonObject().getAsJsonArray("nodes");
+            for (JsonElement node : sceneNodes) {
+                sceneNode.attachChild(loadNode(node.getAsInt()));
+            }
+            root.attachChild(sceneNode);
+        }
+
+        //Setting the default scene cul hint to inherit.
+        int activeChild = 0;
+        if (defaultScene != null) {
+            activeChild = defaultScene.getAsInt();
+        }
+        root.getChild(activeChild).setCullHint(Spatial.CullHint.Inherit);
+        return root;
+    }
+
+    private Spatial loadNode(int nodeIndex) throws IOException {
+        Spatial spatial = fetchFromCache("nodes", nodeIndex, Spatial.class);
+        if (spatial != null) {
+            //If a spatial is referenced several times, it may be attached to different parents,
+            // and it's not possible in JME, so we have to clone it.
+            return spatial.clone();
+        }
+        JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject();
+        Integer meshIndex = getAsInteger(nodeData, "mesh");
+        if (meshIndex != null) {
+            if (meshes == null) {
+                throw new AssetLoadException("Can't find any mesh data, yet a node references a mesh");
+            }
+
+            //TODO material
+            Material mat = defaultMat;
+
+            //there is a mesh in this node, however gltf can split meshes in primitives (some kind of sub meshes),
+            //We don't have this in JME so we have to make one mesh and one Geometry for each primitive.
+            Mesh[] primitives = loadMeshPrimitives(meshIndex);
+            if (primitives.length > 1) {
+                //only one mesh, lets just make a geometry.
+                Geometry geometry = new Geometry(null, primitives[0]);
+                geometry.setMaterial(mat);
+                geometry.updateModelBound();
+                spatial = geometry;
+            } else {
+                //several meshes, let's make a parent Node and attach several geometries to it
+                Node node = new Node();
+                for (Mesh primitive : primitives) {
+                    Geometry geom = new Geometry(null, primitive);
+                    geom.setMaterial(mat);
+                    geom.updateModelBound();
+                    node.attachChild(geom);
+                }
+                spatial = node;
+            }
+
+
+            spatial.setName(loadMeshName(meshIndex));
+
+        } else {
+            //no mesh, we have a node. Can be a camera node or a regular node.
+            //TODO handle camera nodes?
+            Node node = new Node();
+            JsonArray children = nodeData.getAsJsonArray("children");
+            if (children != null) {
+                for (JsonElement child : children) {
+                    node.attachChild(loadNode(child.getAsInt()));
+                }
+            }
+            spatial = node;
+        }
+        if (spatial.getName() == null) {
+            spatial.setName(getAsString(nodeData.getAsJsonObject(), "name"));
+        }
+        spatial.setLocalTransform(loadTransforms(nodeData));
+
+        addToCache("nodes", nodeIndex, spatial, nodes.size());
+        return spatial;
+    }
+
+    private Transform loadTransforms(JsonObject nodeData) {
+        Transform transform = new Transform();
+        JsonArray matrix = nodeData.getAsJsonArray("matrix");
+        if (matrix != null) {
+            //transforms are given as a mat4
+            float[] tmpArray = new float[16];
+            for (int i = 0; i < tmpArray.length; i++) {
+                tmpArray[i] = matrix.get(i).getAsFloat();
+            }
+            Matrix4f mat = new Matrix4f(tmpArray);
+            transform.fromTransformMatrix(mat);
+            return transform;
+        }
+        //no matrix transforms: no transforms or transforms givens as translation/rotation/scale
+        JsonArray translation = nodeData.getAsJsonArray("translation");
+        if (translation != null) {
+            transform.setTranslation(
+                    translation.get(0).getAsFloat(),
+                    translation.get(1).getAsFloat(),
+                    translation.get(2).getAsFloat());
+        }
+        JsonArray rotation = nodeData.getAsJsonArray("rotation");
+        if (rotation != null) {
+            transform.setRotation(new Quaternion(
+                    rotation.get(0).getAsFloat(),
+                    rotation.get(1).getAsFloat(),
+                    rotation.get(2).getAsFloat(),
+                    rotation.get(3).getAsFloat()));
+        }
+        JsonArray scale = nodeData.getAsJsonArray("scale");
+        if (scale != null) {
+            transform.setScale(
+                    scale.get(0).getAsFloat(),
+                    scale.get(1).getAsFloat(),
+                    scale.get(2).getAsFloat());
+        }
+
+        return transform;
+    }
+
+    private Mesh[] loadMeshPrimitives(int meshIndex) throws IOException {
+        Mesh[] meshArray = (Mesh[]) fetchFromCache("meshes", meshIndex, Object.class);
+        if (meshArray != null) {
+            return meshArray;
+        }
+        JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
+        JsonArray primitives = meshData.getAsJsonArray("primitives");
+        if (primitives == null) {
+            throw new AssetLoadException("Can't find any primitives in mesh " + meshIndex);
+        }
+
+        meshArray = new Mesh[primitives.size()];
+        int index = 0;
+        for (JsonElement primitive : primitives) {
+            JsonObject meshObject = primitive.getAsJsonObject();
+            Mesh mesh = new Mesh();
+            Integer mode = getAsInteger(meshObject, "mode");
+            mesh.setMode(getMeshMode(mode));
+            Integer indices = getAsInteger(meshObject, "indices");
+            if (indices != null) {
+                mesh.setBuffer(loadVertexBuffer(indices, VertexBuffer.Type.Index));
+
+            }
+            JsonObject attributes = meshObject.getAsJsonObject("attributes");
+            assertNotNull(attributes, "No attributes defined for mesh " + mesh);
+            for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
+                mesh.setBuffer(loadVertexBuffer(entry.getValue().getAsInt(), getVertexBufferType(entry.getKey())));
+            }
+            meshArray[index] = mesh;
+            index++;
+
+            //TODO material, targets(morph anim...)
+        }
+
+        addToCache("meshes", meshIndex, meshArray, meshes.size());
+        return meshArray;
+    }
+
+    private VertexBuffer loadVertexBuffer(int accessorIndex, VertexBuffer.Type bufferType) throws IOException {
+
+        if (accessors == null) {
+            throw new AssetLoadException("No accessor attribute in the gltf file");
+        }
+        JsonObject accessor = accessors.get(accessorIndex).getAsJsonObject();
+        Integer bufferViewIndex = getAsInteger(accessor, "bufferView");
+        int byteOffset = getAsInteger(accessor, "byteOffset", 0);
+        Integer componentType = getAsInteger(accessor, "componentType");
+        assertNotNull(componentType, "No component type defined for accessor " + accessorIndex);
+        boolean normalized = getAsBoolean(accessor, "normalized", false);
+        Integer count = getAsInteger(accessor, "count");
+        assertNotNull(count, "No count attribute defined for accessor " + accessorIndex);
+        String type = getAsString(accessor, "type");
+        assertNotNull(type, "No type attribute defined for accessor " + accessorIndex);
+
+        VertexBuffer vb = new VertexBuffer(bufferType);
+        VertexBuffer.Format format = getVertexBufferFormat(componentType);
+        int numComponents = getNumberOfComponents(type);
+
+        Buffer buff = VertexBuffer.createBuffer(format, numComponents, count);
+        readBuffer(bufferViewIndex, byteOffset, numComponents * count, buff, numComponents);
+        if (bufferType == VertexBuffer.Type.Index) {
+            numComponents = 3;
+        }
+        vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, buff);
+
+        //TODO min / max
+        //TODO sparse
+        //TODO extensions?
+        //TODO extras?
+        return vb;
+    }
+
+    private void readBuffer(Integer bufferViewIndex, int byteOffset, int bufferSize, Buffer buff, int numComponents) throws IOException {
+        if (bufferViewIndex == null) {
+            //no referenced buffer, specs says to pad the buffer with zeros.
+            padBuffer(buff, bufferSize);
+            return;
+        }
+
+        JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject();
+        Integer bufferIndex = getAsInteger(bufferView, "buffer");
+        assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex);
+        int bvByteOffset = getAsInteger(bufferView, "byteOffset", 0);
+        Integer byteLength = getAsInteger(bufferView, "byteLength");
+        assertNotNull(byteLength, "No byte length defined for bufferView " + bufferViewIndex);
+        int byteStride = getAsInteger(bufferView, "byteStride", 0);
+
+        //target defines ELEMENT_ARRAY_BUFFER or ARRAY_BUFFER, but we already know that since we know we load the indexbuffer or any other...
+        //not sure it's useful for us, but I guess it's useful when you map data directly to the GPU.
+        //int target = getAsInteger(bufferView, "target", 0);
+
+        byte[] data = readData(bufferIndex);
+        populateBuffer(buff, data, bufferSize, byteOffset + bvByteOffset, byteStride, numComponents);
+
+        //TODO extensions?
+        //TODO extras?
+
+    }
+
+    private byte[] readData(int bufferIndex) throws IOException {
+
+        if (buffers == null) {
+            throw new AssetLoadException("No buffer defined");
+        }
+        JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject();
+        String uri = getAsString(buffer, "uri");
+        Integer bufferLength = getAsInteger(buffer, "byteLength");
+        assertNotNull(bufferLength, "No byteLength defined for buffer " + bufferIndex);
+        if (uri != null) {
+            if (uri.startsWith("data:")) {
+                //inlined base64 data
+                //data:<mimeType>;base64,<base64 data>
+                //TODO handle inlined base64
+                throw new AssetLoadException("Inlined base64 data is not supported yet");
+            } else {
+                //external file let's load it
+                if (!uri.endsWith(".bin")) {
+                    throw new AssetLoadException("Cannot load " + uri + ", a .bin extension is required.");
+                }
+                byte[] data = (byte[]) fetchFromCache("buffers", bufferIndex, Object.class);
+                if (data != null) {
+                    return data;
+                }
+                InputStream input = (InputStream) info.getManager().loadAsset(info.getKey().getFolder() + uri);
+                data = new byte[bufferLength];
+                input.read(data);
+                addToCache("buffers", bufferIndex, data, buffers.size());
+
+                return data;
+            }
+        } else {
+            //no URI we are in a binary file so the data is in the 2nd chunk
+            //TODO handle binary GLTF (GLB)
+            throw new AssetLoadException("Binary gltf is not supported yet");
+        }
+
+    }
+
+    private String loadMeshName(int meshIndex) {
+        JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
+        return getAsString(meshData, "name");
+    }
+
+    private <T> T fetchFromCache(String name, int index, Class<T> type) {
+        Object[] data = dataCache.get(name);
+        if (data == null) {
+            return null;
+        }
+        return type.cast(data[index]);
+    }
+
+    private void addToCache(String name, int index, Object object, int maxLength) {
+        Object[] data = dataCache.get(name);
+        if (data == null) {
+            data = new Object[maxLength];
+            dataCache.put(name, data);
+        }
+        data[index] = object;
+    }
+
+}
+

+ 285 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java

@@ -0,0 +1,285 @@
+package com.jme3.scene.plugins.gltf;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.jme3.asset.AssetLoadException;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.util.LittleEndien;
+
+import java.io.*;
+import java.nio.*;
+
+/**
+ * Created by Nehon on 07/08/2017.
+ */
+public class GltfUtils {
+
+    public static Mesh.Mode getMeshMode(Integer mode) {
+        if (mode == null) {
+            return Mesh.Mode.Triangles;
+        }
+        //too bad, we could have returned the enum value from the ordinal
+        //but LineLoop and LineStrip are inverted in the Mesh.Mode Enum declaration.
+        switch (mode) {
+            case 0:
+                return Mesh.Mode.Points;
+            case 1:
+                return Mesh.Mode.Lines;
+            case 2:
+                return Mesh.Mode.LineLoop;
+            case 3:
+                return Mesh.Mode.LineStrip;
+            case 4:
+                return Mesh.Mode.Triangles;
+            case 5:
+                return Mesh.Mode.TriangleStrip;
+            case 6:
+                return Mesh.Mode.TriangleFan;
+        }
+        return Mesh.Mode.Triangles;
+    }
+
+    public static VertexBuffer.Format getVertexBufferFormat(int componentType) {
+        switch (componentType) {
+            case 5120:
+                return VertexBuffer.Format.Byte;
+            case 5121:
+                return VertexBuffer.Format.UnsignedByte;
+            case 5122:
+                return VertexBuffer.Format.Short;
+            case 5123:
+                return VertexBuffer.Format.UnsignedShort;
+            case 5125:
+                return VertexBuffer.Format.UnsignedInt;
+            case 5126:
+                return VertexBuffer.Format.Float;
+            default:
+                throw new AssetLoadException("Illegal component type: " + componentType);
+        }
+    }
+
+    public static int getNumberOfComponents(String type) {
+        switch (type) {
+            case "SCALAR":
+                return 1;
+            case "VEC2":
+                return 2;
+            case "VEC3":
+                return 3;
+            case "VEC4":
+                return 4;
+            case "MAT2":
+                return 4;
+            case "MAT3":
+                return 9;
+            case "MAT4":
+                return 16;
+            default:
+                throw new AssetLoadException("Illegal type: " + type);
+        }
+    }
+
+    public static VertexBuffer.Type getVertexBufferType(String attribute) {
+        switch (attribute) {
+            case "POSITION":
+                return VertexBuffer.Type.Position;
+            case "NORMAL":
+                return VertexBuffer.Type.Normal;
+            case "TANGENT":
+                return VertexBuffer.Type.Tangent;
+            case "TEXCOORD_0":
+                return VertexBuffer.Type.TexCoord;
+            case "TEXCOORD_1":
+                return VertexBuffer.Type.TexCoord2;
+            case "TEXCOORD_2":
+                return VertexBuffer.Type.TexCoord3;
+            case "TEXCOORD_3":
+                return VertexBuffer.Type.TexCoord4;
+            case "TEXCOORD_4":
+                return VertexBuffer.Type.TexCoord5;
+            case "TEXCOORD_5":
+                return VertexBuffer.Type.TexCoord6;
+            case "TEXCOORD_6":
+                return VertexBuffer.Type.TexCoord7;
+            case "TEXCOORD_7":
+                return VertexBuffer.Type.TexCoord8;
+            case "COLOR_0":
+                return VertexBuffer.Type.Color;
+            case "JOINTS_0":
+                return VertexBuffer.Type.BoneIndex;
+            case "WEIGHT_0":
+                return VertexBuffer.Type.BoneWeight;
+            default:
+                throw new AssetLoadException("Unsupported buffer attribute: " + attribute);
+
+        }
+    }
+
+    public static void padBuffer(Buffer buffer, int bufferSize) {
+        buffer.clear();
+        if (buffer instanceof IntBuffer) {
+            IntBuffer ib = (IntBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                ib.put(0);
+            }
+        } else if (buffer instanceof FloatBuffer) {
+            FloatBuffer fb = (FloatBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                fb.put(0);
+            }
+        } else if (buffer instanceof ShortBuffer) {
+            ShortBuffer sb = (ShortBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                sb.put((short) 0);
+            }
+        } else if (buffer instanceof ByteBuffer) {
+            ByteBuffer bb = (ByteBuffer) buffer;
+            for (int i = 0; i < bufferSize; i++) {
+                bb.put((byte) 0);
+            }
+        }
+        buffer.rewind();
+    }
+
+    public static void populateBuffer(Buffer buffer, byte[] source, int length, int byteOffset, int byteStride, int numComponents) throws IOException {
+        buffer.clear();
+
+        if (buffer instanceof ByteBuffer) {
+            populateByteBuffer((ByteBuffer) buffer, source, length, byteOffset, byteStride, numComponents);
+            return;
+        }
+        LittleEndien stream = getStream(source);
+        if (buffer instanceof ShortBuffer) {
+            populateShortBuffer((ShortBuffer) buffer, stream, length, byteOffset, byteStride, numComponents);
+        } else if (buffer instanceof IntBuffer) {
+            populateIntBuffer((IntBuffer) buffer, stream, length, byteOffset, byteStride, numComponents);
+        } else if (buffer instanceof FloatBuffer) {
+            populateFloatBuffer((FloatBuffer) buffer, stream, length, byteOffset, byteStride, numComponents);
+        }
+        buffer.rewind();
+    }
+
+    private static void populateByteBuffer(ByteBuffer buffer, byte[] source, int length, int byteOffset, int byteStride, int numComponents) {
+        int index = byteOffset;
+        int componentSize = 1;
+        while (index < length + byteOffset) {
+            for (int i = 0; i < numComponents; i++) {
+                buffer.put(source[index + i]);
+            }
+            index += Math.max(componentSize * numComponents, byteStride);
+        }
+    }
+
+    private static void populateShortBuffer(ShortBuffer buffer, LittleEndien stream, int length, int byteOffset, int byteStride, int numComponents) throws IOException {
+        int index = byteOffset;
+        int componentSize = 2;
+        int end = length * componentSize + byteOffset;
+        stream.skipBytes(byteOffset);
+        while (index < end) {
+            for (int i = 0; i < numComponents; i++) {
+                buffer.put(stream.readShort());
+            }
+            index += Math.max(componentSize * numComponents, byteStride);
+        }
+    }
+
+    private static void populateIntBuffer(IntBuffer buffer, LittleEndien stream, int length, int byteOffset, int byteStride, int numComponents) throws IOException {
+        int index = byteOffset;
+        int componentSize = 4;
+        int end = length * componentSize + byteOffset;
+        stream.skipBytes(byteOffset);
+        while (index < end) {
+            for (int i = 0; i < numComponents; i++) {
+                buffer.put(stream.readInt());
+            }
+            index += Math.max(componentSize * numComponents, byteStride);
+        }
+    }
+
+    private static void populateFloatBuffer(FloatBuffer buffer, LittleEndien stream, int length, int byteOffset, int byteStride, int numComponents) throws IOException {
+        int index = byteOffset;
+        int componentSize = 4;
+        int end = length * componentSize + byteOffset;
+        stream.skipBytes(byteOffset);
+        while (index < end) {
+            for (int i = 0; i < numComponents; i++) {
+                buffer.put(stream.readFloat());
+            }
+            index += Math.max(componentSize * numComponents, byteStride);
+        }
+    }
+
+    private static LittleEndien getStream(byte[] buffer) {
+        return new LittleEndien(new DataInputStream(new ByteArrayInputStream(buffer)));
+    }
+
+    public static String getAsString(JsonObject parent, String name) {
+        JsonElement el = parent.get(name);
+        return el == null ? null : el.getAsString();
+    }
+
+    public static Integer getAsInteger(JsonObject parent, String name) {
+        JsonElement el = parent.get(name);
+        return el == null ? null : el.getAsInt();
+    }
+
+    public static Integer getAsInteger(JsonObject parent, String name, int defaultValue) {
+        JsonElement el = parent.get(name);
+        return el == null ? defaultValue : el.getAsInt();
+    }
+
+    public static Boolean getAsBoolean(JsonObject parent, String name) {
+        JsonElement el = parent.get(name);
+        return el == null ? null : el.getAsBoolean();
+    }
+
+    public static Boolean getAsBoolean(JsonObject parent, String name, boolean defaultValue) {
+        JsonElement el = parent.get(name);
+        return el == null ? defaultValue : el.getAsBoolean();
+    }
+
+    public static void assertNotNull(Object o, String errorMessage) {
+        if (o == null) {
+            throw new AssetLoadException(errorMessage);
+        }
+    }
+
+    public static void dumpMesh(Mesh m) {
+        for (VertexBuffer vertexBuffer : m.getBufferList().getArray()) {
+            System.err.println(vertexBuffer.getBufferType());
+            System.err.println(vertexBuffer.getFormat());
+            if (vertexBuffer.getData() instanceof FloatBuffer) {
+                FloatBuffer b = (FloatBuffer) vertexBuffer.getData();
+                float[] arr = new float[b.capacity()];
+                b.rewind();
+                b.get(arr);
+                b.rewind();
+                for (float v : arr) {
+                    System.err.print(v + ",");
+                }
+            }
+            if (vertexBuffer.getData() instanceof ShortBuffer) {
+                ShortBuffer b = (ShortBuffer) vertexBuffer.getData();
+                short[] arr = new short[b.capacity()];
+                b.rewind();
+                b.get(arr);
+                b.rewind();
+                for (short v : arr) {
+                    System.err.print(v + ",");
+                }
+            }
+            if (vertexBuffer.getData() instanceof IntBuffer) {
+                IntBuffer b = (IntBuffer) vertexBuffer.getData();
+                int[] arr = new int[b.capacity()];
+                b.rewind();
+                b.get(arr);
+                b.rewind();
+                for (int v : arr) {
+                    System.err.print(v + ",");
+                }
+            }
+            System.err.println("\n---------------------------");
+        }
+    }
+}

+ 50 - 0
jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java

@@ -0,0 +1,50 @@
+package com.jme3.scene.plugins.gltf;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.material.plugin.TestMaterialWrite;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.system.JmeSystem;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Created by Nehon on 07/08/2017.
+ */
+public class GltfLoaderTest {
+
+    private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+
+    private AssetManager assetManager;
+
+    @Before
+    public void init() {
+        assetManager = JmeSystem.newAssetManager(
+                TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg"));
+
+    }
+
+    @Test
+    public void testLoad() {
+        Spatial scene = assetManager.loadModel("gltf/box/box.gltf");
+        dumpScene(scene, 0);
+//        scene = assetManager.loadModel("gltf/hornet/scene.gltf");
+//        dumpScene(scene, 0);
+    }
+
+
+    private void dumpScene(Spatial s, int indent) {
+        System.err.println(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " +
+                s.getLocalTransform().getTranslation().toString() + ", " +
+                s.getLocalTransform().getRotation().toString() + ", " +
+                s.getLocalTransform().getScale().toString());
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                dumpScene(spatial, indent + 1);
+            }
+        }
+    }
+}

二进制
jme3-plugins/src/test/resources/gltf/box/Box0.bin


+ 142 - 0
jme3-plugins/src/test/resources/gltf/box/box.gltf

@@ -0,0 +1,142 @@
+{
+  "asset": {
+    "generator": "COLLADA2GLTF",
+    "version": "2.0"
+  },
+  "scene": 0,
+  "scenes": [
+    {
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "nodes": [
+    {
+      "children": [
+        1
+      ],
+      "matrix": [
+        1.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        -1.0,
+        0.0,
+        0.0,
+        1.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        1.0
+      ]
+    },
+    {
+      "mesh": 0
+    }
+  ],
+  "meshes": [
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 2
+          },
+          "indices": 0,
+          "mode": 4,
+          "material": 0
+        }
+      ],
+      "name": "Mesh"
+    }
+  ],
+  "accessors": [
+    {
+      "bufferView": 0,
+      "byteOffset": 0,
+      "componentType": 5123,
+      "count": 36,
+      "max": [
+        23
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 0,
+      "componentType": 5126,
+      "count": 24,
+      "max": [
+        1.0,
+        1.0,
+        1.0
+      ],
+      "min": [
+        -1.0,
+        -1.0,
+        -1.0
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 288,
+      "componentType": 5126,
+      "count": 24,
+      "max": [
+        0.5,
+        0.5,
+        0.5
+      ],
+      "min": [
+        -0.5,
+        -0.5,
+        -0.5
+      ],
+      "type": "VEC3"
+    }
+  ],
+  "materials": [
+    {
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.800000011920929,
+          0.0,
+          0.0,
+          1.0
+        ],
+        "metallicFactor": 0.0
+      },
+      "name": "Red"
+    }
+  ],
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteOffset": 576,
+      "byteLength": 72,
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteOffset": 0,
+      "byteLength": 576,
+      "byteStride": 12,
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 648,
+      "uri": "Box0.bin"
+    }
+  ]
+}

二进制
jme3-testdata/src/main/resources/Models/gltf/box/Box0.bin


+ 142 - 0
jme3-testdata/src/main/resources/Models/gltf/box/box.gltf

@@ -0,0 +1,142 @@
+{
+  "asset": {
+    "generator": "COLLADA2GLTF",
+    "version": "2.0"
+  },
+  "scene": 0,
+  "scenes": [
+    {
+      "nodes": [
+        0
+      ]
+    }
+  ],
+  "nodes": [
+    {
+      "children": [
+        1
+      ],
+      "matrix": [
+        1.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        -1.0,
+        0.0,
+        0.0,
+        1.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        0.0,
+        1.0
+      ]
+    },
+    {
+      "mesh": 0
+    }
+  ],
+  "meshes": [
+    {
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 2
+          },
+          "indices": 0,
+          "mode": 4,
+          "material": 0
+        }
+      ],
+      "name": "Mesh"
+    }
+  ],
+  "accessors": [
+    {
+      "bufferView": 0,
+      "byteOffset": 0,
+      "componentType": 5123,
+      "count": 36,
+      "max": [
+        23
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 0,
+      "componentType": 5126,
+      "count": 24,
+      "max": [
+        1.0,
+        1.0,
+        1.0
+      ],
+      "min": [
+        -1.0,
+        -1.0,
+        -1.0
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 288,
+      "componentType": 5126,
+      "count": 24,
+      "max": [
+        0.5,
+        0.5,
+        0.5
+      ],
+      "min": [
+        -0.5,
+        -0.5,
+        -0.5
+      ],
+      "type": "VEC3"
+    }
+  ],
+  "materials": [
+    {
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.800000011920929,
+          0.0,
+          0.0,
+          1.0
+        ],
+        "metallicFactor": 0.0
+      },
+      "name": "Red"
+    }
+  ],
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteOffset": 576,
+      "byteLength": 72,
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteOffset": 0,
+      "byteLength": 576,
+      "byteStride": 12,
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 648,
+      "uri": "Box0.bin"
+    }
+  ]
+}

+ 219 - 0
jme3-testdata/src/main/resources/Models/gltf/duck/Duck.gltf

@@ -0,0 +1,219 @@
+{
+    "asset": {
+        "generator": "COLLADA2GLTF",
+        "version": "2.0"
+    },
+    "scene": 0,
+    "scenes": [
+        {
+            "nodes": [
+                0
+            ]
+        }
+    ],
+    "nodes": [
+        {
+            "children": [
+                2,
+                1
+            ],
+            "matrix": [
+                0.009999999776482582,
+                0.0,
+                0.0,
+                0.0,
+                0.0,
+                0.009999999776482582,
+                0.0,
+                0.0,
+                0.0,
+                0.0,
+                0.009999999776482582,
+                0.0,
+                0.0,
+                0.0,
+                0.0,
+                1.0
+            ]
+        },
+        {
+            "matrix": [
+                -0.7289686799049377,
+                0.0,
+                -0.6845470666885376,
+                0.0,
+                -0.4252049028873444,
+                0.7836934328079224,
+                0.4527972936630249,
+                0.0,
+                0.5364750623703003,
+                0.6211478114128113,
+                -0.571287989616394,
+                0.0,
+                400.1130065917969,
+                463.2640075683594,
+                -431.0780334472656,
+                1.0
+            ],
+            "camera": 0
+        },
+        {
+            "mesh": 0
+        }
+    ],
+    "cameras": [
+        {
+            "perspective": {
+                "aspectRatio": 1.5,
+                "yfov": 0.6605925559997559,
+                "zfar": 10000.0,
+                "znear": 1.0
+            },
+            "type": "perspective"
+        }
+    ],
+    "meshes": [
+        {
+            "primitives": [
+                {
+                    "attributes": {
+                        "NORMAL": 1,
+                        "POSITION": 2,
+                        "TEXCOORD_0": 3
+                    },
+                    "indices": 0,
+                    "mode": 4,
+                    "material": 0
+                }
+            ],
+            "name": "LOD3spShape"
+        }
+    ],
+    "accessors": [
+        {
+            "bufferView": 0,
+            "byteOffset": 0,
+            "componentType": 5123,
+            "count": 12636,
+            "max": [
+                2398
+            ],
+            "min": [
+                0
+            ],
+            "type": "SCALAR"
+        },
+        {
+            "bufferView": 1,
+            "byteOffset": 0,
+            "componentType": 5126,
+            "count": 2399,
+            "max": [
+                0.9995989799499512,
+                0.999580979347229,
+                0.9984359741210938
+            ],
+            "min": [
+                -0.9990839958190918,
+                -1.0,
+                -0.9998319745063782
+            ],
+            "type": "VEC3"
+        },
+        {
+            "bufferView": 1,
+            "byteOffset": 28788,
+            "componentType": 5126,
+            "count": 2399,
+            "max": [
+                96.17990112304688,
+                163.97000122070313,
+                53.92519760131836
+            ],
+            "min": [
+                -69.29850006103516,
+                9.929369926452637,
+                -61.32819747924805
+            ],
+            "type": "VEC3"
+        },
+        {
+            "bufferView": 2,
+            "byteOffset": 0,
+            "componentType": 5126,
+            "count": 2399,
+            "max": [
+                0.9833459854125976,
+                0.9800369739532472
+            ],
+            "min": [
+                0.026409000158309938,
+                0.01996302604675293
+            ],
+            "type": "VEC2"
+        }
+    ],
+    "materials": [
+        {
+            "pbrMetallicRoughness": {
+                "baseColorTexture": {
+                    "index": 0
+                },
+                "metallicFactor": 0.0
+            },
+            "emissiveFactor": [
+                0.0,
+                0.0,
+                0.0
+            ],
+            "name": "blinn3-fx"
+        }
+    ],
+    "textures": [
+        {
+            "sampler": 0,
+            "source": 0
+        }
+    ],
+    "images": [
+        {
+            "uri": "DuckCM.png"
+        }
+    ],
+    "samplers": [
+        {
+            "magFilter": 9729,
+            "minFilter": 9986,
+            "wrapS": 10497,
+            "wrapT": 10497
+        }
+    ],
+    "bufferViews": [
+        {
+            "buffer": 0,
+            "byteOffset": 76768,
+            "byteLength": 25272,
+            "target": 34963
+        },
+        {
+            "buffer": 0,
+            "byteOffset": 0,
+            "byteLength": 57576,
+            "byteStride": 12,
+            "target": 34962
+        },
+        {
+            "buffer": 0,
+            "byteOffset": 57576,
+            "byteLength": 19192,
+            "byteStride": 8,
+            "target": 34962
+        }
+    ],
+    "buffers": [
+        {
+            "byteLength": 102040,
+            "uri": "Duck0.bin"
+        }
+    ]
+}

二进制
jme3-testdata/src/main/resources/Models/gltf/duck/Duck0.bin


二进制
jme3-testdata/src/main/resources/Models/gltf/duck/DuckCM.png