javagl / JglTF

Java libraries related to glTF
MIT License
209 stars 61 forks source link

Copying the GLB model. #100

Closed CloseFile closed 2 months ago

CloseFile commented 7 months ago

Avatars from the RPM platform have an issue with the mesh after copying.

GltfModel gltfModelCharacter = gltfModelReader.read(Paths.get(characterFileName));

GltfModelBuilder gltfModelBuilder = GltfModelBuilder.create();
        addAll(gltfModelBuilder, gltfModelCharacter);

        // Build the whole thing and write the result
        DefaultGltfModel newModel = gltfModelBuilder.build();
        GltfModelWriter gltfModelWriter = new GltfModelWriter();
        gltfModelWriter.writeBinary(newModel, Paths.get(outputFileName).toFile());
    }

    private static void addAll(GltfModelBuilder b, GltfModel m ) {
        b.addCameraModels(m.getCameraModels());
        b.addImageModels(m.getImageModels());
        b.addMaterialModels(m.getMaterialModels());
        b.addMeshModels(m.getMeshModels());
        b.addNodeModels(m.getNodeModels());
        b.addSceneModels(m.getSceneModels());
        b.addSkinModels(m.getSkinModels());
        b.addTextureModels(m.getTextureModels());
    }

url

javagl commented 7 months ago

There is one issue that is indirectly related, which was brought up in https://github.com/javagl/JglTF/issues/98 and (hopefully) fixed in https://github.com/javagl/JglTF/pull/99 . But it may have to be tested more thoroughly, and it is not yet part of the current release.


But more importantly: It's not really clear what the goal is in your case. The GltfModelBuilder is rather intended for building a model from scratch. When you pass in data that already has the associated Accessor/BufferView/Buffer structure, then this will mess up the internal state.

I agree that this should NOT be the case. And I'd like to spend a bit more time to make these classes "better" in every way. But all I can say for now is that I'll try to look into it more closely. Since all this is just a spare time project, I cannot make promises.


An aside: If you wanted to copy a model, exactly, element-by-element, then you could just do

package de.javagl.jgltf.test.issue100;

import java.io.IOException;
import java.nio.file.Paths;

import de.javagl.jgltf.model.AccessorModel;
import de.javagl.jgltf.model.AnimationModel;
import de.javagl.jgltf.model.BufferModel;
import de.javagl.jgltf.model.BufferViewModel;
import de.javagl.jgltf.model.CameraModel;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.ImageModel;
import de.javagl.jgltf.model.MaterialModel;
import de.javagl.jgltf.model.MeshModel;
import de.javagl.jgltf.model.NodeModel;
import de.javagl.jgltf.model.SceneModel;
import de.javagl.jgltf.model.SkinModel;
import de.javagl.jgltf.model.TextureModel;
import de.javagl.jgltf.model.impl.DefaultAccessorModel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel;
import de.javagl.jgltf.model.impl.DefaultBufferModel;
import de.javagl.jgltf.model.impl.DefaultBufferViewModel;
import de.javagl.jgltf.model.impl.DefaultCameraModel;
import de.javagl.jgltf.model.impl.DefaultGltfModel;
import de.javagl.jgltf.model.impl.DefaultImageModel;
import de.javagl.jgltf.model.impl.DefaultMeshModel;
import de.javagl.jgltf.model.impl.DefaultNodeModel;
import de.javagl.jgltf.model.impl.DefaultSceneModel;
import de.javagl.jgltf.model.impl.DefaultSkinModel;
import de.javagl.jgltf.model.impl.DefaultTextureModel;
import de.javagl.jgltf.model.io.GltfModelReader;
import de.javagl.jgltf.model.io.GltfModelWriter;
import de.javagl.jgltf.model.v2.MaterialModelV2;

