【Blender】画像のパスを一括で変更するアドオン

投稿者: | 2023-06-15

テクスチャ画像を保存したフォルダを移動した場合、blenderで画像を読み込めなくなります。

画像のパスを一つずつ変更するのは面倒なので、ChatGPTを活用して、一括で変更するアドオンを作ってみました。

スクリプト

すべての画像のパスの一部の文字列を置換します。

__init__.py

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

bl_info = {
    "name": "Texture Path Updater",
    "blender": (3, 5, 0),
    "category": "Object",
}

import bpy
from . import texture_path_updater
from bpy.props import StringProperty

def showDialog(count):
    if count < 1:
        bpy.ops.wm.show_message_box('INVOKE_DEFAULT', message="Texture path update successful!")
    else:
        bpy.ops.wm.show_message_box('INVOKE_DEFAULT', message=f"{count} textures not loaded. Check system console.")

def display_results(paths_not_found):
    if paths_not_found is not None:
        print("\n----- Start of new operation -----")
        for path in paths_not_found:            
            print(path)
        showDialog(len(paths_not_found))

class WM_OT_ShowMessageBox(bpy.types.Operator):
    bl_idname = "wm.show_message_box"
    bl_label = "Message Box"

    message: bpy.props.StringProperty(default="")
    
    def execute(self, context):
        #self.report({'INFO'}, self.message)
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)
        
    def draw(self, context):
        layout = self.layout
        layout.label(text=self.message)

class UPDATE_PATH_OT_UpdateImagePaths(bpy.types.Operator):
    bl_idname = "update_path.update_image_paths"
    bl_label = "Update Image Paths"

    def execute(self, context):
        scene = context.scene
        paths_not_found = texture_path_updater.update_image_paths(scene.old_path, scene.new_path)
        display_results(paths_not_found)
        return {'FINISHED'}


class UPDATE_PATH_OT_CheckImagePaths(bpy.types.Operator):
    bl_idname = "update_path.check_image_paths"
    bl_label = "Check Image Paths"

    def execute(self, context):
        scene = context.scene
        paths_not_found = texture_path_updater.check_image_paths()
        display_results(paths_not_found)

        return {'FINISHED'}


class UPDATE_PATH_PT_Panel(bpy.types.Panel):
    bl_idname = "UPDATE_PATH_PT_Panel"
    bl_label = "Update Texture Paths"
    bl_space_type = 'IMAGE_EDITOR'
    bl_region_type = 'UI'
    bl_category = 'Image'

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

        layout.prop(scene, "old_path")
        layout.prop(scene, "new_path")
        
        layout.operator("update_path.update_image_paths")
        layout.operator("update_path.check_image_paths")


def register():
    bpy.utils.register_class(WM_OT_ShowMessageBox)
    bpy.utils.register_class(UPDATE_PATH_OT_UpdateImagePaths)
    bpy.utils.register_class(UPDATE_PATH_OT_CheckImagePaths)
    bpy.utils.register_class(UPDATE_PATH_PT_Panel)

    bpy.types.Scene.old_path = StringProperty(
        name="Old Path",
        description="Old Path to Replace",
        default="",
        maxlen=1024,
    )
    
    bpy.types.Scene.new_path = StringProperty(
        name="New Path",
        description="New Path to Replace With",
        default="",
        maxlen=1024,
    )


def unregister():
    bpy.utils.unregister_class(UPDATE_PATH_PT_Panel)
    bpy.utils.unregister_class(UPDATE_PATH_OT_CheckImagePaths)
    bpy.utils.unregister_class(UPDATE_PATH_OT_UpdateImagePaths)
    bpy.utils.unregister_class(WM_OT_ShowMessageBox)

    del bpy.types.Scene.old_path
    del bpy.types.Scene.new_path


if __name__ == "__main__":
    register()

texture_path_updater.py

import bpy
import os

