如何导出 DAE 文件以在 Scene Kit 中使用而不会看到“无标题动画”?

Posted

技术标签:

【中文标题】如何导出 DAE 文件以在 Scene Kit 中使用而不会看到“无标题动画”?【英文标题】:How can I export DAE files for use in Scene Kit without seeing "untitled-animations"? 【发布时间】:2014-08-23 17:53:24 【问题描述】:

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

有谁知道如何从 Blender 或 Cheetah 3D 中正确导出这些,以便 Scene Kit 可以使用它们?

【问题讨论】:

如果你改写你的问题,你可能会得到更好的结果,例如“我如何让 Blender 导出 DAE 以在 SceneKit 中使用,使得(功能 X)可以(事物 Y)?” 谢谢瑞克。还有3个字符。大声笑! 我改写了标题和正文以使重点关注您的问题,所以我继续重新打开它。 【参考方案1】:

我对此进行了深入研究,因为它也让我很烦。所有“无标题动画”都是每个骨骼的单独动画。您可以从 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

必须对骨架中的所有骨骼执行此操作。 **烦人!!!*

Apple 将 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 和 presto!完成后(大约半秒)会弹出一个窗口。

【讨论】:

+1 有趣 - 只是一个后续问题:任何教程或说明如何从搅拌机中导出以便我们可以在 SceneKit 中使用?我无法让纹理工作 - 只有网格。 我可以制作一个并上传,但要到今天晚些时候。我还制作了一个 Automater 应用程序,它在终端中使用 sed 命令来处理上述文件修改。我稍后会在这里发布一个链接。 刚刚添加了一个服务链接,用于将文件转换为 Xcode 兼容的 Blender 动画。享受!正在编写纹理和 Scenekit 教程! 真的很期待 Blender 教程。几天来我一直在寻找解决方案 - 没有运气。 Blender 确实不支持 SceneKit,至少现在还没有...... 链接到“Automater collada 转换器下载”不起作用。您是否有机会在某个地方修复或重新上传?【参考方案2】:

这是因为 .dae 文件中的每个骨骼都有自己的 &lt;animation&gt; 标签。

FlippinFun 正确地指出,删除第一个和最后一个的所有打开和关闭 &lt;animation&gt; 标签 except 会将动画组合在一起,使其可以在 Xcode 中通过标识符 FileName-1 访问。 p>

我碰巧在使用 MayaLT > .FBX > .DAE 工作流程,发现他链接的服务对我不起作用。这是因为我的 .dae 文件的格式很差,一些 &lt;source&gt; 标记与双嵌套 &lt;animation&gt; 标记位于同一行。结果整行被删除,损坏了 .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 就可以用作您的场景包动画。

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

【讨论】:

【参考方案4】:

我正在处理同样的问题,但我使用的是 Maya,如果你下载 SceneKit slides for WWDC 2014

文件 AAPLSlideAnimationEvents.m 有一些导入 DAE 文件的示例,如您所述,希望对您有所帮助

【讨论】:

【参考方案5】:

这是我对 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

【讨论】:

【参考方案6】:

删除动画标签(参见 FlippinFun 的解决方案)并阅读下面的链接,了解如何准备 Blender 动画以使用 SceneKit 播放(说明不适用于 SceneKit,但它们工作得很好)。

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

【讨论】:

【参考方案7】:

以下是删除不必要的 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)

【讨论】:

【参考方案8】:

也许以下观察对某人有用: 我直接将未修改的 mixamo .dae(带动画)导入 xcode 10.2.1 在我的例子中,角色“大维加斯”(带有桑巴舞)。 以下代码给出了动画的列表 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)

并将其应用于您的角色。

【讨论】:

【参考方案9】:

这是一个完整的脚本,您可以使用它来做同样的事情,而无需使用正则表达式。将以下代码复制粘贴到 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"namespacelibrary_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()

【讨论】:

【参考方案10】:

对我来说,自动化脚本不起作用..我编写了一个小的 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 &lt;file.dae&gt;

【讨论】:

【参考方案11】:

问题:您的 DAE 文件中有太多 &lt;animation&gt;&lt;/animation&gt; 标签

当您将 DAE 文件作为 XML 或文本文件打开时,您应该会看到很多 &lt;animation id="..." name="..."&gt;&lt;/animation&gt; 对,但您只需要一对就可以在 Xcode 中使用它。

解决方案:从 DAE 文件中删除那些标签,除了一对

所以你需要删除那些标签,除了一个。虽然您可以手动执行此操作,但使用 Python 脚本执行此操作要容易得多。这是写的解决方案。

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)  

您可以通过键入以下命令在终端中运行代码。运行脚本时,确保更改脚本中的输入和输出文件路径。

python3 script.py

【讨论】:

以上是关于如何导出 DAE 文件以在 Scene Kit 中使用而不会看到“无标题动画”?的主要内容,如果未能解决你的问题,请参考以下文章

如何从 SceneKit 中的 .dae 文件访问文本元素?

导出到 DAE 时如何继承相同的纹理映射?

如何在 Scene Kit 或 ARkit 中的 Camera 渲染的视图中调暗灯光?

三个 JS 1 场景 2 文件格式尚未工作

从 A-Frame 或 JS 更新 Collada (.dae) 文件代码

SCNView 导出的 Collada (.dae) 文件看起来不同,无法导入到 Blender