public class JgltfIssue100_NotReally
{
    public static void main(String[] args) throws IOException
    {
        GltfModelReader gltfModelReader = new GltfModelReader();

        String characterFileName = "./data/issue100/rpm_ibm.glb";
        String outputFileName = "./data/issue100/rpm_ibm_copied_somehow.glb";
        GltfModel gltfModelCharacter =
            gltfModelReader.read(Paths.get(characterFileName));

        DefaultGltfModel newModel = new DefaultGltfModel();
        addAll(newModel, gltfModelCharacter);

        GltfModelWriter gltfModelWriter = new GltfModelWriter();
        gltfModelWriter.writeBinary(newModel,
            Paths.get(outputFileName).toFile());
    }

    private static void addAll(DefaultGltfModel t, GltfModel m)
    {
        for (AccessorModel e : m.getAccessorModels())
        {
            t.addAccessorModel((DefaultAccessorModel) e);
        }
        for (AnimationModel e : m.getAnimationModels())
        {
            t.addAnimationModel((DefaultAnimationModel) e);
        }
        for (BufferModel e : m.getBufferModels())
        {
            t.addBufferModel((DefaultBufferModel) e);
        }
        for (BufferViewModel e : m.getBufferViewModels())
        {
            t.addBufferViewModel((DefaultBufferViewModel) e);
        }
        for (CameraModel e : m.getCameraModels())
        {
            t.addCameraModel((DefaultCameraModel) e);
        }
        for (ImageModel e : m.getImageModels())
        {
            t.addImageModel((DefaultImageModel) e);
        }
        for (MaterialModel e : m.getMaterialModels())
        {
            t.addMaterialModel((MaterialModelV2) e);
        }
        for (MeshModel e : m.getMeshModels())
        {
            t.addMeshModel((DefaultMeshModel) e);
        }
        for (NodeModel e : m.getNodeModels())
        {
            t.addNodeModel((DefaultNodeModel) e);
        }
        for (SceneModel e : m.getSceneModels())
        {
            t.addSceneModel((DefaultSceneModel) e);
        }
        for (SkinModel e : m.getSkinModels())
        {
            t.addSkinModel((DefaultSkinModel) e);
        }
        for (TextureModel e : m.getTextureModels())
        {
            t.addTextureModel((DefaultTextureModel) e);
        }
    }
}

But I guess that's not your goal...

CloseFile commented 7 months ago

First of all, thank you so much for your Project. This is the only way to work with the gltf file on Android. You're absolutely right. I'm copying the animation into a new file, like you said in that issue #83) And if the original RPM avatar is pre-passed through Babylon everything works fine with merge to animations. https://drive.google.com/file/d/10vF8ykICoSgEfrTBdKjH_0VgvCOvMLND/view?usp=sharing https://drive.google.com/file/d/14oeIXl8oizFXkNlrmb8T0phOHs7x5l-Z/view?usp=sharing https://drive.google.com/file/d/1dqXkO2dHVaEP0GawRZcFYJbcx4r4KLM4/view?usp=sharing

javagl commented 7 months ago

I had seen that comment // Build the whole thing and write the result in your code, and ... it looked familiar (it was from my workaround in issue 83)


Regarding this issue:

I have analyzed the reason, and ... there's a somewhat fundamental problem here: It is not possible to use the same AccessorModel object in two different models.


If someone wonders why, I'll try to illustrate that with an example that is structurally similar to the given use case, but simplified to the main point:

Imagine you have a model that has two accessors say, for POSITION and NORMAL of a mesh, When you read that from a file, then you'll have the accessor models, as the model.getAccessorModels() list with the two elements.

These models have all the information that an accessor (model) needs. Specifically: They have the BufferViewModel, which in turn refers to the BufferModel, and all these objects have their byteStride and byteOffset and byteLength values set so that they are "consistent".

For example, consider such POSITION and NORMAL accessors

In terms of data, assume that both accessors contain 3 elements, each being 3D float vectors.

In that case, one buffer view has an offset of 0, and the other buffer view has and offset of 36 into that buffer

When you now add only one of these accessors to the GltfModelBuilder and want to build a new model from that, then there's a fundamental inconsistency:

So it's not possible to make this consistent (i.e. as long as one would expect the AccessorModel objects to be ==-identical)


I have found a ""solution"" locally. But I'm nowhere near an idea about how to make this available.

