Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

python:Blenderからワンボタンで自動UV展開できるようにした話

🔗目次

  1. まえがき
  2. UV展開って?
  3. やるべきこと
  4. 実装
  5. 実際に使ってみた
  6. あとがき

🔗まえがき

みなさんどうもこんにちは、この前の記事からだいぶ間が空いてしまいましたが元気です。ECN所属、Fuseです。
みなさんはここ最近新しく始めたりはまったりしたゲームはありますか?
私はVRChatにドはまりしました。
ワールドを巡ったり友達とワイワイ話すのも楽しいのですが、自身のアバターを自分好みに着せ替えたりするのも楽しいんですよね。
私が使っているアバターは対応衣装が少ないしなにしろ私はゴシック系やダーク系を好むのですが頭身が低い都合上着せられそうな衣装が可愛い全振りみたいなのしかありません。
じゃあ自分で作るしかないよねとBlenderを開くのですがそこで大きな壁が立ちはだかります。
UV展開です。
↑目次に戻る

🔗UV展開って?

簡単に言えば、立体の展開図を作る作業です。1つの正方形の上にモデルを構成する面を展開していきます。
これを行うことで、テクスチャを使って色を付けたりすることが可能になります。

▲UV展開とテクスチャの適用のイメージ図

そしてこのUV展開なのですが、すこぶるめんどくさいです。
どこをどう切るのかを考えながらモデリングしないと大変なことになりますし使える面積にも限りがあります。
もっと簡単に、ワンボタンでUV展開を終わらせたい…そう思っていたら、こんなツールを見つけました。

Ministry of FlatというこのツールはOBJファイルのUV展開を自動で行うツールで、

  • あらゆるタイプのメッシュ、キャラクター、マシン、環境に対応。
  • ライトマップやプロシージャルペイントツールに最適。
  • ジオメトリの変更にコストがかからない新しいワークフローを実現。
  • 折り目の多い複雑なメッシュもうまく処理。
  • 高品質な出力:
  • オーバーラップなし。
  • 優れたパッキング。
  • 最小限のストレッチ。
  • インテリジェントに回転。
  • ポリゴンの反転なし。
  • ブリードを防ぐための調整可能なアイランドギャップ。
  • 信頼性が高く、アセットのQAが不要。
  • UI、またはパイプライン統合用のコマンドラインツールとして利用可能。

といった特徴があります。
また、"Cinema 4D S22"にも搭載された実績があります。
これを使えばモデリング作業がもっと楽になると考えたわたしは早速Blenderとこれを連携させることにしました。

↑目次に戻る

🔗やるべきこと

  1. 選択したオブジェクトをobj形式で出力する
  2. 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する
  3. 2で出力されたobjファイルをblenderにインポートする
  4. 1と2の一時ファイルを掃除する(消す)

↑目次に戻る

🔗実装

それではさっそく実装していきましょう。

ひな形を作る

まずは公式のチュートリアルをもとに必要最低限のインポートだけできるひな形を作ります。

bl_info = {
    "name": "Blender Mof Bridge",
    "blender": (2, 80, 0),
    "category": "Object",
}


def register():
    print("Hello World")


def unregister():
    print("Goodbye World")

このアドオンをさっそくBlenderに登録、有効化するとシステムコンソール(Blenderを起動するといっしょに起動するコマンドプロンプト)に"Hello World"と表示されるはずです。

STEP1 メニューから関数を実行できるようにする

これでひな型は完成です。早速アドオンを作っていきましょう。まずは関数をメニューから実行できるようにします。

import bpy
import os
from bpy import context

bl_info = {
    "name": "Brender Mof Bridge",
    "blender": (2, 80, 0),
    "category": "UV",
}


class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        return {"FINISHED"}  # 終了


def menu_func(self, context):
    self.layout.operator(AutoUV.bl_idname)  # メニューに追加


def register():
    bpy.utils.register_class(AutoUV)
    bpy.types.VIEW3D_MT_object.append(menu_func)  # オブジェクトメニューに項目追加


def unregister():
    bpy.utils.unregister_class(AutoUV)
    bpy.types.VIEW3D_MT_object.remove(menu_func)  # オブジェクトメニューから削除


if __name__ == "__main__":
    register()

Blenderにおける各種操作はオペレーターと呼ばれるクラス群で管理されています。そのため、まずはオペレータを作ります。
作ったオペレータはregister()関数内で登録、unregister()関数内でしっかりと登録解除しておきましょう。
これをBlenderに登録して、有効化するとオブジェクトメニューに項目が一つ増えているはずです。
まだ空なのでクリックしても何も起きませんが、しっかりとエラーなく動いているはずです。

STEP2 アドオンの設定項目を作る

アドオンの設定画面を作って、MinistyofFlatがあるフォルダを登録できるようにしていきます。
自分だけが使うなら別にハードコーディングして決め打ちでもいいのですが技術記事となるとそうも行きません。
しっかり作っていきましょう。

