FBX SDK(python) を使おう ネームスペース削除編
言及されることの少ないSDKであるが実は便利
目次
更新履歴
2020-09-23: コード例に考慮漏れがあったので修正
ゲーム開発の現場では、デジタルアセットのやり取りとしてよく使われるFBX。 その内容の編集には通常、専用のソフト・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の活用は様々だ。 たとえば、一括してトランスフォームにオフセットを適用する。単位系を変更する。 軸を入れ替える。階層を変更するなどといったことも可能だ。