public void readAnimation(int animationIndex) throws IOException {
        JsonObject animation = animations.get(animationIndex).getAsJsonObject();
        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
        TrackData[] tracks = new TrackData[nodes.size()];
        boolean hasMorphTrack = false;

        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("weights")) {
//                // 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 track");
//                continue;
//            }

            TrackData trackData = tracks[targetNode];
            if (trackData == null) {
                trackData = new TrackData();
                tracks[targetNode] = trackData;
            }

            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");
            }

            trackData = customContentManager.readExtensionAndExtras("animation.sampler", sampler, trackData);

            float[] times = fetchFromCache("accessors", timeIndex, float[].class);
            if (times == null) {
                times = readAccessorData(timeIndex, floatArrayPopulator);
                addToCache("accessors", timeIndex, times, accessors.size());
            }

            if (targetPath.equals("translation")) {
                trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Translation));
                Vector3f[] translations = readAccessorData(dataIndex, vector3fArrayPopulator);
                trackData.translations = translations;
            } else if (targetPath.equals("scale")) {
                trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Scale));
                Vector3f[] scales = readAccessorData(dataIndex, vector3fArrayPopulator);
                trackData.scales = scales;
            } else if (targetPath.equals("rotation")) {
                trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Rotation));
                Quaternion[] rotations = readAccessorData(dataIndex, quaternionArrayPopulator);
                trackData.rotations = rotations;
            } else {
                trackData.timeArrays.add(new TrackData.TimeData(times, TrackData.Type.Morph));
                float[] weights = readAccessorData(dataIndex, floatArrayPopulator);
                trackData.weights = weights;
                hasMorphTrack = true;
            }
            tracks[targetNode] = customContentManager.readExtensionAndExtras("channel", channel, trackData);
        }

        if (name == null) {
            name = "anim_" + animationIndex;
        }

        List<Spatial> spatials = new ArrayList<>();
        AnimClip anim = new AnimClip(name);
        List<AnimTrack> aTracks = new ArrayList<>();
        int skinIndex = -1;

        List<Joint> usedJoints = new ArrayList<>();
        for (int i = 0; i < tracks.length; i++) {
            TrackData trackData = tracks[i];
            if (trackData == null || trackData.timeArrays.isEmpty()) {
                continue;
            }
            trackData.update();
            Object node = fetchFromCache("nodes", i, Object.class);
            if (node instanceof Spatial) {
                Spatial s = (Spatial) node;
                spatials.add(s);
                if (trackData.rotations != null || trackData.translations != null
                        || trackData.scales != null) {
                    TransformTrack track = new TransformTrack(s, trackData.times,
                            trackData.translations, trackData.rotations, trackData.scales);
                    aTracks.add(track);
                }
                if (trackData.weights != null) {
                    if (s instanceof Node) {
                        s.depthFirstTraversal((Spatial spatial) -> {
                            if (spatial instanceof Geometry) {
                                aTracks.add(toMorphTrack(trackData, spatial));
                            }
                        });
                    } else if (s instanceof Geometry) {
                        aTracks.add(toMorphTrack(trackData, s));
                    }
                }
            } else if (node instanceof JointWrapper) {
                JointWrapper jw = (JointWrapper) node;
                usedJoints.add(jw.joint);

                if (skinIndex == -1) {
                    skinIndex = jw.skinIndex;
                } else {
                    // Check if all joints affected by this animation are from the same skin,
                    // the track will be skipped.
                    if (skinIndex != jw.skinIndex) {
                        logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name
                                + ") applies to joints that are not from the same skin: skin "
                                + skinIndex + ", joint " + jw.joint.getName()
                                + " from skin " + jw.skinIndex);
                        continue;
                    }
                }

                TransformTrack track = new TransformTrack(jw.joint, trackData.times,
                        trackData.translations, trackData.rotations, trackData.scales);
                aTracks.add(track);
            }
        }

        // Check each bone to see if its local pose is different from its bind pose.
        // If it is, we ensure that the bone has an animation track,
        // else the JME way of applying anim transforms will apply the bind pose to those bones,
        // instead of the local pose that is supposed to be the default
        if (skinIndex != -1) {
            SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
            for (Joint joint : skin.joints) {
                if (!usedJoints.contains(joint)) {
                    // create a track
                    float[] times = new float[]{0};

                    Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()};
                    Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()};
                    Vector3f[] scales = new Vector3f[]{joint.getLocalScale()};
                    TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales);
                    aTracks.add(track);
                }
            }
        }
        
        anim.setTracks(aTracks.toArray(new AnimTrack[aTracks.size()]));
        anim = customContentManager.readExtensionAndExtras("animations", animation, anim);

        if (skinIndex != -1) {
            // we have an armature animation.
            SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
            skin.animComposer.addAnimClip(anim);
        }

        if (!spatials.isEmpty()) {
            if (skinIndex != -1) {
                // there are some spatial or morph tracks in this bone animation... or the other way around.
                // Let's add the spatials to the skinnedSpatials.
                SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
                List<Spatial> spat = skinnedSpatials.get(skin);
                spat.addAll(spatials);
                // the animControl will be added in the setupControls();
                if (hasMorphTrack && skin.morphControl == null) {
                    skin.morphControl = new MorphControl();
                }
            } else {
                // Spatial animation
                Spatial spatial = null;
                if (spatials.size() == 1) {
                    spatial = spatials.get(0);
                } else {
                    spatial = findCommonAncestor(spatials);
                }

                AnimComposer composer = spatial.getControl(AnimComposer.class);
                if (composer == null) {
                    composer = new AnimComposer();
                    spatial.addControl(composer);
                }
                composer.addAnimClip(anim);
                if (hasMorphTrack && spatial.getControl(MorphControl.class) == null) {
                    spatial.addControl(new MorphControl());
                }
            }
        }
    }
GLTF 2.0 loader
Created by Nehon on 07/08/2017.
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java