Blenderメッシュ結合済FBX出力スクリプト
類似したことができるアドオンを発見
使ってみたが、コレクションごとのマージはしてくれない?
古すぎて厳しい
ChatGPTに作らせてみる
全然だめだった
code:text
# 命令書
あなたはBlenderプラグインに精通しているpythonプログラマです。
以下の仕様に従ったBlenderプラグイン(以下、本プラグイン)のpythonソースコードを出力してください。
本プラグインはキャラクターモデリングの作成効率向上を目的とします。
本プラグインはモディファイアの適用とオブジェクトの統合を行わないでプロセスを進めることができるアプローチを提供します。
具体的には現在のファイルを破壊しないように、別のBlenderファイル上の別プロセスでモディファイアの適用とオブジェクト統合を行います。
これによってモディファイアの適用とオブジェクトの統合によって修正が難しくなる問題を軽減します。
本プラグインのユーザはBlenderプラグインの設定パネルで以下を指定します。
・モディファイア適用とオブジェクト統合を行うBlenderファイルのファイルパス
・単一のオブジェクトとして統合するコレクション(複数指定できます)
・出力するFBXのファイルパス
「出力するFBXのファイルパス」に出力するFBXファイル
本プラグインを実行すると下記の順序で処理を行います。
・ユーザが指定した「モディファイア適用とオブジェクト統合を行うBlenderファイルのファイルパス」へ現在開いているBlenderプロジェクトファイルをコピーします。これには「bpy.ops.wm.save_as_mainfile」APIを使うことができます。
・コピーしたBlenderプロジェクトファイル上でスクリプトを実行します。これには「subprocess.run」を使うことができます。実行するスクリプトで行う処理は後述します。
コピーしたBlenderファイル上で実行するスクリプトで行う処理を下記に示します。
・すべての表示状態のオブジェクトに設定されたモディファイアをすべて適用します。ただし、Armatureモディファイアは適用しません。
・ユーザが指定した「単一のオブジェクトとして統合するコレクション」以下のオブジェクトをすべて統合します。
「単一のオブジェクトとして統合するコレクション」の中にネストしたコレクションがある場合はそれも一緒に統合します。
そのためこの処理は再帰的に行う必要があります。
・表示状態のオブジェクトをFBX形式でエクスポートします。出力先はユーザが指定した「出力するFBXのファイルパス」です。
回答を出力する前に、前提条件が不足していないかチェックしてください。
不足している情報があれば、どんな情報が必要なのか箇条書きで書き出してください。
TODO
オブジェクトをマージするとき、自動スムーズ設定(角度指定)がアクティブになっていると結合するオブジェクトすべての法線が自動スムーズになる
自動スムーズでないアクティブの選択が必要?
スケール、位置を適用する機能
回転は意図的にしている場合があるので一旦保留?
↑そうでもない
モデルの警告を出す機能(lint)
不要頂点グループの削除
不要オブジェクトの削除(Release用)
Motivation
BlenderのモデルをUnityへ移すとき「メッシュを結合」 → 「FBX出力」の手順を実施している 「メッシュを結合」が破壊的な操作なので一旦別でコピーしてから実施する
これは手間がかかる & 事故ってオリジナルを結合してしまうことがある
→ 非破壊でやりたい
処理内容
コレクション単位で以下を実施
カーブをメッシュに変換
メッシュのモディファイアを適用
コレクション内のメッシュをマージ
FBXへ出力(default : exported.fbx) 使い方
1. 結合するメッシュごとにコレクションを分ける
コレクションを2重にネストすること
↓の場合、「帽子」「マント」「マント飾り」のメッシュにまとめられる
https://gyazo.com/c5905abaea57cf5a7bb772d02a04f245
2. エクスポート対象以外のオブジェクトをすべて非表示にする
3. Scriptingタブからスクリプトを実行する
表示されているオブジェクトの一つを選択状態にすること
https://gyazo.com/465542b9b2ae23d09bf6f90ba5b770e5
別blenderファイルへ出力して処理するように修正(2023/04/17)
環境
blender : 3.6.5
LICENSE:
Issue
BUG: シェイプキーがついたオブジェクトが入ったコレクションを選択しておかないとコレクション内に結合されない?
2023/11/09 修正済
警告:VRCAvatarExporter.py:124: DeprecationWarning: Passing in context overrides is deprecated in favor of Context.temp_override(..), calling "object.join"
Armatureがつかない
64行目:SKKeeper使用時はArmatureも適用してしまう
暫定対策 Shapekeyなし、Armatureありのdummyオブジェクトを一番下一番上においておく SKKeeper使用のオブジェクトはtarget_objectsの一番下におかれなおされる?
Sortすることにした
target_objectsの順番は不明。。。
法線が壊れる
自動スムーズ設定が異なるオブジェクトを統合するのはよくなさそう
History
2023/09/15
カーブをメッシュに変換する機能追加
2023/05/25
モディファイア適用条件をArmature以外に変更(black list方式に変更)
objectモードへ変更する処理を削除(エラー発生するようになったため)
2023/05/31
無効なモディファイアを検出した場合は適用しないで削除するように修正
不可視オブジェクトを無視するように修正
「コレクション内のオブジェクトがすべて可視状態になっていること」の制約はなくなった
2023/11/03
シェイプキーがついたオブジェクトに対するモディファイア適用をできるようにした(SKkeeperが必要) BUG: シェイプキーがついたオブジェクトが入ったコレクションを選択しておかないとコレクション内に結合されない?
2023/11/09
下記のBUG修正
BUG: シェイプキーがついたオブジェクトが入ったコレクションを選択しておかないとコレクション内に結合されない?
2023/12/17
target_collectionをソート
統合時のアクティブオブジェクト制御のために追加
Script
code:python
# -----------------------------------
# FBX Exporter for VRChat
# Author : yuufyu
# -----------------------------------
# Blender import
import bpy
import os
import subprocess
def export_fbx(export_filepath) :
bpy.ops.export_scene.fbx(
filepath=export_filepath,
check_existing=True,
filter_glob="*.fbx",
use_selection=False,
use_visible=True,
use_active_collection=False,
global_scale=1.0,
apply_unit_scale=True,
apply_scale_options='FBX_SCALE_ALL',
bake_space_transform=False, # !EXPERIMENTAL!
object_types={'MESH','ARMATURE'},
use_mesh_modifiers=False, # Apply Modifiers, Apply modifiers to mesh objects (except Armature ones) - WARNING: prevents exporting shape keys
use_mesh_modifiers_render=True, # (DISABLED in Blender 2.8)
mesh_smooth_type='OFF',
colors_type = 'SRGB',
use_subsurf=False,
use_mesh_edges=False,# Export loose edges
use_tspace=False,
use_custom_props=False,
use_triangles=False,
add_leaf_bones=False,
primary_bone_axis='Y',
secondary_bone_axis='X',
use_armature_deform_only=True,
armature_nodetype='NULL',
bake_anim=False,
bake_anim_use_all_bones=False,
bake_anim_use_nla_strips=False,
bake_anim_use_all_actions=False,
bake_anim_force_startend_keying=False,
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'
)
def apply_modifier(obj) :
if len(obj.modifiers) < 1 :
return
shapekeys = obj.data.shape_keys
if shapekeys != None and len(shapekeys.key_blocks) > 0 :
if bpy.ops.sk != None :
bpy.context.view_layer.objects.active = obj
bpy.ops.sk.apply_mods_sk() # SKkeeper apply
else :
for m in obj.modifiers :
if m.show_viewport :
if m.type != "ARMATURE" :
bpy.context.view_layer.objects.active = obj
try :
bpy.ops.object.modifier_apply(modifier = m.name)
except RuntimeError :
print(f"Error applying {m.name} to {obj.name}, removing it instead.")
obj.modifiers.remove(m)
else :
obj.modifiers.remove(m)
def convert_to_mesh(curve) :
bpy.context.view_layer.objects.active = curve
bpy.ops.object.select_all(action='DESELECT')
curve.select_set(True)
bpy.ops.object.convert(target='MESH')
def merge_mesh() :
scene = bpy.context.scene
ctx = bpy.context.copy()
for c in scene.collection.children :
for cc in c.children :
# Select collection for SKkeeper
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, cc.name)
bpy.context.view_layer.active_layer_collection = layerColl
for obj in cc.objects :
if obj.visible_get() :
print("Object : {}".format(obj.name) )
if obj.type=='CURVE' :
convert_to_mesh(obj)
if obj.type == 'MESH' :
apply_modifier(obj)
# Loops because the original obuject is deleted by SKkeeper.
target_objects = []
for obj in cc.objects :
if obj.visible_get() :
if obj.type == 'MESH' :
target_objects.append(obj)
# Sort for selecting active object
target_objects.sort(key = lambda obj : obj.name)
# Join objects
if len(target_objects) > 0 :
print("Join Object : {}".format(cc.name) )
# ctx = bpy.context.copy()
if len(target_objects) > 1 :
bpy.ops.object.join(ctx)
# Recursivly transverse layer_collection for a particular name
def recurLayerCollection(layerColl, collName):
found = None
if (layerColl.name == collName):
return layerColl
for layer in layerColl.children:
found = recurLayerCollection(layer, collName)
if found:
return found
# ----------------------
# Main
# ----------------------
print("--- start ---")
export_fbx_file = "Zebra.fbx"
is_export = bpy.app.background
if is_export :
merge_mesh()
print("--- export ---")
export_fbx(export_fbx_file)
else :
print("request export process")
temp_blender_file = os.path.abspath("_temp_export_fbx.blend")
export_script = __file__
if bpy.context.space_data != None and bpy.context.space_data.type == "TEXT_EDITOR" :
export_script=bpy.context.space_data.text.filepath
bpy.ops.wm.save_as_mainfile(filepath = temp_blender_file, copy = True, check_existing = False)
blender_args = [
bpy.app.binary_path,
"--background",
temp_blender_file,
"--python",
export_script,
]
exported_process = subprocess.run(blender_args, text=True, stderr = subprocess.STDOUT)
print(exported_process.stdout)
os.remove(temp_blender_file)