def update_image_paths(old_path, new_path):
    # ロードできなかった画像のパスを格納するリスト
    images_not_loaded = []

    # 全ての画像データに対してイテレートします
    for img in bpy.data.images:
        if img.name == "Render Result":
            continue
        # 画像がロードされていない場合のみ、パスの更新を試みます
        if not img.has_data:

            new_filepath = img.filepath.replace(old_path, new_path)
            if new_filepath != img.filepath:
                img.filepath = new_filepath
                #img.reload()

            # ファイルパスを更新した後でもファイルが存在しない場合、リストに追加します
            absolute_filepath = bpy.path.abspath(new_filepath)
            if os.path.exists(absolute_filepath):
                img.update()
            else:
                images_not_loaded.append(img.filepath)

    return images_not_loaded

def check_image_paths():
    # ロードできなかった画像のパスを格納するリスト
    images_not_loaded = []

    # 全ての画像データに対してイテレートします
    for img in bpy.data.images:
        if img.name == "Render Result":
            continue
        # 画像がロードされていない場合、リストに追加します
        absolute_filepath = bpy.path.abspath(img.filepath)
        if not os.path.exists(absolute_filepath):
            images_not_loaded.append(img.filepath)

    return images_not_loaded

インストール方法

2つのファイルにスクリプトをそれぞれコピペして、一つの新しいフォルダに保存します。保存するときは文字エンコードをUTF-8にして、拡張子はpyにします。

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

Blenderで Edit > Preferences... を開きます。

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

作成したzipファイルを選択して「Install Add-on」でインストールできます。

アドオンを有効化

インストールするとアドオンのリストに表示されるので、アドオン名の横のチェックボックスをオンにして有効化します。

その横の三角アイコンを開くと、アドオンの保存場所などが表示されます。アドオンを削除するときは「Remove」ボタンを押します。

アドオンが有効化されると、ImageエディタとUVエディタのサイドバーの「Image」タブに新しくパネルが表示されます。

アドオンを実行

現在シーンにCubeが一つあり、4つのテクスチャ画像が設定されています。

マテリアルでは、albedoのテクスチャだけが接続されています。

追加されたパネルの「Check Image Paths」ボタンをクリックすると、プロジェクト内の画像に設定されているパスが、正しいかを確認します。

ダイアログにファイルが存在しない画像の数を表示されます。

システムコンソールにはそれらのパスが表示されます。システムコンソールは Window > Toggle System Console で開きます。

3つのパスが表示されています。

UVエディタでImageタブをクリックし「Image」パネルを表示すると、選択中の画像のパスが表示されます。

画像の名前の横のドロップダウンで他の画像を選択できます。

Render Resultは、レンダリング結果が表示されます。今回のアドオンではスルーします。

アルベド、ノーマル、ラフネス用のテクスチャでは間違ったパスが指定されています。なので、Cubeをマテリアルプレビューすると、赤紫色に表示されます。

すべてのテクスチャ画像は「C:\Tex」フォルダ内に保存されています。3つのテクスチャはパスの冒頭が間違っていて、以下のフォルダに保存されていることになっています。

アルベド:「//..\..\Pictures\Tex

ノーマル:「//..\..\Pictures\Tex\Pictures」

ラフネス:「//..\Pictures\Tex」

もう一つのテクスチャは正しいパスを示しているので、画像が表示されています。

パネルを使って、パスの「//..\..\Pictures\Tex」を「C:\Tex」に置き換えます。

そのためには、パネスのテキストフィールドに検索する文字列と置換後の文字列を入力します。

アルベドを表示中

その下の「Update Image Paths」ボタンをクリックすると一括で置換され、その後まだファイルが存在しないパスの個数がダイアログに表示されます。

アルベドは正しいパスに変更されたので、画像が表示されています。

システムコンソールを見ると、ラフネスはパスが変わっていないことがわかります。ノーマルは置換されましたが、パスが正しくないのでまだ読み込めていません。

残りは個別に書き換えるか、Old Pathの値を変えてもう一度実行すると全て読み込めます。

