Blender OBJ/MTL エクスポータ(UE5モード付き)(ソースコードと実行結果)
UE5モード時は,メッシュ三角形化、座標系変換(X前方、Z上方)、スケール変換(1m=100cm)を行う.

Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
pip install customtkinter

Blender 4 以降のインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
winget install --scope machine --id BlenderFoundation.Blender -e
Blender OBJ/MTL エクスポータGUI版プログラム
ソースコード
# Blender OBJ/MTL エクスポータ GUI版
# 特徴技術名: Blender Python API (bpy)
# 出典: Blender Foundation (2024). Blender Python API Documentation. https://docs.blender.org/api/current/
# 特徴機能: bpy.ops.wm.obj_export()によるパラメータ制御を伴うOBJ/MTL形式エクスポート。座標系変換、スケール調整、メッシュ三角形化などの制御が可能
# 学習済みモデル: 使用なし
# 方式設計:
# - 関連利用技術:
# - customtkinter: UIを提供するtkinterの拡張ライブラリ。ファイル選択ダイアログ、オプション設定、リアルタイムログ表示を実現
# - subprocess: Blenderをバックグラウンドプロセスとして実行し、標準出力をリアルタイムで取得
# - threading: GUI応答性維持のための非同期処理実装
# - pathlib/os/shutil: ファイルシステム操作とテクスチャファイルのコピー処理
# - 入力と出力: 入力: Blenderファイル(.blend)、出力: OBJ/MTLファイルとテクスチャ
# - 処理手順:
# 1. GUIでオプション設定とファイル選択
# 2. Blenderをバックグラウンドプロセスで起動
# 3. bpyを使用してBlenderファイルを開く
# 4. 指定オプションに基づきモディファイヤ適用、マテリアル最適化を実行
# 5. bpy.ops.wm.obj_export()でOBJ/MTL形式にエクスポート(UE5モード時は座標系変換とスケール調整)
# 6. テクスチャのコピーとMTLファイルのパス修正
# - 前処理、後処理:
# - 前処理: モディファイヤの自動適用(全オブジェクト対象、エクスポート時のみ)、重複マテリアルの検出と統合
# - 後処理: MTLファイル内のmap_Kdパスをファイル名のみに修正(UE5互換性向上)、texturesフォルダへのテクスチャ自動コピー
# - 追加処理: UE5モード時のメッシュ三角形化、座標系変換(X前方、Z上方)、スケール変換(1m=100cm)
# - 調整を必要とする設定値: なし(GUIで全オプションを動的に設定可能)
# 将来方策: なし(全設定はGUIで調整可能)
# その他の重要事項:
# - Blender 4.5以降が必要
# - 元のBlenderファイルは変更されない(エクスポート時のみの適用)
# - GUI起動: python <Python プログラムファイル名>
# 前準備:
# - Blenderのインストール(4.5以降推奨)
# - Blenderの実行パスをシステムのPATH環境変数に追加(推奨)
# - pip install customtkinter
import os
import shutil
import customtkinter as ctk
from tkinter import filedialog, messagebox
import threading
import sys
import tempfile
import subprocess
import platform
# 設定定数
WINDOW_WIDTH = 800 # GUIウィンドウの幅
WINDOW_HEIGHT = 700 # GUIウィンドウの高さ
BLENDER_VERSIONS = ['4.5', '4.2', '4.1', '4.0'] # サポートするBlenderバージョン
TEXTURE_FOLDER_NAME = 'textures' # テクスチャ保存フォルダ名
UE5_SCALE = 100.0 # UE5向けスケール変換値(1m = 100cm)
# GUI設定定数
LOG_HEIGHT = 300 # ログ表示エリアの高さ(ピクセル)
ENTRY_WIDTH = 400 # 入力フィールドの幅(ピクセル)
PADDING_X = 10 # 水平方向のパディング
PADDING_Y = 5 # 垂直方向のパディング
# CustomTkinterの設定
ctk.set_appearance_mode('system') # システムの設定に従う(ライト/ダーク)
ctk.set_default_color_theme('blue') # カラーテーマ
# グローバル変数(GUI用)
root = None
log_text = None
blend_path = None
output_dir = None
ue5_mode = None
apply_mods = None
opt_materials = None
copy_tex = None
def find_blender_executable():
"""Blenderの実行可能ファイルを探す"""
# まずPATHから探す
if shutil.which('blender'):
return 'blender'
# Windows用の共通パスを定義
paths = []
for ver in BLENDER_VERSIONS:
paths.append(f'C:\\Program Files\\Blender Foundation\\Blender {ver}\\blender.exe')
paths.extend([
r'C:\Program Files\Blender Foundation\Blender\blender.exe',
r'C:\Program Files (x86)\Blender Foundation\Blender\blender.exe',
])
for path in paths:
if os.path.exists(path):
return path
return None
def log(message):
"""ログメッセージを表示"""
if root and log_text:
root.after(0, lambda: log_text.insert('end', message) or log_text.see('end'))
def browse_blend_file():
"""Blenderファイルを選択"""
filename = filedialog.askopenfilename(
title='Blenderファイルを選択',
filetypes=[('Blender files', '*.blend'), ('All files', '*.*')]
)
if filename:
blend_path.set(filename)
# デフォルトの出力ディレクトリを設定
if not output_dir.get():
output_dir.set(os.path.dirname(filename))
def browse_output_dir():
"""出力ディレクトリを選択"""
dirname = filedialog.askdirectory(title='出力ディレクトリを選択')
if dirname:
output_dir.set(dirname)
def export_thread():
"""エクスポート処理(別スレッド)"""
try:
# Blenderの実行可能ファイルを探す
blender_path = find_blender_executable()
if not blender_path:
log('\nエラー: Blenderが見つかりません\n')
log('以下のいずれかの方法で解決してください:\n')
log('1. BlenderをPATH環境変数に追加\n')
log('2. Blenderを標準的な場所にインストール\n')
messagebox.showerror('エラー', 'Blenderが見つかりません。\nBlenderがインストールされていることを確認してください。')
return
# 出力ディレクトリの処理
output_directory = output_dir.get() if output_dir.get() else os.path.dirname(blend_path.get())
# Blender内で実行するスクリプトを作成
script_content = f'''# -*- coding: utf-8 -*-
import bpy
import os
import shutil
UE5_SCALE = {UE5_SCALE}
TEXTURE_FOLDER_NAME = '{TEXTURE_FOLDER_NAME}'
# 設定値
blend_path = r'{blend_path.get()}'
output_dir = r'{output_directory}'
ue5_mode = {ue5_mode.get()}
apply_mods = {apply_mods.get()}
opt_materials = {opt_materials.get()}
copy_tex = {copy_tex.get()}
try:
# Blenderファイルを開く
bpy.ops.wm.open_mainfile(filepath=blend_path)
# 出力パスの設定
blend_name = os.path.splitext(os.path.basename(blend_path))[0]
# 出力パスを絶対パスに変換
output_dir = os.path.abspath(output_dir)
output_path = os.path.join(output_dir, blend_name + '.obj')
# モディファイヤの適用
if apply_mods:
try:
# オブジェクトモードに切り替え
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# 全オブジェクトを走査してモディファイヤを適用
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.modifiers:
# オブジェクトを選択してアクティブに設定
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# モディファイヤをリストにコピー(削除時のインデックス変更対策)
modifiers_to_apply = [mod.name for mod in obj.modifiers]
for modifier_name in modifiers_to_apply:
try:
if modifier_name in obj.modifiers:
bpy.ops.object.modifier_apply(modifier=modifier_name)
print(f'モディファイヤ適用: {{obj.name}}.{{modifier_name}}')
except Exception as e:
print(f'モディファイヤ {{modifier_name}} の適用に失敗: {{str(e)}}')
except Exception as e:
print(f'モディファイヤ適用中にエラーが発生: {{str(e)}}')
# マテリアルの最適化
if opt_materials:
try:
materials = {{}}
material_map = {{}}
for mat in bpy.data.materials:
if mat.use_nodes:
# マテリアルの主要プロパティをキーとして使用
key = (
round(mat.diffuse_color[0], 3),
round(mat.diffuse_color[1], 3),
round(mat.diffuse_color[2], 3),
round(mat.metallic, 3),
round(mat.roughness, 3)
)
if key in materials:
material_map[mat.name] = materials[key]
print(f'マテリアル統合: {{mat.name}} -> {{materials[key]}}')
else:
materials[key] = mat.name
material_map[mat.name] = mat.name
# オブジェクトのマテリアルを統合されたものに置き換え
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data.materials:
for i, mat in enumerate(obj.data.materials):
if mat and mat.name in material_map:
new_mat_name = material_map[mat.name]
if new_mat_name != mat.name:
obj.data.materials[i] = bpy.data.materials[new_mat_name]
except Exception as e:
print(f'マテリアル最適化中にエラーが発生: {{str(e)}}')
# 全オブジェクトを選択
bpy.ops.object.select_all(action='SELECT')
# OBJエクスポート設定
print(f'エクスポート開始: {{output_path}}')
if ue5_mode:
# UE5向け設定:座標系変換とスケール調整
bpy.ops.wm.obj_export(
filepath=output_path,
export_selected_objects=False,
apply_modifiers=False, # モディファイヤは事前に適用済み
export_uv=True,
export_normals=True,
export_colors=True,
export_materials=True,
export_triangulated_mesh=True,
export_object_groups=True,
export_material_groups=True,
forward_axis='X',
up_axis='Z',
global_scale=UE5_SCALE
)
print(f'UE5モード: 座標系変換(X前方,Z上方), スケール={{UE5_SCALE}}, 三角形化')
else:
# 標準設定
bpy.ops.wm.obj_export(
filepath=output_path,
export_selected_objects=False,
apply_modifiers=False, # モディファイヤは事前に適用済み
export_uv=True,
export_normals=True,
export_colors=True,
export_materials=True,
export_triangulated_mesh=False,
export_object_groups=True,
export_material_groups=True,
forward_axis='NEGATIVE_Z',
up_axis='Y',
global_scale=1.0
)
print(f'標準モード: Blenderデフォルト座標系')
# テクスチャのコピーとMTLファイルの修正
if copy_tex:
mtl_path = output_path.replace('.obj', '.mtl')
if os.path.exists(mtl_path):
try:
# texturesフォルダ作成
textures_dir = os.path.join(output_dir, TEXTURE_FOLDER_NAME)
os.makedirs(textures_dir, exist_ok=True)
print(f'テクスチャフォルダ作成: {{textures_dir}}')
# MTLファイルを読み込んで修正
with open(mtl_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
texture_count = 0
for line in lines:
if line.strip().startswith('map_'):
parts = line.split(None, 1)
if len(parts) == 2:
texture_path = parts[1].strip()
# Windowsパスのバックスラッシュをスラッシュに変換
texture_path = texture_path.replace('\\\\', '/')
# テクスチャファイルの絶対パス取得
if not os.path.isabs(texture_path):
# 相対パスの場合、Blenderファイルのディレクトリを基準とする
texture_path = os.path.join(os.path.dirname(blend_path), texture_path)
texture_path = os.path.normpath(texture_path)
tex_name = os.path.basename(texture_path)
# テクスチャファイルが存在する場合のみコピー
if os.path.exists(texture_path) and os.path.isfile(texture_path):
dest_path = os.path.join(textures_dir, tex_name)
try:
shutil.copy2(texture_path, dest_path)
print(f'テクスチャコピー成功: {{tex_name}}')
texture_count += 1
except Exception as e:
print(f'テクスチャコピー失敗 {{tex_name}}: {{str(e)}}')
else:
print(f'テクスチャファイルが見つかりません: {{texture_path}}')
# MTLファイル内のパスを相対パスに変更(スラッシュを使用)
new_line = f'{{parts[0]}} {{TEXTURE_FOLDER_NAME}}/{{tex_name}}\\n'
new_lines.append(new_line)
else:
new_lines.append(line)
else:
new_lines.append(line)
# MTLファイルを上書き
with open(mtl_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f'MTLファイル更新完了: {{texture_count}}個のテクスチャを処理')
except Exception as e:
print(f'テクスチャ処理中にエラーが発生: {{str(e)}}')
mode_str = 'UE5モード' if ue5_mode else '標準モード'
print(f'\\nエクスポート完了({{mode_str}})')
print(f'OBJファイル: {{output_path}}')
print(f'MTLファイル: {{output_path.replace(".obj", ".mtl")}}')
# 出力ファイルサイズを表示(MB単位)
if os.path.exists(output_path):
obj_size = os.path.getsize(output_path) / (1000 * 1000) # MB単位に修正
print(f'OBJファイルサイズ: {{obj_size:.2f}} MB')
except Exception as e:
print(f'エクスポート中にエラーが発生しました: {{str(e)}}')
import traceback
traceback.print_exc()
'''
# 一時スクリプトファイル作成
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
f.write(script_content)
temp_script = f.name
# Blenderをバックグラウンドで実行
cmd = [
blender_path,
'--background',
'--python', temp_script
]
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding='utf-8'
)
# 出力をリアルタイムで表示
for line in process.stdout:
log(line)
process.wait()
# 一時ファイル削除
try:
os.unlink(temp_script)
except OSError as e:
log(f'一時ファイル削除失敗: {e}\n')
if process.returncode == 0:
log('\nエクスポート完了!\n')
messagebox.showinfo('完了', 'エクスポートが完了しました')
else:
log('\nエラーが発生しました\n')
messagebox.showerror('エラー', 'エクスポート中にエラーが発生しました')
except Exception as e:
log(f'\nエラー: {str(e)}\n')
messagebox.showerror('エラー', str(e))
def export():
"""エクスポート実行"""
if not blend_path.get():
messagebox.showerror('エラー', 'Blenderファイルを選択してください')
return
# ログクリア
log_text.delete('1.0', 'end')
# ガイダンス表示
log('========================================\n')
log('Blender OBJ/MTL エクスポータ\n')
log('========================================\n')
log('エクスポート設定:\n')
log(f' 入力ファイル: {os.path.basename(blend_path.get())}\n')
log(f' 出力先: {output_dir.get() or os.path.dirname(blend_path.get())}\n')
log(f' UE5モード: {"有効" if ue5_mode.get() else "無効"}\n')
log(f' モディファイヤ適用: {"有効" if apply_mods.get() else "無効"}\n')
log(f' マテリアル最適化: {"有効" if opt_materials.get() else "無効"}\n')
log(f' テクスチャコピー: {"有効" if copy_tex.get() else "無効"}\n')
log('========================================\n')
log('処理を開始します...\n\n')
# 別スレッドで実行
thread = threading.Thread(target=export_thread)
thread.daemon = True
thread.start()
def create_gui():
"""GUI作成"""
global root, log_text, blend_path, output_dir, ue5_mode, apply_mods, opt_materials, copy_tex
print("========================================")
print("Blender OBJ/MTL エクスポータ GUI版")
print("========================================")
print("概要: BlenderファイルをOBJ/MTL形式でエクスポート")
print("操作方法:")
print(" 1. Blenderファイルを選択")
print(" 2. 必要に応じてオプションを設定")
print(" 3. エクスポート実行ボタンをクリック")
print("注意事項:")
print(" - Blender 4.5以降が必要です")
print(" - 元のBlenderファイルは変更されません")
print("========================================")
root = ctk.CTk()
root.title('Blender OBJ/MTL エクスポータ')
root.geometry(f'{WINDOW_WIDTH}x{WINDOW_HEIGHT}')
# 変数初期化
blend_path = ctk.StringVar()
output_dir = ctk.StringVar()
ue5_mode = ctk.BooleanVar(value=True)
apply_mods = ctk.BooleanVar(value=False)
opt_materials = ctk.BooleanVar(value=False)
copy_tex = ctk.BooleanVar(value=False)
# メインフレーム
main_frame = ctk.CTkScrollableFrame(root)
main_frame.pack(fill='both', expand=True, padx=PADDING_X, pady=PADDING_Y)
# プログラム概要
o_frame = ctk.CTkFrame(main_frame)
o_frame.pack(fill='x', padx=PADDING_X, pady=PADDING_Y)
ctk.CTkLabel(o_frame, text='プログラム概要', font=('', 16, 'bold')).pack(pady=(10, 5))
overview_text = '''このプログラムは、BlenderファイルをOBJ/MTL形式でエクスポートします。
Unreal Engine 5向けの最適化機能を備えており、座標系変換、スケール調整、
メッシュの三角形化などを自動で行います。'''
ctk.CTkLabel(o_frame, text=overview_text, justify='left').pack(padx=20, pady=(5, 10))
# 利用上の注意点
n_frame = ctk.CTkFrame(main_frame)
n_frame.pack(fill='x', padx=PADDING_X, pady=PADDING_Y)
ctk.CTkLabel(n_frame, text='利用上の注意点', font=('', 16, 'bold')).pack(pady=(10, 5))
notice_text = '''• Blender 4.5以降が必要です
• UE5モードではメッシュが自動的に三角形化されます
• テクスチャコピー機能使用時は、MTLファイルが自動修正されます
• モディファイヤ適用は元に戻せません(元ファイルは変更されません)'''
ctk.CTkLabel(n_frame, text=notice_text, justify='left').pack(padx=20, pady=(5, 10))
# ファイル選択
file_frame = ctk.CTkFrame(main_frame)
file_frame.pack(fill='x', padx=PADDING_X, pady=PADDING_Y)
ctk.CTkLabel(file_frame, text='ファイル設定', font=('', 16, 'bold')).pack(pady=(10, 5))
# 入力ファイル
input_frame = ctk.CTkFrame(file_frame)
input_frame.pack(fill='x', padx=20, pady=5)
ctk.CTkLabel(input_frame, text='Blenderファイル:').pack(side='left', padx=(10, 5))
ctk.CTkEntry(input_frame, textvariable=blend_path, width=ENTRY_WIDTH).pack(side='left', padx=5)
ctk.CTkButton(input_frame, text='参照', command=browse_blend_file, width=80).pack(side='left', padx=(5, 10))
# 出力ディレクトリ
output_frame = ctk.CTkFrame(file_frame)
output_frame.pack(fill='x', padx=20, pady=(5, 10))
ctk.CTkLabel(output_frame, text='出力ディレクトリ:').pack(side='left', padx=(10, 5))
ctk.CTkEntry(output_frame, textvariable=output_dir, width=ENTRY_WIDTH).pack(side='left', padx=5)
ctk.CTkButton(output_frame, text='参照', command=browse_output_dir, width=80).pack(side='left', padx=(5, 10))
# オプション設定
opt_frame = ctk.CTkFrame(main_frame)
opt_frame.pack(fill='x', padx=PADDING_X, pady=PADDING_Y)
ctk.CTkLabel(opt_frame, text='エクスポートオプション', font=('', 16, 'bold')).pack(pady=(10, 5))
# オプションのコンテナ
options_container = ctk.CTkFrame(opt_frame)
options_container.pack(fill='x', padx=20, pady=(5, 10))
# UE5モード
ue5_frame = ctk.CTkFrame(options_container)
ue5_frame.pack(fill='x', pady=5)
ctk.CTkCheckBox(ue5_frame, text='UE5モード', variable=ue5_mode).pack(side='left', padx=(10, 20))
ctk.CTkLabel(ue5_frame, text='座標系とスケールをUE5向けに変換、メッシュを三角形化',
text_color='gray').pack(side='left')
# モディファイヤ適用
mod_frame = ctk.CTkFrame(options_container)
mod_frame.pack(fill='x', pady=5)
ctk.CTkCheckBox(mod_frame, text='モディファイヤ適用', variable=apply_mods).pack(side='left', padx=(10, 20))
ctk.CTkLabel(mod_frame, text='全モディファイヤを適用してエクスポート(元ファイルは変更されません)',
text_color='gray').pack(side='left')
# マテリアル最適化
mat_frame = ctk.CTkFrame(options_container)
mat_frame.pack(fill='x', pady=5)
ctk.CTkCheckBox(mat_frame, text='マテリアル最適化', variable=opt_materials).pack(side='left', padx=(10, 20))
ctk.CTkLabel(mat_frame, text='同一設定のマテリアルを統合して出力',
text_color='gray').pack(side='left')
# テクスチャコピー
tex_frame = ctk.CTkFrame(options_container)
tex_frame.pack(fill='x', pady=5)
ctk.CTkCheckBox(tex_frame, text='テクスチャコピー', variable=copy_tex).pack(side='left', padx=(10, 20))
ctk.CTkLabel(tex_frame, text='使用テクスチャをtexturesフォルダにコピーし、パスを相対化',
text_color='gray').pack(side='left')
# 実行ボタン
ctk.CTkButton(root, text='エクスポート実行', command=export, height=40).pack(pady=PADDING_Y)
# ログ出力エリア
log_frame = ctk.CTkFrame(root)
log_frame.pack(fill='both', expand=True, padx=PADDING_X, pady=PADDING_Y)
ctk.CTkLabel(log_frame, text='実行ログ', font=('', 14, 'bold')).pack(pady=(5, 0))
log_text = ctk.CTkTextbox(log_frame, height=LOG_HEIGHT)
log_text.pack(fill='both', expand=True, padx=5, pady=5)
root.mainloop()
# メイン処理
if __name__ == '__main__':
create_gui()