The solution essentially is: When BufferStructureBuilder#addAccessorModel is called, then the given AccessorModel is modified and "reset": Its BufferViewModel is removed, its byteOffset and byteStride are reset, and it receives new AcessorData. But this would break a bunch of workflows, obviously. Something like

GltfModel model = reader.read(input);

// THIS WILL CHANGE THE ACCESSOR!
someBufferStructureBuilder.addAccessorModel(
    model.getAccessorModels().get(0));

writer.writeBinary(model, output);

would result in a broken model.

I'll have to think about a solution that basically copies/clones the ...Model instances at the right place. Probably, when they are added to the GltfModelBuilder. But that will require some refactoring (and possibly breaking changes). I'll try to allocate some time for that, but as I said: I still have to pay my rent, so can not make promises about the timeline here...

CloseFile commented 7 months ago

Thank you for the detailed explanation.

javagl commented 7 months ago

@CloseFile I'm still curious about the intended use case and desired usage pattern.

Very broadly speaking, there are constraints for the handling of the ...Model classes.

(These constraints are not explicitly documented. The documentation on the conceptual level has to be extended. There should be documentation that resembles a "tutorial", explaining the concepts and the intended usage patterns)

One constraint is: A ...Model object can not be used in different GltfModel instances.

There may be a few cases where this is possible. For example, a CameraModel just stores a bunch of float values, and it does not matter where this is used. But the constraint mainly applies to things that refer to other ...Model objects. For example: It is nearly impossible to ensure consistency when a NodeModel is used in two different GltfModels. The node might appear in different places in the hierarchy, and refer to a MeshModel that then also has to be present in both GltfModels, with all the implications along the line of the mesh->accessor->bufferView->buffer ownership chain, down to the lowest level like the byteOffset and byteLength of the buffer.

For your case, this mainly refers to the use of the GltfModelBuilder: It may cause inconsistencies when it receives ...Model objects that are already part of another GltfModel.

(I know, I did this in the linked issue. But I didn't have these constraints on the radar back then, and I have to develop clearer guidelines about what is and what isn't possible here...)


For me, some questions now are:


1. How to handle that internally

I could implement very strict checks. Very roughly: Every ...Model object could internally store a GltfModel owner, and whenever the object is added to a GltfModel, I could do a if (this.owner != null) throw new Exception("Object already has an owner") But really checking that everywhere could be a bit of effort, and I'll have to think this through more thorougly to see where this may have undesired consequences.


2. How to make these constraints clear (and easier to handle) for users

This is related to 1., insofar that the user would just receive such an exception, but then would be left with the question of how to achieve the actual goal. One approach could be (again, very roughly speaking) to offer some convenient "cloning" functionality. Roughly like that:

GltfModel gltfModelA = ...;
GltfModel gltfModelB = ...;

AccessorModel a = gltfModelA.getAccessorModels().get(0);

// NO! This would throw, because it's the wrong owner!
// gltfModelB.addAccessorModel(a); 

// The model 'B' may receive a "shallow copy", though:
gltfModelB.addAccessorModel(a.createShallowCopy());

But the concept of a "shallow copy" would raise some tricky questions as well, and I'll also have to think more about that.


3. What do you actually want to achieve?

From the snippet that you posted, it looks like you just wanted to copy a model, "in-memory". And it would be relatively easy to create a method like

GltfModel copy = GltfModels.createCopy(inputModel);

But I assume that you also wanted to do... something else (e.g. manipulate the model).


I'm aware of the requirement and the desired behavior. It would be cool if it was possible to just do something like

GltfModel model = ...;

// Remove some node
model.getNodeModels().remove(42);

// Create a new node and add it to the model
NodeModel node = createNewNode(maybeEventContainingSomeMesh);
add(model, node);

write(model, "output.glb");

and always ensure that the model is "consistent". This is an ambitious goal. (For example, the node that is removed there could be the target node of an animation channel. What should happen with the animation? Should the channel be removed automatically? Should the whole animation be removed? Should it cause an error message or warning?...)

But I'll try to allocte some time to find answers to the questions of...

CloseFile commented 7 months ago
  • how to make manipulations of existing GltfModel objects easier and less error-prone

