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] 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()