How can I export DAE files for use in Scene Kit without seeing "untitled-animations"?
Asked Answered
A

11

16

I am trying to load animations created in Cheetah 3D and Blender 3D into Scene Kit, but all I get is a bunch of "untitled-animations" with each one being the same animation.

Does anyone know how to properly export these from Blender or Cheetah 3D so that Scene Kit can use them?

Aback answered 2/7, 2014 at 19:35 Comment(5)
You might have better results if you rephrase your question, e.g. "how do I get Blender to export DAE for use in SceneKit such that (feature X) does (thing Y)?"Grog
Thanks rick. 3 more characters to go. LOL!Aback
I reworded the title and body to make this focused on your question, so I went ahead and reopened it.Jessamyn
Does this answer your question? How to get a ordinary Mixamo character animation working in SceneKit?Comment
Absolutely total 2023 solution: https://mcmap.net/q/245407/-how-to-get-a-ordinary-mixamo-character-animation-working-in-scenekitComment
U
35

I dug into this because it was annoying me too. All of the "untitled animations" are individual animations for each bone. You can get the id from the attributes inspecter in the panel on the right side of xcode. Using swift like so, you can get your animation.

let urlOfScene = Bundle.main.url(forResources: "your url", withExtension: "dae")
let source = SCNSceneSource(url: urlOfScene, options: nil)
let armature = source.entryWithIdentifier("Armature", withClass: SCNNode.self) as SCNNode

let animation = armature.entryWithIdentifier("your bone id", withClass: CAAnimation.self) as CAAnimation

This must be done for All the bones in your armature. **Annoying!!!*

Apple used 3dmax for all their sample projects which only show one animation for each collada file. This is because 3dmax exports all the bones under one animation while blender seperates each bone.

Temp Work Around Use TextEdit or append a .xml extension on the end of your .dae file and open it with an xml editor (plenty are free online). xml editors are a little easier to work with. Scroll down to the animation start block. It will look like so...

<library_animations>
<animation id= "first_bone"> 
<source id= "first_bone-input"> 

Change it to...

<library_animations>
<animation>                      ---this is the only modified line
<source id="first_bone-input"> 

At the end of each animation will be an end block like so...

</animtion>                      ---delete this line as long as its not your last bone    
<animation id="second_bone">     ---delete this line too
<source id="second_bone-input">  

Of course at the end of your last bone leave the animation end block like so...

</animation>
</library_animations>

This will give you a single animation within your .dae file which has the same name as your file with a -1 appended to the end!

EDIT - Here is a link to an Automator service that will convert the above code for you!

Automater collada converter download

Unzip and drop the file in your ~/Library/services folder. From there you can just right click on your collada file and scroll down to ConvertToXcodeCollada and presto! A window will pop up when complete (about half a second).

Uda answered 24/7, 2014 at 3:35 Comment(8)
+1 interesting - just a follow up question: any tutorial or instructions somewhere how to export from blender so that we can use in SceneKit? I can't get textures working - only the meshes.Dichromatism
I can make one and upload but not until later today. I also made an Automater app which uses a sed command in terminal to take care of the above file modification. I will post a link here later.Uda
Just added a link to a service to convert file to Xcode compatible Blender animation. Enjoy! Working on tutorial for textures and Scenekit!Uda
Really looking forward to the Blender tutorial. I have been looking around for a solution for a couple of days now - no luck. There really is no support from Blender regarding SceneKit, at least not yet...Dichromatism
Jonny I just posted in the blender sight where you asked about up axis and textures some pointer walk throughs. Hope thats helps till I can make an in depth tutorial.Uda
Jonny dont forget to select as chosen answer if this fixed your problem. Thx!Uda
@Uda Thanks, I spent too much time trying to understand the problem, your solution works fine : convert to xml, change the animations line, add my own "id", and convert back to .dae . It works fine. Thanks!Sententious
Link to "Automater collada converter download" doesn't work. Any chance you can fix or re-upload somewhere?Phyllotaxis
W
4

