From 3509aa95486fc700c3539a288981a57a1f78ada1 Mon Sep 17 00:00:00 2001 From: Mathias Westerdahl Date: Wed, 20 Sep 2023 15:33:37 +0200 Subject: [PATCH] Removed the `skeleton` property from the `.animationset` resource. (#7990) * Remap bones to index at runtime * test fix * Removed skeleton from the animationset * Updated tests * Fixed ModelUtil rename of root animation track to "root" * Fix for editor tests * test fixes * Fixed failing editor save test --------- Co-authored-by: Mats Gisselson --- .../dynamo/bob/pipeline/ColladaUtilTest.java | 27 +------ .../dynamo/bob/pipeline/ModelUtilTest.java | 11 ++- .../bob/pipeline/AnimationSetBuilder.java | 44 ++--------- .../com/dynamo/bob/pipeline/ColladaUtil.java | 73 ++++++++++++------- .../dynamo/bob/pipeline/MeshsetBuilder.java | 6 +- .../com/dynamo/bob/pipeline/ModelUtil.java | 71 +++++++----------- editor/src/clj/editor/animation_set.clj | 60 +++++---------- .../test/integration/animation_set_test.clj | 14 +--- editor/test/integration/build_test.clj | 1 - editor/test/integration/save_test.clj | 16 ++-- .../gamesys/resources/res_animationset.cpp | 6 +- .../src/gamesys/resources/res_skeleton.cpp | 4 +- .../dynamo/bob/pipeline/ModelImporter.java | 1 + engine/rig/proto/rig/rig_ddf.proto | 1 - engine/rig/src/test/test_rig.cpp | 6 +- 15 files changed, 126 insertions(+), 215 deletions(-) diff --git a/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ColladaUtilTest.java b/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ColladaUtilTest.java index 059a863f80a..5a465c98f62 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ColladaUtilTest.java +++ b/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ColladaUtilTest.java @@ -412,8 +412,8 @@ public void testObjectAnimations() throws Exception { @Test public void testSkeleton() throws Exception { - String[] boneIds = {"Bone", "Bone_001", "Bone_002", "Bone_003", "Bone_004"}; - String[] parentIds = {null, "Bone", "Bone", "Bone", "Bone"}; + String[] boneIds = {"root", "Bone_001", "Bone_002", "Bone_003", "Bone_004"}; + String[] parentIds = {null, "root", "root", "root", "root"}; Rig.Skeleton.Builder skeleton = Rig.Skeleton.newBuilder(); ColladaUtil.loadSkeleton(load("bone_influences.dae"), skeleton, new ArrayList()); @@ -808,29 +808,6 @@ public void testBlenderRotation() throws Exception { } } - /* - * Test that MeshSets and AnimationSets can have bones specified in different order. - */ - @Test - public void testBoneList() throws Exception { - Rig.MeshSet.Builder meshSetBuilder = Rig.MeshSet.newBuilder(); - Rig.AnimationSet.Builder animSetBuilder = Rig.AnimationSet.newBuilder(); - ColladaUtil.loadMesh(load("bonelist_mesh_test.dae"), meshSetBuilder); - ColladaUtil.loadAnimations(load("bonelist_anim_test.dae"), animSetBuilder, "", new ArrayList()); - - int meshBoneListCount = meshSetBuilder.getBoneListCount(); - int animBoneListCount = animSetBuilder.getBoneListCount(); - - assertEquals(3, meshBoneListCount); - assertEquals(3, animBoneListCount); - - for (int i = 0; i < meshBoneListCount; i++) { - Long meshBone = meshSetBuilder.getBoneList(i); - Long animBone = animSetBuilder.getBoneList(i); - assertEquals(meshBone, animBone); - } - } - /* * Test collada file with a bone animation that includes both translation and rotation. */ diff --git a/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ModelUtilTest.java b/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ModelUtilTest.java index cc2878c51ef..eb9fc1e4973 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ModelUtilTest.java +++ b/com.dynamo.cr/com.dynamo.cr.bob.test/src/com/dynamo/bob/pipeline/ModelUtilTest.java @@ -3,10 +3,10 @@ // Copyright 2009-2014 Ragnar Svensson, Christian Murray // Licensed under the Defold License version 1.0 (the "License"); you may not use // this file except in compliance with the License. -// +// // You may obtain a copy of the License, together with FAQs at // https://www.defold.com/license -// +// // Unless required by applicable law or agreed to in writing, software distributed // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -235,8 +235,7 @@ private ModelImporter.Scene loadBuiltScene(String path, ModelUtil.loadSkeleton(scene, skeletonBuilder); ArrayList animationIds = new ArrayList<>(); - ArrayList bones = ModelUtil.loadSkeleton(scene); - ModelUtil.loadAnimations(scene, bones, animSetBuilder, "top_anim", animationIds); + ModelUtil.loadAnimations(scene, animSetBuilder, "top_anim", animationIds); } return scene; } @@ -333,8 +332,8 @@ public void testBoneInfluences() throws Exception { @Test public void testSkeleton() throws Exception { - String[] boneIds = {"Bottom", "Middle", "Top"}; - String[] parentIds = {null, "Bottom", "Middle"}; + String[] boneIds = {"root", "Middle", "Top"}; + String[] parentIds = {null, "root", "Middle"}; Rig.Skeleton.Builder skeletonBuilder = Rig.Skeleton.newBuilder(); ModelImporter.Scene scene = loadBuiltScene("bend2bones.gltf", skeletonBuilder); diff --git a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/AnimationSetBuilder.java b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/AnimationSetBuilder.java index 5ef41c67d5b..45802f40ed6 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/AnimationSetBuilder.java +++ b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/AnimationSetBuilder.java @@ -57,11 +57,6 @@ public static void collectAnimations(Task.TaskBuilder taskBuilder, Project collectAnimations(taskBuilder, project, owner, subAnimSetDescBuilder); } } - - if(!animSetDescBuilder.getSkeleton().isEmpty()) { - IResource skeleton = BuilderUtil.checkResource(project, owner, "skeleton", animSetDescBuilder.getSkeleton()); - taskBuilder.addInput(skeleton); - } } @@ -92,7 +87,7 @@ private void validateAndAddFile(Task task, String path, ArrayList } private void buildAnimations(Task task, ModelImporter.DataResolver dataResolver, AnimationSetDesc.Builder animSetDescBuilder, AnimationSet.Builder animationSetBuilder, - String parentId, ArrayList bones, ArrayList animFiles) throws CompileExceptionError, IOException { + String parentId, ArrayList animFiles) throws CompileExceptionError, IOException { ArrayList idList = new ArrayList<>(animSetDescBuilder.getAnimationsCount()); for(AnimationInstanceDesc instance : animSetDescBuilder.getAnimationsList()) { @@ -103,7 +98,7 @@ private void buildAnimations(Task 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()), bones, animFiles); + buildAnimations(task, dataResolver, subAnimSetDescBuilder, animationSetBuilder, FilenameUtils.getBaseName(animFile.getPath()), animFiles); continue; } IResource animFile = BuilderUtil.checkResource(this.project, task.input(0), "animation", instance.getAnimation()); @@ -126,7 +121,7 @@ private void buildAnimations(Task task, ModelImporter.DataResolver dataRes if (isCollada) loadColladaAnimations(animBuilder, animFileIS, animId, parentId); else - loadModelAnimations(bones, animBuilder, animFileIS, dataResolver, animId, parentId, animFile.getPath(), animationIds); + loadModelAnimations(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); @@ -169,7 +164,7 @@ static void loadColladaAnimations(AnimationSet.Builder animationSetBuilder, Inpu animationSetBuilder.addAllAnimations(animBuilder.getAnimationsList()); } - static void loadModelAnimations(ArrayList bones, AnimationSet.Builder animationSetBuilder, + static void loadModelAnimations(AnimationSet.Builder animationSetBuilder, InputStream is, ModelImporter.DataResolver dataResolver, String animId, String parentId, String path, ArrayList animationIds) throws IOException { @@ -180,7 +175,7 @@ static void loadModelAnimations(ArrayList bones, AnimationSe // Currently, by design choice, each file must only contain one animation. // Our current approach is to choose the longest animation (to eliminate target poses etc) - ModelUtil.loadAnimations(scene, bones, animBuilder, animId, localAnimationIds); + ModelUtil.loadAnimations(scene, animBuilder, animId, localAnimationIds); animationSetBuilder.addAllAnimations(animBuilder.getAnimationsList()); @@ -213,7 +208,7 @@ public byte[] getData(String path, String uri) { }; // For the editor - static public void buildAnimations(List paths, ArrayList bones, List streams, ModelImporter.DataResolver dataResolver, List parentIds, + static public void buildAnimations(List paths, List streams, ModelImporter.DataResolver dataResolver, List parentIds, AnimationSet.Builder animationSetBuilder, ArrayList animationIds) throws IOException, CompileExceptionError { @@ -241,31 +236,17 @@ static public void buildAnimations(List paths, ArrayList buildSkeleton(IResource skeletonFile, ModelImporter.DataResolver dataResolver) throws IOException { - String suffix = BuilderUtil.getSuffix(skeletonFile.getPath()); - if (suffix.equals("dae")) { - return ColladaUtil.loadSkeleton(skeletonFile.getContent()); // Until our model importer supports collada - } - - ModelImporter.Scene skeletonScene = ModelUtil.loadScene(skeletonFile.getContent(), skeletonFile.getPath(), new ModelImporter.Options(), dataResolver); - ArrayList bones = ModelUtil.loadSkeleton(skeletonScene); - - return bones; - } - @Override public void build(Task task) throws CompileExceptionError, IOException { @@ -274,14 +255,7 @@ public void build(Task task) throws CompileExceptionError, IOException { AnimationSetDesc.Builder animSetDescBuilder = AnimationSetDesc.newBuilder(); TextFormat.merge(animSetDescISR, animSetDescBuilder); - IResource skeletonFile = BuilderUtil.checkResource(this.project, task.input(0), "skeleton", animSetDescBuilder.getSkeleton()); - ResourceDataResolver dataResolver = new ResourceDataResolver(this.project); - ArrayList bones = buildSkeleton(skeletonFile, dataResolver); - - if (bones.size() == 0) { - throw new CompileExceptionError(skeletonFile, -1, "No skeleton found in file!"); - } // evaluate hierarchy AnimationSet.Builder animationSetBuilder = AnimationSet.newBuilder(); @@ -290,9 +264,7 @@ public void build(Task task) throws CompileExceptionError, IOException { animFiles = new ArrayList(); animFiles.add(task.input(0).getAbsPath()); - buildAnimations(task, dataResolver, animSetDescBuilder, animationSetBuilder, "", bones, animFiles); - - ModelUtil.setBoneList(animationSetBuilder, bones); + buildAnimations(task, dataResolver, animSetDescBuilder, animationSetBuilder, "", animFiles); // write merged animationset ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024); diff --git a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ColladaUtil.java b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ColladaUtil.java index f12857331be..fbfe443388f 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ColladaUtil.java +++ b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ColladaUtil.java @@ -3,10 +3,10 @@ // Copyright 2009-2014 Ragnar Svensson, Christian Murray // Licensed under the Defold License version 1.0 (the "License"); you may not use // this file except in compliance with the License. -// +// // You may obtain a copy of the License, together with FAQs at // https://www.defold.com/license -// +// // Unless required by applicable law or agreed to in writing, software distributed // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -332,6 +332,7 @@ private static void sampleScaleTrack(Rig.RigAnimation.Builder animBuilder, Rig.A } public static void createAnimationTracks(Rig.RigAnimation.Builder animBuilder, + String boneName, Bone bone, RigUtil.AnimationTrack posTrack, RigUtil.AnimationTrack rotTrack, @@ -340,7 +341,7 @@ public static void createAnimationTracks(Rig.RigAnimation.Builder animBuilder, double spf = 1.0 / sampleRate; Rig.AnimationTrack.Builder animTrackBuilder = Rig.AnimationTrack.newBuilder(); - animTrackBuilder.setBoneId(MurmurHash.hash64(bone.getSourceId())); + animTrackBuilder.setBoneId(MurmurHash.hash64(boneName)); samplePosTrack(animBuilder, animTrackBuilder, posTrack, duration, startTime, sampleRate, spf, true); sampleRotTrack(animBuilder, animTrackBuilder, rotTrack, duration, startTime, sampleRate, spf, true); @@ -422,6 +423,12 @@ private static void boneAnimToDDF(XMLCOLLADA collada, Rig.RigAnimation.Builder a if (refIndex == null) continue; + String boneName = bone.getSourceId(); + if (bi == 0 || refIndex == 0) + { + boneName = "root"; + } + Matrix4d localToParent = getBoneLocalToParent(bone); AssetSpace assetSpace = getAssetSpace(collada.asset); if (bi != 0) { @@ -449,7 +456,7 @@ private static void boneAnimToDDF(XMLCOLLADA collada, Rig.RigAnimation.Builder a ExtractKeys(bone, localToParent, assetSpace, animation, posTrack, rotTrack, sclTrack); - createAnimationTracks(animBuilder, bone, posTrack, rotTrack, sclTrack, refIndex, (float)duration, sceneStartTime, sceneFrameRate); + createAnimationTracks(animBuilder, boneName, bone, posTrack, rotTrack, sclTrack, refIndex, (float)duration, sceneStartTime, sceneFrameRate); break; // we only support one animation per file/bone } @@ -529,8 +536,6 @@ public static void loadAnimations(XMLCOLLADA collada, Rig.AnimationSet.Builder a Long boneRef = MurmurHash.hash64(boneId); boneRefMap.put(boneRef, boneIndex); ++boneIndex; - - animationSetBuilder.addBoneList(boneRef); } // Animation clips @@ -555,7 +560,7 @@ public static void loadAnimations(XMLCOLLADA collada, Rig.AnimationSet.Builder a } String boneTarget = animation.getTargetBone(); - if (!boneToAnimations.containsKey(animation.getTargetBone())) { + if (!boneToAnimations.containsKey(boneTarget)) { boneToAnimations.put(boneTarget, new ArrayList()); } boneToAnimations.get(boneTarget).add(animation); @@ -984,10 +989,10 @@ public final int hashCode() { // fnv 32 bit hash meshSetBuilder.addModels(modelBuilder); meshSetBuilder.setMaxBoneCount(max_bone_count); - List boneRefArray = createBoneReferenceList(collada); - if (boneRefArray != null && !boneRefArray.isEmpty()) { - for (int i = 0; i < boneRefArray.size(); i++) { - meshSetBuilder.addBoneList(MurmurHash.hash64(boneRefArray.get(i))); + ArrayList bones = loadSkeleton(collada); + if (bones != null) { + for (ModelImporter.Bone bone : bones) { + meshSetBuilder.addBoneList(MurmurHash.hash64(bone.name)); } } } @@ -1286,14 +1291,20 @@ private static boolean validateMatrix4d(Matrix4d m) { private static void toDDF(ArrayList ddfBones, Bone bone, int parentIndex, Matrix4d parentWorldTransform) { com.dynamo.rig.proto.Rig.Bone.Builder b = com.dynamo.rig.proto.Rig.Bone.newBuilder(); - b.setName(bone.getName()); + // We'd like to do it outside, but since the Bone instances are read-only, we cannot do it. + if (parentIndex == BONE_NO_PARENT) { + b.setName("root"); + b.setId(MurmurHash.hash64("root")); + } else { + b.setName(bone.getName()); + b.setId(MurmurHash.hash64(bone.getSourceId())); + } + b.setParent(parentIndex); - b.setId(MurmurHash.hash64(bone.getSourceId())); Matrix4d localMatrix = getBoneLocalToParent(bone); if (!validateMatrix4d(localMatrix)) { logger.severe(String.format("Found invalid local matrix in bone '%s', replacing with identity matrix", bone.getName())); - System.out.printf("'%s' local matrix:\n", bone.getName()); printMatrix4d(localMatrix); localMatrix.setIdentity(); } @@ -1303,7 +1314,6 @@ private static void toDDF(ArrayList ddfBones, Bon if (!validateMatrix4d(worldMatrix)) { logger.severe(String.format("Found invalid world matrix in bone '%s', replacing with identity matrix", bone.getName())); - System.out.printf("'%s' world matrix:\n", bone.getName()); printMatrix4d(worldMatrix); worldMatrix.setIdentity(); } @@ -1458,6 +1468,27 @@ public static XMLCOLLADA loadScene(InputStream is) throws IOException, XMLStream return loadDAE(is); } + private static void flattenBoneTree(Bone colladaBone, ModelImporter.Bone parent, ArrayList out) { + String originalName = colladaBone.getSourceId(); + String newName = originalName; + if (parent == null) + newName = "root"; + + ModelImporter.Bone bone = new ModelImporter.Bone(); + bone.name = newName; + bone.parent = parent; + bone.node = null; + bone.index = out.size(); + bone.invBindPose = null; + + out.add(bone); + + for (int i = 0 ; i < colladaBone.numChildren(); ++i) { + Bone child = colladaBone.getChild(i); + flattenBoneTree(child, bone, out); + } + } + public static ArrayList loadSkeleton(XMLCOLLADA scene) throws IOException, XMLStreamException, LoaderException { ArrayList boneIds = new ArrayList<>(); ArrayList colladaBones = loadSkeleton(scene, boneIds); @@ -1466,17 +1497,7 @@ public static ArrayList loadSkeleton(XMLCOLLADA scene) throw return null; ArrayList bones = new ArrayList<>(); - for (Bone colladaBone : colladaBones) - { - ModelImporter.Bone bone = new ModelImporter.Bone(); - bone.name = colladaBone.getSourceId(); - bone.parent = null; // Do we need it in the editor? - bone.node = null; - bone.index = bones.size(); - bone.invBindPose = null; - - bones.add(bone); - } + flattenBoneTree(colladaBones.get(0), null, bones); return bones; } diff --git a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/MeshsetBuilder.java b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/MeshsetBuilder.java index 490c033b982..0b16be2ff28 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/MeshsetBuilder.java +++ b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/MeshsetBuilder.java @@ -187,11 +187,9 @@ public void build(Task task) throws CompileExceptionError, IOException { // Animationset { - ArrayList skeleton = ModelUtil.loadSkeleton(scene); - AnimationSet.Builder animationSetBuilder = AnimationSet.newBuilder(); - if (skeleton.size() > 0) { - ModelUtil.loadAnimations(scene, skeleton, animationSetBuilder, FilenameUtils.getBaseName(task.input(0).getPath()), new ArrayList()); + if (ModelUtil.getNumAnimations(scene) > 0) { + ModelUtil.loadAnimations(scene, animationSetBuilder, FilenameUtils.getBaseName(task.input(0).getPath()), new ArrayList()); } ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024); diff --git a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ModelUtil.java b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ModelUtil.java index 469975e8d65..619eab84902 100644 --- a/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ModelUtil.java +++ b/com.dynamo.cr/com.dynamo.cr.bob/src/com/dynamo/bob/pipeline/ModelUtil.java @@ -224,11 +224,11 @@ private static void copyKeys(ModelImporter.KeyFrame keys[], int componentSize, L } public static void createAnimationTracks(Rig.RigAnimation.Builder animBuilder, ModelImporter.NodeAnimation nodeAnimation, - Bone bone, double duration, double startTime, double sampleRate) { + String bone_name, double duration, double startTime, double sampleRate) { double spf = 1.0 / sampleRate; Rig.AnimationTrack.Builder animTrackBuilder = Rig.AnimationTrack.newBuilder(); - animTrackBuilder.setBoneId(MurmurHash.hash64(bone.name)); + animTrackBuilder.setBoneId(MurmurHash.hash64(bone_name)); { RigUtil.AnimationTrack sparseTrack = new RigUtil.AnimationTrack(); @@ -254,16 +254,10 @@ public static void createAnimationTracks(Rig.RigAnimation.Builder animBuilder, M animBuilder.addTracks(animTrackBuilder.build()); } - public static void setBoneList(Rig.AnimationSet.Builder animationSetBuilder, ArrayList bones) { - for (Bone bone : bones) { - animationSetBuilder.addBoneList(MurmurHash.hash64(bone.name)); - } - } - public static void loadAnimations(byte[] content, String suffix, ModelImporter.Options options, ModelImporter.DataResolver dataResolver, - ArrayList bones, Rig.AnimationSet.Builder animationSetBuilder, String parentAnimationId, ArrayList animationIds) throws IOException { + Rig.AnimationSet.Builder animationSetBuilder, String parentAnimationId, ArrayList animationIds) throws IOException { Scene scene = loadScene(content, suffix, options, dataResolver); - loadAnimations(scene, bones, animationSetBuilder, parentAnimationId, animationIds); + loadAnimations(scene, animationSetBuilder, parentAnimationId, animationIds); } private static Bone findBoneByName(ArrayList bones, String name) { @@ -283,21 +277,7 @@ public int compare(ModelImporter.Animation a, ModelImporter.Animation b) { } } - private static class SortOnNodeIndex implements Comparator { - public SortOnNodeIndex(ArrayList bones) { - this.bones = bones; - } - public int compare(ModelImporter.NodeAnimation a, ModelImporter.NodeAnimation b) { - Bone bonea = findBoneByName(this.bones, a.node.name); - Bone boneb = findBoneByName(this.bones, b.node.name); - int indexa = bonea == null ? 0xffffffff : bonea.index; - int indexb = boneb == null ? 0xffffffff : boneb.index; - return indexa - indexb; - } - private ArrayList bones; - } - - public static void loadAnimations(Scene scene, ArrayList fullSetBones, Rig.AnimationSet.Builder animationSetBuilder, + public static void loadAnimations(Scene scene, Rig.AnimationSet.Builder animationSetBuilder, String parentAnimationId, ArrayList animationIds) { Arrays.sort(scene.animations, new SortAnimations()); @@ -306,9 +286,11 @@ public static void loadAnimations(Scene scene, ArrayList ful System.out.printf("Scene contains more than one animation. Picking the the longest one ('%s')\n", scene.animations[0].name); } - // We want to use the internal skeleton from this scene to calculate the relative - // animation keys - ArrayList bones = ModelUtil.loadSkeleton(scene); + ArrayList bones = loadSkeleton(scene); + String prevRootName = null; + if (!bones.isEmpty()) { + prevRootName = bones.get(0).node.name; + } for (ModelImporter.Animation animation : scene.animations) { @@ -351,30 +333,19 @@ public static void loadAnimations(Scene scene, ArrayList ful animBuilder.setDuration(animation.duration); - Arrays.sort(animation.nodeAnimations, new SortOnNodeIndex(fullSetBones)); - for (ModelImporter.NodeAnimation nodeAnimation : animation.nodeAnimations) { - - Bone skeleton_bone = findBoneByName(fullSetBones, nodeAnimation.node.name); - Bone anim_bone = findBoneByName(bones, nodeAnimation.node.name); - if (skeleton_bone == null) { - System.out.printf("Warning: Animation uses bone '%s' that isn't present in the skeleton!\n", nodeAnimation.node.name); - continue; - } - if (anim_bone == null) { - System.out.printf("Warning: Animation uses bone '%s' that isn't present in the animation file!\n", nodeAnimation.node.name); - continue; + String nodeName = nodeAnimation.node.name; + if (prevRootName != null && prevRootName.equals(nodeName)) { + nodeName = "root"; } - createAnimationTracks(animBuilder, nodeAnimation, skeleton_bone, animation.duration, startTime, sampleRate); + createAnimationTracks(animBuilder, nodeAnimation, nodeName, animation.duration, startTime, sampleRate); } animationSetBuilder.addAnimations(animBuilder.build()); break; // we only support one animation per file } - - ModelUtil.setBoneList(animationSetBuilder, bones); } public static ArrayList loadMaterialNames(Scene scene) { @@ -756,6 +727,10 @@ public static int getNumSkins(Scene scene) { return scene.skins.length; } + public static int getNumAnimations(Scene scene) { + return scene.animations.length; + } + public static ArrayList loadSkeleton(Scene scene) { ArrayList skeleton = new ArrayList<>(); @@ -814,6 +789,10 @@ public static boolean loadSkeleton(Scene scene, com.dynamo.rig.proto.Rig.Skeleto // Generate DDF representation of bones. ArrayList ddfBones = new ArrayList(); for (Bone bone : boneList) { + if (bone.index == 0) { + bone.name = "root"; + } + boneToDDF(bone, ddfBones); } skeletonBuilder.addAllBones(ddfBones); @@ -928,16 +907,16 @@ public static void main(String[] args) throws IOException { System.out.printf("--------------------------------------------\n"); Rig.MeshSet.Builder meshSetBuilder = Rig.MeshSet.newBuilder(); - loadModels(scene, meshSetBuilder); + loadModels(scene, meshSetBuilder); // testing the function Rig.Skeleton.Builder skeletonBuilder = Rig.Skeleton.newBuilder(); - loadSkeleton(scene, skeletonBuilder); + loadSkeleton(scene, skeletonBuilder); // testing the function System.out.printf("Animations:\n"); Rig.AnimationSet.Builder animationSetBuilder = Rig.AnimationSet.newBuilder(); ArrayList animationIds = new ArrayList<>(); - loadAnimations(scene, bones, animationSetBuilder, file.getName(), animationIds); + loadAnimations(scene, animationSetBuilder, file.getName(), animationIds); for (ModelImporter.Animation animation : scene.animations) { System.out.printf(" Animation: %s\n", animation.name); diff --git a/editor/src/clj/editor/animation_set.clj b/editor/src/clj/editor/animation_set.clj index 41e0fb28c61..8588ec58f28 100644 --- a/editor/src/clj/editor/animation_set.clj +++ b/editor/src/clj/editor/animation_set.clj @@ -35,14 +35,13 @@ (def ^:private animation-set-icon "icons/32/Icons_24-AT-Animation.png") (defn is-animation-set? [resource] - (= (:ext resource) "animationset")) + (= (:ext resource) "animationset")) (defn- animation-instance-desc-pb-msg [resource] {:animation (resource/resource->proj-path resource)}) -(g/defnk produce-desc-pb-msg [skeleton animation-resources] - (let [pb {:skeleton (resource/resource->proj-path skeleton) - :animations (mapv animation-instance-desc-pb-msg animation-resources)}] +(g/defnk produce-desc-pb-msg [animation-resources] + (let [pb {:animations (mapv animation-instance-desc-pb-msg animation-resources)}] pb)) (defn- update-animation-info [resource animations-resource info] @@ -56,7 +55,7 @@ (-> info (assoc :parent-id parent-id) (assoc :parent-resource (or parent-resource resource))))) - + ;; Also used by model.clj (g/defnk produce-animation-info [resource animations-resource animation-infos] (into [] @@ -64,7 +63,7 @@ (map (partial update-animation-info resource animations-resource))) animation-infos)) -(defn- load-and-validate-animation-set [resource animations-resource bones animation-info] +(defn- load-and-validate-animation-set [resource animations-resource animation-info] (let [animations-resource (if (nil? animations-resource) resource animations-resource) paths (map (fn [x] (:path x)) animation-info) streams (map (fn [x] (io/input-stream (:resource x))) animation-info) @@ -78,19 +77,18 @@ project-path (workspace/project-path workspace) data-resolver (ModelUtil/createFileDataResolver project-path)] - (AnimationSetBuilder/buildAnimations paths bones streams data-resolver parent-ids animation-set-builder animation-ids) + (AnimationSetBuilder/buildAnimations 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)}))) ;; Also used by model.clj -(g/defnk produce-animation-set-info [_node-id bones resource animations-resource skeleton-resource animation-info] +(g/defnk produce-animation-set-info [_node-id resource animations-resource animation-info] (try - (if (or (empty? animation-info) - (nil? bones)) + (if (empty? animation-info) {:animation-set [] :animation-ids []} - (load-and-validate-animation-set resource animations-resource bones animation-info)) + (load-and-validate-animation-set resource animations-resource animation-info)) (catch LoaderException e (log/error :message (str "Error loading: " (resource/resource->proj-path resource)) :exception e) (g/->error _node-id :animations :fatal resource @@ -111,9 +109,8 @@ {:resource resource :content (protobuf/map->bytes Rig$AnimationSet (:animation-set user-data))}) -(g/defnk produce-animation-set-build-target [_node-id resource bones animation-set] - (when (not (or (nil? bones) - (empty? animation-set))) +(g/defnk produce-animation-set-build-target [_node-id resource animation-set] + (when (not (empty? animation-set)) (bt/with-content-hash {:node-id _node-id :resource (workspace/make-build-resource resource) @@ -124,12 +121,7 @@ {:navigation false :sections [{:title "Animation Set" - :fields [{:path [:skeleton] - :type :resource - :filter model-scene/model-file-types - :label "Skeleton" - :default nil} - {:path [:animations] + :fields [{:path [:animations] :type :list :label "Animations" :element {:type :resource @@ -142,9 +134,8 @@ (defn- clear-form-op [{:keys [node-id]} [property]] (g/clear-property! node-id property)) -(g/defnk produce-form-data [_node-id skeleton animation-resources] - (let [values {[:skeleton] skeleton - [:animations] animation-resources}] +(g/defnk produce-form-data [_node-id animation-resources] + (let [values {[:animations] animation-resources}] (-> form-sections (assoc :form-ops {:user-data {:node-id _node-id} :set set-form-op @@ -154,26 +145,11 @@ (g/defnode AnimationSetNode (inherits resource-node/ResourceNode) - (input skeleton-resource resource/Resource) (input animations-resource resource/Resource) (input animation-resources resource/Resource :array) (input animation-sets g/Any :array) (input animation-infos g/Any :array) (output animation-info g/Any :cached produce-animation-info) - - (input bones g/Any) - (output bones g/Any (gu/passthrough bones)) - - (property skeleton resource/Resource - (value (gu/passthrough skeleton-resource)) - (set (fn [evaluation-context self old-value new-value] - (project/resource-setter evaluation-context self old-value new-value - [:resource :skeleton-resource] - [:bones :bones]))) - (dynamic error (g/fnk [_node-id skeleton] - (or (validation/prop-error :info _node-id :skeleton validation/prop-nil? skeleton "Skeleton") - (validation/prop-error :fatal _node-id :skeleton validation/prop-resource-not-exists? skeleton "Skeleton")))) - (dynamic edit-type (g/constantly {:type resource/Resource :ext model-scene/model-file-types}))) (property animations resource/ResourceVec (value (gu/passthrough animation-resources)) @@ -205,18 +181,20 @@ (defn- load-animation-set [_project self resource pb] (let [proj-path->resource (partial workspace/resolve-resource resource) animation-proj-paths (map :animation (:animations pb)) - animation-resources (mapv proj-path->resource animation-proj-paths) - skeleton (workspace/resolve-resource resource (:skeleton pb))] + animation-resources (mapv proj-path->resource animation-proj-paths)] (g/set-property self - :skeleton skeleton :animations animation-resources))) +(defn- sanitize-animation-set [animation-set-desc] + (dissoc animation-set-desc :skeleton)) ; Deprecated field. + (defn register-resource-types [workspace] (resource-node/register-ddf-resource-type workspace :ext "animationset" :icon animation-set-icon :label "Animation Set" :load-fn load-animation-set + :sanitize-fn sanitize-animation-set :node-type AnimationSetNode :ddf-type Rig$AnimationSetDesc :view-types [:cljfx-form-view])) diff --git a/editor/test/integration/animation_set_test.clj b/editor/test/integration/animation_set_test.clj index 0ec1024d8fd..f9e5987361c 100644 --- a/editor/test/integration/animation_set_test.clj +++ b/editor/test/integration/animation_set_test.clj @@ -24,18 +24,14 @@ (deftest animation-set-test (test-util/with-loaded-project (let [node-id (test-util/resource-node project "/model/treasure_chest.animationset") - {:keys [animations bone-list]} (g/node-value node-id :animation-set)] - - (is (= 3 (count bone-list))) + {:keys [animations]} (g/node-value node-id :animation-set)] + (is (= 3 (count animations))) (is (= #{(murmur/hash64 "treasure_chest") (murmur/hash64 "treasure_chest_sub_animation/treasure_chest_anim_out") (murmur/hash64 "treasure_chest_sub_sub_animation/treasure_chest_anim_out")} - (set (map :id animations)))) - (let [animation (first animations) - bone-count (count bone-list)] - (testing "All bones are animated" - (is (<= bone-count (count (:tracks animation))))) + (set (map :id animations)))) + (let [animation (first animations)] (testing "No events present" (is (= 0 (count (:event-tracks animation))))) @@ -50,8 +46,6 @@ (map #(dissoc % :bone-id)) (map remove-empty-channels) (apply merge-with (constantly ::conflicting-data)))])))] - (testing "Bone exists in skeleton" - (is (some #(= bone-id %) bone-list))) (testing "Channels are not animated by multiple tracks" (doseq [[channel data] data-by-channel] diff --git a/editor/test/integration/build_test.clj b/editor/test/integration/build_test.clj index e368a79ea92..5c9158d89e1 100644 --- a/editor/test/integration/build_test.clj +++ b/editor/test/integration/build_test.clj @@ -179,7 +179,6 @@ (is (= (murmur/hash64 "Cube.006") (-> mesh-set :models first :id))) (is (= 3 (count (:bones skeleton)))) - (is (= (set (:bone-list mesh-set)) (set (:bone-list animation-set)))) (is (set/subset? (:bone-list mesh-set) (set (map :id (:bones skeleton))))) (is (contains? targets (:material (first (:materials pb))))) (is (contains? targets (:texture (first (:textures (first (:materials pb)))))))))}] diff --git a/editor/test/integration/save_test.clj b/editor/test/integration/save_test.clj index 6cc6bdc570d..5595e7995b1 100644 --- a/editor/test/integration/save_test.clj +++ b/editor/test/integration/save_test.clj @@ -262,11 +262,11 @@ ["/live_update/live_update.settings" #(set-setting! % ["liveupdate" "mode"] "Zip")]]] (with-clean-system (let [workspace (test-util/setup-scratch-workspace! world) - project (test-util/setup-project! workspace)] - (let [xf (comp (map :resource) - (map resource/resource->proj-path) - (filter (complement black-list))) - clean? (fn [] (empty? (into [] xf (g/node-value project :dirty-save-data))))] + project (test-util/setup-project! workspace)] + (let [save-data->proj-path (comp resource/resource->proj-path :resource) + xf (comp (map save-data->proj-path) + (filter (complement black-list))) + get-dirty-proj-paths (fn [] (into (sorted-set) xf (g/node-value project :dirty-save-data)))] ;; This first check is intended to verify that changes to the file ;; formats do not cause undue changes to existing content in game ;; projects. For example, adding fields to component protobuf @@ -280,7 +280,7 @@ ;; added protobuf field has a default value. But more drastic file ;; format changes have happened in the past, and you can find other ;; examples of :sanitize-fn usage in non-component resource types. - (is (clean?)) + (is (= #{} (get-dirty-proj-paths))) (doseq [[path f] paths] (testing (format "Verifying %s" path) (let [resource (test-util/resource workspace path) @@ -305,9 +305,9 @@ (let [node-id (test-util/resource-node project path)] (f node-id) (is (true? (dirty? node-id)))))) - (is (not (clean?))) + (is (= (into (sorted-set) (map first) paths) (get-dirty-proj-paths))) (test-util/save-project! project) - (is (clean?))))))) + (is (= #{} (get-dirty-proj-paths)))))))) (defn- setup-scratch [ws-graph] diff --git a/engine/gamesys/src/gamesys/resources/res_animationset.cpp b/engine/gamesys/src/gamesys/resources/res_animationset.cpp index 5f74484d0b1..b2136e8a7fa 100644 --- a/engine/gamesys/src/gamesys/resources/res_animationset.cpp +++ b/engine/gamesys/src/gamesys/resources/res_animationset.cpp @@ -3,10 +3,10 @@ // Copyright 2009-2014 Ragnar Svensson, Christian Murray // Licensed under the Defold License version 1.0 (the "License"); you may not use // this file except in compliance with the License. -// +// // You may obtain a copy of the License, together with FAQs at // https://www.defold.com/license -// +// // Unless required by applicable law or agreed to in writing, software distributed // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -101,5 +101,3 @@ namespace dmGameSystem } DM_DECLARE_RESOURCE_TYPE(ResourceTypeAnimationSet, "animationsetc", dmGameSystem::RegisterResourceTypeAnimationSet, 0); - - diff --git a/engine/gamesys/src/gamesys/resources/res_skeleton.cpp b/engine/gamesys/src/gamesys/resources/res_skeleton.cpp index e76a79a3050..779259cb327 100644 --- a/engine/gamesys/src/gamesys/resources/res_skeleton.cpp +++ b/engine/gamesys/src/gamesys/resources/res_skeleton.cpp @@ -3,10 +3,10 @@ // Copyright 2009-2014 Ragnar Svensson, Christian Murray // Licensed under the Defold License version 1.0 (the "License"); you may not use // this file except in compliance with the License. -// +// // You may obtain a copy of the License, together with FAQs at // https://www.defold.com/license -// +// // Unless required by applicable law or agreed to in writing, software distributed // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, either express or implied. See the License for the diff --git a/engine/modelc/src/java/com/dynamo/bob/pipeline/ModelImporter.java b/engine/modelc/src/java/com/dynamo/bob/pipeline/ModelImporter.java index fa422a06d60..71475586ea5 100644 --- a/engine/modelc/src/java/com/dynamo/bob/pipeline/ModelImporter.java +++ b/engine/modelc/src/java/com/dynamo/bob/pipeline/ModelImporter.java @@ -12,6 +12,7 @@ public class ModelImporter { + static final String ROOT_BONE_NAME = "root"; static final String LIBRARY_NAME = "modelc_shared"; static { diff --git a/engine/rig/proto/rig/rig_ddf.proto b/engine/rig/proto/rig/rig_ddf.proto index a2061f3604b..d47b71c4ab5 100644 --- a/engine/rig/proto/rig/rig_ddf.proto +++ b/engine/rig/proto/rig/rig_ddf.proto @@ -76,7 +76,6 @@ message RigAnimation message AnimationSet { repeated RigAnimation animations = 1; - repeated uint64 bone_list = 2; } message AnimationInstanceDesc diff --git a/engine/rig/src/test/test_rig.cpp b/engine/rig/src/test/test_rig.cpp index 2bd8061a699..b387c31a234 100644 --- a/engine/rig/src/test/test_rig.cpp +++ b/engine/rig/src/test/test_rig.cpp @@ -861,15 +861,11 @@ void SetUpSimpleRig(dmArray& bind_pose, dmHashTable64& CreateTestMesh(mesh_set, 1, 0, Vector4(0.5f, 0.4f, 0.3f, 0.2f)); CreateTestMesh(mesh_set, 1, 1, Vector4(1.0f, 0.9f, 0.8f, 0.7f)); - // We create bone lists for both the meshset and animationset, - // that is in "inverted" order of the skeleton hirarchy. mesh_set->m_BoneList.m_Data = new uint64_t[bone_count]; mesh_set->m_BoneList.m_Count = bone_count; - animation_set->m_BoneList.m_Data = mesh_set->m_BoneList.m_Data; - animation_set->m_BoneList.m_Count = bone_count; for (uint32_t i = 0; i < bone_count; ++i) { - mesh_set->m_BoneList.m_Data[i] = bone_count-i-1; + mesh_set->m_BoneList.m_Data[i] = i; } }