NTTドコモR&Dの技術ブログです。

Recraft AI × Unreal Pythonでテクスチャ・マテリアル自動生成を試みる

NTTドコモ R&D戦略部の北川巧です。今回の記事はUnreal Engineで使える Unreal Pythonと、最近話題のRecraft AIを使ってマテリアルを自動で作ってみようという記事になります。 なお、本記事の内容は個人環境における試行であり業務には関連の無い内容となります。

本記事で達成すること

  1. Recraft AIをAPI経由でPythonから使う
  2. Editor Utility Widget機能でUnreal EditorからRecraft AIを使う
  3. Unreal Pythonでテクスチャとマテリアル作成を自動化する

以下動画のようにUnrealEditor上から直接プロンプトを書き、そのテクスチャとマテリアルをぱぱっと生成できるようになります。

UnrealEditor上から生成AIを使う

今回やること

今回はUnreal Editorで使えるEditor Utility Widget 機能を活用し、エディタ上から直接プロンプトを入力して画像を生成、生成した画像を使ったマテリアルを自動で作成するところまで一気通貫で実現してみたいと思います。

画像生成からマテリアル生成までの流れ

準備編

Recraft AIとは?

最近Recraft AIが話題になっていたので触ってみたいなーと思っていました。Recraft AIはUIがFigmaのようになっていてデザイナーも使いやすく、APIも整備されていてエンジニアとしても使いやすいツールになっていることから、ビジネスユースに持ってこいなサービスになっている印象です。詳しくは以下のような記事を読んでみてください。なお、フリープランの場合商用利用は不可ですのでお気をつけください。 www.itmedia.co.jp

ainow.jp

WebからRecraft AIを使ってみた

また、本記事の主旨からは少し外れますが、Recraft AIのイメージを軽く掴んでもらうためにWeb上で作ってみた画像も載せてみます。Recraft AIは既存画像のスタイルに合わせて画像を生成する機能もあり、画像中の木のテクスチャは葛飾北斎の富嶽三十六景 江戸 日本橋*1のスタイルで生成した画像となります。結構いい感じのスタイルで生成できているのではないでしょうか。

Web上でテクスチャ画像を作ってみた

Recraft AIのAPIを準備する

ある程度テクスチャ生成の期待も持てそうなので、UnrealEditorから使えるようにしたいと思います。まずはRecraft AIにてアカウントを作った後にAPIのページを開き、1000Unitsを1ドルでカード決済するだけです。(手持ちのVisaは駄目で、Masterカードは使えました)

API発行画面
課金後、Generate new Keyボタンを押して、名前を任意に設定して自分のプログラムから利用するだけです。(APIキー流出には気をつけましょう)

Unreal PythonとUnreal Editorの準備をする

APIキーが準備出来たので、次にUnreal Python上からこのAPIを動かす準備を行います。 今回は生成した画像をローカルに保存して扱うため、幾つか必要そうなPythonモジュールをインストールしてから始めることにします。

なお、Unreal PythonはUnreal Engine自体のフォルダに内包されているため、ここに必要なモジュールを導入していきます。 Python(REPL)から

import sys
print(sys.executable)

などで今使っているUEプロジェクトからEditorの場所を探してみます。

Unreal Editorの場所がわかる

上記画像中のエンジンフォルダにて検索し、UE5.3では下記パスにPythonの実行環境があることがわかりました。

C:\Program Files\Epic Games\UE_5.3\Engine\Binaries\ThirdParty\Python3\Win64

次にこのPythonと同じディレクトリにて必要なモジュールをインストールしていきます。

python -m pip install openai
python -m pip install opencv-contrib-python
python -m pip install pillow

Openaiの追加
Opencvのインストール
Pillowのインストール

実装編

Editor Utility WidgetでUIを作る

Recraft AIをUnrealEditor上から使いたいので、そのためのインタフェースを作ります。 コンテンツブラウザで任意の場所、ディレクトリで右クリックし、Editor Utility Widgetを「RecraftWidget」という名前で作成しましょう。要素の配置を設定する画面が出てきたらStack Boxを選びます。

コンテンツブラウザでEditor Utility Widgetを作成
RecraftWidgetができる

RecraftWidgetを作ったら、編集画面を開くと画面デザインモードになっています。ここで図の様に、EditorUtilityEditableTextとEditorUtilityButtonを好きに配置しましょう。(筆者の環境起因かテキストボックスが文字化けしていますが無視でOKです)

Widgetの画面配置

前者が「Recraft AIへ送信するプロンプトの設定欄」、後者が「プロンプトを使ってマテリアル生成を開始する」役割を持ちます。

Editor Utility Widgetのイベントグラフを書く