これで、画像のパスを簡単に変更することができました。

スクリプト詳細

__init__.py

まずアドオンの情報を定義します。

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

bl_info = {
    "name": "Texture Path Updater",
    "blender": (3, 5, 0),
    "category": "Object",
}

必要なモジュールをインポートします。

import bpy
from . import texture_path_updater
from bpy.props import StringProperty

ダイアログを表示するメソッドを定義しています。引数の整数によってメッセージを変えています。ファイルが見つからなかったパスの個数を渡します。

def showDialog(count):
    if count < 1:
        bpy.ops.wm.show_message_box('INVOKE_DEFAULT', message="Texture path update successful!")
    else:
        bpy.ops.wm.show_message_box('INVOKE_DEFAULT', message=f"{count} textures not loaded. Check system console.")

ボタンを押した結果を表示するメソッドも定義しています。どちらのボタンを押しても実行されます。このメソッドでは、システムコンソールに開始の区切りを表示したあと、見つかったパスをすべて表示し、上のダイアログを表示するメソッドを呼びます。

def display_results(paths_not_found):
    if paths_not_found is not None:
        print("\n----- Start of new operation -----")
        for path in paths_not_found:            
            print(path)
        showDialog(len(paths_not_found))

ダイアログに関連するオペレーターを定義します。

class WM_OT_ShowMessageBox(bpy.types.Operator):
    bl_idname = "wm.show_message_box"
    bl_label = "Message Box"

    message: bpy.props.StringProperty(default="")
    
    def execute(self, context):
        #self.report({'INFO'}, self.message)
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)
        
    def draw(self, context):
        layout = self.layout
        layout.label(text=self.message)

showDialog関数でオペレーターが呼び出されたときに、まずinvokeメソッドが実行されて、その中のinvoke_props_dialogメソッドでダイアログがされます。ダイアログの内容はdrawメソッドに実装されています。

ダイアログにはデフォルトでボタンがついていて、ボタンをクリックするとexecuteメソッドが実行されます。

次に、「Update Image Paths」ボタンが押されたときに実行されるオペレーターを定義します。

class UPDATE_PATH_OT_UpdateImagePaths(bpy.types.Operator):
    bl_idname = "update_path.update_image_paths"
    bl_label = "Update Image Paths"

    def execute(self, context):
        scene = context.scene
        paths_not_found = texture_path_updater.update_image_paths(scene.old_path, scene.new_path)
        display_results(paths_not_found)
        return {'FINISHED'}

ボタンが押されると、executeメソッドが実行されます。texture_path_updater.pyで定義されたメソッドで、ファイルが見つからないパス返ります。それを、display_resultsメソッドの引数に渡しています。

「Check Image Paths」ボタンのオペレーターも定義します。

class UPDATE_PATH_OT_CheckImagePaths(bpy.types.Operator):
    bl_idname = "update_path.check_image_paths"
    bl_label = "Check Image Paths"

    def execute(self, context):
        scene = context.scene
        paths_not_found = texture_path_updater.check_image_paths()
        display_results(paths_not_found)

        return {'FINISHED'}

このクラスのexecuteメソッドでも、texture_path_updater.pyのもう一つのメソッドからリストを取得して、display_resultsメソッドの引数に渡しています。

パネルも定義します。パネルはbpy.types.Panelクラスを継承します。

class UPDATE_PATH_PT_Panel(bpy.types.Panel):
    bl_idname = "UPDATE_PATH_PT_Panel"
    bl_label = "Update Texture Paths"
    bl_space_type = 'IMAGE_EDITOR'
    bl_region_type = 'UI'
    bl_category = 'Image'

このパネルは「UPDATE_PATH_PT_Panel」という一意の名前を持っていて、UVエディタとImageエディタのサイドバーの「Image」タブに「Update Texture Paths」というラベルで表示されます。

