Blender に3Dアセット(obj,mtl)の配置(ソースコードと実行結果)


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/
その他の前準備
Blenderをインストールしておく.
Blender への3Dアセット(obj,mtl)の配置プログラム
概要
このプログラムは、ユーザーの要求に基づいて複数の3Dオブジェクトを配置する。ランダム配置では、衝突回避を考慮しながら配置を計画する。Blenderをバックグラウンド実行して、配置を行い Blender ファイルを保存する。
主要技術
- 衝突回避アルゴリズム: 各オブジェクト間の距離を計算し、設定された最小距離以上を保証する位置を探索する[1]。
参考文献
[1] LaValle, S. M. (2006). Planning algorithms. Cambridge University Press. Chapter 5: Sampling-Based Motion Planning.
ソースコード
# Blender への3Dアセット(obj,mtl)の配置プログラム
# [説明]
# Blender Python API (bpy) の基本構造
# コンテキストとデータ構造
#
# bpy.context: 現在のシーン、選択オブジェクト等のコンテキスト情報
# bpy.data: メッシュ、マテリアル、テクスチャ等の全データブロック
# bpy.ops: オペレーター(インポート、変換等の操作)
# bpy.types: クラス定義(オブジェクト、メッシュ等)
# 座標系
#
# Blenderは右手系Z-up座標系を使用
# OBJファイル(通常Y-up)からの変換が必要な場合あり
# 2. OBJ/MTLインポート
# 1種類 いまあるBlenderファイルにインポートと考える
#
# 軸変換の重要性
#
# TripoSR等のAI生成モデルは座標系が異なる場合が多い
# axis_forwardとaxis_upパラメータで適切に設定
# 一般的な設定パターン:
# Y-up to Z-up: axis_forward='-Z', axis_up='Y'
# Z-up維持: axis_forward='Y', axis_up='Z'
# 3. 大量アセット管理のベストプラクティス
# コレクション(Collection)による階層管理
#
# # コレクション作成
# collection = bpy.data.collections.new("3D_Assets")
# bpy.context.scene.collection.children.link(collection)
#
# # オブジェクトをコレクションに追加
# obj = bpy.context.selected_objects[0]
# collection.objects.link(obj)
# bpy.context.scene.collection.objects.unlink(obj)
# 命名規則とメタデータ
#
# 一貫した命名規則(例:Asset_Category_Number_Variant)
# カスタムプロパティによるメタデータ付与
# obj["asset_type"] = "prop"
# obj["source"] = "tripoSR"
# obj["import_date"] = "2024-01-01"
# 4. パフォーマンス最適化
# インスタンス化(Instancing)
#
# # リンク複製によるメモリ効率化
# original = bpy.data.objects['original_object']
# for i in range(100):
# instance = original.copy()
# instance.data = original.data # メッシュデータを共有
# instance.location = (i * 2, 0, 0)
# collection.objects.link(instance)
# 5. マテリアルとテクスチャの最適化
# マテリアルの統合
#
# def consolidate_materials(obj):
# # 重複マテリアルの削除
# materials = {}
# for slot in obj.material_slots:
# if slot.material:
# mat_name = slot.material.name.split('.')[0]
# if mat_name not in materials:
# materials[mat_name] = slot.material
# else:
# slot.material = materials[mat_name]
# テクスチャパスの管理
#
# import os
#
# def fix_texture_paths(base_path):
# for image in bpy.data.images:
# if image.source == 'FILE':
# filename = os.path.basename(image.filepath)
# new_path = os.path.join(base_path, filename)
# if os.path.exists(new_path):
# image.filepath = new_path
# ランダム配置with衝突回避
#
# import random
# from mathutils import Vector
#
# def random_placement_with_collision(objects, bounds=(-10, 10), min_distance=2.0):
# placed_positions = []
#
# for obj in objects:
# valid_position = False
# attempts = 0
#
# while not valid_position and attempts < 100:
# pos = Vector((
# random.uniform(bounds[0], bounds[1]),
# random.uniform(bounds[0], bounds[1]),
# 0
# ))
#
# valid_position = True
# for placed_pos in placed_positions:
# if (pos - placed_pos).length < min_distance:
# valid_position = False
# break
#
# attempts += 1
#
# if valid_position:
# obj.location = pos
# placed_positions.append(pos)
import os
import sys
import json
import random
import math
import numpy as np
import cv2
import tempfile
import subprocess
from datetime import datetime
import winreg
try:
import tkinter as tk
from tkinter import filedialog, messagebox
except ImportError:
print("tkinterが利用できません。ファイルパスを手動入力してください。")
tk = None
class BlenderObjectPlacer:
def __init__(self):
self.bounds = (-10, 10)
self.min_distance = 2.0
self.num_objects = 10
self.obj_path = None
self.mtl_path = None
self.use_primitive = False
self.primitive_type = 'cube'
self.placements = []
self.blender_path = None
self.placement_mode = 'random' # 'random' or 'custom'
self.input_blend_path = None # 既存のBlenderファイル
def find_blender_windows(self):
"""Windows環境でBlenderのパスを検索"""
possible_paths = []
# レジストリから検索
try:
reg_paths = [
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Blender Foundation"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Blender Foundation"),
]
for hkey, subkey in reg_paths:
try:
with winreg.OpenKey(hkey, subkey) as key:
for i in range(winreg.QueryInfoKey(key)[0]):
version = winreg.EnumKey(key, i)
with winreg.OpenKey(key, version) as version_key:
install_path = winreg.QueryValue(version_key, "")
blender_exe = os.path.join(install_path, "blender.exe")
if os.path.exists(blender_exe):
possible_paths.append(blender_exe)
except:
pass
except:
pass
# 一般的なインストール場所を確認
common_paths = [
r"C:\Program Files\Blender Foundation",
r"C:\Program Files (x86)\Blender Foundation",
os.path.expanduser(r"~\AppData\Local\Blender Foundation"),
]
for base_path in common_paths:
if os.path.exists(base_path):
for item in os.listdir(base_path):
blender_exe = os.path.join(base_path, item, "blender.exe")
if os.path.exists(blender_exe):
possible_paths.append(blender_exe)
# PATH環境変数を確認
for path in os.environ.get("PATH", "").split(os.pathsep):
blender_exe = os.path.join(path, "blender.exe")
if os.path.exists(blender_exe):
possible_paths.append(blender_exe)
# 重複を削除
return list(set(possible_paths))
def select_blender_path(self):
"""Blenderの実行パスを選択"""
found_paths = self.find_blender_windows()
if found_paths:
print("\n検出されたBlenderのインストール:")
for i, path in enumerate(found_paths):
print(f"{i+1}: {path}")
choice = input(f"\n使用するBlenderを選択 (1-{len(found_paths)}) または手動入力は0: ").strip()
try:
idx = int(choice)
if 1 <= idx <= len(found_paths):
self.blender_path = found_paths[idx-1]
return True
except:
pass
# 手動入力
if tk:
root = tk.Tk()
root.withdraw()
self.blender_path = filedialog.askopenfilename(
title="blender.exeを選択",
filetypes=[("Executable", "*.exe"), ("All files", "*.*")]
)
root.destroy()
else:
self.blender_path = input("\nblender.exeのフルパスを入力: ").strip()
if self.blender_path and os.path.exists(self.blender_path):
return True
else:
print("エラー: Blenderが見つかりません")
return False
def load_custom_placement(self):
"""カスタム配置JSONファイルを読み込み"""
# サンプルJSON表示
print("\nJSONファイルのサンプル:")
sample = '''[
{"x": 0, "y": 0, "z": 0, "rotation": 0},
{"x": 5, "y": 5, "z": 0, "rotation": 1.57},
{"x": -5, "y": 3, "z": 0, "rotation": 3.14}
]'''
print(sample)
print("\nJSONファイルの指定")
# ファイル選択
if tk:
root = tk.Tk()
root.withdraw()
json_path = filedialog.askopenfilename(
title="配置JSONファイルを選択",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
root.destroy()
else:
json_path = input("JSONファイルのパスを入力: ").strip().replace('"', '')
if not json_path or not os.path.exists(json_path):
print("ファイルが見つかりません")
return False
# JSON読み込み
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, list):
print("エラー: JSONはリスト形式である必要があります")
return False
# 各エントリを検証
valid_placements = []
for i, entry in enumerate(data):
try:
# 必須フィールドチェック
if not all(key in entry for key in ['x', 'y', 'z', 'rotation']):
print(f"行 {i+1}: {entry}")
print("スキップ")
continue
# 数値チェック
x = float(entry['x'])
y = float(entry['y'])
z = float(entry['z'])
rotation = float(entry['rotation'])
valid_placements.append({
'position': [x, y, z],
'rotation': rotation
})
except (ValueError, TypeError) as e:
print(f"行 {i+1}: {entry}")
print("スキップ")
continue
if not valid_placements:
print("有効な配置データがありません")
return False
self.placements = valid_placements
print(f"\n{len(self.placements)}個の配置データを読み込みました")
return True
except json.JSONDecodeError as e:
print(f"JSONパースエラー: {e}")
return False
except Exception as e:
print(f"ファイル読み込みエラー: {e}")
return False
def get_user_inputs(self):
"""ユーザーからの入力を取得"""
print("\n=== 3Dオブジェクト配置ツール (Windows版) ===")
# Blenderパス設定
if not self.select_blender_path():
return False
# 既存Blenderファイル選択
print("\n既存のBlenderファイルを選択")
if tk:
root = tk.Tk()
root.withdraw()
messagebox.showinfo("ファイル選択", "オブジェクトを配置する既存のBlenderファイルを選択してください")
self.input_blend_path = filedialog.askopenfilename(
title="既存のBlenderファイルを選択(配置を追加するファイル)",
filetypes=[("Blend files", "*.blend"), ("All files", "*.*")]
)
root.destroy()
else:
self.input_blend_path = input("\n既存のBlenderファイルのパスを入力: ").strip().replace('"', '')
if not self.input_blend_path or not os.path.exists(self.input_blend_path):
print("エラー: Blenderファイルが見つかりません")
return False
# オブジェクトタイプ選択
choice = input("\n1: OBJ/MTLファイルを使用\n2: プリミティブ形状を使用\n選択してください (1 or 2) [1]: ").strip()
if choice == '2':
self.use_primitive = True
print("\nプリミティブ形状:")
print("1: 立方体 (Cube)")
print("2: 球 (Sphere)")
print("3: 円柱 (Cylinder)")
print("4: 六角柱 (Hexagonal Prism)")
prim_choice = input("選択してください [1]: ").strip()
prim_map = {'1': 'cube', '2': 'sphere', '3': 'cylinder', '4': 'hexagon'}
self.primitive_type = prim_map.get(prim_choice, 'cube')
else:
# ファイル選択
if tk:
root = tk.Tk()
root.withdraw()
print("\nOBJファイルを選択してください...")
self.obj_path = filedialog.askopenfilename(
title="OBJファイルを選択",
filetypes=[("OBJ files", "*.obj"), ("All files", "*.*")]
)
if self.obj_path:
# MTLファイルを自動検索
base_name = os.path.splitext(self.obj_path)[0]
auto_mtl = base_name + ".mtl"
if os.path.exists(auto_mtl):
use_auto = input(f"\nMTLファイルを検出: {os.path.basename(auto_mtl)}\nこれを使用しますか? (Y/n): ").strip().lower()
if use_auto != 'n':
self.mtl_path = auto_mtl
if not self.mtl_path:
print("\nMTLファイルを選択してください...")
self.mtl_path = filedialog.askopenfilename(
title="MTLファイルを選択",
initialdir=os.path.dirname(self.obj_path),
filetypes=[("MTL files", "*.mtl"), ("All files", "*.*")]
)
root.destroy()
else:
self.obj_path = input("\nOBJファイルのパスを入力: ").strip().replace('"', '')
self.mtl_path = input("MTLファイルのパスを入力: ").strip().replace('"', '')
# 配置方法選択
print("\n配置方法を選択:")
print("1: ランダム配置")
print("2: カスタム配置(JSONファイル)")
placement_choice = input("選択してください (1 or 2) [1]: ").strip()
if placement_choice == '2':
self.placement_mode = 'custom'
if not self.load_custom_placement():
return False
else:
self.placement_mode = 'random'
# ランダム配置のパラメータ
print(f"\n現在の設定:")
print(f"配置範囲: {self.bounds[0]} ~ {self.bounds[1]}")
print(f"最小距離: {self.min_distance}")
print(f"配置数: {self.num_objects}")
if input("\n設定を変更しますか? (y/N): ").strip().lower() == 'y':
try:
range_input = input(f"配置範囲 (min max) [{self.bounds[0]} {self.bounds[1]}]: ").strip()
if range_input:
min_val, max_val = map(float, range_input.split())
self.bounds = (min_val, max_val)
dist_input = input(f"最小距離 [{self.min_distance}]: ").strip()
if dist_input:
self.min_distance = float(dist_input)
num_input = input(f"配置数 [{self.num_objects}]: ").strip()
if num_input:
self.num_objects = int(num_input)
except ValueError:
print("無効な入力です。デフォルト値を使用します。")
return True
def generate_placements(self):
"""ランダム配置を生成"""
self.placements = []
placed_positions = []
for i in range(self.num_objects):
valid_position = False
attempts = 0
while not valid_position and attempts < 100:
x = random.uniform(self.bounds[0], self.bounds[1])
y = random.uniform(self.bounds[0], self.bounds[1])
z = 0
pos = np.array([x, y, z])
valid_position = True
for placed_pos in placed_positions:
if np.linalg.norm(pos[:2] - placed_pos[:2]) < self.min_distance:
valid_position = False
break
attempts += 1
if valid_position:
rotation = random.uniform(0, 2 * math.pi)
self.placements.append({
'position': pos.tolist(),
'rotation': rotation
})
placed_positions.append(pos)
def preview_placement(self):
"""OpenCVで配置をプレビュー"""
if not self.placements:
print("配置データがありません")
return False
# 配置範囲を計算
positions = [p['position'] for p in self.placements]
x_coords = [p[0] for p in positions]
y_coords = [p[1] for p in positions]
if self.placement_mode == 'custom':
# カスタム配置の場合は範囲を自動計算
margin = 5
min_x, max_x = min(x_coords) - margin, max(x_coords) + margin
min_y, max_y = min(y_coords) - margin, max(y_coords) + margin
bounds = (min(min_x, min_y), max(max_x, max_y))
else:
bounds = self.bounds
img_size = 800
scale = img_size / (bounds[1] - bounds[0])
center = img_size // 2
img = np.ones((img_size, img_size, 3), dtype=np.uint8) * 255
# グリッド描画
grid_step = int(scale)
for i in range(0, img_size, grid_step):
cv2.line(img, (i, 0), (i, img_size), (200, 200, 200), 1)
cv2.line(img, (0, i), (img_size, i), (200, 200, 200), 1)
# 原点線
origin_x = int((0 - bounds[0]) * scale)
origin_y = int(img_size - (0 - bounds[0]) * scale)
cv2.line(img, (origin_x, 0), (origin_x, img_size), (100, 100, 100), 2)
cv2.line(img, (0, origin_y), (img_size, origin_y), (100, 100, 100), 2)
# オブジェクト描画
for i, placement in enumerate(self.placements):
pos = placement['position']
rotation = placement['rotation']
px = int((pos[0] - bounds[0]) * scale)
py = int(img_size - (pos[1] - bounds[0]) * scale)
# Z座標に応じて色を変える
if pos[2] > 0:
color = (255, 0, 0) # 青(浮いている)
else:
color = (0, 0, 255) # 赤(地面)
radius = int(self.min_distance * scale / 2) if self.placement_mode == 'random' else 20
cv2.circle(img, (px, py), radius, color, -1)
cv2.circle(img, (px, py), radius, (0, 0, 0), 2)
# 方向を矢印で表現
arrow_length = radius
arrow_end_x = px + int(arrow_length * math.cos(rotation))
arrow_end_y = py - int(arrow_length * math.sin(rotation))
cv2.arrowedLine(img, (px, py), (arrow_end_x, arrow_end_y), (0, 255, 0), 2)
# インデックス表示
cv2.putText(img, str(i), (px-10, py+5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
info_text = [
f"Mode: {self.placement_mode.upper()}",
f"Objects: {len(self.placements)}",
f"Range: {bounds[0]:.1f} to {bounds[1]:.1f}",
"Press any key to continue..."
]
y_offset = 20
for text in info_text:
cv2.putText(img, text, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)
y_offset += 25
cv2.imshow("Placement Preview", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
return input("\nこの配置で続行しますか? (Y/n): ").strip().lower() != 'n'
def create_blender_script(self, json_path, input_blend_path, output_path):
"""Blender内で実行するスクリプトを作成"""
# Windowsパスの処理を改善
output_path = os.path.normpath(output_path)
input_blend_path = os.path.normpath(input_blend_path)
json_path = os.path.normpath(json_path)
script = f'''
import bpy
import json
import os
from mathutils import Vector, Euler
from datetime import datetime
# JSONファイルから設定を読み込み
with open(r"{json_path}", "r", encoding="utf-8") as f:
config = json.load(f)
placements = config["placements"]
use_primitive = config["use_primitive"]
primitive_type = config["primitive_type"]
obj_path = config.get("obj_path", "")
mtl_path = config.get("mtl_path", "")
# 既存のBlenderファイルを開く
bpy.ops.wm.open_mainfile(filepath=r"{input_blend_path}")
def consolidate_materials(obj):
"""重複マテリアルの削除"""
materials = {{}}
for slot in obj.material_slots:
if slot.material:
mat_name = slot.material.name.split('.')[0]
if mat_name not in materials:
materials[mat_name] = slot.material
else:
slot.material = materials[mat_name]
def fix_texture_paths(base_path):
"""テクスチャパスの管理"""
for image in bpy.data.images:
if image.source == 'FILE':
filename = os.path.basename(image.filepath)
new_path = os.path.join(base_path, filename)
if os.path.exists(new_path):
image.filepath = new_path
image.reload()
print(f"テクスチャパス修正: {{filename}}")
def create_primitive(name, prim_type='cube'):
"""プリミティブ形状を作成"""
if prim_type == 'cube':
bpy.ops.mesh.primitive_cube_add(size=2)
elif prim_type == 'sphere':
bpy.ops.mesh.primitive_uv_sphere_add(radius=1)
elif prim_type == 'cylinder':
bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2)
elif prim_type == 'hexagon':
bpy.ops.mesh.primitive_cylinder_add(vertices=6, radius=1, depth=2)
obj = bpy.context.active_object
obj.name = name
return obj
def import_obj_file():
"""OBJファイルをインポート"""
before_import = set(bpy.data.objects)
bpy.ops.import_scene.obj(
filepath=obj_path,
axis_forward='-Z',
axis_up='Y',
split_mode='OFF'
)
imported_objects = list(set(bpy.data.objects) - before_import)
# テクスチャパスの修正
if obj_path:
texture_base_path = os.path.dirname(obj_path)
fix_texture_paths(texture_base_path)
# マテリアルの統合
for obj in imported_objects:
if obj.type == 'MESH':
consolidate_materials(obj)
return imported_objects[0] if imported_objects else None
# コレクション作成または取得
collection_name = "Random_Placed_Objects"
if collection_name in bpy.data.collections:
collection = bpy.data.collections[collection_name]
else:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection)
# レイヤーコレクションをアクティブに
layer_collection = bpy.context.view_layer.layer_collection.children[collection_name]
bpy.context.view_layer.active_layer_collection = layer_collection
# オリジナルオブジェクトを取得/作成
if use_primitive:
original = create_primitive("Original_" + primitive_type, primitive_type)
else:
original = import_obj_file()
if not original:
raise Exception("Failed to import object")
# オリジナルをコレクションに追加(デフォルトコレクションから移動)
if original.name not in collection.objects:
if original.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(original)
collection.objects.link(original)
# オリジナルを非表示
original.hide_set(True)
original.hide_render = True
# 配置実行
for i, placement in enumerate(placements):
pos = placement['position']
rotation = placement['rotation']
# インスタンス作成
instance = original.copy()
instance.data = original.data # メッシュデータを共有
instance.name = f"Instance_{{i:03d}}"
# 位置と回転設定
instance.location = Vector((pos[0], pos[1], pos[2]))
instance.rotation_euler = Euler((0, 0, rotation), 'XYZ')
# コレクションに追加
collection.objects.link(instance)
# メタデータ追加
instance["placement_index"] = i
instance["source"] = "tripoSR" if not use_primitive else "primitive"
instance["import_date"] = datetime.now().strftime("%Y-%m-%d")
# マテリアル統計情報
material_count = len([m for m in bpy.data.materials if m.users > 0])
texture_count = len([i for i in bpy.data.images if i.users > 0])
print(f"マテリアル数: {{material_count}}")
print(f"テクスチャ数: {{texture_count}}")
print(f"配置オブジェクト数: {{len(placements)}}")
# ファイル保存
bpy.ops.wm.save_as_mainfile(filepath=r"{output_path}")
'''
return script
def execute_blender(self):
"""Blenderをバックグラウンドで実行"""
# 一時ファイル作成
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
# パスをWindowsフォーマットに変換(改善版)
config = {
"placements": self.placements,
"use_primitive": self.use_primitive,
"primitive_type": self.primitive_type,
"obj_path": os.path.normpath(self.obj_path) if self.obj_path else "",
"mtl_path": os.path.normpath(self.mtl_path) if self.mtl_path else ""
}
json.dump(config, f, indent=2, ensure_ascii=False)
json_path = f.name
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
# 出力ファイルパス
if tk:
root = tk.Tk()
root.withdraw()
messagebox.showinfo("保存先選択",
"配置後のBlenderファイルの保存先を指定してください\n" +
"(元のファイルとは別名で保存することを推奨します)")
default_name = "配置後_" + os.path.basename(self.input_blend_path)
output_path = filedialog.asksaveasfilename(
title="保存するBlenderファイル名(配置後のファイル)",
defaultextension=".blend",
initialfile=default_name,
filetypes=[("Blend files", "*.blend"), ("All files", "*.*")]
)
root.destroy()
# .lnkファイルの場合の処理
if output_path and output_path.endswith('.lnk'):
output_path = output_path[:-4] # .lnkを削除
else:
print("\n保存するBlenderファイルのパス")
print(f"推奨: 配置後_{os.path.basename(self.input_blend_path)}")
output_path = input("パスを入力 (.blend): ").strip().replace('"', '')
if not output_path:
print("キャンセルされました")
return
if not output_path.endswith('.blend'):
output_path += '.blend'
# デバッグ情報を追加
print(f"\n保存先パス(raw): {repr(output_path)}")
print(f"保存先ディレクトリ: {os.path.dirname(output_path)}")
print(f"ディレクトリ存在確認: {os.path.exists(os.path.dirname(output_path))}")
# スクリプト作成
script_content = self.create_blender_script(json_path, self.input_blend_path, output_path)
f.write(script_content)
script_path = f.name
try:
# Blender実行
cmd = [
self.blender_path,
"--background",
"--python", script_path
]
print(f"\nBlenderを実行中...")
print(f"コマンド: {' '.join(cmd)}")
# エンコーディングを指定して実行(システムのデフォルトエンコーディングを使用)
result = subprocess.run(cmd, capture_output=True, text=True, errors='replace')
# デバッグ出力
print(f"\n--- stdout ---")
print(result.stdout)
print(f"\n--- stderr ---")
print(result.stderr)
print(f"\n--- return code: {result.returncode} ---")
if result.returncode == 0:
# ファイルの存在確認
if os.path.exists(output_path):
print(f"\n成功!ファイルを保存しました: {output_path}")
print(f"ファイルサイズ: {os.path.getsize(output_path)} bytes")
else:
print(f"\nエラー: ファイルが作成されませんでした: {output_path}")
# 出力から統計情報を表示
for line in result.stdout.split('\n'):
if 'マテリアル数:' in line or 'テクスチャ数:' in line or '配置オブジェクト数:' in line:
print(line)
else:
print(f"\nエラーが発生しました (return code: {result.returncode})")
finally:
# 一時ファイル削除
try:
os.unlink(json_path)
os.unlink(script_path)
except:
pass
def run(self):
"""メイン実行"""
if not self.get_user_inputs():
return
# 配置生成とプレビュー
if self.placement_mode == 'random':
while True:
self.generate_placements()
if self.preview_placement():
break
print("\n配置を再生成します...")
else:
# カスタム配置の場合はプレビューのみ
if not self.preview_placement():
return
# Blender実行
self.execute_blender()
# 配置情報をCSV形式で表示
print("\n=== 配置情報 (CSV形式) ===")
print("index,x,y,z,rotation_z")
for i, placement in enumerate(self.placements):
pos = placement['position']
rotation = placement['rotation']
print(f"{i},{pos[0]:.6f},{pos[1]:.6f},{pos[2]:.6f},{rotation:.6f}")
if __name__ == "__main__":
placer = BlenderObjectPlacer()
placer.run()