FBX SDK(python) を使おう ネームスペース削除編

言及されることの少ないSDKであるが実は便利


目次


更新履歴

2020-09-23: コード例に考慮漏れがあったので修正


ゲーム開発の現場では、デジタルアセットのやり取りとしてよく使われるFBX。 その内容の編集には通常、専用のソフト・DCCツールを用いることが多い。 しかしちょっとした内容・人手を介す必要のない作業であれば、専用ソフトを起動するまでもなく、 小さいツールの作成をもってこれにあたると便利である。


たとえば、`Unity` (いい加減対応して・・・) ではいわゆるネームスペースに対応していないため、DCCツールでの作業、データの出力 の際に問題となることがある。問題の解消にはどこかでネームスペースを消す作業が必要であるのだが、 これを単独のツールで行えるようにしておくと、何かと都合が良い。今回はそのようなツールについて解説する。


インストール

Autodeksのサイト からダウンロード可能だ。 いくつか種類があるのがわかる

  • FBX SDK Downloads: c++ アプリケーションを作成するためのもの
  • FBX Python SDK: python から fbxの機能を呼び出せるもの
  • FBX Python Bindings: python で記述&ビルドしアプリケーショを作成するためのもの

また、autodesk以外から他言語bindingが公開されているものがあるが、今回は取り扱わない。(素直にcppで書いたほうが楽だと思うよ) ここでは、真ん中の FBX Python SDKを利用し、ツールを作っていこう。

ダウンロードしたインストーラを実行すると C:\Program Files\Autodesk\FBX\FBX Python SDK\2020.0.1 以下に展開される。 lib フォルダ以下に pythonバージョンに応じたモジュールが配置されているため、処理系に応じたものを PYTHONPATH に通しておく

コード

    import FbxCommon

    sdk_manager, fbx_scene = FbxCommon.InitializeSdkObjects()
    result = FbxCommon.LoadScene(sdk_manager, fbx_scene, fbx_path)

これで fbx_path のFBXファイルを読み込むことができる。読み込み後、fbx_scene からシーンの情報が 取得、編集ができるので必要な処理を記述してやればよい。たとえばルート(トランスフォーム)の取得は以下。

    root = fbx_scene.GetRootNode()

今回はネームスペースの削除なので、全要素をたどって名前の変更をかけてやればよい。 名前の衝突については考慮しないことにする。全ノード列挙のような便利なものはないので、 削除したいノードの種類に注意が必要。種類別に数え上げる必要がある。

以下に実装例をのせる。仕様はいくらか考えられるが、必要に応じて加工するなりしてほしい。

# -*- coding: utf-8 -*-
"""Utility script for fbx file, removing namespace from nodes.

Example
-------

    $ python remove_namespace_recursively.py input_fbx_file_path output_file_path(optional)


Development Notes
-----------------
see http://help.autodesk.com/view/FBX/2020/ENU/?guid=FBX_Developer_Help_cpp_ref_index_html
"""
# ----------------------------------------------------------------------------
import sys
import argparse

import FbxCommon

from logging import (  # noqa:F401 pylint: disable=unused-import, wrong-import-order
    StreamHandler,
    getLogger,
    WARN,
    DEBUG,
    INFO
)

if False:  # pylint: disable=using-constant-test, wrong-import-order
    # For type annotation
    from typing import (  # NOQA: F401 pylint: disable=unused-import
        Optional,
        Dict,
        List,
        Tuple,
        Pattern,
        Callable,
        Any,
        Text,
        Generator,
        Union
    )
    from pathlib import Path  # NOQA: F401, F811 pylint: disable=unused-import,reimported
    from types import ModuleType  # NOQA: F401 pylint: disable=unused-import
    from six.moves import reload_module as reload  # NOQA: F401 pylint: disable=unused-import
# ----------------------------------------------------------------------------
handler = StreamHandler()

logger = getLogger(__name__)
logger.setLevel(INFO)
logger.addHandler(handler)
logger.propagate = False

# ----------------------------------------------------------------------------