This is because each bone in your .dae file has its own <animation> tag.

FlippinFun correctly states that removing all opening and closing <animation> tags except for the very first and last will group the animation together, making it accessible within Xcode by the identifier FileName-1.

I happen to be using a MayaLT > .FBX > .DAE workflow and found that the service he linked does not work for me. This is because my .dae file was formatted poorly with some <source> tags on the same line as double nested <animation> tags. As a result the entire line was removed, corrupting the .dae file.

For anyone else using this workflow, here is the sed command I am running to clean up, hope it's helpful to someone!

sed -i .bak -e 's/\(.*\)<animation id.*><animation>\(.*\)/\1\2/g; s/\(.*\)<\/animation><\/animation>\(.*\)/\1\2/g; s/\(.*\)<library_animations>\(.*\)/\1<library_animations><animation>\2/g; s/\(.*\)<\/library_animations>\(.*\)/\1<\/animation><\/library_animations>\2/g' Walk.dae
Withdrawal answered 8/10, 2017 at 16:48 Comment(0)
D
3

In case anyone else finds it useful I've written a python script which does just this. Providing an array of file paths the script will combine the animations into a single animation, remove geometry, and remove materials.

This new, slimmer dae can be used as your scenekit animation as long as the bones of your model you are applying the animation to are named and match exactly (as they should).

#!/usr/local/bin/python
# Jonathan Cardasis, 2018
#
# Cleans up a collada `dae` file removing all unnessasary data
# only leaving animations and bone structures behind.
# Combines multiple animation sequences into a single animation
# sequence for Xcode to use.
import sys
import os
import re
import subprocess

def print_usage(app_name):
  print 'Usage:'
  print '  {} [path(s) to collada file(s)...]'.format(app_name)
  print ''

def xml_is_collada(xml_string):
    return bool(re.search('(<COLLADA).*(>)', xml_string))

################
##    MAIN    ##
################
DAE_TAGS_TO_STRIP = ['library_geometries', 'library_materials', 'library_images']

if len(sys.argv) < 2:
    app_name = os.path.basename(sys.argv[0])
    print_usage(app_name)
    sys.exit(1)

print 'Stripping collada files of non-animation essential features...'
failed_file_conversions = 0

for file_path in sys.argv[1:]:
    try:
        print 'Stripping {} ...'.format(file_path)
        dae_filename = os.path.basename(file_path)
        renamed_dae_path = file_path + '.old'

        dae = open(file_path, 'r')
        xml_string = dae.read().strip()
        dae.close()

        # Ensure is a collada file
        if not xml_is_collada(xml_string):
            raise Exception('Not a proper Collada file.')

        # Strip tags
        for tag in DAE_TAGS_TO_STRIP:
            xml_string = re.sub('(?:<{tag}>)([\s\S]+?)(?:</{tag}>)'.format(tag=tag), '', xml_string)

        # Combine animation keys into single key:
        #  1. Remove all <animation> tags.
        #  2. Add leading and trailing <library_animation> tags with single <animation> tag between.
        xml_string = re.sub(r'\s*(<animation[^>]*>)\s*', '\n', xml_string)
        xml_string = re.sub(r'\s*(<\/animation\s*>.*)\s*', '', xml_string)

        xml_string = re.sub(r'\s*(<library_animations>)\s*', '<library_animations>\n<animation>\n', xml_string)
        xml_string = re.sub(r'\s*(<\/library_animations>)\s*', '\n</animation>\n</library_animations>', xml_string)

        # Rename original and dump xml to previous file location
        os.rename(file_path, renamed_dae_path)
        with open(file_path, 'w') as new_dae:
            new_dae.write(xml_string)
            print 'Finished processing {}. Old file can be found at {}.\n'.format(file_path, renamed_dae_path)
    except Exception as e:
        print '[!] Failed to correctly parse {}: {}'.format(file_path, e)
        failed_file_conversions += 1

