【Blender】シェイプキー付のままミラーを自動で適用するアドオン

投稿者: | 2023-05-27

ChatGPTを使って、シェイプキーのあるオブジェクトのミラーモディファイアを自動で適用するBlenderアドオンを作りました。

ミラーモディファイア

キャラクターモデルの顔に表情のシェイプキーを2つ付けました。

Laugh
Smile

ミラーモディファイアを適用しようとするとエラーがでます。

このままUnityにインポートすると、シェイプキーのスライダーが表示されません。

エクスポートのときに「Apply Modifiers」のチェックを外すと、Unityでシェイプキーが使えますが、ミラーモディファイアが適用されません。

Blenderでエクスポート
UnityのRendererコンポーネント

解決法

ミラーモディファイアを適用するには、シェイプキーごとにオブジェクトを複製してから、個別にミラーモディファイアを適用し、シェイプキーとして統合します。

操作が多くて面倒なので、ChatGPTにこれらを自動化するアドオンを作ってもらいました。

スクリプト

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

bl_info = {
    "name": "ShapeKeySplitAndMerge",
    "category": "Object",
    "blender": (3, 5, 0),
    "description": "Duplicate mesh objects for each shape key and merge them into the original object"
}

import bpy

class SKSAM_OT_operator(bpy.types.Operator):
    bl_idname = "object.shape_key_split_and_merge"
    bl_label = "Shape Key Split and Merge"
    bl_description = "Duplicate mesh objects for each shape key and merge them into the original object"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        objs = context.selected_objects
        if len(objs) == 0 or not all([obj.type == 'MESH' for obj in objs]):
            self.report({'ERROR'}, "No mesh object selected")
            return {'CANCELLED'}

        for obj in objs:
            if not any(mod.type == 'MIRROR' for mod in obj.modifiers):
                self.report({'ERROR'}, f"No mirror modifier in {obj.name}")
                return {'CANCELLED'}

            if obj.data.shape_keys is None or len(obj.data.shape_keys.key_blocks) == 0:
                self.report({'ERROR'}, f"No shape keys in {obj.name}")
                return {'CANCELLED'}

            duplicates = []
            for key_block in obj.data.shape_keys.key_blocks[1:]:
                duplicate = obj.copy()
                duplicate.data = obj.data.copy()
                bpy.context.collection.objects.link(duplicate)
                duplicate.name = key_block.name
                duplicates.append(duplicate)

            bpy.ops.object.select_all(action='DESELECT')
            obj.select_set(True)
            context.view_layer.objects.active = obj
            bpy.ops.object.shape_key_remove(all=True)

            for duplicate in duplicates:
                for key_block in duplicate.data.shape_keys.key_blocks[:]:
                    if key_block.name != duplicate.name:
                        duplicate.shape_key_remove(key_block)
                        
                bpy.ops.object.select_all(action='DESELECT')
                duplicate.select_set(True)
                context.view_layer.objects.active = duplicate
                bpy.ops.object.shape_key_remove(all=True)

            for target in [obj] + duplicates:
                bpy.ops.object.select_all(action='DESELECT')
                target.select_set(True)
                context.view_layer.objects.active = target
                if target.modifiers.get("Mirror"):
                    bpy.ops.object.modifier_apply({"object": target}, modifier="Mirror")

            bpy.ops.object.select_all(action='DESELECT')
            for target in [obj] + duplicates:
                target.select_set(True)
            context.view_layer.objects.active = obj
            bpy.ops.object.join_shapes()

            for duplicate in duplicates:
                bpy.ops.object.select_all(action='DESELECT')
                duplicate.select_set(True)
                bpy.ops.object.delete()

        return {'FINISHED'}


class SKSAM_PT_panel(bpy.types.Panel):
    bl_label = "Shape Key Split and Merge"
    bl_category = "Shape Key SAM"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        layout.operator("object.shape_key_split_and_merge")


def register():
    bpy.utils.register_class(SKSAM_OT_operator)
    bpy.utils.register_class(SKSAM_PT_panel)


def unregister():
    bpy.utils.unregister_class(SKSAM_PT_panel)
    bpy.utils.unregister_class(SKSAM_OT_operator)

インストール方法

Blenderにインストールするには、まずメモ帳などに上のスクリプトをコピペして、名前を付けて保存し、拡張子を.pyに変更します。

Blenderを開き、Edit > Preferences… をクリックします。

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

保存したスクリプトを選択して「Install Add-on」をクリックします。

インストールされると一覧に表示されるので、チェックを入れて有効化します。

横の三角アイコンを開くと、アドオンの情報が表示されます。アドオンを削除するときは「Remove」ボタンをクリックします。

使い方

有効化すると、3Dビューポートのサイドバーに新しいタブとカスタムパネルが表示されます。

ミラーモディファイアを適用したいオブジェクトだけを選択して、パネルのボタンをクリックします。

すると、シェイプキーが付いたまま、ミラーモディファイアが適用されています。

Unityでもうまくインポートされました。

ChatGPT

今回もChatGPT(GPT-4)に指示を出して、スクリプトを書いてもらいました。

Blender(3.5)のアドオンを作ってください。