def remove_namespace_recursively(node, namespace):
    # type: (FbxCommon.fbx.FbxNode, Text) -> None
    """Remove namespace from given node's name and its children recursively."""

    for i in range(node.GetChildCount()):
        kid = node.GetChild(i)
        remove_namespace_recursively(kid, namespace)

    new_name = node.GetName().replace("{}:".format(namespace), "")
    node.SetName(new_name)


def remove_namespace(fbx_path, output_path):
    # type: (Text, Text) -> None
    """Remove namespace root nodes of given fbx file and save to output_path"""
    sdk_manager, fbx_scene = FbxCommon.InitializeSdkObjects()

    logger.info("Load File: %s", fbx_path)
    result = FbxCommon.LoadScene(sdk_manager, fbx_scene, fbx_path)

    if not result:
        logger.error("An error occurred while loading the scene...")
        return

    root = fbx_scene.GetRootNode()
    ns = None
    for i in range(root.GetChildCount()):
        kid = root.GetChild(i)
        ns = ":".join(kid.GetName().split(":")[0:-1])

        _remove_namespace(fbx_scene, ns)

    FbxCommon.SaveScene(sdk_manager, fbx_scene, output_path)

    # Destroy all objects created by the FBX SDK.
    sdk_manager.Destroy()


def _remove_namespace(scene, namespace=None):
    # type: (FbxCommon.FbxScene, Text) -> None

    def __get_new_name(name):
        if False and namespace:
            return name.replace("{}:".format(namespace), "")
        else:
            return name.split(":")[-1]

    general_funcs = [
        # [scene.GetGenericNodeCount, scene.GetGenericNode],
        [scene.GetCharacterCount, scene.GetCharacter],
        # [scene.GetControlSetPlugCount, scene.GetControlSetPlug],
        [scene.GetCharacterPoseCount, scene.GetCharacterPose],
        [scene.GetPoseCount, scene.GetPose],
        [scene.GetNodeCount, scene.GetNode],
        [scene.GetMaterialCount, scene.GetMaterial],
        [scene.GetTextureCount, scene.GetTexture],
        [scene.GetVideoCount, scene.GetVideo],
    ]

    for countup, getter in general_funcs:
        for i in range(countup()):
            node = getter(i)
            new_name = __get_new_name(node.GetName())
            node.SetName(new_name)

    for i in range(scene.GetGeometryCount()):

        geo = scene.GetGeometry(i)
        geo_funcs = [
            [geo.GetShapeCount, geo.GetShape],
            [geo.GetDeformerCount, geo.GetDeformer],
        ]

        for countup, getter in geo_funcs:
            for j in range(countup()):
                try:
                    node = getter(j, 0)
                    new_name = __get_new_name(node.GetName())
                    node.SetName(new_name)

                    for k in range(node.GetBlendShapeChannelCount()):
                        channel = node.GetBlendShapeChannel(k)
                        print(channel.GetName())
                        new_name = __get_new_name(channel.GetName())
                        channel.SetName(new_name)
                except Exception as e:
                    logger.error(e)
                    # pass

        new_name = __get_new_name(geo.GetName())
        geo.SetName(new_name)


def parse_args(args):

    parser = argparse.ArgumentParser(description="Namespace remover for fbx")

    parser.add_argument('fbx_path',
                        help='The input fbx file to be removed namespace.')

    parser.add_argument('target_node',
                        help='The input fbx file to be removed namespace.')

    parser.add_argument('output_path', nargs='?',
                        help='The output fbx file to saved.')

    args = parser.parse_args(args)
    if not args.output_path:
        output_path = args.fbx_path
    else:
        output_path = args.output_path

    return args.fbx_path, output_path


def main(*args):
    """Main entry point, parse args and execute the function."""

    if 0 < len(args):
        pass

    elif 1 < len(sys.argv):
        args = sys.argv[1:]

    else:
        logger.error("argument required")

    fbx_path, output_path = parse_args(args)
    remove_namespace(fbx_path, output_path)


if __name__ == "__main__":
    main()

ほか

今回はネームスペースの削除を紹介したが、ほかにもSDKの活用は様々だ。 たとえば、一括してトランスフォームにオフセットを適用する。単位系を変更する。 軸を入れ替える。階層を変更するなどといったことも可能だ。