ここまでで画面レイアウトは完成したので、その動作を書いていきます。まずはテキスト入力欄の文字列を保存するPrompt変数を作り、これを更新するイベントを作ります。

更新するイベントはテキスト入力欄を選択した状態で詳細パネルのOntextCommittedイベントから作れます。

OnTextCommittedをクリックしてイベントを作る

イベントグラフが開いたら、まずは左ペインでPrompt変数を作ります。

変数の+ボタンからPrompt変数を作る

その後、入力されたテキストをPrompt変数に反映する処理を書きます。

イベントグラフからStringをPrompt変数にセットする

次にマテリアル生成開始用のボタンをクリックしたときの処理を書きます。 こちらもテキスト入力欄と同様、ボタンを選択した状態の詳細パネルからOnClickedイベントを追加することができます。

ボタンを選択し、詳細パネルからOnclickedイベントを選択

ここで、ボタンを押した後の全てのイベントグラフを記載すると、以下のようになります。 基本的には「Recraft AIのAPIでテクスチャ生成後、PNG画像で保存するPythonコードを実行」→「保存されたPNG画像を元にテクスチャアセット化&マテリアル化するPythonコードを実行」の二段構成になっています。Prompt変数の中身やファイル名はPythonコードの実行時の引数として渡しています。 この図の通りにイベントグラフを構築したら、あとは2つPythonファイルを準備して、ファイルパスをこのグラフ上で設定するだけで動くものが完成します。

次にそれぞれのPythonファイルの中身を見ていきます。

Recraft AIで画像生成後テクスチャを保存するコード

今回、promptToTexture.pyとして以下のようなコードを準備しました。このコードは起動時引数の内容をプロンプトとしてRecraft AIで画像を生成、結果をwebp形式でディレクトリに保存後、png画像へ変換しているものとなります。

from openai import OpenAI
import requests
import os
import sys
from PIL import Image

args = sys.argv

def download_image(url, save_path):
    response = requests.get(url)
    if response.status_code == 200:
        with open(save_path, 'wb') as file:
            file.write(response.content)
        print(f"画像を保存しました: {save_path}")
    else:
        print(f"画像のダウンロードに失敗しました。ステータスコード: {response.status_code}")

def convert_webp_to_png(input_path, output_path):
    with Image.open(input_path) as img:
        img.save(output_path, 'PNG')
        print(f"画像をPNG形式で保存しました: {output_path}")

my_key = "ここにあなたのAPI Keyを入力"
client = OpenAI(base_url='https://external.api.recraft.ai/v1', api_key=my_key)

# プロンプトから画像を生成する
response = client.images.generate(
  prompt=args[1],
  style='digital_illustration',
  extra_body={'substyle': 'pixel_art'},
)
image_url = response.data[0].url

# 保存先のディレクトリ
save_directory = 'C:\\Users\\~ユーザー名~\\Documents\\Unreal Projects\\AC\\Content\\Python\\downloaded_images'
if not os.path.exists(save_directory):
    os.makedirs(save_directory)

# 画像のファイル名を設定(第二引数を使う)
file_name = args[2]
webp_save_path = os.path.join(save_directory, file_name)

# 画像をダウンロードして保存
download_image(image_url, webp_save_path)

# PNG形式で保存
png_save_path = os.path.splitext(webp_save_path)[0] + '.png'
convert_webp_to_png(webp_save_path, png_save_path)

次はこのpng画像を使ってMaterialを生成してみます。

保存したテクスチャからMaterialを生成するコード

このコードはtextureToMaterial.pyとして作成しました。特徴的なのはunrealモジュールがインポートされていることで、これによってテクスチャをアセットとしてインポートしたり、Materialを作ることが出決ます。

コードの流れとしては、「保存したテクスチャファイルをテクスチャアセットとしてUnreal Editorにインポート」→「テクスチャアセットを使ったマテリアルを作成し、タイリング用途にノードを調整」しています。

import unreal
import sys

args = sys.argv