import bpy
import os
from bpy import context

bl_info = {
    "name": "Brender Mof Bridge",
    "blender": (2, 80, 0),
    "category": "UV",
}


class MyAddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__

    folder_path: bpy.props.StringProperty(
        name="folder_path",
        description="Ministy of Flatの存在するフォルダパスを入力してください",
        subtype="DIR_PATH",
    )

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "folder_path")


class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        return {"FINISHED"}  # 終了


def menu_func(self, context):
    self.layout.operator(AutoUV.bl_idname)  # メニューに追加


def register():
    bpy.utils.register_class(MyAddonPreferences)
    bpy.utils.register_class(AutoUV)
    bpy.types.VIEW3D_MT_object.append(menu_func)  # オブジェクトメニューに項目追加


def unregister():
    bpy.utils.unregister_class(MyAddonPreferences)
    bpy.utils.unregister_class(AutoUV)
    bpy.types.VIEW3D_MT_object.remove(menu_func)  # オブジェクトメニューから削除


if __name__ == "__main__":
    register()

bpy.types.AddonPreferencesを継承したクラスを使って設定項目を定義していきます。
名前と項目を決めたらdrawメソッド内で描画処理を定義します。
うまく行けばこのように設定項目が表示されるはずです。

STEP3 関数の中身を作る

オペレーターの登録に成功したので、ここからは先述のやるべきことに従って関数の中身を完成させていきます。

STEP3-1 選択したオブジェクトをobj形式で出力する

まずは選択中のオブジェクトをMoFに展開させるためにobj形式でエクスポートします。

class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        # STEP3-1 選択オブジェクトのエクスポート
        obj = context.active_object
        fn = os.path.join("/tmp", obj.name + ".obj")  # ファイル名
        # 選択しているオブジェクトをエクスポート
        bpy.ops.wm.obj_export(
            filepath=fn,
            export_selected_objects=True,
            export_materials=False,
        )

        return {"FINISHED"}  # 終了

ここからはコード量が増えるためAutoUVクラスのみ抜粋していきます。
bpy.ops.wm.obj_export()関数でシーンをobjファイルとしてエクスポートできます。
パスは後々一時ファイルを片付けるときに使うので変数にしておきます。
注意点としてはexport_materialsをFalseに設定しないとマテリアルの情報が別ファイルでエクスポートされることとexport_selected_objectsをTrueに設定しないと選択してないオブジェクトもエクスポートされてしまうことです。

STEP3-2 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する

次に先ほどエクスポートしたオブジェクトをMinistry of Flatに投入、UV展開を行わせます。

class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        # STEP3-1 選択オブジェクトのエクスポート
        obj = context.active_object
        fn = os.path.join("/tmp", obj.name + ".obj")  # ファイル名
        # 選択しているオブジェクトをエクスポート
        bpy.ops.wm.obj_export(
            filepath=fn,
            export_selected_objects=True,
            export_materials=False,
        )
        # STEP3-2 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する
        fn2 = os.path.join("/tmp", obj.name + "_unpacked.obj")  # 展開後のファイル名
        preferences = context.preferences.addons[__name__].preferences
        folder_path = (
            preferences.folder_path
        )  # Ministry of Flatがあるディレクトリを取得
        os.system(f"{os.path.join(folder_path,"UnWrapConsole3.exe")} {fn} {fn2}")  # 実行

        return {"FINISHED"}  # 終了

STEP2で設定したプリファレンスからディレクトリを取得、フルパスを完成させて実行します。

STEP3-3 2で出力されたobjファイルをblenderにインポートする

UV展開したobjファイルをblenderにインポートします。

class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        # STEP3-1 選択オブジェクトのエクスポート
        obj = context.active_object
        fn = os.path.join("/tmp", obj.name + ".obj")  # ファイル名
        # 選択しているオブジェクトをエクスポート
        bpy.ops.wm.obj_export(
            filepath=fn,
            export_selected_objects=True,
            export_materials=False,
        )
        # STEP3-2 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する
        fn2 = os.path.join("/tmp", obj.name + "_unpacked.obj")  # 展開後のファイル名
        preferences = context.preferences.addons[__name__].preferences
        folder_path = (
            preferences.folder_path
        )  # Ministry of Flatがあるディレクトリを取得
        os.system(f"{os.path.join(folder_path,"UnWrapConsole3.exe")} {fn} {fn2}")  # 実行
        # STEP3-3 2で出力されたobjファイルをblenderにインポートする
        bpy.ops.import_scene.obj(filepath=fn2)
        return {"FINISHED"}  # 終了

bpy.ops.import_scene.obj()でUV展開をすましたobjファイルをインポートします。

STEP3-4 1と2の一時ファイルを掃除する(消す)