I have not used the jgltf library before, and it is a black box for me at the moment, so please forgive me if my suggestion is not useful. In my view, the JGLTF potentially have all the options for manipulating the glb file. My current use case is to simply add animations to GLB file (in new or existed). As I mentioned earlier, if the file from Babylon, then animations can be added without any problems. Not only individual files, but they can also be successfully copied from a basic skeleton with up to 200 different animations. And it's very fast male.glb Also, it would be great to create a one scene from a different models and manipulate materials.

javagl commented 7 months ago

There are many things that would be "nice to have". In some cases, implementing these features requires a considerable amount of time and effort, and a thorough understanding of both the structure of glTF and the expectations (use cases) that people might have. For example, when you say that you want to "simply add animations", then I could ask many questions about details. For many of them, your answer could very well be "I don't care, it should just be simple, and it should work". But that's where the implementor has to put in all the effort.

However, I did some experiments. I'm doing this in the branch of https://github.com/javagl/JglTF/pull/99 , so everything that I write here refers to the state of this PR. Right now, this is very unstructured and experimental. But it contains a few smaller fixes (for the linked issues), and it already contains a check that refers to some of the points that I mentioned above (roughly: It will now throw an Exception when trying to do the thing that causes the inconsistencies).

For your goal of adding an animation to an existing model: That's theoretically already possible. And... you don't have to "copy" the model for that.

Here is a QUICK DRAFT that shows how an animation could be added to the model:

package de.javagl.jgltf.test.issue100;

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

import de.javagl.jgltf.model.AnimationModel.Channel;
import de.javagl.jgltf.model.AnimationModel.Interpolation;
import de.javagl.jgltf.model.AnimationModel.Sampler;
import de.javagl.jgltf.model.GltfConstants;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.MeshModel;
import de.javagl.jgltf.model.NodeModel;
import de.javagl.jgltf.model.creation.AccessorModels;
import de.javagl.jgltf.model.creation.BufferStructure;
import de.javagl.jgltf.model.creation.BufferStructureBuilder;
import de.javagl.jgltf.model.impl.DefaultAccessorModel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel.DefaultChannel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel.DefaultSampler;
import de.javagl.jgltf.model.impl.DefaultGltfModel;
import de.javagl.jgltf.model.impl.DefaultNodeModel;
import de.javagl.jgltf.model.io.Buffers;
import de.javagl.jgltf.model.io.GltfModelReader;
import de.javagl.jgltf.model.io.GltfModelWriter;

public class JgltfIssue100_Fix
{
    public static void main(String[] args) throws IOException
    {
        GltfModelReader gltfModelReader = new GltfModelReader();

        String characterFileName = "./data/issue100/rpm_ibm.glb";
        String outputFileName = "./data/issue100/rpm_ibm_with_animation.glb";
        GltfModel gltfModelCharacter =
            gltfModelReader.read(Paths.get(characterFileName));

        DefaultGltfModel model = (DefaultGltfModel) gltfModelCharacter;

        // Add an animation to the model:

        // Define the key frame times, and the values (weights) for the 
        // animation, and put them into accessors.
        float[] timesArray = new float[] { 0.0f, 1.0f, 2.0f, 3.0f, 4.0f };
        float[] valuesArray = new float[] { 
            0.0f, 0.0f, 
            0.0f, 1.0f, 
            1.0f, 0.0f, 
            1.0f, 1.0f, 
            0.0f, 0.0f, 
        };
        DefaultAccessorModel times = AccessorModels.createFloatScalar(
            FloatBuffer.wrap(timesArray));
        DefaultAccessorModel values =
            AccessorModels.create(GltfConstants.GL_FLOAT, "SCALAR", false,
                Buffers.createByteBufferFrom(FloatBuffer.wrap(valuesArray)));

        // Define the animation sampler that will interpolate
        // linearly between the times and values
        Sampler sampler =
            new DefaultSampler(times, Interpolation.LINEAR, values);

        // Find the node that contains the mesh with the `weights` 
        // that should be animated (done based on the name here...)
        String meshName = "Wolf3D_Head";
        DefaultNodeModel nodeModel = findNodeWithMeshWithName(model, meshName);
        if (nodeModel == null) {
            System.out.println("Node not found");
            return;
        }

        // Create the animation channel from the sampler and the node
        Channel channel = new DefaultChannel(sampler, nodeModel, "weights");

        // Create the actual animation
        DefaultAnimationModel animationModel = new DefaultAnimationModel();
        animationModel.addChannel(channel);

        // Build the buffer/bufferView/accessor structure for the new
        // animation data: Add the time/value accessor to the buffer
        // structure builder, and create the buffer structure
        BufferStructureBuilder b = new BufferStructureBuilder();
        b.addAccessorModel("times", times);
        b.addAccessorModel("vlaues", values);
        b.createBufferViewModel("animation", null);
        b.createBufferModel("animation", "animation.bin");
        BufferStructure bufferStructure = b.build();

        // Add the animation and buffers/bufferViews/accessors to the model
        model.addAnimationModel(animationModel);
        model.addAccessorModels(bufferStructure.getAccessorModels());
        model.addBufferViewModels(bufferStructure.getBufferViewModels());
        model.addBufferModels(bufferStructure.getBufferModels());

        GltfModelWriter gltfModelWriter = new GltfModelWriter();
        gltfModelWriter.writeBinary(model,
            Paths.get(outputFileName).toFile());
    }