@unreal.uclass()
class MaterialCreatorWidget(unreal.EditorUtilityWidget):
    
    @unreal.ufunction(meta=dict(Category="Material Creation"))
    def create_material_with_tiled_texture(self):
        # コマンドライン引数からテクスチャファイルの名前を取得
        texture_file_name = args[1]
        
        # 拡張子を除去
        texture_u_name = unreal.Paths.get_base_filename(texture_file_name)
        destination_path = "/Game/Python/downloaded_images"
        
        # インポートタスクの設定
        import_task = unreal.AssetImportTask()
        import_task.filename = "C:\\Users\\~ユーザー名~\\Documents\\Unreal Projects\\AC\\Content\\Python\\downloaded_images\\"+texture_file_name
        import_task.destination_path = destination_path
        import_task.automated = True
        
        # テクスチャファイルのアセット化
        unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([import_task])
        
        # インポート結果の確認
        if not import_task.imported_object_paths:
            unreal.log_error(f"Failed to import texture: {texture_file_name}. Please check the file path and format.")
            return
        
        texture_path = import_task.imported_object_paths[0]
        texture = unreal.EditorAssetLibrary.load_asset(texture_path)
        
        if not texture:
            unreal.log_warning(f"Texture not found at path: {texture_path}")
            return
        
        # 新しいマテリアルの作成
        material_factory = unreal.MaterialFactoryNew()
        asset_name = f"{texture_u_name}_Mat"
        package_name = f"{destination_path}/{asset_name}"
        
        new_material = unreal.AssetToolsHelpers.get_asset_tools().create_asset(asset_name, destination_path, unreal.Material, material_factory)
        
        if not new_material:
            unreal.log_error("Failed to create new material")
            return
        
        # テクスチャサンプルノードの追加
        texture_sample = unreal.MaterialEditingLibrary.create_material_expression(new_material, unreal.MaterialExpressionTextureSample, -384, 0)
        texture_sample.texture = texture
        
        # TextureCoordinateノードの追加
        tex_coord = unreal.MaterialEditingLibrary.create_material_expression(new_material, unreal.MaterialExpressionTextureCoordinate, -768, 0)
        
        # Multiplyノードの追加(UVスケール調整用)
        multiply = unreal.MaterialEditingLibrary.create_material_expression(new_material, unreal.MaterialExpressionMultiply, -576, 0)
        
        # Constantノードの追加(タイリング倍率)
        tiling_factor = unreal.MaterialEditingLibrary.create_material_expression(new_material, unreal.MaterialExpressionConstant2Vector, -768, 128)
        tiling_factor.r = 80.0  # X方向のタイリング倍率
        tiling_factor.g = 80.0  # Y方向のタイリング倍率
        
        # ノード間の接続
        unreal.MaterialEditingLibrary.connect_material_expressions(tex_coord, "", multiply, "A")
        unreal.MaterialEditingLibrary.connect_material_expressions(tiling_factor, "", multiply, "B")
        unreal.MaterialEditingLibrary.connect_material_expressions(multiply, "", texture_sample, "UVs")
        
        # Base Colorにテクスチャを接続
        unreal.MaterialEditingLibrary.connect_material_property(texture_sample, "", unreal.MaterialProperty.MP_BASE_COLOR)
        
        # マテリアルの更新と保存
        unreal.MaterialEditingLibrary.recompile_material(new_material)
        unreal.EditorAssetLibrary.save_asset(package_name, True)
        
        unreal.log(f"Created new material with tiled texture: {package_name}")

# ウィジェットのインスタンス化(エディタで使用する場合)
widget = MaterialCreatorWidget()
widget.create_material_with_tiled_texture()

実際に自動で生成されたMaterialファイルを覗いてみると、しっかりとマテリアルグラフが構築されていることがわかります。

生成されたMaterial

実際に動かしてみる

上記の2つのPythonファイルを先のRecraftWidgetのイベントグラフ上に設定したら、実際にこのWidgetを動作させてみます。これは簡単でWidgetのタブ上部の「ユーティリティウィジェットを実行」を押すだけで、テキストボックスとボタンが配置されたウィンドウが起動します。

ユーティリティウィジェットを実行すると動作する
RecraftWidgetというタブで、プロンプト入力欄とボタンをもつ画面が出る
あとは、好きなプロンプトを入力してボタンを押すと、マテリアルがどんどん生成されます。(なお、執筆時点で操作1回あたり日本円換算で7円弱)

まとめと所感

今回はRecraft AIからMaterial自動作成をやってみたのと、実際に活用するときのためにUnreal Editorから使えるようにしてみました。最初は「これ要るかな?」と疑問に思うところもありましたが、実際ワンクリックで生成AIを使えるようにしてみるとなかなか便利な感触はありました。

しかしながら今回実際にAPI経由で生成された画像を見てみると、ループ画像になってなかったり普通の絵みたいになっていたりと課題も多く見えました。加えてテクスチャだけだと実際には使い物にはならないため、ノーマルマップやラフネスマップにも手を出す必要があるでしょう。

このあたりはプロンプトの工夫や、インペイント機能・マスク領域の指定の活用・組み合わせによって解決できるかもしれません。実際Recraft AIのWeb上で生成した画像はもっとよかったので、色々調整すればなんとかなりそうです。

将来的にテクスチャ、ラフネスマップ、ノーマルマップの一括生成に留まらず、対象物周辺のアセット情報を参考にして各アセットが自動で生成・修正されるようなところまでいけば、夢があるなと感じました。

*1:パブリックドメインのデータを使用しています