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

Added support for multiple animated nodes in a .gltf file #8038

Merged
merged 8 commits into from Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -86,7 +86,7 @@ private void validateAndAddFile(Task<Void> task, String path, ArrayList<String>
animFiles.add(path);
}

private void buildAnimations(Task<Void> task, ModelImporter.DataResolver dataResolver, AnimationSetDesc.Builder animSetDescBuilder, AnimationSet.Builder animationSetBuilder,
private void buildAnimations(Task<Void> task, boolean isAnimationSet, ModelImporter.DataResolver dataResolver, AnimationSetDesc.Builder animSetDescBuilder, AnimationSet.Builder animationSetBuilder,
String parentId, ArrayList<String> animFiles) throws CompileExceptionError, IOException {
ArrayList<String> idList = new ArrayList<>(animSetDescBuilder.getAnimationsCount());

Expand All @@ -98,12 +98,16 @@ private void buildAnimations(Task<Void> task, ModelImporter.DataResolver dataRes
InputStreamReader subAnimSetDescBuilderISR = new InputStreamReader(animFileIS);
AnimationSetDesc.Builder subAnimSetDescBuilder = AnimationSetDesc.newBuilder();
TextFormat.merge(subAnimSetDescBuilderISR, subAnimSetDescBuilder);
buildAnimations(task, dataResolver, subAnimSetDescBuilder, animationSetBuilder, FilenameUtils.getBaseName(animFile.getPath()), animFiles);
buildAnimations(task, true, dataResolver, subAnimSetDescBuilder, animationSetBuilder, FilenameUtils.getBaseName(animFile.getPath()), animFiles);
continue;
}
IResource animFile = BuilderUtil.checkResource(this.project, task.input(0), "animation", instance.getAnimation());
validateAndAddFile(task, animFile.getAbsPath(), animFiles);

// Previously we only allowed a model file to contain a single animation,
// but now we grab all animations inside it.
// However, for .animationset files we still expect each model file to contain one animation

String animId = (parentId.isEmpty() ? "" : parentId + "/" ) + FilenameUtils.getBaseName(animFile.getPath());
if(idList.contains(animId)) {
throw new CompileExceptionError(task.input(0), -1, "Animation id already exists: " + animId);
Expand All @@ -121,7 +125,7 @@ private void buildAnimations(Task<Void> task, ModelImporter.DataResolver dataRes
if (isCollada)
loadColladaAnimations(animBuilder, animFileIS, animId, parentId);
else
loadModelAnimations(animBuilder, animFileIS, dataResolver, animId, parentId, animFile.getPath(), animationIds);
loadModelAnimations(isAnimationSet, animBuilder, animFileIS, dataResolver, animId, parentId, animFile.getPath(), animationIds);

} catch (XMLStreamException e) {
throw new CompileExceptionError(animFile, e.getLocation().getLineNumber(), "Failed to load animation: " + e.getLocalizedMessage(), e);
Expand Down Expand Up @@ -164,7 +168,7 @@ static void loadColladaAnimations(AnimationSet.Builder animationSetBuilder, Inpu
animationSetBuilder.addAllAnimations(animBuilder.getAnimationsList());
}

static void loadModelAnimations(AnimationSet.Builder animationSetBuilder,
static void loadModelAnimations(boolean isAnimationSet, AnimationSet.Builder animationSetBuilder,
InputStream is, ModelImporter.DataResolver dataResolver, String animId, String parentId,
String path, ArrayList<String> animationIds) throws IOException {

Expand All @@ -173,9 +177,10 @@ static void loadModelAnimations(AnimationSet.Builder animationSetBuilder,
ArrayList<String> localAnimationIds = new ArrayList<String>();
AnimationSet.Builder animBuilder = AnimationSet.newBuilder();

// Currently, by design choice, each file must only contain one animation.
// Currently, by design choice (for animation sets), each file must only contain one animation.
// Our current approach is to choose the longest animation (to eliminate target poses etc)
ModelUtil.loadAnimations(scene, animBuilder, animId, localAnimationIds);
boolean topLevel = parentId.isEmpty();
ModelUtil.loadAnimations(scene, animBuilder, isAnimationSet ? animId : "", localAnimationIds);

animationSetBuilder.addAllAnimations(animBuilder.getAnimationsList());

Expand Down Expand Up @@ -208,7 +213,7 @@ public byte[] getData(String path, String uri) {
};

// For the editor
static public void buildAnimations(List<String> paths, List<InputStream> streams, ModelImporter.DataResolver dataResolver, List<String> parentIds,
static public void buildAnimations(boolean isAnimationSet, List<String> paths, List<InputStream> streams, ModelImporter.DataResolver dataResolver, List<String> parentIds,
AnimationSet.Builder animationSetBuilder, ArrayList<String> animationIds) throws IOException, CompileExceptionError {


Expand All @@ -220,6 +225,10 @@ static public void buildAnimations(List<String> paths, List<InputStream> streams

String baseName = FilenameUtils.getBaseName(path);

// Previously we only allowed a model file to contain a single animation,
// but now we grab all animations inside it.
// However, for .animationset files we still expect each model file to contain one animation

String animId = (parentId.isEmpty() ? "" : parentId + "/" ) + baseName;
if(animationIds.contains(animId)) {
throw new CompileExceptionError(String.format("Animation set contains duplicate entries for animation id '%s'", animId));
Expand All @@ -236,7 +245,7 @@ static public void buildAnimations(List<String> paths, List<InputStream> streams
if (isCollada)
loadColladaAnimations(animationSetBuilder, stream, animId, parentId);
else
loadModelAnimations(animationSetBuilder, stream, dataResolver, animId, parentId, path, animationIds);
loadModelAnimations(isAnimationSet, animationSetBuilder, stream, dataResolver, animId, parentId, path, animationIds);

} catch (XMLStreamException e) {
throw new CompileExceptionError(String.format("File %s:%d: Failed to load animation: %s", path, e.getLocation().getLineNumber(), e.getLocalizedMessage()), e);
Expand Down Expand Up @@ -264,7 +273,8 @@ public void build(Task<Void> task) throws CompileExceptionError, IOException {
animFiles = new ArrayList<String>();
animFiles.add(task.input(0).getAbsPath());

buildAnimations(task, dataResolver, animSetDescBuilder, animationSetBuilder, "", animFiles);
String suffix = BuilderUtil.getSuffix(task.input(0).getPath());
buildAnimations(task, suffix.equals("animationset"), dataResolver, animSetDescBuilder, animationSetBuilder, "", animFiles);

// write merged animationset
ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024);
Expand Down
Expand Up @@ -189,7 +189,7 @@ public void build(Task<Void> task) throws CompileExceptionError, IOException {
{
AnimationSet.Builder animationSetBuilder = AnimationSet.newBuilder();
if (ModelUtil.getNumAnimations(scene) > 0) {
ModelUtil.loadAnimations(scene, animationSetBuilder, FilenameUtils.getBaseName(task.input(0).getPath()), new ArrayList<String>());
ModelUtil.loadAnimations(scene, animationSetBuilder, "", new ArrayList<String>());
}

ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024);
Expand Down
Expand Up @@ -255,7 +255,8 @@ public static void createAnimationTracks(Rig.RigAnimation.Builder animBuilder, M
}

public static void loadAnimations(byte[] content, String suffix, ModelImporter.Options options, ModelImporter.DataResolver dataResolver,
Rig.AnimationSet.Builder animationSetBuilder, String parentAnimationId, ArrayList<String> animationIds) throws IOException {
Rig.AnimationSet.Builder animationSetBuilder, String parentAnimationId, boolean selectLongest,
ArrayList<String> animationIds) throws IOException {
Scene scene = loadScene(content, suffix, options, dataResolver);
loadAnimations(scene, animationSetBuilder, parentAnimationId, animationIds);
}
Expand All @@ -280,18 +281,15 @@ public int compare(ModelImporter.Animation a, ModelImporter.Animation b) {
public static void loadAnimations(Scene scene, Rig.AnimationSet.Builder animationSetBuilder,
String parentAnimationId, ArrayList<String> animationIds) {

Arrays.sort(scene.animations, new SortAnimations());

if (scene.animations.length > 1) {
System.out.printf("Scene contains more than one animation. Picking the the longest one ('%s')\n", scene.animations[0].name);
}

ArrayList<ModelImporter.Bone> bones = loadSkeleton(scene);
String prevRootName = null;
if (!bones.isEmpty()) {
prevRootName = bones.get(0).node.name;
}

boolean topLevel = parentAnimationId.isEmpty();
boolean selectLongest = !topLevel;

for (ModelImporter.Animation animation : scene.animations) {

Rig.RigAnimation.Builder animBuilder = Rig.RigAnimation.newBuilder();
Expand All @@ -303,15 +301,20 @@ public static void loadAnimations(Scene scene, Rig.AnimationSet.Builder animatio

animBuilder.setSampleRate(sampleRate);

// Each file is supposed to only have one animation
// And the animation name is created from outside of this function, depending on the source
// E.g. if the animation is from within nested .animationset files
// So we _dont_ do this:
// animationName = animation.name;
// but instead do this:
String animationName = parentAnimationId;
animBuilder.setId(MurmurHash.hash64(animationName));
animationIds.add(animationName);
// If the model file was selected directly
if (topLevel) {
animBuilder.setId(MurmurHash.hash64(animation.name));
animationIds.add(animation.name);
}
else {
// For animation sets, each model file should be named after the model file itself
// Each file is supposed to only have one animation
// And the animation name is created from outside of this function, depending on the source
// E.g. if the animation is from within nested .animationset files
animBuilder.setId(MurmurHash.hash64(parentAnimationId));
animationIds.add(parentAnimationId);
selectLongest = true;
}

// TODO: add the start time to the Animation struct!
for (ModelImporter.NodeAnimation nodeAnimation : animation.nodeAnimations) {
Expand Down Expand Up @@ -344,10 +347,22 @@ public static void loadAnimations(Scene scene, Rig.AnimationSet.Builder animatio

animationSetBuilder.addAnimations(animBuilder.build());

break; // we only support one animation per file
if (selectLongest)
{
break;
}
Comment on lines +350 to +353
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug fix.
Previously, we stored all animations (just renaming them all to the same name).
Now we only store the longest.

}
}

// For editor
public static ArrayList<String> getAnimationNames(Scene scene) {
ArrayList<String> names = new ArrayList<>();
for (ModelImporter.Animation animation : scene.animations) {
names.add(animation.name);
}
return names;
}

public static ArrayList<String> loadMaterialNames(Scene scene) {
ArrayList<String> materials = new ArrayList<>();
for (Material material : scene.materials) {
Expand Down Expand Up @@ -627,8 +642,9 @@ private static Rig.Model loadModel(Node node, Model model, ArrayList<ModelImport
modelBuilder.addMeshes(loadMesh(mesh));
}

modelBuilder.setId(MurmurHash.hash64(model.name));
modelBuilder.setId(MurmurHash.hash64(node.name)); // the node name is the human readable name (e.g Sword)
modelBuilder.setLocal(toDDFTransform(node.local));
modelBuilder.setBoneId(MurmurHash.hash64(model.boneParentName));

return modelBuilder.build();
}
Expand All @@ -645,34 +661,28 @@ private static void loadModelInstances(Node node, ArrayList<ModelImporter.Bone>
}
}

private static Node findFirstModelNode(Node node) {
private static void findModelNodes(Node node, List<Node> modelNodes) {
if (node.model != null) {
return node;
modelNodes.add(node);
}

for (Node child : node.children) {
Node modelNode = findFirstModelNode(child);
if (modelNode != null) {
return modelNode;
}
findModelNodes(child, modelNodes);
}

return null;
}

private static ModelImporter.Vec4 calcCenter(Scene scene) {
ModelImporter.Vec4 center = new ModelImporter.Vec4(0.0f, 0.0f, 0.0f, 0.0f);
float count = 0.0f;
for (Node root : scene.rootNodes) {
Node modelNode = findFirstModelNode(root);
if (modelNode == null) {
continue;
ArrayList<Node> modelNodes = new ArrayList<>();
findModelNodes(root, modelNodes);

for (Node modelNode : modelNodes) {
center.x += modelNode.local.translation.x;
center.y += modelNode.local.translation.y;
center.z += modelNode.local.translation.z;
count++;
}
center.x += modelNode.local.translation.x;
center.y += modelNode.local.translation.y;
center.z += modelNode.local.translation.z;
count++;
break; // TODO: Support more than one root node
}
center.x /= (float)count;
center.z /= (float)count;
Expand All @@ -682,21 +692,23 @@ private static ModelImporter.Vec4 calcCenter(Scene scene) {

private static void shiftModels(Scene scene, ModelImporter.Vec4 center) {
for (Node root : scene.rootNodes) {
Node modelNode = findFirstModelNode(root);
if (modelNode == null)
continue;
modelNode.local.translation.x -= center.x;
modelNode.local.translation.y -= center.y;
modelNode.local.translation.z -= center.z;
modelNode.world.translation.x -= center.x;
modelNode.world.translation.y -= center.y;
modelNode.world.translation.z -= center.z;
ArrayList<Node> modelNodes = new ArrayList<>();
findModelNodes(root, modelNodes);

for (Node modelNode : modelNodes) {
modelNode.local.translation.x -= center.x;
modelNode.local.translation.y -= center.y;
modelNode.local.translation.z -= center.z;
modelNode.world.translation.x -= center.x;
modelNode.world.translation.y -= center.y;
modelNode.world.translation.z -= center.z;
}
}
}

private static Scene loadInternal(Scene scene, Options options) {
ModelImporter.Vec4 center = calcCenter(scene);
shiftModels(scene, center); // We might make this optional
// Sort on duration. This allows us to return a list of sorted animation names
Arrays.sort(scene.animations, new SortAnimations());
return scene;
}

Expand All @@ -707,13 +719,12 @@ public static void loadModels(Scene scene, Rig.MeshSet.Builder meshSetBuilder) {

ArrayList<Rig.Model> models = new ArrayList<>();
for (Node root : scene.rootNodes) {
Node modelNode = findFirstModelNode(root);
if (modelNode == null) {
continue;
}
ArrayList<Node> modelNodes = new ArrayList<>();
findModelNodes(root, modelNodes);

loadModelInstances(modelNode, skeleton, models);
break; // TODO: Support more than one root node
for (Node modelNode : modelNodes) {
loadModelInstances(modelNode, skeleton, models);
}
}
meshSetBuilder.addAllModels(models);
meshSetBuilder.setMaxBoneCount(skeleton.size());
Expand Down Expand Up @@ -916,7 +927,7 @@ public static void main(String[] args) throws IOException {

Rig.AnimationSet.Builder animationSetBuilder = Rig.AnimationSet.newBuilder();
ArrayList<String> animationIds = new ArrayList<>();
loadAnimations(scene, animationSetBuilder, file.getName(), animationIds);
loadAnimations(scene, animationSetBuilder, "", animationIds);

for (ModelImporter.Animation animation : scene.animations) {
System.out.printf(" Animation: %s\n", animation.name);
Expand Down
3 changes: 2 additions & 1 deletion editor/src/clj/editor/animation_set.clj
Expand Up @@ -65,6 +65,7 @@

(defn- load-and-validate-animation-set [resource animations-resource animation-info]
(let [animations-resource (if (nil? animations-resource) resource animations-resource)
is-animation-set (is-animation-set? animations-resource)
paths (map (fn [x] (:path x)) animation-info)
streams (map (fn [x] (io/input-stream (:resource x))) animation-info)
;; clean up the parent-id if it's the current resource
Expand All @@ -77,7 +78,7 @@
project-path (workspace/project-path workspace)
data-resolver (ModelUtil/createFileDataResolver project-path)]

(AnimationSetBuilder/buildAnimations paths streams data-resolver parent-ids animation-set-builder animation-ids)
(AnimationSetBuilder/buildAnimations is-animation-set paths streams data-resolver parent-ids animation-set-builder animation-ids)
(let [animation-set (protobuf/pb->map (.build animation-set-builder))]
{:animation-set animation-set
:animation-ids (vec animation-ids)})))
Expand Down
4 changes: 2 additions & 2 deletions editor/src/clj/editor/model.clj
Expand Up @@ -46,12 +46,12 @@
(when is-single-anim
(rig/make-animation-set-build-target (resource/workspace resource) _node-id animation-set))))

(g/defnk produce-animation-ids [_node-id resource animations-resource animation-set-info animation-set]
(g/defnk produce-animation-ids [_node-id resource animations-resource animation-set-info animation-set animation-ids]
(let [is-single-anim (or (empty? animation-set)
(not (animation-set/is-animation-set? animations-resource)))]
(if is-single-anim
(if animations-resource
[(resource/base-name animations-resource)] ; single animation file
animation-ids
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While technically "breaking" the api here, I also consider it a bug fix.
Also, I believe few users actually set a single .gltf file here, and they got the longest animation in the file.

At most, they'll get a build error, which is easy to fix.

Note, I haven't change any code for the Collada code path, as we have deprecated that. So that should remain the same.

[])
(:animation-ids animation-set-info))))

Expand Down
2 changes: 1 addition & 1 deletion editor/src/clj/editor/model_loader.clj
Expand Up @@ -56,7 +56,7 @@
scene (ModelUtil/loadScene stream ^String path options data-resolver)
bones (ModelUtil/loadSkeleton scene)
material-ids (ModelUtil/loadMaterialNames scene)
animation-ids (ArrayList.)]
animation-ids (ModelUtil/getAnimationNames scene)] ; sorted on duration (largest first)
(when-not (empty? bones)
(ModelUtil/skeletonToDDF bones skeleton-builder))
(ModelUtil/loadModels scene mesh-set-builder)
Expand Down