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 index f3e442096..db4204295 100644 --- 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 @@ -2,6 +2,9 @@ package com.jme3.scene.plugins.gltf; import com.google.gson.*; import com.google.gson.stream.JsonReader; +import com.jme3.animation.AnimControl; +import com.jme3.animation.Animation; +import com.jme3.animation.SpatialTrack; import com.jme3.asset.*; import com.jme3.material.Material; import com.jme3.material.RenderState; @@ -41,9 +44,14 @@ public class GltfLoader implements AssetLoader { private JsonArray textures; private JsonArray images; private JsonArray samplers; + private JsonArray animations; + private Material defaultMat; private AssetInfo info; + private FloatArrayPopulator floatArrayPopulator = new FloatArrayPopulator(); + private Vector3fArrayPopulator vector3fArrayPopulator = new Vector3fArrayPopulator(); + private QuaternionArrayPopulator quaternionArrayPopulator = new QuaternionArrayPopulator(); private static Map defaultMaterialAdapters = new HashMap<>(); private boolean useNormalsFlag = false; @@ -85,6 +93,7 @@ public class GltfLoader implements AssetLoader { textures = root.getAsJsonArray("textures"); images = root.getAsJsonArray("images"); samplers = root.getAsJsonArray("samplers"); + animations = root.getAsJsonArray("animations"); JsonPrimitive defaultScene = root.getAsJsonPrimitive("scene"); @@ -127,6 +136,13 @@ public class GltfLoader implements AssetLoader { root.attachChild(sceneNode); } + //Loading animations + if (animations != null) { + for (JsonElement animation : animations) { + loadAnimation(animation.getAsJsonObject()); + } + } + //Setting the default scene cul hint to inherit. int activeChild = 0; if (defaultScene != null) { @@ -147,9 +163,7 @@ public class GltfLoader implements AssetLoader { JsonArray children = nodeData.getAsJsonArray("children"); 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"); - } + assertNotNull(meshes, "Can't find any mesh data, yet a node references a mesh"); //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. @@ -242,9 +256,8 @@ public class GltfLoader implements AssetLoader { } 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); - } + assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex); + String name = getAsString(meshData, "name"); geomArray = new Geometry[primitives.size()]; @@ -256,13 +269,12 @@ public class GltfLoader implements AssetLoader { mesh.setMode(getMeshMode(mode)); Integer indices = getAsInteger(meshObject, "indices"); if (indices != null) { - mesh.setBuffer(loadVertexBuffer(indices, VertexBuffer.Type.Index)); - + mesh.setBuffer(loadAccessorData(indices, new VertexBufferPopulator(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()))); + mesh.setBuffer(loadAccessorData(entry.getValue().getAsInt(), new VertexBufferPopulator(getVertexBufferType(entry.getKey())))); } Geometry geom = new Geometry(null, mesh); @@ -297,11 +309,10 @@ public class GltfLoader implements AssetLoader { return geomArray; } - private VertexBuffer loadVertexBuffer(int accessorIndex, VertexBuffer.Type bufferType) throws IOException { + private R loadAccessorData(int accessorIndex, Populator populator) throws IOException { + + assertNotNull(accessors, "No accessor attribute in the gltf file"); - 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); @@ -313,30 +324,16 @@ public class GltfLoader implements AssetLoader { 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; + + return populator.populate(bufferViewIndex, componentType, type, count, byteOffset); } - 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; - } + private void readBuffer(Integer bufferViewIndex, int byteOffset, int bufferSize, Object store, int numComponents) throws IOException { + JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject(); Integer bufferIndex = getAsInteger(bufferView, "buffer"); @@ -351,7 +348,7 @@ public class GltfLoader implements AssetLoader { //int target = getAsInteger(bufferView, "target", 0); byte[] data = readData(bufferIndex); - populateBuffer(buff, data, bufferSize, byteOffset + bvByteOffset, byteStride, numComponents); + populateBuffer(store, data, bufferSize, byteOffset + bvByteOffset, byteStride, numComponents); //TODO extensions? //TODO extras? @@ -360,9 +357,8 @@ public class GltfLoader implements AssetLoader { private byte[] readData(int bufferIndex) throws IOException { - if (buffers == null) { - throw new AssetLoadException("No buffer defined"); - } + assertNotNull(buffers, "No buffer defined"); + JsonObject buffer = buffers.get(bufferIndex).getAsJsonObject(); String uri = getAsString(buffer, "uri"); Integer bufferLength = getAsInteger(buffer, "byteLength"); @@ -398,9 +394,8 @@ public class GltfLoader implements AssetLoader { } private Material loadMaterial(int materialIndex) { - if (materials == null) { - throw new AssetLoadException("There is no material defined yet a mesh references one"); - } + assertNotNull(materials, "There is no material defined yet a mesh references one"); + JsonObject matData = materials.get(materialIndex).getAsJsonObject(); JsonObject pbrMat = matData.getAsJsonObject("pbrMetallicRoughness"); @@ -448,12 +443,9 @@ public class GltfLoader implements AssetLoader { return null; } Integer textureIndex = getAsInteger(texture, "index"); - if (textureIndex == null) { - throw new AssetLoadException("Texture as no index"); - } - if (textures == null) { - throw new AssetLoadException("There are no textures, yet one is referenced by a material"); - } + assertNotNull(textureIndex, "Texture as no index"); + assertNotNull(textures, "There are no textures, yet one is referenced by a material"); + JsonObject textureData = textures.get(textureIndex).getAsJsonObject(); Integer sourceIndex = getAsInteger(textureData, "source"); Integer samplerIndex = getAsInteger(textureData, "sampler"); @@ -487,6 +479,115 @@ public class GltfLoader implements AssetLoader { } + private void loadAnimation(JsonObject animation) throws IOException { + JsonArray channels = animation.getAsJsonArray("channels"); + JsonArray samplers = animation.getAsJsonArray("samplers"); + String name = getAsString(animation, "name"); + assertNotNull(channels, "No channels for animation " + name); + assertNotNull(samplers, "No samplers for animation " + name); + + //temp data storage of track data + AnimData[] animatedNodes = new AnimData[nodes.size()]; + + for (JsonElement channel : channels) { + + JsonObject target = channel.getAsJsonObject().getAsJsonObject("target"); + + Integer targetNode = getAsInteger(target, "node"); + String targetPath = getAsString(target, "path"); + if (targetNode == null) { + //no target node for the channel, specs say to ignore the channel. + continue; + } + assertNotNull(targetPath, "No target path for channel"); + + if (targetPath.equals("weight")) { + //Morph animation, not implemented in JME, let's warn the user and skip the channel + logger.log(Level.WARNING, "Morph animation is not supported by JME yet, skipping animation"); + continue; + } + AnimData animData = animatedNodes[targetNode]; + if (animData == null) { + animData = new AnimData(); + animatedNodes[targetNode] = animData; + } + + Integer samplerIndex = getAsInteger(channel.getAsJsonObject(), "sampler"); + assertNotNull(samplerIndex, "No animation sampler provided for channel"); + JsonObject sampler = samplers.get(samplerIndex).getAsJsonObject(); + Integer timeIndex = getAsInteger(sampler, "input"); + assertNotNull(timeIndex, "No input accessor Provided for animation sampler"); + Integer dataIndex = getAsInteger(sampler, "output"); + assertNotNull(dataIndex, "No output accessor Provided for animation sampler"); + + String interpolation = getAsString(sampler, "interpolation"); + if (interpolation == null || !interpolation.equals("LINEAR")) { + //JME anim system only supports Linear interpolation (will be possible with monkanim though) + //TODO rework this once monkanim is core, or allow a hook for animation loading to fit custom animation systems + logger.log(Level.WARNING, "JME only supports linear interpolation for animations"); + } + + float[] times = fetchFromCache("accessors", timeIndex, float[].class); + if (times == null) { + times = loadAccessorData(timeIndex, floatArrayPopulator); + addToCache("accessors", timeIndex, times, accessors.size()); + } + if (animData.times == null) { + animData.times = times; + } else { + //check if we are loading the same time array + if (animData.times != times) { + throw new AssetLoadException("Channel has different input accessors for samplers"); + } + } + if (animData.length == null) { + //animation length is the last timestamp + animData.length = times[times.length - 1]; + } + if (targetPath.equals("translation")) { + Vector3f[] translations = loadAccessorData(dataIndex, vector3fArrayPopulator); + animData.translations = translations; + } else if (targetPath.equals("scale")) { + Vector3f[] scales = loadAccessorData(dataIndex, vector3fArrayPopulator); + animData.scales = scales; + } else if (targetPath.equals("rotation")) { + Quaternion[] rotations = loadAccessorData(dataIndex, quaternionArrayPopulator); + animData.rotations = rotations; + } + + } + + for (int i = 0; i < animatedNodes.length; i++) { + AnimData animData = animatedNodes[i]; + if (animData == null) { + continue; + } + Object node = fetchFromCache("nodes", i, Object.class); + if (node instanceof Spatial) { + Spatial s = (Spatial) node; + AnimControl control = s.getControl(AnimControl.class); + if (control == null) { + control = new AnimControl(); + s.addControl(control); + } + if (name == null) { + name = s.getName() + "_anim_" + control.getAnimationNames().size(); + } + Animation anim = new Animation(name, animData.length); + anim.addTrack(new SpatialTrack(animData.times, animData.translations, animData.rotations, animData.scales)); + control.addAnim(anim); + + } else { + //At some pont we'll have bone animation + //TODO support for bone animation. + } + } + + + } + + //private void readAnimationSampler() + private void readSampler(int samplerIndex, Texture2D texture) { if (samplers == null) { throw new AssetLoadException("No samplers defined"); @@ -529,5 +630,112 @@ public class GltfLoader implements AssetLoader { data[index] = object; } + private class AnimData { + Float length; + float[] times; + Vector3f[] translations; + Quaternion[] rotations; + Vector3f[] scales; + float[] weights; + } + + private interface Populator { + T populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException; + } + + private class VertexBufferPopulator implements Populator { + VertexBuffer.Type bufferType; + + public VertexBufferPopulator(VertexBuffer.Type bufferType) { + this.bufferType = bufferType; + } + + @Override + public VertexBuffer populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException { + + VertexBuffer vb = new VertexBuffer(bufferType); + VertexBuffer.Format format = getVertexBufferFormat(componentType); + int numComponents = getNumberOfComponents(type); + + Buffer buff = VertexBuffer.createBuffer(format, numComponents, count); + int bufferSize = numComponents * count; + if (bufferViewIndex == null) { + //no referenced buffer, specs says to pad the buffer with zeros. + padBuffer(buff, bufferSize); + } else { + readBuffer(bufferViewIndex, byteOffset, bufferSize, buff, numComponents); + } + + if (bufferType == VertexBuffer.Type.Index) { + numComponents = 3; + } + vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, buff); + + + return vb; + } + + } + + private class FloatArrayPopulator implements Populator { + + @Override + public float[] populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException { + + int numComponents = getNumberOfComponents(type); + int dataSize = numComponents * count; + float[] data = new float[count]; + + if (bufferViewIndex == null) { + //no referenced buffer, specs says to pad the data with zeros. + padBuffer(data, dataSize); + } else { + readBuffer(bufferViewIndex, byteOffset, dataSize, data, numComponents); + } + + return data; + } + + } + + private class Vector3fArrayPopulator implements Populator { + + @Override + public Vector3f[] populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException { + + int numComponents = getNumberOfComponents(type); + int dataSize = numComponents * count; + Vector3f[] data = new Vector3f[count]; + + if (bufferViewIndex == null) { + //no referenced buffer, specs says to pad the data with zeros. + padBuffer(data, dataSize); + } else { + readBuffer(bufferViewIndex, byteOffset, dataSize, data, numComponents); + } + + return data; + } + } + + private class QuaternionArrayPopulator implements Populator { + + @Override + public Quaternion[] populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException { + + int numComponents = getNumberOfComponents(type); + int dataSize = numComponents * count; + Quaternion[] data = new Quaternion[count]; + + if (bufferViewIndex == null) { + //no referenced buffer, specs says to pad the data with zeros. + padBuffer(data, dataSize); + } else { + readBuffer(bufferViewIndex, byteOffset, dataSize, data, numComponents); + } + + return data; + } + } } 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 index 8ed1b21bd..eb7032328 100644 --- 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 @@ -3,6 +3,8 @@ package com.jme3.scene.plugins.gltf; import com.google.gson.*; import com.jme3.asset.AssetLoadException; import com.jme3.math.ColorRGBA; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; import com.jme3.texture.Texture; @@ -109,7 +111,7 @@ public class GltfUtils { return VertexBuffer.Type.Color; case "JOINTS_0": return VertexBuffer.Type.BoneIndex; - case "WEIGHT_0": + case "WEIGHTS_0": return VertexBuffer.Type.BoneWeight; default: throw new AssetLoadException("Unsupported buffer attribute: " + attribute); @@ -166,48 +168,79 @@ public class GltfUtils { } } - 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); + public static void padBuffer(Object store, int bufferSize) { + if (store instanceof Buffer) { + Buffer buffer = (Buffer) store; + 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); + } } - } else if (buffer instanceof FloatBuffer) { - FloatBuffer fb = (FloatBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - fb.put(0); + buffer.rewind(); + } + if (store instanceof float[]) { + float[] array = (float[]) store; + for (int i = 0; i < array.length; i++) { + array[i] = 0; } - } else if (buffer instanceof ShortBuffer) { - ShortBuffer sb = (ShortBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - sb.put((short) 0); + } else if (store instanceof Vector3f[]) { + Vector3f[] array = (Vector3f[]) store; + for (int i = 0; i < array.length; i++) { + array[i] = new Vector3f(); } - } else if (buffer instanceof ByteBuffer) { - ByteBuffer bb = (ByteBuffer) buffer; - for (int i = 0; i < bufferSize; i++) { - bb.put((byte) 0); + } else if (store instanceof Quaternion[]) { + Quaternion[] array = (Quaternion[]) store; + for (int i = 0; i < array.length; i++) { + array[i] = new Quaternion(); } } - buffer.rewind(); } - public static void populateBuffer(Buffer buffer, byte[] source, int length, int byteOffset, int byteStride, int numComponents) throws IOException { - buffer.clear(); + public static void populateBuffer(Object store, byte[] source, int length, int byteOffset, int byteStride, int numComponents) throws IOException { - if (buffer instanceof ByteBuffer) { - populateByteBuffer((ByteBuffer) buffer, source, length, byteOffset, byteStride, numComponents); + if (store instanceof Buffer) { + Buffer buffer = (Buffer) store; + 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(); 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); + if (store instanceof float[]) { + populateFloatArray((float[]) store, stream, length, byteOffset, byteStride, numComponents); + } else if (store instanceof Vector3f[]) { + populateVector3fArray((Vector3f[]) store, stream, length, byteOffset, byteStride, numComponents); + } else if (store instanceof Quaternion[]) { + populateQuaternionArray((Quaternion[]) store, stream, length, byteOffset, byteStride, numComponents); } - buffer.rewind(); } private static void populateByteBuffer(ByteBuffer buffer, byte[] source, int length, int byteOffset, int byteStride, int numComponents) { @@ -260,6 +293,61 @@ public class GltfUtils { } } + private static void populateFloatArray(float[] array, 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); + int arrayIndex = 0; + while (index < end) { + for (int i = 0; i < numComponents; i++) { + array[arrayIndex] = stream.readFloat(); + arrayIndex++; + } + + index += Math.max(componentSize * numComponents, byteStride); + } + } + + private static void populateVector3fArray(Vector3f[] array, 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); + int arrayIndex = 0; + while (index < end) { + array[arrayIndex] = new Vector3f( + stream.readFloat(), + stream.readFloat(), + stream.readFloat() + ); + + arrayIndex++; + + index += Math.max(componentSize * numComponents, byteStride); + } + } + + private static void populateQuaternionArray(Quaternion[] array, 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); + int arrayIndex = 0; + while (index < end) { + array[arrayIndex] = new Quaternion( + stream.readFloat(), + stream.readFloat(), + stream.readFloat(), + stream.readFloat() + ); + + arrayIndex++; + + index += Math.max(componentSize * numComponents, byteStride); + } + } + private static LittleEndien getStream(byte[] buffer) { return new LittleEndien(new DataInputStream(new ByteArrayInputStream(buffer))); }