パネルの内容はdrawメソッドに実装します。

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

        layout.prop(scene, "old_path")
        layout.prop(scene, "new_path")
        
        layout.operator("update_path.update_image_paths")
        layout.operator("update_path.check_image_paths")

UILayout.propメソッドの引数にStringプロパティを渡すとテキストフィールドが表示され、UILayout.operatorにオペレーターを渡すとボタンが表示されます。オペレーターのbl_idnameはここでオペレーターの指定に使われています。

オペレーターやパネルは、bpy.utils.register_classメソッドでBlenderのシステムに登録します。ここで2つのStringプロパティも定義しています。

def register():
    bpy.utils.register_class(WM_OT_ShowMessageBox)
    bpy.utils.register_class(UPDATE_PATH_OT_UpdateImagePaths)
    bpy.utils.register_class(UPDATE_PATH_OT_CheckImagePaths)
    bpy.utils.register_class(UPDATE_PATH_PT_Panel)

    bpy.types.Scene.old_path = StringProperty(
        name="Old Path",
        description="Old Path to Replace",
        default="",
        maxlen=1024,
    )
    
    bpy.types.Scene.new_path = StringProperty(
        name="New Path",
        description="New Path to Replace With",
        default="",
        maxlen=1024,
    )

登録解除のメソッドもあります。

def unregister():
    bpy.utils.unregister_class(UPDATE_PATH_PT_Panel)
    bpy.utils.unregister_class(UPDATE_PATH_OT_CheckImagePaths)
    bpy.utils.unregister_class(UPDATE_PATH_OT_UpdateImagePaths)
    bpy.utils.unregister_class(WM_OT_ShowMessageBox)

    del bpy.types.Scene.old_path
    del bpy.types.Scene.new_path

インストールして使用した場合、__name__が「__name__」になりませんが、テキストエディタでRun Scriptボタンを押して実行した場合にはregisterメソッドが呼ばれます。

if __name__ == "__main__":
    register()

texture_path_updater.py

必要なモジュールをインポートします。ファイルの存在確認にosモジュールを使います。

import bpy
import os

まず、パスを更新するメソッドを定義しています。検索と置換する文字列を引数にしています。

def update_image_paths(old_path, new_path):
    # ロードできなかった画像のパスを格納するリスト
    images_not_loaded = []

すべての画像を走査します。名前が「Render Result」の場合はスキップします。

    for img in bpy.data.images:
        if img.name == "Render Result":
            continue

画像がメモリに読み込まれていない場合、パスの文字列のreplaceメソッドで置換して、new_filepath変数にいれます。

        if not img.has_data:

            new_filepath = img.filepath.replace(old_path, new_path)

ファイルパスとnew_filepathが異なる場合置換されたので、ファイルパスに新しいパスを代入します。

            if new_filepath != img.filepath:
                img.filepath = new_filepath

新しいパスでファイルの存在を確認をします。その前にos.path.existsメソッドの引数に渡す絶対パスを作っています。ここではパスが正しくてもhas_dataメソッドがまだfalseを返しました。

            absolute_filepath = bpy.path.abspath(new_filepath)
            if os.path.exists(absolute_filepath):

存在する場合は表示画像を更新します。

                img.update()

存在しない場合はリストに追加します。存在しない画像のupdateメソッドを呼ぶとエラーになります。

            else:
                images_not_loaded.append(img.filepath)

最後にリストを返します。

    return images_not_loaded

次に、「Check Image Paths」ボタンを押したときに実行するメソッドを定義しました。

def check_image_paths():
    # ロードできなかった画像のパスを格納するリスト
    images_not_loaded = []

同様に、「Render Result」以外のすべての画像を処理します。

    for img in bpy.data.images:
        if img.name == "Render Result":
            continue

絶対パスでファイルの存在を確認し、無ければリストに追加します。

        absolute_filepath = bpy.path.abspath(img.filepath)
        if not os.path.exists(absolute_filepath):
            images_not_loaded.append(img.filepath)

リストを返します。

    return images_not_loaded

コメントを残す

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