FBX SDK Python2020 の使用例2(マテリアル書き込み編 ブレンダーからクリップスタジオモデラーマテリアル画面へ)



・シリーズの3回目です

シリーズの3回目です。今回はマテリアルを読み込んで書き込みしてみます。では行ってみましょう。


・下記がコードです。



import fbx

def create_material(scene):
  
    node = scene.GetRootNode().GetChild(0)
    material = node.GetMaterial(0)
    # マテリアルの名前をテストで表示
    print("Material Name:", material.GetName())
    # マテリアルに鏡面光の情報を設定
    material.DiffuseFactor.Set(1)  # カラーの色をセット
    material.Diffuse.Set(fbx.FbxColor(0.5,0.5,0.5))  # カラーの色を指定
    material.ReflectionFactor.Set(0.5)  # 鏡面光の反射係数を設定
    material.SpecularFactor.Set(1)     # 鏡面反射の強さを設定
    material.Specular.Set(fbx.FbxDouble3(0.2, 0.8, 0.8))  # 鏡面反射の色を設定
    material.AmbientFactor.Set(1)
    material.Ambient.Set(fbx.FbxColor(1,1,0))
    material.Emissive.Set(fbx.FbxColor(1,1,1))
        
    return material

def import_fbx(scene, manager, filen_path=""):
    # インポーターのインスタンスを作成
    importer = fbx.FbxImporter.Create(manager, "MyScene")

    # FBXファイルを初期化
    importer.Initialize(filen_path)

    # シーンにインポート
    importer.Import(scene)
    # インポートを閉じる
    importer.Destroy()


    # マテリアルの情報を変更
    material=create_material(scene)

    # シーンにマテリアルを追加 
    # これをしないとUNITYなどでは上手くマテリアルが読み込めない
    node = fbx.FbxNode.Create(scene, "MyNode")
    node.AddMaterial(material)

def export_fbx(scene, filename):
    # エクスポーターのインスタンスを作成
    exporter = fbx.FbxExporter.Create(scene, "")

    # エクスポーターにファイル名を設定
    exporter.Initialize(filename, -1)

    # ファイルにシーンをエクスポート
    exporter.Export(scene)

    # エクスポーターを閉じる
    exporter.Destroy()


    
# FBX Managerのインスタンスを作成
manager = fbx.FbxManager.Create()

# シーンのインスタンスを作成
scene = fbx.FbxScene.Create(manager, "MyScene")


import_fbx(scene, manager, filen_path = "1.fbx")


# シーンをエクスポート
export_fbx(scene, "output.fbx")


・結果がクリップスタジオモデラーのマテリアル画面でこれ。




・クリップスタジオの仕様がこれ。

下記が公式のPDFです。



