Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jgltf model builder question #102

Closed
Suicolen opened this issue Mar 7, 2024 · 14 comments
Closed

jgltf model builder question #102

Suicolen opened this issue Mar 7, 2024 · 14 comments

Comments

@Suicolen
Copy link

Suicolen commented Mar 7, 2024

Hi, it's been close to 2 years since i used this library but I'd like to once again attempt exporting my own custom model format to glTF (and also vice versa, but for now exporting is more important) last time i managed to successfully do it but it was quite difficult to do so, internal classes like BufferStructureBuilder did make it quite a bit more bearable however

I remember jgltf model builder being mentioned in my last issue: #78 however i decided to put a hold on the project at that time as some other things came up.

My question being, is it still actively being worked on and a lot of it might change or is it mostly finished and i should probably use it?
There's this note: This library is still subject to change.
But if there won't be any major changes then i guess i could use it.

And are there any examples of it being used, if not I'd very much appreciate if some simple examples could be made, preferably of a skinned mesh, because in my case i need to export the mesh, the armature and potentially also animations, altho the latter isn't that important and i mainly need to export the mesh and the armature for now.

@javagl
Copy link
Owner

javagl commented Mar 7, 2024

I'm aware of the responsibility that comes with publishing a library that people rely on. I hate it when things change randomly, in an incompatible way. I try to avoid that.

There are some things that should be improved and extended (and made "better" and "more stable") in the project as a whole. (There are some pending PRs/changes (not API related, but bugfixes) that are not yet part of the last release). And there are things that should be better supported (i.e. made more convenient) in the jgltf-model-builder project in particular. I'm trying to find some time for that, and always trying to avoid breaking changes, and if there are any, try to find a reasonable migration path. But in order to make strict (semantic versioning) backward compatibility guarantees, I'd have to spend orders of magnitude more time for this project.

However, a recent issue with an example for creating skinning data is, for example, in #94 (comment)

I'm also aware that there should be more documentation that is based on Markdown/Diagrams/Examples (and not only the - sometimes admittedly pretty shallow - JavaDoc documentation). But I'll also have to allocate more time for that.

@Suicolen
Copy link
Author

Suicolen commented Mar 7, 2024

Thank you for the very quick reply, i will take a look at the issue you linked.

@javagl
Copy link
Owner

javagl commented Mar 7, 2024

There are some low-hanging fruits for making things a bit "nicer". For example, the AccessorModels class has some issues, in terms of the number of methods, and the questions which methods it should contain exactly. This might be a place where the API might change. But these will probably be "smaller" changes. For example, instead of

        DefaultAccessorModel joints = AccessorModels.create(
            GltfConstants.GL_UNSIGNED_SHORT, "VEC4", false, 
            Buffers.createByteBufferFrom(ShortBuffer.wrap(new short[] 
            {
                0, 0, 0, 0, 
                0, 0, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0, 
                0, 1, 0, 0
            }
        )));

