【Blender】複数スクリプトを同梱したアドオンを作る

投稿者: | 2023-05-25

ChatGPTを使って、複数のスクリプトが同梱されたBlenderのアドオンパッケージを作ってみました。

新規マテリアルにノードを設定してオブジェクトに割り当てると、三角形の数によってオブジェクトに自動で名前をつけるの処理を、一つのボタンで同時に行います。

スクリプト

今回は3つのスクリプトを作りました。Blender 3.5と互換性があります。

__init__.py

# -*- coding: utf-8 -*-

bl_info = {
    "name": "Simple Tool",
    "author": "Your Name",
    "version": (1, 0),
    "blender": (3, 5, 0),
    "location": "View3D > Sidebar > Simple Tool",
    "description": "Simple tool for input and action",
    "category": "3D View",
}

import bpy
from . import rename_utils
from . import material_setup

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Apply Name"

    def execute(self, context):
        rename_utils.rename_by_triangle_count(context.scene.my_string_prop)
        material_setup.setup_material(context.scene.my_string_prop)
        return {'FINISHED'}

class SimpleToolPanel(bpy.types.Panel):
    bl_label = "Simple Tool"
    bl_idname = "OBJECT_PT_simple_tool"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Simple Tool"

    def draw(self, context):
        layout = self.layout

        row = layout.row()
        row.prop(context.scene, "my_string_prop")

        row = layout.row()
        row.operator("object.simple_operator")

def register():
    bpy.utils.register_class(SimpleToolPanel)
    bpy.utils.register_class(SimpleOperator)
    bpy.types.Scene.my_string_prop = bpy.props.StringProperty(name = "New Name")

def unregister():
    bpy.utils.unregister_class(SimpleToolPanel)
    bpy.utils.unregister_class(SimpleOperator)
    del bpy.types.Scene.my_string_prop

material_setup.py

import bpy


def initialize_material_nodes_and_textures(material):
    
    material_name = material.name
    
    # マテリアルのノードを作成
    material.use_nodes = True
    material.node_tree.nodes.clear()

    nodes = material.node_tree.nodes
    links = material.node_tree.links
    
    
    # デフォルトのマテリアルアウトプットノード
    material_output = nodes.new('ShaderNodeOutputMaterial')
    material_output.location = (300, 25)

    # Principled BSDFノード
    principled_bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    principled_bsdf.location = (0, 0)
    links.new(principled_bsdf.outputs['BSDF'], material_output.inputs['Surface'])

    # Normal Mapノード
    normal_map = nodes.new('ShaderNodeNormalMap')
    normal_map.location = (-250, -730)
    links.new(normal_map.outputs['Normal'], principled_bsdf.inputs['Normal'])

    
    map_size = bpy.context.scene["my_enum_prop"]
        
    # Image Textureノード(Albedo)
    image_albedo = nodes.new('ShaderNodeTexImage')
    image_albedo.location = (-400, 100)
    image_albedo.image = bpy.data.images.new(name=material_name + '_albedo', width=map_size, height=map_size)
    image_albedo.image.colorspace_settings.name = 'sRGB'
    image_albedo.image.use_fake_user = True
    links.new(image_albedo.outputs['Color'], principled_bsdf.inputs['Base Color'])

    # Image Textureノード(Metallic)
    image_metallic = nodes.new('ShaderNodeTexImage')
    image_metallic.location = (-400, -170)
    image_metallic.image = bpy.data.images.new(name=material_name + '_metallic', width=map_size, height=map_size)
    image_metallic.image.colorspace_settings.name = 'Non-Color'
    image_metallic.image.use_fake_user = True
    links.new(image_metallic.outputs['Color'], principled_bsdf.inputs['Metallic'])
    
    # Image Textureノード(Roughness)
    image_roughness = nodes.new('ShaderNodeTexImage')
    image_roughness.location = (-400, -440)
    image_roughness.image = bpy.data.images.new(name=material_name + '_roughness', width=map_size, height=map_size)
    image_roughness.image.colorspace_settings.name = 'Non-Color'
    image_roughness.image.use_fake_user = True
    links.new(image_roughness.outputs['Color'], principled_bsdf.inputs['Roughness'])
    
    # Image Textureノード(Normal)
    image_normal = nodes.new('ShaderNodeTexImage')
    image_normal.location = (-600, -760)
    image_normal.image = bpy.data.images.new(name=material_name + '_normal', width=map_size, height=map_size)
    image_normal.image.colorspace_settings.name = 'Non-Color'
    image_normal.image.use_fake_user = True
    links.new(image_normal.outputs['Color'], normal_map.inputs['Color'])
    
    for n in nodes:
        n.select = False

    
def setup_material(material_name):
    # 現在のシーンとテキストフィールドの値を取得
    scene = bpy.context.scene


    # オブジェクトが選択されているか確認
    selected_objects = bpy.context.selected_objects
    if not selected_objects:
        raise Exception("No objects selected.")

    # マテリアルを作成
    material = bpy.data.materials.get(material_name)
    
    if material is None :
        material = bpy.data.materials.new(name=material_name)
        initialize_material_nodes_and_textures(material)
    
    material.use_fake_user = True
    
    # 選択された全てのオブジェクトをループ
    for obj in bpy.context.selected_objects:
        # オブジェクトをアクティブに設定
        bpy.context.view_layer.objects.active = obj

        # マテリアルがある場合のみ処理を行う
        if obj.type == 'MESH' and obj.material_slots:
            # 全てのマテリアルのuse_fake_userをTrueに設定
            for slot in obj.material_slots:
                if slot.material is not None:  # マテリアルスロットが空でないことを確認
                    slot.material.use_fake_user = True
            # 全てのマテリアルスロットを削除
            for _ in range(len(obj.material_slots)):
                bpy.ops.object.material_slot_remove()

    # マテリアルを選択されたオブジェクトに割り当て
    for obj in selected_objects:
        if obj.type == 'MESH':
            obj.data.materials.append(material)