・パネルにボタンを表示する。ボタンを押すと以降の処理を行う。
・Meshオブジェクトが選択されていることを確認する
・選択がなければエラーを出力する。
・ミラーモディファイアが追加されていることを確認する。なければエラーを出力する。
・activeなオブジェクトのシェイプキーの数を確認する。
・シェイプキーがなければエラーを出力する。
・Basisも含めて、シェイプキーと同じ数になるようにオブジェクトを複製する。
・オリジナルのオブジェクト以外を、シェイプキーと同じ名前に変更する。
・オリジナルのオブジェクトのシェイプキーをすべて削除する。Basisが最後に削除されるようにする。
・他のオブジェクトのシェイプキーをすべて削除する。同じ名前のシェイプキーが最後に削除されるようにする。
・オリジナルとすべての複製オブジェクトのミラーモディファイアを適用する。
・オリジナルがactiveになるように、オリジナルとすべての複製オブジェクトを同時選択する。
・Join as Shapesを実行する。
・複製オブジェクトをすべて削除する。

指示を何度か修正しましたが、細かい部分が伝わらなかったので、自分でスクリプトを修正しました。

また、はじめはオブジェクトをシェイプキーとして統合するための「Join as Shapes」を実行するAPIが無いと言われ、そのためのスクリプトを個別に作ってもらいました。

Blenderの公式APIでは、シェイプキーの結合(Join as Shapes)に直接対応する機能は提供されていません。しかし、Pythonの下層APIを使ってこの機能を再現することは可能です。以下にそのためのスクリプトを記載します。
def join_as_shapes(active_obj, obj_list):
    # Ensure active object has a Basis shape key
    if active_obj.data.shape_keys is None or "Basis" not in active_obj.data.shape_keys.key_blocks:
        active_obj.shape_key_add(name="Basis")

    for obj in obj_list:
        if obj != active_obj:
            # Add a new shape key to the active object with the same name as the selected object
            shape_key = active_obj.shape_key_add(name=obj.name)
            for i, vert in enumerate(obj.data.vertices):
                # Copy the coordinate data from the selected object to the new shape key
                shape_key.data[i].co = vert.co

# Example usage
# join_as_shapes(bpy.context.active_object, bpy.context.selected_objects)

しかし、二回目に指示したときは、bpy.ops.object.join_shapesメソッドを使ったコードを出力してくれました。

参考:https://docs.blender.org/api/current/bpy.ops.object.html#bpy.ops.object.join_shapes

また、アドオンを作ってくださいというと、インストールして使うためのコードを書いてくれますが、テストが面倒なのでまずはBlenderのテキストエディタで実行するようなスクリプトにしたほうが良いと思います。

スクリプト詳細

    duplicates = []
    for key_block in obj.data.shape_keys.key_blocks[1:]:
        duplicate = obj.copy()
        duplicate.data = obj.data.copy()
        bpy.context.collection.objects.link(duplicate)
        duplicate.name = key_block.name
        duplicates.append(duplicate)

オブジェクトを複製した後は、bpy.context.collection.objects.linkメソッドを使って、コレクションとリンクします。これをしないと、複製オブジェクトがBlenderのアウトライナーなどに表示されません。

    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    context.view_layer.objects.active = obj
    bpy.ops.object.shape_key_remove(all=True)

オブジェクトを複製すると、それらのオブジェクトが選択された状態になっているので、bpy.ops.object.select_all(action=’DESELECT’)で選択を解除しています。

bpy.ops.object.shape_key_remove(all=True)で、アクティブなオブジェクトのすべてのシェイプキーを削除します。そのために、直前にオリジナルのオブジェクトをアクティブにしています。

    obj.select_set(True)
    context.view_layer.objects.active = obj

obj.select_set(True)を使わないとアクティブ選択されません。

    for duplicate in duplicates:
        for key_block in duplicate.data.shape_keys.key_blocks[:]:
            if key_block.name != duplicate.name:
                duplicate.shape_key_remove(key_block)
                
        bpy.ops.object.select_all(action='DESELECT')
        duplicate.select_set(True)
        context.view_layer.objects.active = duplicate
        bpy.ops.object.shape_key_remove(all=True)

ここでは、複製オブジェクトのシェイプキーのうち、オブジェクトと同じ名前のシェイプキー以外を削除したあと、上と同じ方法ですべてのシェイプキーを削除しています。

スクリプト全体で、bpy.ops.object.select_all(action=’DESELECT’)メソッドでの選択解除が多用されていますが、必要なオブジェクトのselect_setメソッドを使って個別に選択を解除することもできます。

    for target in [obj] + duplicates:
        bpy.ops.object.select_all(action='DESELECT')
        target.select_set(True)
        context.view_layer.objects.active = target
        if target.modifiers.get("Mirror"):
            bpy.ops.object.modifier_apply({"object": target}, modifier="Mirror")

オリジナルと複製のすべてのオブジェクトを一つずつアクティブ選択して、ミラーモディファイアを適用しています。

    bpy.ops.object.select_all(action='DESELECT')
    for target in [obj] + duplicates:
        target.select_set(True)
    context.view_layer.objects.active = obj
    bpy.ops.object.join_shapes()

bpy.ops.object.select_all(action=’DESELECT’)メソッドですべての選択を解除してから、オリジナルがアクティブになるように、すべてのオブジェクトを同時選択し、Join as Shapesを実行しています。

これで、selectedオブジェクトがactiveオブジェクトのシェイプキーになります。

    for duplicate in duplicates:
        bpy.ops.object.select_all(action='DESELECT')
        duplicate.select_set(True)
        bpy.ops.object.delete()

ここでも、すべての複製オブジェクトに対して、まずbpy.ops.object.select_all(action=’DESELECT’)メソッドですべてのオブジェクトの選択を解除してから、そのオブジェクトを選択し削除しています。

bpy.ops.object.deleteメソッドは、選択されたすべてのオブジェクトを削除できるので、すべての複製オブジェクトを選択してから一度だけ削除を実行してもうまくいきます。

コメントを残す

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