    private static DefaultNodeModel findNodeWithMeshWithName(
        GltfModel gltfModel, String name)
    {
        List<NodeModel> nodeModels = gltfModel.getNodeModels();
        for (NodeModel nodeModel : nodeModels)
        {
            List<MeshModel> meshModels = nodeModel.getMeshModels();
            for (MeshModel meshModel : meshModels)
            {
                if (meshModel.getName().equals(name))
                {
                    return (DefaultNodeModel) nodeModel;
                }
            }
        }
        return null;

    }

}

The result is this... somehow creepy looking animated model:

JglTF Issue 100 animation

Attached here:

rpm_ibm_with_animation.zip

Now, the code in that is necessary for that is ... a QUICK DRAFT, and one could imagine that this could should be "simpler" in many ways. One could now natively wrap this into a function like

void addAnimation(GltfModel model, String nodeName, float times[], float values[]) {
....
}

But from a "library perspective", one has to think more broadly: What if someone wants to ...

In all these cases, one could wish for a "simple" solution. But properly hiding the nitty, gritty details about accessors/bufferViews/buffers, still having a simple API, and still offering all the power and flexibility that one could have by manually editing the JSON data has some challenges.

I'll try to let that sink in, and maybe try to come up with further ideas here...

CloseFile commented 7 months ago

Thank you very much for this clear example of morph animation. This is very helpful for understanding the GLTF format. I just added the accessories, byfferViewModels and byfferModels (thanks for the morph example) to the code from issue_83 Now animation can be successfully added to rpm avatar But I confuset new validator numInfos like: { "code": "UNUSED_OBJECT", "message": "This object may be unused.", "severity": 2, "pointer": "/accessors/269" },

            ...
            if (!newChannels.isEmpty()) {
                for (Channel newChannel : newChannels) {
                    newAnimationModel.addChannel(newChannel);
                }
//                newAnimationModels.add(newAnimationModel);
                model.addAnimationModel(newAnimationModel);

                // With this variant work with rpm & babylon models
                for (AccessorModel am : gltfModelAnimation.getAccessorModels()) {
                    model.addAccessorModel((DefaultAccessorModel) am);
                }
                for (BufferViewModel bvm : gltfModelAnimation.getBufferViewModels()) {
                    model.addBufferViewModel((DefaultBufferViewModel) bvm);
                }
                for (BufferModel bm : gltfModelAnimation.getBufferModels()) {
                    model.addBufferModel((DefaultBufferModel) bm);
                }
            }
        }

        new GltfModelWriter().writeBinary(model, Paths.get(outputFileName).toFile());
    }
javagl commented 7 months ago

Unrelated hint: If you want to insert a code block, then you can use three "backticks", as in

```
// Your code here
// With multiple lines
```

to format the code properly:

// Your code here
// With multiple lines

