diff --git a/jme3-core/src/main/resources/com/jme3/asset/General.cfg b/jme3-core/src/main/resources/com/jme3/asset/General.cfg index 66ed68e49..016dcccf2 100644 --- a/jme3-core/src/main/resources/com/jme3/asset/General.cfg +++ b/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 diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java new file mode 100644 index 000000000..5ad4afae3 --- /dev/null +++ b/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")); + } + + +} diff --git a/jme3-plugins/build.gradle b/jme3-plugins/build.gradle index b26434cdd..0c2280b31 100644 --- a/jme3-plugins/build.gradle +++ b/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') } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BinLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/BinLoader.java new file mode 100644 index 000000000..b4b7fbf4d --- /dev/null +++ b/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(); + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java new file mode 100644 index 000000000..99473a8f1 --- /dev/null +++ b/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 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 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:;base64, + //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 fetchFromCache(String name, int index, Class 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; + } + +} + diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java new file mode 100644 index 000000000..254a6bda6 --- /dev/null +++ b/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---------------------------"); + } + } +} diff --git a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java new file mode 100644 index 000000000..8dce19e13 --- /dev/null +++ b/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); + } + } + } +} \ No newline at end of file diff --git a/jme3-plugins/src/test/resources/gltf/box/Box0.bin b/jme3-plugins/src/test/resources/gltf/box/Box0.bin new file mode 100644 index 000000000..d7798abb5 Binary files /dev/null and b/jme3-plugins/src/test/resources/gltf/box/Box0.bin differ diff --git a/jme3-plugins/src/test/resources/gltf/box/box.gltf b/jme3-plugins/src/test/resources/gltf/box/box.gltf new file mode 100644 index 000000000..4689ef8d6 --- /dev/null +++ b/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" + } + ] +} \ No newline at end of file diff --git a/jme3-testdata/src/main/resources/Models/gltf/box/Box0.bin b/jme3-testdata/src/main/resources/Models/gltf/box/Box0.bin new file mode 100644 index 000000000..d7798abb5 Binary files /dev/null and b/jme3-testdata/src/main/resources/Models/gltf/box/Box0.bin differ diff --git a/jme3-testdata/src/main/resources/Models/gltf/box/box.gltf b/jme3-testdata/src/main/resources/Models/gltf/box/box.gltf new file mode 100644 index 000000000..4689ef8d6 --- /dev/null +++ b/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" + } + ] +} \ No newline at end of file diff --git a/jme3-testdata/src/main/resources/Models/gltf/duck/Duck.gltf b/jme3-testdata/src/main/resources/Models/gltf/duck/Duck.gltf new file mode 100644 index 000000000..b80c842ce --- /dev/null +++ b/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" + } + ] +} diff --git a/jme3-testdata/src/main/resources/Models/gltf/duck/Duck0.bin b/jme3-testdata/src/main/resources/Models/gltf/duck/Duck0.bin new file mode 100644 index 000000000..5f01f88ac Binary files /dev/null and b/jme3-testdata/src/main/resources/Models/gltf/duck/Duck0.bin differ diff --git a/jme3-testdata/src/main/resources/Models/gltf/duck/DuckCM.png b/jme3-testdata/src/main/resources/Models/gltf/duck/DuckCM.png new file mode 100644 index 000000000..9fa2dd4cc Binary files /dev/null and b/jme3-testdata/src/main/resources/Models/gltf/duck/DuckCM.png differ