ブレンダーでもある程度制御できるのですが、
ラフネスとエミッション(光沢)とカラーしかできなく、しかもカラーはガンマを考慮してないので色が少し暗くなります。
また、ラフネスは光沢強度が変わるといった仕様なようです。

    # マテリアルに鏡面光の情報を設定
    material.DiffuseFactor.Set(1)  # カラーの色をセット
    material.Diffuse.Set(fbx.FbxColor(0.5,0.5,0.5))  # カラーの色を指定
    material.ReflectionFactor.Set(0.5)  # 鏡面光の反射係数を設定
    material.SpecularFactor.Set(1)     # 鏡面反射の強さを設定
    material.Specular.Set(fbx.FbxDouble3(0.2, 0.8, 0.8))  # 鏡面反射の色を設定
    material.AmbientFactor.Set(1)
    material.Ambient.Set(fbx.FbxColor(1,1,0))
    material.Emissive.Set(fbx.FbxColor(1,1,1)

上記の情報でモデラーの殆どの情報が入れ替わると思います。

そもそも、余談ですがFBXはBlenderのPBRのことを全くサポートしていないようで、BlenderのFBX制御プログラムで適当に情報をインプットしてるようです。ですから、普通にFBXファイルをモデラーなどで開くと下記のような適当な値が出てくるようです。

・コードの動作関して


今回は読み込みと書き込みを組み合わせてます。
ポイントはインポートにはシーンとマネージャーがいりますが、
エクスポートにはシーンだけで大丈夫な点です。関数化してみると分かりやすいですね。



■もうちょっとレベル高いスクリプト 出力した後それを変更


・01_fbx_sdk_test.py

import fbx
import sys,bpy

sys.modules["export_fbxfile"] = bpy.data.texts['export_fbx.py'].as_module()
from export_fbxfile import export_fbx

sys.modules["rgb_to_hex"] = bpy.data.texts['rgb_to_hex.py'].as_module()
from rgb_to_hex import rgb_to_rgb_ganma

output_path="test_file.fbx"
export_fbx(output_path=output_path)

def create_material(scene):
    a=scene.GetSceneInfo()
    print(a)
    node = scene.GetRootNode().GetChild(0)
    material = node.GetMaterial(0)
    # マテリアルの名前をテストで表示
    print("Node Name:", node.GetName())
    print("Material Name:", material.GetName())
    # マテリアルに鏡面光の情報を設定
    print("MAT diff", *material.Diffuse.Get())
    covert_rgb = rgb_to_rgb_ganma(*material.Diffuse.Get(),1)
    print("変換後のカラーRGB", covert_rgb)# 変換後のカラーRGB
    material.DiffuseFactor.Set(1)  # カラーの色をセット
    
    material.Diffuse.Set(fbx.FbxColor(*covert_rgb[1]))  # カラーの色を指定
    
    material.ReflectionFactor.Set(0.5)  # 鏡面光の反射係数を設定
    material.SpecularFactor.Set(1)     # 鏡面反射の強さを設定
    material.Specular.Set(fbx.FbxDouble3(0.2, 0.8, 0.8))  # 鏡面反射の色を設定
    material.AmbientFactor.Set(1)
    material.Ambient.Set(fbx.FbxColor(1,1,0))
    material.Emissive.Set(fbx.FbxColor(1,1,1))
        
    return material

def import_fbx(scene, manager, filen_path=""):
    # インポーターのインスタンスを作成
    importer = fbx.FbxImporter.Create(manager, "MyScene")

    # FBXファイルを初期化
    importer.Initialize(filen_path)

    # シーンにインポート
    importer.Import(scene)
    # インポートを閉じる
    importer.Destroy()
    
    
    # マテリアルの情報を変更
    material=create_material(scene)

    # シーンにマテリアルを追加 
    # これをしないとUNITYなどでは上手くマテリアルが読み込めない
    node = fbx.FbxNode.Create(scene, "MyNode")
    node.AddMaterial(material)


    
def export_fbx(scene, filename):
    # エクスポーターのインスタンスを作成
    exporter = fbx.FbxExporter.Create(scene, "")

    # エクスポーターにファイル名を設定
    exporter.Initialize(filename, -1)

    # ファイルにシーンをエクスポート
    exporter.Export(scene)

    # エクスポーターを閉じる
    exporter.Destroy()

    
# FBX Managerのインスタンスを作成
manager = fbx.FbxManager.Create()

# シーンのインスタンスを作成
scene = fbx.FbxScene.Create(manager, "MyScene")


import_fbx(scene, manager, filen_path = output_path)


# シーンをエクスポート
export_fbx(scene, "conver_output.fbx")

・export_fbx.py

シンプルなエクスポートするスクリプト

選択したオブジェクトのみが指定のファイルパスに出力されます。

import bpy

def export_fbx(output_path = "test_file.fbx"):
    # 出力するファイルのパスとファイル名を指定します

    # FBX出力の設定を指定します
    bpy.ops.export_scene.fbx(
        filepath=output_path,
        check_existing=True,                   # 既存のファイルをチェックするかどうか
        filter_glob='*.fbx',                   # 出力するファイルの拡張子を指定します
        use_selection=True,                   # 選択されたオブジェクトのみをエクスポートするかどうか
        use_visible=False,                     # 表示されているオブジェクトのみをエクスポートするかどうか
        use_active_collection=False,           # アクティブなコレクションのオブジェクトのみをエクスポートするかどうか
        global_scale=1.0,                      # グローバルスケールを指定します
        apply_unit_scale=True,                 # ユニットスケールを適用するかどうか
        apply_scale_options='FBX_SCALE_NONE',  # スケールのオプションを指定します
        use_space_transform=True,              # 空間変換を使用するかどうか
        bake_space_transform=False,            # 空間変換をベイクするかどうか
        object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, # 出力するオブジェクトのタイプを指定します
        use_mesh_modifiers=True,               # メッシュモディファイアを適用するかどうか
        use_mesh_modifiers_render=True,        # レンダリング時のメッシュモディファイアを適用するかどうか
        mesh_smooth_type='OFF',                # スムージングのタイプを指定します
        colors_type='SRGB',                    # 色のタイプを指定します
        prioritize_active_color=False,         # アクティブなカラーを優先するかどうか
        use_subsurf=False,                     # サブサーフを適用するかどうか
        use_mesh_edges=False,                  # メッシュエッジをエクスポートするかどうか
        use_tspace=False,                      # テクスチャ座標をエクスポートするかどうか
        use_triangles=False,                   # 三角形を使用するかどうか
        use_custom_props=False,                # カスタムプロパティをエクスポートするかどうか
        add_leaf_bones=True,                   # 葉のボーンを追加するかどうか
        primary_bone_axis='Y',                 # プライマリボーンの軸を指定します
        secondary_bone_axis='X',               # セカンダリボーンの軸を指定します
        use_armature_deform_only=False,        # アーマチュア変形のみを使用するかどうか
        armature_nodetype='NULL',              # アーマチュアのノードタイプを指定します
        bake_anim=True,                        # アニメーションをベイクするかどうか
        bake_anim_use_all_bones=True,          # すべてのボーンを使用してアニメーションをベイクするかどうか
        bake_anim_use_nla_strips=True,         # NLAストリップを使用してアニメーションをベイクするかどうか
        bake_anim_use_all_actions=True,        # すべてのアクションを使用してアニメーションをベイクするかどうか
        bake_anim_force_startend_keying=True,  # スタート/エンドキーイングを強制するかどうか
        bake_anim_step=1.0,                    # ステップサイズを指定します
        bake_anim_simplify_factor=1.0,         # 簡略化ファクターを指定します
        path_mode='AUTO',                      # パスのモードを指定します
        embed_textures=False,                  # テクスチャを埋め込むかどうか
        batch_mode='OFF',                      # バッチモードを指定します
        use_batch_own_dir=True,                # 独自のディレクトリでバッチモードを使用するかどうか
        use_metadata=True,                     # メタデータを使用するかどうか
        axis_forward='-Z',                     # 正面方向の軸を指定します
        axis_up='Y'                            # 上方向の軸を指定します
    )

    print("Exported FBX file:", output_path)

・rgb_to_hex.py

参考
※Blenderではデフォルだとガンマ補正がかかった表現になっているので値を書き込んだあと変更するスクリプト


import bpy
def linearrgb_to_srgb(c):
    if c < 0:
        return 0
    elif c < 0.0031308:
        return 12.92 * c
    else:
        return 1.055 * (c ** (1/2.4)) - 0.055

def rgb_to_rgb_ganma(r, g, b,a):
    r_value = int(linearrgb_to_srgb(r) * 255 + 0.5)
    g_value = int(linearrgb_to_srgb(g) * 255 + 0.5)
    b_value = int(linearrgb_to_srgb(b) * 255 + 0.5)
    r_normalized_value = int(linearrgb_to_srgb(r) * 255 + 0.5) / 255
    g_normalized_value = int(linearrgb_to_srgb(g) * 255 + 0.5) / 255
    b_normalized_value = int(linearrgb_to_srgb(b) * 255 + 0.5) / 255
    return (r_value,g_value,b_value), (r_normalized_value,g_normalized_value,b_normalized_value)


hex_color = rgb_to_rgb_ganma(*bpy.data.materials['Material_test'].node_tree.nodes["Principled BSDF"].inputs[0].default_value)
print('###0-0###mat name', bpy.context.object.material_slots[0].name)
print(hex_color)  


・コードの解説


・他のモジュールの定義

下記のコードはBlender内のテキスト情報をインポートするおまじないです。
sys.modulesによってモジュールをPythonに定義します。

これを覚えるとモジュールを分けてBlender上でコードを実行できるので、
テキスト内がゴジャゴジャしてきたら使ってみると便利です。

sys.modules["export_fbxfile"] = bpy.data.texts['export_fbx.py'].as_module()
from export_fbxfile import export_fbx


・だが、UNITYだとシーンにマテリアルを追加しないと読み込めないよう


なぜかUNITYだとマテリアル情報が消えます。Blenderだと読み込めたり他のソフトでも読み込めるので、不思議。
とりあえず変更でもシーンに追加した方が良さげみたいです。

    # マテリアルの情報を変更
    material=create_material(scene)

    # シーンにマテリアルを追加 
    # これをしないとUNITYなどでは上手くマテリアルが読み込めない
    node = fbx.FbxNode.Create(scene, "MyNode")
    node.AddMaterial(material)


■マテリアルの情報を出力するスクリプト

・check_fbx_info.py


私の作ったモデル
192_コーヒーマシーン.fbxという情報を抜き出してみました。

import fbx

def export_node_info(scene, output_file):
    with open(output_file, 'w') as file:
        node = scene.GetRootNode()
        if node:
            write_node_info(node, file)



def write_node_info(node, file):
    node_stack = [(node, 0)]  # ノードと深さを追跡するスタック

    while node_stack:
      
        current_node, depth = node_stack.pop()
        indentation = '| ' * depth  # 深さに応じてインデントを設定
        node_name = current_node.GetName()
        file.write(f"{indentation}└─ Node Depth {depth}: {node_name}\n")

        # ノードにマテリアルがある場合は情報を出力
        for i in range(current_node.GetMaterialCount()):
            material = current_node.GetMaterial(i)
            if material:
                material_name = material.GetName()
                file.write(f"{indentation}|   └ Material {i}: {material_name}\n")

        # 子ノードをスタックに追加
        for i in range(current_node.GetChildCount()):
            child_node = current_node.GetChild(i)
            node_stack.append((child_node, depth + 1))


            
# Initialize the FBX SDK
manager = fbx.FbxManager.Create()
if not manager:
    print("Error: Unable to create FBX Manager!")


# Create a scene
scene = fbx.FbxScene.Create(manager, "")

# Open the FBX file
fbx_file_path = "192_コーヒーマシーン.fbx"
importer = fbx.FbxImporter.Create(manager, "")
importer.Initialize(fbx_file_path)
    
   

importer.Import(scene)
importer.Destroy()

# Output node info to a text file
output_file = "node_info.txt"

export_node_info(scene, output_file)

# Destroy the scene and manager
scene.Destroy()
manager.Destroy()


・node_info.txt

結果がこれ。なんとか成功してるっぽいです。(AIまかせ)
AIだと自己再帰するプログラムを提案してくるんですが、それだとちょっとわかりにくいというかデバックしづらいので、WHILE文で出してもらいました。

いい感じで出力されてると思います。

└─ Node Depth 0: RootNode
| └─ Node Depth 1: コーヒーメーカー_
| | └─ Node Depth 2: コーヒーメーカー_029
| | |   └ Material 0: kinzoku
| | | └─ Node Depth 3: コーヒーメーカー_031
| | | |   └ Material 0: ColorMaterial_0.20_0.22_0.23_1.00
| | | | └─ Node Depth 4: コーヒーメーカー_032
| | | | |   └ Material 0: ColorMaterial_1.00_1.00_1.00_1.002
| | | └─ Node Depth 3: コーヒーメーカー_030
| | | |   └ Material 0: ColorMaterial_0.00_0.00_0.00_1.001
| | └─ Node Depth 2: コーヒーメーカー_036
| | |   └ Material 0: ColorMaterial_0.72_0.72_0.72_1.00
| | |   └ Material 1: ColorMaterial_0.46_0.46_0.46_1.00
| | └─ Node Depth 2: コーヒーメーカー_035
| | |   └ Material 0: ColorMaterial_0.56_0.60_0.62_1.00
| | └─ Node Depth 2: コーヒーメーカー_034
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_033
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_028
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_027
| | |   └ Material 0: ColorMaterial_0.00_0.00_0.00_1.002
| | └─ Node Depth 2: コーヒーメーカー_026
| | |   └ Material 0: ColorMaterial_0.52_0.54_0.56_1.00
| | |   └ Material 1: ColorMaterial_0.00_0.00_0.00_1.00
| | |   └ Material 2: ColorMaterial_0.00_0.00_0.00_1.001
| | |   └ Material 3: ColorTexture_0.72_0.72_0.72_1.00
| | └─ Node Depth 2: コーヒーメーカー_024
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_023
| | |   └ Material 0: ColorMaterial_0.56_0.60_0.62_1.00
| | └─ Node Depth 2: コーヒーメーカー_022
| | |   └ Material 0: ColorMaterial_0.52_0.54_0.56_1.00
| | └─ Node Depth 2: コーヒーメーカー_021
| | |   └ Material 0: ColorMaterial_0.31_0.33_0.34_1.00
| | └─ Node Depth 2: コーヒーメーカー_020
| | |   └ Material 0: garasu_
| | └─ Node Depth 2: コーヒーメーカー_019
| | |   └ Material 0: ColorMaterial_1.00_1.00_1.00_1.002
| | └─ Node Depth 2: コーヒーメーカー_018
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_017
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_016
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_015
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_014
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_013
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_012
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_011
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_010
| | |   └ Material 0: ColorMaterial_0.35_0.35_0.35_1.00
| | └─ Node Depth 2: コーヒーメーカー_009
| | |   └ Material 0: ColorMaterial_1.00_1.00_1.00_1.00
| | |   └ Material 1: ColorTexture_0.65_0.70_0.73_1.00
| | └─ Node Depth 2: コーヒーメーカー_008
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_007
| | |   └ Material 0: ColorMaterial_0.52_0.54_0.56_1.00
| | └─ Node Depth 2: コーヒーメーカー_006
| | |   └ Material 0: ColorMaterial_0.20_0.22_0.23_1.00
| | └─ Node Depth 2: コーヒーメーカー_005
| | |   └ Material 0: ColorMaterial_0.72_0.72_0.72_1.00
| | └─ Node Depth 2: コーヒーメーカー_004
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_003
| | |   └ Material 0: kinzoku
| | |   └ Material 1: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_002
| | |   └ Material 0: kinzoku
| | └─ Node Depth 2: コーヒーメーカー_001
| | |   └ Material 0: kinzoku
| └─ Node Depth 1: カップ
| |   └ Material 0: garasu_
| | └─ Node Depth 2: カップ_001
| | |   └ Material 0: coffie