You can also use

```java
public class Example { ... }
```

For code highlighting, giving

public class Example { ... }

Regarding the validator warning: This warning does not appear for the output that I attached earlier. So you might have accidentally inserted an unused accessor. Maybe you can post more complete code (preferably, a complete, compileable example like the one that I posted above), or attach the output file here, so that we can figure out what that unused /accessors/269 actually is.

CloseFile commented 7 months ago

only one animation at a time

public class JgltfIssue83_MergeGlbs {
    public static void main(String[] args) throws Exception {

        GltfModelReader gltfModelReader = new GltfModelReader();

        String characterFileName = "data/mergeGlbFromIssue83/rpm_ibm.glb";
        String outputFileName = "data/mergeGlbFromIssue83/output.glb";
        String animationFileName = "data/mergeGlbFromIssue83/male-idle-animation.glb";

        GltfModel gltfModelAnimation = gltfModelReader.read(Paths.get(animationFileName));
        GltfModel gltfModelCharacter = gltfModelReader.read(Paths.get(characterFileName));

        DefaultGltfModel model = (DefaultGltfModel) gltfModelCharacter;

//        // Assume that there is exactly one skin model in the character:
        List<SkinModel> skinModels = gltfModelCharacter.getSkinModels();
        SkinModel skinModel = skinModels.get(0);

        List<AnimationModel> donorAnimationModels = gltfModelAnimation.getAnimationModels();
//        List<AnimationModel> newAnimationModels = new ArrayList<AnimationModel>();

        DefaultAnimationModel newAnimationModel = new DefaultAnimationModel();

         AnimationModel donorAnimationModel = donorAnimationModels.get(0);

            List<AnimationModel.Channel> newChannels = new ArrayList<Channel>();
            List<Channel> donorChannels = donorAnimationModel.getChannels();
            for (Channel donorChannel : donorChannels) {
                NodeModel channelNode = donorChannel.getNodeModel();
                String nodeName = channelNode.getName();

                // Try to find a node in the joints of the skin model
                // of the character input that has the same name as the
                // node of the animation channel of the animation input
                Optional<NodeModel> optionalNodeModel = skinModel.getJoints()
                        .stream()
                        .filter(n -> n.getName().equals(nodeName))
                        .findFirst();
                if (optionalNodeModel.isPresent()) {
                    System.out.println("Adding animation for " + nodeName);
                    Sampler sampler = donorChannel.getSampler();
                    NodeModel nodeModel = optionalNodeModel.get();
                    String path = donorChannel.getPath();
                    DefaultChannel newChannel = new DefaultChannel(sampler, nodeModel, path);
                    newChannels.add(newChannel);
                }
            }
            if (!newChannels.isEmpty()) {
                for (Channel newChannel : newChannels) {
                    newAnimationModel.addChannel(newChannel);
                }
//                newAnimationModels.add(newAnimationModel);
                model.addAnimationModel(newAnimationModel);
            }

            // With this variant work with rpm & babylon

            for (AccessorModel am : gltfModelAnimation.getAccessorModels()) {
                model.addAccessorModel((DefaultAccessorModel) am);
            }
            for (BufferViewModel bvm : gltfModelAnimation.getBufferViewModels()) {
                model.addBufferViewModel((DefaultBufferViewModel) bvm);
            }
            for (BufferModel bm : gltfModelAnimation.getBufferModels()) {
                model.addBufferModel((DefaultBufferModel) bm);
            }

        new GltfModelWriter().writeBinary(model, Paths.get(outputFileName).toFile());
    }
}
{
  "uri": "output.glb",
  "mimeType": "model/gltf-binary",
  "validatorVersion": "2.0.0-dev.3.9",
  "validatedAt": "2024-03-03T17:14:38.423Z",
  "issues": {
    "numErrors": 0,
    "numWarnings": 9,
    "numInfos": 16,
    "numHints": 0,
    "messages": [
      {
        "code": "NODE_SKINNED_MESH_NON_ROOT",
        "message": "Node with a skinned mesh is not root. Parent transforms will not affect a skinned mesh.",
        "severity": 1,
        "pointer": "/nodes/67"
      },
...
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/accessors/269"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/0"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/1"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/2"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/3"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/4"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/5"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/6"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/7"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/8"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/9"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/10"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/11"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/12"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/13"
      },
      {
        "code": "UNUSED_OBJECT",
        "message": "This object may be unused.",
        "severity": 2,
        "pointer": "/bufferViews/14"
      }
    ],
    "truncated": false
  },