if failed_file_conversions > 0:
    print '\nFailed {} conversion(s).'.format(failed_file_conversions)
    sys.exit(1)

Usage: python cleanupForXcodeColladaAnimation.py dancing_anim.dae

https://gist.github.com/joncardasis/e815ec69f81ed767389aa7a878f3deb6

Dactylography answered 12/6, 2018 at 3:10 Comment(0)
D
1

I'm working on the same issue but I'm using Maya, if you download SceneKit slides for WWDC 2014

The file AAPLSlideAnimationEvents.m has some examples of importing DAE files with multiple “untitled-animations” as you describe, hope it helps

Disingenuous answered 8/7, 2014 at 4:18 Comment(0)
P
1

Here is my modification of n33kos's script to work with Mixamo resources.

sed -i .bak -e 's/\(.*\)<animation id.*<source\(.*\)/\1<source\2/g; s/\(.*\)<\/animation>\(.*\)/\1\2/g; s/\(.*\)<library_animations>\(.*\)/\1<library_animations><animation>\2/g; s/\(.*\)<\/library_animations>\(.*\)/\1<\/animation><\/library_animations>\2/g' Standing_Idle.dae
Paradies answered 10/11, 2017 at 8:40 Comment(0)
Y
1

Maybe the following observation is useful for someone: i directly imported untouched mixamo .dae (with animations) into xcode 10.2.1 In my case the Character "Big Vegas" (with a samba dance). the following code gives a list id's of animations:

var sceneUrl = Bundle.main.url(forResource: "Art.scnassets/Elvis/SambaDancingFixed", withExtension: "dae")!

    if let sceneSource = SCNSceneSource(url: sceneUrl, options: nil){

        let caAnimationIDs = sceneSource.identifiersOfEntries(withClass: CAAnimation.self)

        caAnimationIDs.forEach({id in
            let anAnimation = sceneSource.entryWithIdentifier(id, withClass: CAAnimation.self)
            print(id,anAnimation)
        })    
    }

OUTPUT:

animation/1 Optional(<CAAnimationGroup:0x283c05fe0; animations = (
"SCN_CAKeyframeAnimation 0x28324f5a0 (duration=23.833332, keyPath:/newVegas_Hips.transform)",
"SCN_CAKeyframeAnimation 0x28324f600 (duration=23.833332, keyPath:/newVegas_Pelvis.transform)",
"SCN_CAKeyframeAnimation 0x28324f690 (duration=23.833332, keyPath:/newVegas_LeftUpLeg.transform)",
"SCN_CAKeyframeAnimation 0x28324f750 (duration=23.833332, keyPath:/newVegas_LeftLeg.transform)",
"SCN_CAKeyframeAnimation 0x28324f810 (duration=23.833332, keyPath:/newVegas_LeftFoot.transform)",
"SCN_CAKeyframeAnimation 0x28324f8d0 (duration=23.833332, keyPath:/newVegas_RightUpLeg.transform)",
... and so on ...

As you may notice, "animation/1" appears to be an animation group which can be accessed with:

let sambaAnimation = sceneSource.entryWithIdentifier("animation/1", withClass: CAAnimation.self)

"sambaAnimation" can be applied to the parent-node of "Big Vegas":

self.addAnimation(sambaAnimation, forKey: "Dance")

in the case you download the same character with other animations on it you can pull out the animation as described:

let animation = sceneSource.entryWithIdentifier("animation/1", withClass: CAAnimation.self)

and apply it to your character.

Yiyid answered 27/6, 2019 at 9:26 Comment(1)
priceless info! but I can't seem to get it to play as such ...Comment
B
1

For me automator script don't work.. I wrote small python script to combine all animations into one.

import sys
import re

fileNameIn = sys.argv[1]
fileNameOut = fileNameIn + '-e' #Output file will contain suffix '-e'

fileIn = open(fileNameIn, 'r')

data = fileIn.read()

fileIn.close()

splitted = re.split(r'<animation id=[^>]+>', data)

result = splitted[0] + '<animation>' + "".join(splitted[1:])

splitted = result.split('</animation>')

result = "".join(splitted[:-1]) + '</animation>' + splitted[-1]

fileOut = open(fileNameOut, 'wt')

fileOut.write(result)

fileOut.close()

You can fount it here: link

Usage: python fix_dae_script.py <file.dae>

Bassesalpes answered 4/1, 2020 at 15:51 Comment(1)
surely python fix_dae_script.py <file.dae> these days on Mac!Comment
G
0

Delete the animations tags (see FlippinFun's solution) and read the link below on how to prepare Blender animations for play back with SceneKit (instructions are not for SceneKit but they work pretty well).

http://trac.wildfiregames.com/wiki/AnimationExportTutorial

Garlaand answered 5/8, 2014 at 3:10 Comment(0)
L
0

Here is how you can delete unnecessary xml nodes:

let currentDirectory = NSFileManager.defaultManager().currentDirectoryPath
let files = (try! NSFileManager.defaultManager().contentsOfDirectoryAtPath(currentDirectory)).filter { (fname:String) -> Bool in
    return NSString(string: fname).pathExtension.lowercaseString == "dae"
    }.map { (fname: String) -> String in
    return "\(currentDirectory)/\(fname)"
}
//print(files)

for file in files {
    print(file)
    var fileContent = try! NSString(contentsOfFile: file, encoding: NSUTF8StringEncoding)

    // remove all but not last </animation>
    let closing_animation = "</animation>"
    var closing_animation_last_range = fileContent.rangeOfString(closing_animation, options: NSStringCompareOptions.CaseInsensitiveSearch.union(NSStringCompareOptions.BackwardsSearch))
    var closing_animation_current_range = fileContent.rangeOfString(closing_animation, options: NSStringCompareOptions.CaseInsensitiveSearch)
    while closing_animation_current_range.location != closing_animation_last_range.location {
        fileContent = fileContent.stringByReplacingCharactersInRange(closing_animation_current_range, withString: "")
        closing_animation_current_range = fileContent.rangeOfString(closing_animation, options: NSStringCompareOptions.CaseInsensitiveSearch)
        closing_animation_last_range = fileContent.rangeOfString(closing_animation, options: NSStringCompareOptions.CaseInsensitiveSearch.union(NSStringCompareOptions.BackwardsSearch))
    }

    // remove all but not first <animation .. >
    let openning_animation_begin = "<animation "
    let openning_animation_end = ">"

    let openning_animation_begin_range = fileContent.rangeOfString(openning_animation_begin, options:NSStringCompareOptions.CaseInsensitiveSearch)
    var openning_animation_end_range = fileContent.rangeOfString(openning_animation_begin, options: NSStringCompareOptions.CaseInsensitiveSearch.union(NSStringCompareOptions.BackwardsSearch))

    while openning_animation_begin_range.location != openning_animation_end_range.location {
        let openning_animation_end_location = fileContent.rangeOfString(openning_animation_end, options: .CaseInsensitiveSearch, range: NSRange.init(location: openning_animation_end_range.location, length: openning_animation_end.characters.count))
        let lengthToRemove = NSString(string: fileContent.substringFromIndex(openning_animation_end_range.location)).rangeOfString(openning_animation_end, options:NSStringCompareOptions.CaseInsensitiveSearch).location + openning_animation_end.characters.count
        let range = NSRange.init(location: openning_animation_end_range.location, length: lengthToRemove)
        fileContent = fileContent.stringByReplacingCharactersInRange(range, withString: "")
        openning_animation_end_range = fileContent.rangeOfString(openning_animation_begin, options: NSStringCompareOptions.CaseInsensitiveSearch.union(NSStringCompareOptions.BackwardsSearch))
    }

    // save
    try! fileContent.writeToFile(file, atomically: true, encoding: NSUTF8StringEncoding)
}
Lonna answered 11/2, 2016 at 19:24 Comment(0)
C
0

Here's a full script that you can use for doing the same thing without regular expressions. Copy paste the below code into a file like prep_dae_for_scenekit.py.

Convert your file by doing ./prep_dae_for_scenekit.py input.dae -o output.dae.

#!/usr/bin/env python3
import xml.etree.ElementTree as ET
import argparse


def main():
    """Read the existing filename, retrieve all animation elements and combine all the sub elements into a single animation element."""

    input_filename, output_filename = parse_arguments()

    # Register default namespace. We want to set this so our new file isn't prepended with ns0.
    # We have to set this before reading the file so thats why we're parsing twice.
    tree = ET.parse(input_filename)
    root = tree.getroot()

    namespace_url = root.tag.split("{")[1].split("}")[0]
    namespace = f"{{{namespace_url}}}"

    ET.register_namespace("", namespace_url)

    # Parse the file
    print(f"Parsing filename '{input_filename}'")
    tree = ET.parse(input_filename)
    root = tree.getroot()

    library_animations_tag = f"{namespace}library_animations"

    # Create a new compressed element with only a single animation tag
    compressed_library = ET.Element(library_animations_tag)
    compressed_animation = ET.SubElement(compressed_library, "animation")

    for animation_item in root.find(library_animations_tag):
        for item in animation_item:
            compressed_animation.append(item)

    # Overwrite existing library animations element with new one.
    for idx, item in enumerate(root):
        if item.tag == library_animations_tag:
            break

    root[idx] = compressed_library

    # Write to file
    print(f"Writing compressed file to '{output_filename}'")
    tree.write(output_filename, xml_declaration=True, encoding="utf-8", method="xml")


def parse_arguments():
    """Parse command line arguments.

    :return: (input_filename, output_filename)
    """
    parser = argparse.ArgumentParser(
        description="Script to collapse multiple animation elements into a single animation element. Useful for cleaning up .dae files before importing into iOS SceneKit."
    )
    parser.add_argument("filename", help="The input .dae filename")
    parser.add_argument(
        "-o",
        "--output-filename",
        help="The input .dae filename. defaults to new-<your filename>",
        default=None,
    )

    args = parser.parse_args()
    if args.output_filename is None:
        output_filename = f"new-{args.filename}"
    else:
        output_filename = args.output_filename

    return args.filename, output_filename


if __name__ == "__main__":
    main()
Christoforo answered 19/11, 2019 at 10:27 Comment(0)
O
0

Problem: There are too many <animation></animation> tags inside your DAE file

When you open DAE file as XML or text file, you should see many <animation id="..." name="..."></animation> pairs, but you only need one pair to use it in Xcode.

Solution: Delete those tags from the DAE file except a pair

So you need to delete those tags except one. While you can do it manually, it is much easier when you do it with Python script. Here is the solution that wrote.

import re

input_file = open("/file/path/to/input/.dae", "r")
output_file = open("/file/path/to/output/.dae", "w")

# Scan the file to count the number of <animation>

count = 0
lines = []

for line in input_file.readlines():
    if re.search("^\s+<animation.*?>", line):
        count += 1
    lines.append(line)

# Delete all <animation> tags except the first one, 
# and delete all </animation> tags except the last one.

count_start = 0
count_end = 0

for line in lines:
    result = re.findall("^\s+<animation.*?>", line)
    if len(result) > 0:
        count_start += 1
        # Check if the <animation> tag is the first one
        if count_start == 1:
            line = re.sub("^\s+<animation.*?>", "<animation>", line)
            output_file.write(line)
            continue
        line = re.sub("^\s+<animation.*?>", "", line)
    result = re.findall("</animation>", line)
    if len(result) > 0:
        count_end += 1
        # Check if the </animation> tag is the last one
        if count_end == count:
            output_file.write(line)
            continue
        line = re.sub("</animation>", "", line)
    output_file.write(line)  

You can run the code in your terminal by typing the following command. When you run the script, make sure to change the input and output file path in the script.

python3 script.py
Opportunist answered 21/2, 2021 at 16:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.