如何在不显示“untitled-animations”的情况下导出DAE文件以供在Scene Kit中使用?

16

我正在尝试将在Cheetah 3D和Blender 3D中创建的动画加载到Scene Kit中,但是我得到的只是一堆“未命名动画”,每个动画都是相同的。

有人知道如何正确地从Blender或Cheetah 3D导出这些动画,以便Scene Kit可以使用吗?


如果您重新表述问题,例如,“如何使Blender导出DAE以便在SceneKit中使用,使(功能X)执行(操作Y)?”则可能会获得更好的结果。 - rickster
谢谢Rick。还差3个字符。哈哈! - user160632
1
我重新修改了标题和正文,使其更加关注您的问题,因此我重新打开了它。 - Brad Larson
这个回答解决了你的问题吗?如何在SceneKit中使用普通的Mixamo角色动画? - Fattie
绝对完整的2023解决方案:https://dev59.com/78Xsa4cB1Zd3GeqPsbUh#75093081 - Fattie
11个回答

35

我深入研究了这个问题,因为它也让我感到很烦恼。所有的“untitled animations”都是每个骨骼的单独动画。您可以从Xcode右侧面板中的属性检查器中获取ID。使用Swift代码,您可以获取您的动画。

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

这必须针对你的骨架中所有的骨骼进行操作。**真让人恼火!*

苹果公司在他们的示例项目中使用了3dmax,每个collada文件仅显示一个动画。这是因为3dmax将所有骨骼导出到一个动画下,而Blender则分离每个骨骼。

临时解决方案 使用TextEdit或将 .dae 文件扩展名改为 .xml,并使用 xml 编辑器(许多在网上免费提供)打开它。 xml 编辑器更容易使用。向下滚动到动画起始块。看起来像这样...

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

更改为...

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

每个动画的结尾都会有一个像这样的结束块...

</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">  

当然,在您的最后一根骨头处,像这样留下动画结束块...

</animation>
</library_animations>
这将在你的.dae文件中提供一个单一的动画,该动画与你的文件名相同,末尾加上了“-1”!
编辑 - 这里有一个Automator服务的链接,可以为您转换上面的代码! Automater collada converter download 解压缩并将文件放入您的~/Library/services文件夹中。从那里,您只需右键单击您的collada文件,向下滚动到ConvertToXcodeCollada,然后唰!完成时会弹出一个窗口(大约半秒钟)。

+1 有趣 - 只是一个跟进的问题:是否有任何教程或说明可以从Blender导出,以便我们可以在SceneKit中使用?我无法让纹理工作 - 只有网格。 - Jonny
我可以制作一个并上传,但要等到今天晚些时候。我还制作了一个自动化程序,它使用终端中的sed命令来处理上述文件修改。我稍后会在这里发布链接。 - FlippinFun
2
刚刚添加了一个链接,可以将文件转换为Xcode兼容的Blender动画。享受吧!正在制作有关纹理和Scenekit的教程! - FlippinFun
@FlippinFun 非常感谢,我花了太多时间来理解这个问题,你的解决方案很好:将其转换为xml,更改动画行,添加自己的“id”,然后将其转换回.dae。它可以很好地工作。谢谢! - Paul
1
“Automater collada converter download” 的链接无法使用。您有没有可能修复或重新上传到其他地方? - drewster
显示剩余3条评论

4
这是因为你的 .dae 文件中的每个骨骼都有自己的 <animation> 标签。
FlippinFun 正确地指出,删除除第一个和最后一个之外的所有开放和关闭的 <animation> 标签将会组合动画在一起,并通过标识符 FileName-1 在 Xcode 中访问。
我恰好使用 MayaLT > .FBX > .DAE 工作流,并发现他链接的服务对我无效。这是因为我的 .dae 文件格式不佳,一些 <source> 标签与双重嵌套的 <animation> 标签在同一行上。结果整行都被删除,破坏了 .dae 文件。
对于其他人使用这个工作流的人,这里是我正在运行的 sed 命令来清理,希望对某些人有所帮助!
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

3

如果有其他人发现这个对他们有用,我写了一个Python脚本可以完成此操作。提供文件路径的数组,脚本将把动画组合成一个动画,删除几何和材料。

这个新的、更轻量级的dae文件可以作为你的scenekit动画使用,只要应用动画的模型的骨骼命名完全一致(如它们应该是的)。

#!/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)

使用方法: python cleanupForXcodeColladaAnimation.py dancing_anim.dae

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


1
这是我对n33kos脚本进行的修改,以适用于Mixamo资源。
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

1
也许以下观察对某些人有用: 我直接将未经处理的mixamo .dae(带有动画)导入到xcode 10.2.1中。 在我的情况下,角色“Big Vegas”(伴随桑巴舞蹈)。 以下代码提供了动画的id列表:
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)
        })    
    }

输出:
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 ...

您可能已经注意到,“animation/1”似乎是一个动画组,可以通过以下方式访问:

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

"

“sambaAnimation”可以应用于“Big Vegas”的父级节点:

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

如果您下载了带有其他动画的相同字符,则可以按照以下方式提取动画:
let animation = sceneSource.entryWithIdentifier("animation/1", withClass: CAAnimation.self)

并将其应用到您的角色中。

无价的信息!但我似乎无法使其正常播放... - Fattie

1
我正在处理相同的问题,但是我正在使用Maya,如果你下载WWDC 2014的SceneKit幻灯片,文件AAPLSlideAnimationEvents.m中有一些导入具有多个“untitled-animations”的DAE文件的示例,希望能对你有所帮助。

1

对我来说,自动化脚本无法正常工作。我编写了一个小的Python脚本将所有动画合并为一个。

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()

你可以在这里找到它:link

用法:python fix_dae_script.py <file.dae>


1
现在在 Mac 上,肯定是用 python fix_dae_script.py <file.dae> 了! - Fattie

0

以下是如何删除不必要的 XML 节点:

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)
}

0
这是一个完整的脚本,可以用于执行相同的操作,而无需使用正则表达式。将下面的代码复制粘贴到一个名为prep_dae_for_scenekit.py的文件中。

通过执行 ./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()

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接