...
    ],
    "animationCount": 1,
    "materialCount": 8,
    "hasMorphTargets": true,
    "hasSkins": true,
    "hasTextures": true,
    "hasDefaultScene": true,
    "drawCallCount": 9,
    "totalVertexCount": 7074,
    "totalTriangleCount": 11574,
    "maxUVs": 1,
    "maxInfluences": 4,
    "maxAttributes": 6
  }
}
javagl commented 7 months ago

You are adding all accessors/bufferViews/buffers from the "donor" model to the target model here:

            for (AccessorModel am : gltfModelAnimation.getAccessorModels()) {
                model.addAccessorModel((DefaultAccessorModel) am);
            }
            for (BufferViewModel bvm : gltfModelAnimation.getBufferViewModels()) {
                model.addBufferViewModel((DefaultBufferViewModel) bvm);
            }
            for (BufferModel bm : gltfModelAnimation.getBufferModels()) {
                model.addBufferModel((DefaultBufferModel) bm);
            }

Only some of these acccessor/bufferView/buffers refer to the animation. Most of them refer to the geometry (mesh data), and ... you're not using them in the target model. (And ... you may have noticed that the file became twice as large...)

I have no idea why you iterated over the "skin" nodes in the code that you posted. I cannot imagine that this worked for any model, and I assume that this was just the result of copying-and-pasting code together.

There are aspects of the "model building and handling" that should be improved and made more user-friendly in JglTF. This is on the radar. Beyond that, issues here should be used for tracking actual issues in JglTF. If you want me to write code to solve your problems: I'm a freelancer. You can hire me.

However, here's an example of how it's currently possible to transfer animations from one model to the other. You have to create new accessors in the target model, based on the animation channel sampler input/output accessors of the source model.

package de.javagl.jgltf.test.issue100;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import de.javagl.jgltf.model.AccessorData;
import de.javagl.jgltf.model.AccessorModel;
import de.javagl.jgltf.model.AnimationModel;
import de.javagl.jgltf.model.AnimationModel.Channel;
import de.javagl.jgltf.model.AnimationModel.Sampler;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.NodeModel;
import de.javagl.jgltf.model.creation.AccessorModels;
import de.javagl.jgltf.model.creation.BufferStructure;
import de.javagl.jgltf.model.creation.BufferStructureBuilder;
import de.javagl.jgltf.model.impl.DefaultAccessorModel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel.DefaultChannel;
import de.javagl.jgltf.model.impl.DefaultAnimationModel.DefaultSampler;
import de.javagl.jgltf.model.impl.DefaultGltfModel;
import de.javagl.jgltf.model.io.GltfModelReader;
import de.javagl.jgltf.model.io.GltfModelWriter;

public class JgltfIssue100_TransferAnimations
{
    public static void main(String[] args) throws IOException
    {
        GltfModelReader gltfModelReader = new GltfModelReader();

        String characterFileName = "./data/issue100/rpm_ibm.glb";
        String animationFileName = "./data/issue100/rpm_ibm_with_animation.glb";

        String outputFileName =
            "./data/issue100/rpm_ibm_merged_with_animation.glb";

        GltfModel gltfModelAnimation =
            gltfModelReader.read(Paths.get(animationFileName));
        GltfModel gltfModelCharacter =
            gltfModelReader.read(Paths.get(characterFileName));

        DefaultGltfModel model = (DefaultGltfModel) gltfModelCharacter;

        transferAnimation(gltfModelAnimation, model, 0);

        GltfModelWriter w = new GltfModelWriter();
        w.writeBinary(model, Paths.get(outputFileName).toFile());
    }