ここまで来たらあと少しです。変数に保存しておいたパスを利用して一時ファイルを削除します。

class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        # STEP3-1 選択オブジェクトのエクスポート
        obj = context.active_object
        fn = os.path.join("/tmp", obj.name + ".obj")  # ファイル名
        # 選択しているオブジェクトをエクスポート
        bpy.ops.wm.obj_export(
            filepath=fn,
            export_selected_objects=True,
            export_materials=False,
        )
        # STEP3-2 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する
        fn2 = os.path.join("/tmp", obj.name + "_unpacked.obj")  # 展開後のファイル名
        preferences = context.preferences.addons[__name__].preferences
        folder_path = (
            preferences.folder_path
        )  # Ministry of Flatがあるディレクトリを取得
        path = os.path.join(folder_path, "UnWrapConsole3.exe")
        os.system(f"{path} {fn} {fn2}")  # 実行
        # STEP3-3 2で出力されたobjファイルをblenderにインポートする
        bpy.ops.wm.obj_import(filepath=fn2)
        # STEP3-4 1と2の一時ファイルを掃除する(消す)
        os.remove(fn)
        os.remove(fn2)

        return {"FINISHED"}  # 終了

これで完成です。お疲れさまでした。

全体図

import bpy
import os
from bpy import context

bl_info = {
    "name": "Brender Mof Bridge",
    "blender": (2, 80, 0),
    "category": "UV",
}


class MyAddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__

    folder_path: bpy.props.StringProperty(
        name="folder_path",
        description="Ministy of Flatの存在するフォルダパスを入力してください",
        subtype="DIR_PATH",
    )

    def draw(self, context):
        layout = self.layout
        layout.prop(self, "folder_path")


class AutoUV(bpy.types.Operator):
    """Ministy of Flatを使って自動でUV展開を行います"""  # ホバー時に表示される説明文

    bl_idname = "object.autouv"  # メニューのID、小文字でなければならない
    bl_label = "自動UV展開"  # UIにて表示される名前
    bl_options = {"REGISTER"}

    def execute(self, context):  # 実行時の処理
        # STEP3-1 選択オブジェクトのエクスポート
        obj = context.active_object
        fn = os.path.join("/tmp", obj.name + ".obj")  # ファイル名
        # 選択しているオブジェクトをエクスポート
        bpy.ops.wm.obj_export(
            filepath=fn,
            export_selected_objects=True,
            export_materials=False,
        )
        # STEP3-2 1のobjファイルを引数に与えMinistry of Flatをコマンドラインから実行する
        fn2 = os.path.join("/tmp", obj.name + "_unpacked.obj")  # 展開後のファイル名
        preferences = context.preferences.addons[__name__].preferences
        folder_path = (
            preferences.folder_path
        )  # Ministry of Flatがあるディレクトリを取得
        path = os.path.join(folder_path, "UnWrapConsole3.exe")
        os.system(f"{path} {fn} {fn2}")  # 実行
        # STEP3-3 2で出力されたobjファイルをblenderにインポートする
        bpy.ops.wm.obj_import(filepath=fn2)
        # STEP3-4 1と2の一時ファイルを掃除する(消す)
        os.remove(fn)
        os.remove(fn2)

        return {"FINISHED"}  # 終了


def menu_func(self, context):
    self.layout.operator(AutoUV.bl_idname)  # メニューに追加


def register():
    bpy.utils.register_class(MyAddonPreferences)
    bpy.utils.register_class(AutoUV)
    bpy.types.VIEW3D_MT_object.append(menu_func)  # オブジェクトメニューに項目追加


def unregister():
    bpy.utils.unregister_class(MyAddonPreferences)
    bpy.utils.unregister_class(AutoUV)
    bpy.types.VIEW3D_MT_object.remove(menu_func)  # オブジェクトメニューから削除


if __name__ == "__main__":
    register()

↑目次に戻る

🔗実際に使ってみた

それでは実際に使ってみましょう。用意したのは低等身アバター用に1から制作したプレートメイル。UV展開がされておらずぶっちゃけ使い物になりません。

自動UV展開を行うと真っ白な新しい鎧が出現、UVを確認すると…?

大きさの調整こそ必要そうですが、きれいにUV展開されていますね。

↑目次に戻る

🔗あとがき

いかがでしたか?これで小物づくりもはかどりそうです。それでは、皆さん良いモデリングライフを!

↑目次に戻る


株式会社ECNはPHP、JavaScriptを中心にお客様のご要望に合わせたwebサービス、システム開発を承っております。
ビジネスの最初から最後までサポートを行い
お客様のイメージに合わせたWebサービス、システム開発、デザインを行います。


関連記事

python:pygameで生態系[第一章]植物編

Python: AIエージェント用OSSフレームワーク"SAGA"でAI闘技場


関連記事

該当する記事がありません。

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。