one could make a case for offering something that avoids this (unintuitive) Buffers.createByteBufferFrom(ShortBuffer.wrap(new short[]... part. But the details are still TBD...

@Suicolen
Copy link
Author

Suicolen commented Mar 13, 2024

Yeah that would improve it a bit but honestly it's already really good the way it is currently, I've managed to almost reproduce what i had a few years ago, in just a few hours so creating gltf models with this model-builder api is already great.

However i ran into an issue with skins, the armature it exports looks exactly as I'd expect but the mesh does not display properly (It's just a single triangle in the middle of the model) without the armature, it looks correct.
I've had some ideas on why this might happen (explained in the comments) especially above the method findJointIndex
If you could take a look, I'd very much appreciate it

Here is my current code:

import de.javagl.jgltf.model.GltfConstants;
import de.javagl.jgltf.model.creation.AccessorModels;
import de.javagl.jgltf.model.creation.GltfModelBuilder;
import de.javagl.jgltf.model.impl.*;
import de.javagl.jgltf.model.io.Buffers;
import de.javagl.jgltf.model.io.GltfModelWriter;
import de.javagl.jgltf.model.v2.MaterialModelV2;
import editor.definition.SeqDefs;
import editor.definition.SeqFrameDefs;
import editor.render.animation.experimental.SkeletonHierarchyGenerator;
import editor.render.animation.experimental.model.BoneModel;
import editor.rs.RSModel;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector3i;

import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter
@Setter
public class GltfExporter {

    private RSModel model;

    private final SeqDefs seqDefs;
    private final SeqFrameDefs seqFrameDefs;
    // in my case a bone just has a local position, a parent (null for root bone) and a list of children
    private List<BoneModel> bones;

    // temporary, figure out a better way to handle this rather than storing it as a global variable
    private DefaultNodeModel rootJointNode;

    public GltfExporter(SeqDefs seqDefs, SeqFrameDefs seqFrameDefs) {
        this.seqDefs = seqDefs;
        this.seqFrameDefs = seqFrameDefs;
    }

    @SneakyThrows(IOException.class)
    public void export(Path path) {
        SkeletonHierarchyGenerator skeletonHierarchyGenerator = new SkeletonHierarchyGenerator(seqDefs, seqFrameDefs);
        skeletonHierarchyGenerator.setModel(model);
        bones = skeletonHierarchyGenerator.infer(824);
        // this just inverts the x/z axis of the bone position and also scales it by 0.02 (same thing is done below for vertices)
        bones.forEach(BoneModel::postProcess);
        // this computes the global transform as: globalTransform = new Matrix4f().translate(globalPos);
        // and the inverse bind transform as: inverseBindTransform = globalTransform.invert(new Matrix4f());
        bones.forEach(BoneModel::computeMatrices);
        DefaultMeshModel mesh = new DefaultMeshModel();
        DefaultMeshPrimitiveModel meshPrimitive = getMeshPrimitiveModel();
        mesh.addMeshPrimitiveModel(meshPrimitive);
        DefaultNodeModel nodeWithMesh = new DefaultNodeModel();
        nodeWithMesh.addMeshModel(mesh);

        DefaultSkinModel skin = getSkinModel();
        nodeWithMesh.setSkinModel(skin);

        DefaultSceneModel scene = new DefaultSceneModel();
        scene.addNode(nodeWithMesh);
        scene.addNode(rootJointNode);

        GltfModelBuilder gltfModelBuilder = GltfModelBuilder.create();
        gltfModelBuilder.addSceneModel(scene);
        DefaultGltfModel gltfModel = gltfModelBuilder.build();

        GltfModelWriter writer = new GltfModelWriter();
        writer.writeEmbedded(gltfModel, path.toFile());
    }

    private DefaultMeshPrimitiveModel getMeshPrimitiveModel() {
        DefaultMeshPrimitiveModel meshPrimitive = new DefaultMeshPrimitiveModel(4);
        DefaultAccessorModel vertices = getVerticesAccessorModel();
        DefaultAccessorModel indices = getIndicesAccessorModel();
        DefaultAccessorModel weights = getWeightsAccessorModel();
        DefaultAccessorModel joints = getJointsAccessorModel();
        meshPrimitive.setIndices(indices);
        meshPrimitive.putAttribute("POSITION", vertices);
        meshPrimitive.putAttribute("JOINTS_0", joints);
        meshPrimitive.putAttribute("WEIGHTS_0", weights);
        // just use a default material for all triangles for now
        MaterialModelV2 materialModel = new MaterialModelV2();
        materialModel.setBaseColorFactor(new float[]{0.15f, 0.15f, 0.15f, 1f});
        meshPrimitive.setMaterialModel(materialModel);
        return meshPrimitive;
    }

    private DefaultSkinModel getSkinModel() {
        DefaultSkinModel skin = new DefaultSkinModel();
        DefaultAccessorModel inverseBindMatrices = getInverseBindMatricesAccessorModel();
        skin.setInverseBindMatrices(inverseBindMatrices);

        Map<Integer, DefaultNodeModel> idToJointNode = new HashMap<>();
        for (BoneModel bone : bones) {
            DefaultNodeModel jointNode = new DefaultNodeModel();
            // local position is defined as globalPosition - parentPosition (if parent is null then parentPosition = zero vector)
            jointNode.setTranslation(bone.getLocalPositionAsFloatArray());
            jointNode.setRotation(new float[]{0f, 0f, 0f, 1f});
            idToJointNode.put(bone.getId(), jointNode);
            skin.addJoint(jointNode);
        }

        // set children
        for (BoneModel bone : bones) {
            DefaultNodeModel jointNode = idToJointNode.get(bone.getId());
            for (BoneModel child : bone.getChildren()) {
                DefaultNodeModel childNode = idToJointNode.get(child.getId());
                jointNode.addChild(childNode);
            }
            if (bone.isRoot()) {
                rootJointNode = jointNode;
            }
        }

        return skin;
    }

    private DefaultAccessorModel getVerticesAccessorModel() {
        FloatBuffer verticesBuffer = FloatBuffer.allocate(model.vertexCount * 3);
        for (int i = 0; i < model.vertexCount; i++) {
            Vector3f vertex = new Vector3f(model.verticesX[i], model.verticesY[i] * -1, model.verticesZ[i] * -1).mul(0.02f);
            verticesBuffer.put(vertex.x).put(vertex.y).put(vertex.z);
        }
        verticesBuffer.flip();
        return AccessorModels.createFloat3D(verticesBuffer);
    }

    private DefaultAccessorModel getIndicesAccessorModel() {
        IntBuffer indicesBuffer = IntBuffer.allocate(model.triangleCount * 3);
        for (int i = 0; i < model.triangleCount; i++) {
            Vector3i index = new Vector3i(model.faceIndicesA[i], model.faceIndicesB[i], model.faceIndicesC[i]);
            indicesBuffer.put(index.x).put(index.y).put(index.z);
        }
        indicesBuffer.flip();
        return AccessorModels.createUnsignedShortScalar(indicesBuffer);
    }

    private DefaultAccessorModel getWeightsAccessorModel() {
        FloatBuffer weightsBuffer = FloatBuffer.allocate(model.vertexCount * 4);
        for (int i = 0; i < model.vertexCount; i++) {
            // each vertex will only be influenced by one bone in my case
            weightsBuffer.put(1f).put(0f).put(0f).put(0f);
        }
        weightsBuffer.flip();
        return AccessorModels.createFloat4D(weightsBuffer);
    }

    private DefaultAccessorModel getJointsAccessorModel() {
        IntBuffer jointsBuffer = IntBuffer.allocate(model.vertexCount * 4);
        for (int i = 0; i < model.vertexCount; i++) {
            int jointIndex = findJointIndex(i);
            jointsBuffer.put(jointIndex).put(0).put(0).put(0);
        }
        jointsBuffer.flip();
        return AccessorModels.create(GltfConstants.GL_UNSIGNED_INT, "VEC4", false, Buffers.createByteBufferFrom(jointsBuffer));
    }

    private DefaultAccessorModel getInverseBindMatricesAccessorModel() {
        FloatBuffer inverseBindMatricesBuffer = FloatBuffer.allocate(bones.size() * 16);
        int idx = 0;
        for (BoneModel bone : bones) {
            putMatrixToBuffer(bone.getInverseBindTransform(), idx * 16, inverseBindMatricesBuffer);
            idx++;
        }
        inverseBindMatricesBuffer.flip();
        return AccessorModels.create(GltfConstants.GL_FLOAT, "MAT4", false, Buffers.createByteBufferFrom(inverseBindMatricesBuffer));
    }

    private void putMatrixToBuffer(Matrix4f m, int offset, FloatBuffer dest) {
        dest.put(offset, m.m00());
        dest.put(offset + 1, m.m01());
        dest.put(offset + 2, m.m02());
        dest.put(offset + 3, m.m03());
        dest.put(offset + 4, m.m10());
        dest.put(offset + 5, m.m11());
        dest.put(offset + 6, m.m12());
        dest.put(offset + 7, m.m13());
        dest.put(offset + 8, m.m20());
        dest.put(offset + 9, m.m21());
        dest.put(offset + 10, m.m22());
        dest.put(offset + 11, m.m23());
        dest.put(offset + 12, m.m30());
        dest.put(offset + 13, m.m31());
        dest.put(offset + 14, m.m32());
        dest.put(offset + 15, m.m33());
    }

    // could this be the problem?
    // the joints array is: "joints" : [ 1, 3, 2, 8, 9, 10, 11, 12, 13, 4, 5, 6, 7 ]
    // however this is not the order the bones (read joints) appear in, they're all sequential starting from 0 (root bone is first)
    // So it'd be: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
    // I guess the reason why they appear in this order has to do with their children
    // As if i don't add any children bones then the order is sequential (goes from 1 to 13 which makes sense)
    // if this is indeed the issue, then i guess i have to figure out the indices of the joints as they appear in the glTF file, somehow
    private int findJointIndex(int vertex) {
        for (int i = 0; i < bones.size(); i++) {
            BoneModel bone = bones.get(i);
            if (bone.influences(model, vertex)) {
                return i;
            }
        }
        throw new IllegalStateException(STR."Didn't find a bone that influenced vertex \{vertex}");
    }
}

glTF file that the code produces for my specific input (a mesh of a male character):
test.zip

glTF file that the same code produces if i comment out the following:

//DefaultSkinModel skin = getSkinModel();
//nodeWithMesh.setSkinModel(skin);
//scene.addNode(rootJointNode);

test1.zip

@javagl
Copy link
Owner

javagl commented Mar 13, 2024

It's a bit difficult to analyze the code, without the necessary classes (BoneModel seems to be important...)

Also, without the original model, there will always be some guesses involved.

Things that I can say for sure:

  • The "working" one, test, does not contain a skin...
  • The one that is not working contains invalid inverseBindMatrices. You can drag-and-drop these models into the validator at https://github.khronos.org/glTF-Validator/, and receive the corresponding reports (or use the snippet below to print them)
  • The JOINTS have an invalid type. According to the specification at https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview (and as also pointed out in the validation error), the joints may not have the component type GL_UNSIGNED_INT. You can fix this easily in your getJointsAccessorModel function, by putting the joint indices (casted to short if necessary), putting them into a ShortBuffer, and using GL_UNSIGNED_SHORT as the component type
  • Random note: I only recently started using JOML (that Unsafe sfuff - why, oh why?). But it has a function in the Matrix4f class that may make your putMatrixToBuffer obsolete: You can just call m.get(offset, dest) instead.

When using identity matrices for the IBM, and fixing the JOINTS_0 component type, the result is the following:

identities.zip

Yeah, it looks wrong, but with the right IBMs, it might not be totally wrong... I guess.


An aside: I actually took some time recently, and did a few experiments for extending the builder functionality. The current state is in master...builder-extensions , for example, for adding morph target support to the builder classes ( 4bc4c46 ). But all this is still in flight. Better builder support for skinning and animations and all that may involve quite a bit of work and thought, and may take some time. But maybe you have some ideas or feedback.


The inverse bind matrices can be printed with something like this:

package de.javagl.jgltf.test.issue102;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;

import de.javagl.jgltf.model.AccessorData;
import de.javagl.jgltf.model.AccessorFloatData;
import de.javagl.jgltf.model.AccessorModel;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.SkinModel;
import de.javagl.jgltf.model.impl.DefaultSkinModel;
import de.javagl.jgltf.model.io.GltfModelReader;

public class ReadIssue102
{
    public static void main(String[] args) throws IOException
    {
        GltfModelReader r = new GltfModelReader();
        GltfModel model = r.read(Paths.get("./data/issue102/test/test.gltf"));
        List<SkinModel> skinModels = model.getSkinModels();
        DefaultSkinModel skinModel = (DefaultSkinModel) skinModels.get(0);
        AccessorModel am = skinModel.getInverseBindMatrices();
        AccessorData ad = am.getAccessorData();
        AccessorFloatData afd = (AccessorFloatData) ad;
        System.out.println(afd.createString(Locale.ENGLISH, "%8.3f", 1));
    }
}

... which will print all zeros for these matrices.

@Suicolen
Copy link
Author

Thank you again for your quick reply

  1. I know the working one did not contain a skin, i just wanted to show how the mesh is supposed to look like without an armature
  2. that was the issue... It's because i flipped the buffer but since i used absolute put methods it doesn't increment the position so i shouldn't have flipped it
  3. Oh okay, i did not know that (should've read the specification) I made it an IntBuffer since doing the casts was slightly inconvenient, changed it back to use a ShortBuffer and GL_UNSIGNED_SHORT now
  4. Thanks, i haven't used JOML that much before, i did try that however and i just get a nasty 'fatal error detected by the JRE...'
    Code i tried:
    bone.getInverseBindTransform().get(idx * 16, inverseBindMatricesBuffer);

Here is the BoneModel class

import editor.cache.model.sequence.TransformType;
import editor.rs.RSModel;
import javafx.geometry.Point3D;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.joml.Matrix4f;
import org.joml.Vector3f;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class BoneModel {
    private int id;
    private boolean root;
    private BoneModel parent;
    private List<BoneModel> children = new ArrayList<>();
    private Vector3f globalPos;
    private Vector3f localPos;
    @EqualsAndHashCode.Include
    private TransformType type;
    @EqualsAndHashCode.Include
    private Set<Integer> labels = new HashSet<>();

    private Matrix4f localTransform;
    private Matrix4f globalTransform;
    private Matrix4f inverseBindTransform;

    // could probably name this better
    public void removeLabelsFromParents() {
        removeLabelsFromParent(this.parent);
    }

    private void removeLabelsFromParent(BoneModel parent) {
        if (parent == null) {
            return;
        }

        parent.getLabels().removeAll(labels);
        removeLabelsFromParent(parent.parent);
    }

    public boolean influences(RSModel model, int vertex) {
        for (int label : labels) {
            int[] vertexGroup = model.getVertexGroup(label);
            if (vertexGroup == null) {
                continue;
            }
            for (int v : vertexGroup) {
                if (v == vertex) {
                    return true;
                }
            }
        }
        return false;
    }

    public void postProcess() {
        globalPos = new Vector3f(globalPos.x(), globalPos.y() * -1, globalPos.z() * -1).mul(0.02f);
    }

    public void computeMatrices() {
        Vector3f parentPos = parent != null ? new Vector3f(parent.globalPos) : new Vector3f();
        // doesn't need to really be stored, can extract this from localTransform but keep it for now
        localPos = new Vector3f(globalPos).sub(parentPos);
        localTransform = new Matrix4f().translate(localPos);
        globalTransform = new Matrix4f().translate(globalPos);
        inverseBindTransform = globalTransform.invert(new Matrix4f());
    }

    // some utility methods
    public float[] getLocalPositionAsFloatArray() {
        return new float[] {localPos.x, localPos.y, localPos.z};
    }

    // delete later, only used for some debug code atm
    public Point3D getOriginAsPoint3D() {
        return new Point3D(globalPos.x, globalPos.y, globalPos.z);
    }
    
}

It's fairly specific to this engine I'm working on tho, e.g. you may ask what are labels

I'd provide the original model file but sadly it's in a custom format used by this engine so I'd have to provide quite a few more classes.

Anyway the issue was 2) and it all works perfectly now
bit strange that this piece of code for simplifying putting a Matrix4f to a buffer throws that error

@javagl
Copy link
Owner

javagl commented Mar 14, 2024

  • Random note: I only recently started using JOML (that Unsafe sfuff - why, oh why?).

bit strange that this piece of code for simplifying putting a Matrix4f to a buffer throws that error

That's what my remark about 'that Unsafe stuff' referred to. For me, even the most trivial, basic JOML operations threw an error. This might be related to the fact that I ran this on Java 8 (and they claim support only for Java 9+). But ... that's the whole point: "Write once, run everywhere" (that once was the motto of Java), and Unsafe is ... well, unsafe (to begin with), brittle, discouraged, deprecated, and just not stable in any way. I even brought that up once as an issue in the JOML repo, but it was ignored. It's a pity that there is no good "pure" Java vector math library (and "pure" means: Without Unsafe). I now ""solved"" this by putting a

    static 
    {
        System.setProperty("joml.nounsafe", "true");
    }

as a static initializer into each class that uses JOML, which is... meh.
If I had infinite time, I'd just fork that whole thing, throw Unsafe out, and publish it as JOML-Safe or so...


However, good to hear that it is resolved now.

I also found flip() to be confusing, occasionally, because it modifies three internal variables. When I want to set the position to 0, I'm now just using b.position(0) (there's hardly ever a reason to fiddle with the limit or mark anyhow...).