    private static void transferAnimation(
        GltfModel sourceModel, DefaultGltfModel targetModel, int animationIndex)
    {
        List<AnimationModel> sourceAnimationModels =
            sourceModel.getAnimationModels();
        AnimationModel sourceAnimationModel =
            sourceAnimationModels.get(animationIndex);

        // Try to translate the channels from the source, to put them
        // into the target. 
        List<Channel> targetChannels = new ArrayList<Channel>();

        // Collect accessor models for the animation.channel.sampler.input
        // and output data that have to be put into the target
        List<DefaultAccessorModel> targetAccessorModels =
            new ArrayList<DefaultAccessorModel>();

        List<Channel> sourceChannels = sourceAnimationModel.getChannels();
        for (Channel sourceChannel : sourceChannels)
        {
            NodeModel channelNode = sourceChannel.getNodeModel();
            String nodeName = channelNode.getName();

            // Try to find a node in the target that has the same name as the
            // node of the animation channel of the source
            Optional<NodeModel> optionalNodeModel = 
                targetModel.getNodeModels().stream()
                    .filter(n -> n.getName().equals(nodeName))
                    .findFirst();
            if (optionalNodeModel.isPresent())
            {
                System.out.println("Adding animation for " + nodeName);

                Sampler sourceSampler = sourceChannel.getSampler();
                AccessorModel sourceInput = sourceSampler.getInput();
                AccessorModel sourceOutput = sourceSampler.getOutput();

                // Create copies of the input/output accessors of
                // the sampler
                DefaultAccessorModel targetInput = copy(sourceInput);
                DefaultAccessorModel targetOutput = copy(sourceOutput);
                DefaultSampler targetSampler = new DefaultSampler(targetInput,
                    sourceSampler.getInterpolation(), targetOutput);

                targetAccessorModels.add(targetInput);
                targetAccessorModels.add(targetOutput);

                String path = sourceChannel.getPath();

                NodeModel targetNodeModel = optionalNodeModel.get();
                DefaultChannel targetChannel =
                    new DefaultChannel(targetSampler, targetNodeModel, path);
                targetChannels.add(targetChannel);

            }
            else
            {
                System.out.println("Could not find node " + nodeName);
            }
        }
        if (targetChannels.size() == 0) 
        {
            System.out.println("Could not create target channels");
            return;
        }

        DefaultAnimationModel targetAnimationModel =
            new DefaultAnimationModel();
        for (Channel targetChannel : targetChannels)
        {
            targetAnimationModel.addChannel(targetChannel);
        }

        // Build the buffer/bufferView/accessor structure for the new
        // animation data
        BufferStructureBuilder b = new BufferStructureBuilder();
        for (DefaultAccessorModel targetAccessorModel : targetAccessorModels)
        {
            b.addAccessorModel("animation", targetAccessorModel);
        }
        b.createBufferViewModel("animation", null);
        b.createBufferModel("animation", "animation.bin");
        BufferStructure bufferStructure = b.build();

        // Add the animation and buffers/bufferViews/accessors to the model
        targetModel.addAnimationModel(targetAnimationModel);
        targetModel.addAccessorModels(bufferStructure.getAccessorModels());
        targetModel.addBufferViewModels(bufferStructure.getBufferViewModels());
        targetModel.addBufferModels(bufferStructure.getBufferModels());
    }

    private static DefaultAccessorModel copy(AccessorModel accessorModel)
    {
        AccessorData accessorData = accessorModel.getAccessorData();
        ByteBuffer accessorDataBuffer = accessorData.createByteBuffer();
        DefaultAccessorModel copy =
            AccessorModels.create(accessorModel.getComponentType(),
                accessorModel.getElementType().toString(),
                accessorModel.isNormalized(), accessorDataBuffer);
        return copy;
    }

}
CloseFile commented 7 months ago

I would like to thank you very much for showing me the correct method. And thank you for your suggestion. If I need a feature to create one scene from several models, I will contact you.

javagl commented 2 months ago

This issue did dive into some specific details of the particular model and the code for handling it. Some of the issues brought up here have been addressed in https://github.com/javagl/JglTF/pull/108 . Further thoughts are tracked in https://github.com/javagl/JglTF/issues/105