NTTドコモ R&D戦略部の北川巧です。今回の記事はUnreal Engineで使える Unreal Pythonと、最近話題のRecraft AIを使ってマテリアルを自動で作ってみようという記事になります。 なお、本記事の内容は個人環境における試行であり業務には関連の無い内容となります。
本記事で達成すること
- Recraft AIをAPI経由でPythonから使う
- Editor Utility Widget機能でUnreal EditorからRecraft AIを使う
- Unreal Pythonでテクスチャとマテリアル作成を自動化する
以下動画のようにUnrealEditor上から直接プロンプトを書き、そのテクスチャとマテリアルをぱぱっと生成できるようになります。
今回やること
今回はUnreal Editorで使えるEditor Utility Widget 機能を活用し、エディタ上から直接プロンプトを入力して画像を生成、生成した画像を使ったマテリアルを自動で作成するところまで一気通貫で実現してみたいと思います。
準備編
Recraft AIとは?
最近Recraft AIが話題になっていたので触ってみたいなーと思っていました。Recraft AIはUIがFigmaのようになっていてデザイナーも使いやすく、APIも整備されていてエンジニアとしても使いやすいツールになっていることから、ビジネスユースに持ってこいなサービスになっている印象です。詳しくは以下のような記事を読んでみてください。なお、フリープランの場合商用利用は不可ですのでお気をつけください。 www.itmedia.co.jp
WebからRecraft AIを使ってみた
また、本記事の主旨からは少し外れますが、Recraft AIのイメージを軽く掴んでもらうためにWeb上で作ってみた画像も載せてみます。Recraft AIは既存画像のスタイルに合わせて画像を生成する機能もあり、画像中の木のテクスチャは葛飾北斎の富嶽三十六景 江戸 日本橋*1のスタイルで生成した画像となります。結構いい感じのスタイルで生成できているのではないでしょうか。
Recraft AIのAPIを準備する
ある程度テクスチャ生成の期待も持てそうなので、UnrealEditorから使えるようにしたいと思います。まずはRecraft AIにてアカウントを作った後にAPIのページを開き、1000Unitsを1ドルでカード決済するだけです。(手持ちのVisaは駄目で、Masterカードは使えました)
課金後、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の場所を探してみます。
上記画像中のエンジンフォルダにて検索し、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
実装編
Editor Utility WidgetでUIを作る
Recraft AIをUnrealEditor上から使いたいので、そのためのインタフェースを作ります。 コンテンツブラウザで任意の場所、ディレクトリで右クリックし、Editor Utility Widgetを「RecraftWidget」という名前で作成しましょう。要素の配置を設定する画面が出てきたらStack Boxを選びます。
RecraftWidgetを作ったら、編集画面を開くと画面デザインモードになっています。ここで図の様に、EditorUtilityEditableTextとEditorUtilityButtonを好きに配置しましょう。(筆者の環境起因かテキストボックスが文字化けしていますが無視でOKです)
前者が「Recraft AIへ送信するプロンプトの設定欄」、後者が「プロンプトを使ってマテリアル生成を開始する」役割を持ちます。
Editor Utility Widgetのイベントグラフを書く
ここまでで画面レイアウトは完成したので、その動作を書いていきます。まずはテキスト入力欄の文字列を保存するPrompt変数を作り、これを更新するイベントを作ります。
更新するイベントはテキスト入力欄を選択した状態で詳細パネルのOntextCommittedイベントから作れます。
イベントグラフが開いたら、まずは左ペインでPrompt変数を作ります。
その後、入力されたテキストをPrompt変数に反映する処理を書きます。
次にマテリアル生成開始用のボタンをクリックしたときの処理を書きます。 こちらもテキスト入力欄と同様、ボタンを選択した状態の詳細パネルから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ファイルを覗いてみると、しっかりとマテリアルグラフが構築されていることがわかります。
実際に動かしてみる
上記の2つのPythonファイルを先のRecraftWidgetのイベントグラフ上に設定したら、実際にこのWidgetを動作させてみます。これは簡単でWidgetのタブ上部の「ユーティリティウィジェットを実行」を押すだけで、テキストボックスとボタンが配置されたウィンドウが起動します。
あとは、好きなプロンプトを入力してボタンを押すと、マテリアルがどんどん生成されます。(なお、執筆時点で操作1回あたり日本円換算で7円弱)まとめと所感
今回はRecraft AIからMaterial自動作成をやってみたのと、実際に活用するときのためにUnreal Editorから使えるようにしてみました。最初は「これ要るかな?」と疑問に思うところもありましたが、実際ワンクリックで生成AIを使えるようにしてみるとなかなか便利な感触はありました。
しかしながら今回実際にAPI経由で生成された画像を見てみると、ループ画像になってなかったり普通の絵みたいになっていたりと課題も多く見えました。加えてテクスチャだけだと実際には使い物にはならないため、ノーマルマップやラフネスマップにも手を出す必要があるでしょう。
このあたりはプロンプトの工夫や、インペイント機能・マスク領域の指定の活用・組み合わせによって解決できるかもしれません。実際Recraft AIのWeb上で生成した画像はもっとよかったので、色々調整すればなんとかなりそうです。
将来的にテクスチャ、ラフネスマップ、ノーマルマップの一括生成に留まらず、対象物周辺のアセット情報を参考にして各アセットが自動で生成・修正されるようなところまでいけば、夢があるなと感じました。
*1:パブリックドメインのデータを使用しています