rename_utils.py

import bpy

def calculate_triangle_count(obj):
    if obj.type != 'MESH':
        return 0
    mesh = obj.data
    mesh.calc_loop_triangles()
    return len(mesh.loop_triangles)

def rename_by_triangle_count(new_name):
    selected_objects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']

    if not selected_objects:
        print("No object selected.")
    else:
        object_triangle_counts = {obj: calculate_triangle_count(obj) for obj in selected_objects}
        sorted_objects = sorted(object_triangle_counts, key=object_triangle_counts.get, reverse=True)

        for i, obj in enumerate(sorted_objects):
            obj.name = new_name + f"_LOD{i}"

インストール方法

まず「simple_tool」というフォルダを作り、上の3つのスクリプトをそれぞれメモ帳などにコピペして、名前を付けてこのフォルダに保存します。拡張子は.pyに変更します。

このフォルダをzipファイルに圧縮します。

Blenderを開き、Edit > Preferences… を開きます。

Add-onsの「Install…」ボタンをクリックします。

上のzipファイルを選び、「Install Add-ons」ボタンを押します。

アドオンがインストールされるので、一覧でチェックを入れて有効化します。

その横の三角アイコンでアドオンの情報が見れます。アドオンを削除するときは「Remove」ボタンを押します。

使い方

有効化すると、3Dビューポートのサイドバーに新しいタブとカスタムパネルが表示されます。パネルにはテキストフィールドとボタンが一つずつあります。

シーンに適当にCube、Cylinder、Suzanneの3つのメッシュオブジェクトを追加しました。

この3つをすべて選択し、テキストフィールドに名前を入力して、その下のボタンを押します。

すると、オブジェクトの名前が変更されました。

三角形の数の多いSuzanne、Cylinder、Cubeの順になっています。

また、テキストフィールドと同じ名前のマテリアルが作られ、3つのオブジェクトに割当てられています。

Image Textureノードには同名の新しいテクスチャが設定されていて、ノードも指定したとおりに接続されています。

これで、複数スクリプトを同梱したアドオンを作ることができました。

ChatGPT

今回も、ChatGPT(GPT-4)にスクリプトを作ってもらいました。前の記事のチャットに続けて、名前を変更するスクリプトの全体と、ボタンを押したときにその処理をしたい旨を伝えました。

すると、他のスクリプトのメソッドを使う方法や、アドオンには__init__.pyを含めることなどを教えてくれました。

通常、Pythonでは他のPythonファイル(.py)をモジュールとしてインポートすることで再利用します。rename_by_triangle_count.py ファイルが同じディレクトリにある場合、その内容を import ステートメントでインポートすることが可能です。

以下の例では、rename_by_triangle_count.py ファイルから calculate_triangle_count 関数と rename_objects 関数をインポートします。これらの関数は、ChangeNamesOperator の execute メソッドから呼び出すことが可能となります。
# -*- coding: utf-8 -*-

import bpy
from .rename_by_triangle_count import calculate_triangle_count, rename_objects  # Assuming these functions are defined in rename_by_triangle_count.py

# ...

class ChangeNamesOperator(bpy.types.Operator):
    bl_idname = "object.change_names"
    bl_label = "Change Object Names"

    def execute(self, context):
        selected_objects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']

        if not selected_objects:
            print("No object selected.")
        else:
            rename_objects(selected_objects, context.scene.simple_tool.my_string)

        return {'FINISHED'}

# ...
パッケージは、特定のディレクトリに配置された一連の .py ファイルで、そのディレクトリには __init__.py ファイルが含まれている必要があります。

具体的なディレクトリ構成や作成手順も聞きました。

simple_tool/
  ├── __init__.py    # メインのアドオンスクリプト
  └── rename_utils.py  # 三角形の数に基づいてオブジェクトの名前を変更するユーティリティ
具体的には以下のような手順となります:

  • simple_tool ディレクトリを右クリックし、「Send to」 > 「Compressed (zipped) folder」を選択します(Windowsの場合)。
  • 生成されたZIPファイルを適当な名前にリネームします(例えば simple_tool.zip)。
  • Blenderを開き、「Edit」 > 「Preferences」を選択します。
  • 「Add-ons」タブをクリックし、「Install…」ボタンをクリックします。
  • ダイアログボックスが開くので、生成したZIPファイル(simple_tool.zip)を選択し、「Install Add-on」ボタンをクリックします。

  • これにより、アドオンがインストールされ、Blenderの「Preferences」メニューの「Add-ons」セクションに表示されるようになります。

    その後、マテリアルを設定するスクリプトも含めて、全体のスクリプトを出力してもらいました。自分で修正したスクリプトを送ると修正してくれます。

    ChatGPTを使うことで、素早くアドオンパッケージが作れました。

    コメントを残す

    メールアドレスが公開されることはありません。