From db19e7dce9d1e3fd05ad920a0062982cc1a6958e Mon Sep 17 00:00:00 2001 From: ittai Date: Tue, 26 May 2026 22:10:42 +0300 Subject: [PATCH 1/2] Fix glTF cubic spline animation sampling --- .../jme3/scene/plugins/gltf/GltfLoader.java | 31 ++++++++ .../scene/plugins/gltf/GltfLoaderTest.java | 11 +++ .../test/resources/gltf/cubicSplineScale.gltf | 77 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 jme3-plugins/src/test/resources/gltf/cubicSplineScale.gltf 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 bbca2de105..ea6ce631ed 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 @@ -61,6 +61,7 @@ import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.LinkedList; @@ -1135,6 +1136,7 @@ public void readAnimation(int animationIndex) throws IOException { assertNotNull(dataIndex, "No output accessor Provided for animation sampler"); String interpolation = getAsString(sampler, "interpolation"); + boolean cubicSpline = "CUBICSPLINE".equals(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, @@ -1153,18 +1155,30 @@ public void readAnimation(int animationIndex) throws IOException { if (targetPath.equals("translation")) { trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Translation)); Vector3f[] translations = readAccessorData(dataIndex, vector3fArrayPopulator); + if (cubicSpline) { + translations = getCubicSplineValues(translations); + } trackData.translations = translations; } else if (targetPath.equals("scale")) { trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Scale)); Vector3f[] scales = readAccessorData(dataIndex, vector3fArrayPopulator); + if (cubicSpline) { + scales = getCubicSplineValues(scales); + } trackData.scales = scales; } else if (targetPath.equals("rotation")) { trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Rotation)); Quaternion[] rotations = readAccessorData(dataIndex, quaternionArrayPopulator); + if (cubicSpline) { + rotations = getCubicSplineValues(rotations); + } trackData.rotations = rotations; } else { trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph)); float[] weights = readAccessorData(dataIndex, floatArrayPopulator); + if (cubicSpline) { + weights = getCubicSplineValues(weights, times.length); + } trackData.weights = weights; hasMorphTrack = true; } @@ -1495,6 +1509,23 @@ private MorphTrack toMorphTrack(TrackData data, Spatial spatial) { return new MorphTrack(g, data.times, data.weights, nbMorph); } + private T[] getCubicSplineValues(T[] data) { + T[] values = Arrays.copyOf(data, data.length / 3); + for (int i = 0; i < values.length; i++) { + values[i] = data[i * 3 + 1]; + } + return values; + } + + private float[] getCubicSplineValues(float[] data, int timesLength) { + int valuesPerTime = data.length / timesLength / 3; + float[] values = new float[timesLength * valuesPerTime]; + for (int i = 0; i < timesLength; i++) { + System.arraycopy(data, (i * 3 + 1) * valuesPerTime, values, i * valuesPerTime, valuesPerTime); + } + return values; + } + public T fetchFromCache(String name, int index, Class type) { Object[] data = dataCache.get(name); if (data == null) { 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 index d485ec2f31..f1de567f71 100644 --- 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 @@ -91,6 +91,17 @@ public void testLoadEmptyScene() { } } + @Test + public void testLoadCubicSplineScaleAnimation() { + try { + Spatial scene = assetManager.loadModel("gltf/cubicSplineScale.gltf"); + dumpScene(scene, 0); + } catch (AssetLoadException ex) { + ex.printStackTrace(); + Assertions.fail("Failed to import gltf model with cubic spline scale animation"); + } + } + @Test public void testLightsPunctualExtension() { try { diff --git a/jme3-plugins/src/test/resources/gltf/cubicSplineScale.gltf b/jme3-plugins/src/test/resources/gltf/cubicSplineScale.gltf new file mode 100644 index 0000000000..ae018f52bb --- /dev/null +++ b/jme3-plugins/src/test/resources/gltf/cubicSplineScale.gltf @@ -0,0 +1,77 @@ +{ + "asset": { + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "name": "AnimatedNode" + } + ], + "animations": [ + { + "name": "CubicSplineScale", + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "scale" + } + } + ], + "samplers": [ + { + "input": 0, + "interpolation": "CUBICSPLINE", + "output": 1 + } + ] + } + ], + "buffers": [ + { + "uri": "data:application/octet-stream;base64,AAAAAAAAgD8AAABAAABAQAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAAAAQAAAAEAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAEAAAAAAAABAQAAAQEAAAAAAAABAQAAAQEAAAAAAAABAQAAAQEAAAAAAAACAQAAAgEAAAAAAAACAQAAAgEAAAAAAAACAQAAAgEA=", + "byteLength": 200 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 20 + }, + { + "buffer": 0, + "byteOffset": 20, + "byteLength": 180 + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 5, + "type": "SCALAR", + "min": [ + 0 + ], + "max": [ + 4 + ] + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 15, + "type": "VEC3" + } + ] +} From 66aca874e0f1400a292f6e1cb4c9cc645105b55b Mon Sep 17 00:00:00 2001 From: ittai Date: Wed, 27 May 2026 07:35:25 +0300 Subject: [PATCH 2/2] Validate cubic spline animation outputs --- .../java/com/jme3/scene/plugins/gltf/GltfLoader.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 ea6ce631ed..b6f84a41bf 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 @@ -1510,6 +1510,12 @@ private MorphTrack toMorphTrack(TrackData data, Spatial spatial) { } private T[] getCubicSplineValues(T[] data) { + if (data == null) { + throw new AssetLoadException("No output data defined for cubic spline animation sampler"); + } + if (data.length % 3 != 0) { + throw new AssetLoadException("Cubic spline animation sampler output does not contain tangent/value triplets"); + } T[] values = Arrays.copyOf(data, data.length / 3); for (int i = 0; i < values.length; i++) { values[i] = data[i * 3 + 1]; @@ -1518,6 +1524,12 @@ private T[] getCubicSplineValues(T[] data) { } private float[] getCubicSplineValues(float[] data, int timesLength) { + if (data == null) { + throw new AssetLoadException("No output data defined for cubic spline animation sampler"); + } + if (timesLength <= 0 || data.length % timesLength != 0 || (data.length / timesLength) % 3 != 0) { + throw new AssetLoadException("Cubic spline animation sampler output does not match input times"); + } int valuesPerTime = data.length / timesLength / 3; float[] values = new float[timesLength * valuesPerTime]; for (int i = 0; i < timesLength; i++) {