If you have any ideas or thoughts about possible extensions or improvements of the builder package, just let me know. There are quite a few things in JglTF that I'm not satisfied with (and can no longer be changed), but I hope that I find some time to gradually improve it and reduce the pain-points based on the feedback from people who are actually using it.

@Suicolen
Copy link
Author

Suicolen commented Mar 14, 2024

ah yeah using b.position(0) makes more sense in this (i actually realized i had that in my old code as well)

I'm not sure about improvements, i feel like it's already really easy to use, one would be the Buffers.createByteBufferFrom(ShortBuffer.wrap(new short[]... you already mentioned, but it's a rather minor thing for me

I wanted to ask if you could provide an example for reading animation data, clearly the way I'm doing it now is not correct

import de.javagl.jgltf.model.*;
import de.javagl.jgltf.model.io.GltfModelReader;
import editor.cache.skeletal.math.Quaternionf;
import editor.cache.skeletal.math.Vector3f;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

public class GltfReadTests {
    public static void main(String[] args) throws IOException {
        GltfModelReader reader = new GltfModelReader();
        GltfModel model = reader.read(Path.of("./savedgltfmodels/simpleskin.gltf"));
        List<AnimationModel> animationModels = model.getAnimationModels();
        AnimationModel animation = animationModels.getFirst();
        for (AnimationModel.Channel channel : animation.getChannels()) {
            AnimationModel.Sampler sampler = channel.getSampler();
            AccessorModel input = sampler.getInput();
            AccessorModel output = sampler.getOutput();
            AccessorFloatData inputData = (AccessorFloatData) input.getAccessorData();
            AccessorFloatData outputData = (AccessorFloatData) output.getAccessorData();
            for (int i = 0; i < inputData.getNumElements(); i++) {
                float t = inputData.get(i, 0);
                System.out.println(STR."t = \{t}");
                for (int o = 0; o < outputData.getNumElements(); o++) {
                    switch (channel.getPath()) {
                        case "rotation" -> {
                            float x = outputData.get(o, 0);
                            float y = outputData.get(o, 1);
                            float z = outputData.get(o, 2);
                            float w = outputData.get(o, 3);
                            System.out.println(STR."Rotation: \{new Quaternionf(x, y, z, w)}");
                        }

                        case "translation" -> {
                            float x = outputData.get(o, 0);
                            float y = outputData.get(o, 1);
                            float z = outputData.get(o, 2);
                            System.out.println(STR."Translation: \{new Vector3f(x, y, z)}");
                        }

                        case "scale" -> {
                            float x = outputData.get(o, 0);
                            float y = outputData.get(o, 1);
                            float z = outputData.get(o, 2);
                            System.out.println(STR."Scale: \{new Vector3f(x, y, z)}");
                        }
                    }
                }
                System.out.println("-".repeat(100));
            }
        }

    }
}

This requires Java 21 to run however (if needed i can adjust it to work with java 8/11)

This code clearly just prints all the same values for each t which is not what i'd want, i can see why the code does that tho, I'm just not sure how to adjust it so it reads it the way you'd expect

Edit: i believe i got it right now, correct me if i made any mistakes please but the data looks correct (z and w seems flipped compared to what's in the GenerateSimpleSkin file however)

   public static void main(String[] args) throws IOException {
        GltfModelReader reader = new GltfModelReader();
        GltfModel model = reader.read(Path.of("./savedgltfmodels/simpleskin.gltf"));
        List<AnimationModel> animationModels = model.getAnimationModels();
        AnimationModel animation = animationModels.getFirst();
        for (AnimationModel.Channel channel : animation.getChannels()) {
            AnimationModel.Sampler sampler = channel.getSampler();
            AccessorModel input = sampler.getInput();
            AccessorModel output = sampler.getOutput();
            AccessorFloatData inputData = (AccessorFloatData) input.getAccessorData();
            AccessorFloatData outputData = (AccessorFloatData) output.getAccessorData();
            // i guess inputData.getNumElements() should always be equal to outputData.getNumElements() anyway?
            for (int i = 0; i < outputData.getNumElements(); i++) {
                float t = inputData.get(i, 0);
                System.out.println(STR."t = \{t}");
                switch (channel.getPath()) {
                    case "rotation" -> {
                        float x = outputData.get(i, 0);
                        float y = outputData.get(i, 1);
                        float z = outputData.get(i, 2);
                        float w = outputData.get(i, 3);
                        System.out.println(STR."Rotation: \{new Quaternionf(x, y, z, w)}");
                    }

                    case "translation" -> {
                        float x = outputData.get(i, 0);
                        float y = outputData.get(i, 1);
                        float z = outputData.get(i, 2);
                        System.out.println(STR."Translation: \{new Vector3f(x, y, z)}");
                    }

                    case "scale" -> {
                        float x = outputData.get(i, 0);
                        float y = outputData.get(i, 1);
                        float z = outputData.get(i, 2);
                        System.out.println(STR."Scale: \{new Vector3f(x, y, z)}");
                    }
                }
                System.out.println("-".repeat(100));
            }
        }

    }
}

Edit: actually for my own mesh, i created an animation in blender consisting of 3 keyframes, however inputData.getNumElements() returns 1, i'd have expected it to be 3

Edit 2: nevermind, there was just multiple AnimationModels (i expected there to only be one)
My assumption of inputData.getNumElements() == outputData.getNumElements() turned out to be false, if i turn off "Sample all animations" in blender when exporting then this isn't the case

@javagl
Copy link
Owner

javagl commented Mar 14, 2024

Animations are one of the next things that I'll tackle in the "builder" package. There are probably some low-hanging fruits, but ... the devil is in the detail.

The updated method looks correct.

As an example for the Accessor*Data: There may be an accessor that contains VEC3 data, and it contains 5 of these "3D vectors". Then

  • getNumElements will return 5
  • getNumComponentsPerElement will return 3 (for the 3D vectors)
  • getTotalNumComponents will return 5 * 3 = 15

And it has two methods for accessing the data, namely,

  • get(int globalComponentIndex)
  • get(int elementIndex, int componentIndex)

So when you're calling
float y = outputData.get(i, 1);
that's the right way for accessing the y-component of the i'th 3D vector (and you could access the same element with its "global component index", using get(i * 3 + 1)).

My assumption of inputData.getNumElements() == outputData.getNumElements() turned out to be false

That's very surprising for me (and I'll probably try that out and validate it locally). But I have to admit that there are some complexities in the Blender animation functionality (and correspondingly, some configuration options and caveats for exporting these animations to glTF) that I don't have on the radar right now.

@Suicolen
Copy link
Author

Thanks for the explanation

Yeah blender/glTF exporting is somewhat weird.. displaying the model in an online glTF viewer shows that it has 3 animations, the first one does nothing, the 2nd and 3rd ones are identical
expected output was just one animation.

After creating an animation again (similar one) it exported just fine (1 animation) but o well this is probably just some blender glTF exporter related thing
After further testing inputData.getNumElements() == outputData.getNumElements() seems to hold as long as 'Sample all animations' is turned on (which is the default) the reason why i turned it off when exporting is that i didn't want the interpolated keyframes to also be interpolated along with all other bone/joint nodes that weren't even animated at all.
Whether it's exported with that option on or off the online glTF viewer i tested could still read the file just fine and render the animation exactly as I'd expect (the JglTF one probably can as well, yet to test it as it was easier to just drag & drop the model into an online viewer) so that at least tells me that the data it exported produces the correct result, i just have to figure out how to interpret it and convert it to my custom format

@javagl
Copy link
Owner

javagl commented Mar 15, 2024

I'll have to check that inputData.getNumElements() == outputData.getNumElements() thing (maybe tomorrow, but that's already pretty packed). A first guess might be that the animation is using CUBICPLINE (where the number of output elements is three times the number of input elements).

@Suicolen
Copy link
Author

Yeah that was actually it the interpolation mode was CUBICSPLINE

@javagl
Copy link
Owner

javagl commented Mar 28, 2024

@Suicolen You may have seen that there is an open PR for the model builder package at #103 . (It has, once more, been open for too long - I've been distracted with other stuff). I'll probably try to wrap this up and merge it soon. Maybe you want to have a look, maybe try it out, and provide other feedback.

(I considered to start adding something like an AnimationBuilder, but ... that requires more planning, so I'd probably do that in a separate PR. The linked one should probably be merged soon, because it contains a few bugfixes...)

@Suicolen
Copy link
Author

@javagl I will try it out asap (currently working on some other things i need to finish first before continuing with my glTF related project)

as for AnimationBuilder that'd actually be really great addition :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants