UE の Static Mesh を Blender で読み込んでみる
Blender の tutorial をやったので、UE で買ったアセットの Mesh なんかがどうなってるかも見てみたいし、それをサクッと改造できるのであれば夢が広がるなと思い方法を探してみた。
FBX としてのエクスポート
いくつか方法はあるのかと思いますが、Static Mesh を FBX Export するのはコンテンツブラウザのコンテクストメニューから簡単にできるようです。ここでは、バッグの Mesh を export しています。
オプションはそのままにしました。
Blender に読み込んでみる
すると上記のような状態で読み込まれます。形が全然違う。。
アウトライナを見ると2つ読み込まれているようです。形状的におそらく collision のようです。なので消してしまいます。
バッグの形状が現れました。
ただ、この状態では material preview や rendered の view にしても、見た目はこのままです。マテリアルはあるのですが、Texture が無いようです。
Texture を Bake した状態で再度エクスポートする
Exporting From Unreal Engine Tutorial – GameFromScratch.com
上記を参考にして、UE で Material を Bake して、それを個別にエクスポートすることにしました。
すると画像が 3 枚生成されます。
これらを個別に TGA として Export し、さらに、Mesh も再度 FBX に Export します。
再度 Import し、マテリアルを整える
マテリアルは以下のようにします。
すると無事、UE での Mesh と似たような見た目になりました。
下は UE の表示です。
CGWORLD Online Tutorial の「ゼロから学ぶ3DCG教室」をやってみた
【半額セール】ゼロから学ぶ3DCG教室《3DCGモデリングの基礎編セット》 | CGWORLD Online Tutorials
右も左もという感じだったので、Youtube の Tutorial を探す前に一度コース的なものをしてみたくて買ってみた。
普通にやるとビデオを見るだけで 7.5 時間だが、1.5 倍速で再生しつつやってみた。とはいえ、止めては手元で作業しの繰り返しなので結局同じぐらいの時間はかかっていると思う。
この講座のいいところの一つは、講師の方と一緒に視聴者と似たような習熟度の方が操作するという形式なので、自分で詰まってしまうような部分が、そちらの方の疑問で解消されたりして良かった。
加えて、ショートカットを多用しないので、初心者としてはありがたかった。
成果物
一応最後まで完走したということで貼っておく。
感想
- 意味不明だった上部の
Layout
,Modeling
,UV Editing
,Texture Paint
といったレイアウトの意味がわかった - テンキー 5 のパース有り無しの表示切り替えが分かってなかったのが知れた
- UV が XYZ の前の意味しかないというのがびっくり
- 3 面図を読み込めるのが便利だった
- Mirror Modifier がずっと大活躍。UV 展開時にも使えるとは。
Unreal Engine でも Mesh 編集が入っていたので多少そこで触ったりしていたし、アセットを使うなかでなんとなく分かっていたようなことも理解できてないことがよくわかった。
このコースは現在も続いているようで、100 回を超えているようです。去年末まで行われていた車のコースはすごく面白そうでした 😇 (ただ、5万ぐらいかかりそうなので躊躇。。)
第99回:クルマモデリング(52)~レンダリング&Blender3.0注目昨日解説~【無料】 │ ゼロから学ぶ3DCG教室
上記の無料回も見てみましたが、車のレンダリングも素敵でしたが Blender 3 系もわくわくする内容でした。UE もいろんな機能を取り込んでいるようですが、Blender も同様なのだなぁと。
上記の動画を参考に Cycles でもレンダリングしてみましたが、ローポリなのであんまり違わないですが、影がいい感じについてるなと分かります。
引き続き触っていきたいなと思いました。
CGWORLD Online Tutorial の動画表示をブラウザを縮小しても見やすくする
Blender の勉強をしようと思い、CGWORLD Online Tutorial の初心者用のコースを購入しました。
ちょっとした問題
私は複数ディスプレイが苦手なので、一枚のディスプレイで作業しているのですが、チュートリアルの動画と Blender を横並びにする場合には、動画のブラウザを縮小する必要がでてきます。
すると上記のように、ブラウザの画面よりだいぶ小さく動画が出てきてしまいます。サイドバーあたりが、responsive にサイズに合わせて移動してくれたりするといいのですが、そうはなっていません。
動画閲覧時は動画にフォーカスできるようにした
最初は開発者ツールで、動画周りのスタイルをいじったりしていたのですが、チュートリアルの動画が切り替わるたびにやり直しになるのでストレスに。。
なので、動画閲覧時にだけ、動画にフォーカスするスタイルを restore するツールを探してみました。
Stylebot
GitHub - ankit/stylebot: Change the appearance of the web instantly
私は Edge を使っていますので、上記を使うことにしました。Stylebot は、簡単にフォントや各種サイズを変更できるのですが、それ以外にも直接 CSS を記述することもできます。
また、それをサイトごとに保存でき、かつ ON/OFF を容易に切り替えることが可能です。
利用する Style
#header { display: none; } main { width: 100%; } body > div.container { width: 100%; padding: 0px; margin: 0px; } body > div.container > div > main > div > section:nth-child(1) { padding: 0px; }
特に凝ったことはしておらず、上記の画像のようになるように、
- ヘッダの削除
- 動画周りの
margin/padding
の削除 - 動画を最大で表示できるように幅を調整
ぐらいしかしていません。
ON/OFF
上記のように、拡張機能のボタンで、対象のサイトの Stylebot の設定を適用するかどうかを ON/OFF できるので、動画閲覧時以外の CGWORLD Online Tutorial のサイトはスタイル OFF 状態で閲覧できます。
Editor Utility Widget と Python で CSV とやり取りする
いろんな方法があると思うのですが、自分でやってみた記録として。
環境: UE5 Preview 2
Plugin の有効化
UE5 Preview 2 の場合、最初から有効化されていました。
- Editor Scripting Utilities
- Python Editor Script Plugin
目標
簡単な例として、レベルに配置されているアクターの location を CSV に吐き出し、それを CSV から読み込み再配置できるだけというものにします。
CSV ファイルも固定パス・固定名とします。
Editor Utility Widget の作成
今回は WBP_AssetLocationStore
としました。
UI
必要最低限だけ配置しています。
ひとまず実行・配置
ここまでで、Editor Utility Widget を実行し、レベルエディタに配置しておくことにします。
イベントハンドルとログ出力
単純にハンドルしておきます。
Log String
でもいいですが、ここでは Print String
で。
Output Log
ウィンドウで一旦 Clear しておくと確認しやすいです。ログの状況を確認したい場合は Window メニューから Output Log をもう一つだしておいてもいいかもしれません。
それぞれのボタンをクリックするとログが流れることが確認できました。
Python を呼び出す
Python を使用したエディタのスクリプティング | Unreal Engine ドキュメント
プロジェクトのフォルダの下にある「Content/Python」サブフォルダ。
が読み込まれるとなっているので、そこにスクリプトを配置することにします。
ひとまず内容は以下のようにして、save.py
, load.py
と命名します
import unreal unreal.log("save")
Python Script を Blueprint から呼出す
どうやら失敗しました。読み込めていないようです。
ここでは前に進めるためにプロジェクト固有でロードパスを追加します。
ドキュメントにもありますが、エディタの再起動が必要です。
Python Script のログも表示されるようになりました。
Actor の Location を CSV で読み書きする
対象の Actor
を BP_Cube
として作成します。
Static Mesh
を持つだけの単純なものです。
雑にレベルに配置します。
CSV に保存するスクリプト
保存先は、Content/Data/CubeLocation.csv
にします。事前にディレクトまで作成しておきます。
CSV は 4 column で、アセット名,X座標,Y座標,Z座標
を書き出すことにします。
import unreal import csv unreal.log("save") actor = unreal.EditorAssetLibrary.load_blueprint_class('/Game/Sample/BP_Cube') # Content/Sample/BP_Cube.uasset sub = unreal.UnrealEditorSubsystem() # レベル上のすべての対象 Actor を集める actors_in_level = unreal.GameplayStatics.get_all_actors_of_class(sub.get_editor_world(), actor) header = ['', 'X', 'Y', 'Z'] with open(unreal.Paths.combine([unreal.Paths.project_content_dir(), 'Data/CubeLocation.csv']), 'w', newline='') as csvfile: writer = csv.writer(csvfile) writer.writerow(header) for a in actors_in_level: asset_name = a.get_name() location = a.get_actor_location() writer.writerow([asset_name, location.x, location.y, location.z]) unreal.log("finished saving to csv")
Save
をクリックすると以下のような CSV が出力されれば成功です。
,X,Y,Z BP_Cube_C_UAID_A8A1596048F915FE00_1762746744,270.0,1010.0,128.00009999999747 BP_Cube_C_UAID_A8A1596048F915FE00_2071157745,-130.0,1010.0,128.0001 BP_Cube_C_UAID_A8A1596048F915FE00_2073499746,-530.0,1010.0,128.0001
CSV から呼び出しレベルに配置するスクリプト
実行前にレベルから先程の Actor を削除しておきます。
import unreal import csv unreal.log("load") actor = unreal.EditorAssetLibrary.load_blueprint_class('/Game/Sample/BP_Cube') sub = unreal.UnrealEditorSubsystem() with open(unreal.Paths.combine([unreal.Paths.project_content_dir(), 'Data/CubeLocation.csv']), newline='') as csvfile: reader = csv.DictReader(csvfile) for row in reader: unreal.EditorLevelLibrary.spawn_actor_from_class(actor, (unreal.StringLibrary.conv_string_to_float(row['X']), unreal.StringLibrary.conv_string_to_float(row['Y']), unreal.StringLibrary.conv_string_to_float(row['Z']))) unreal.log("finished loading from csv")
Load
をクリックすると、先程と同じ場所に Cube が出現すれば成功です。
Python API について
Unreal Python API Documentation — Unreal Python 5.0 (Experimental) documentation
上記から Blueprint の関数名(もちろん C++ API に精通していればそっちも)を参考に探していくことになります。
今回書いたスクリプトも、もっとスッキリさせられそうな気がする( StringLibrary
の使い方あってるのかどうかなど)のでいろいろ試してみたいです。
参考にさせていただいたページ 🙏
- Python を使用したエディタのスクリプティング | Unreal Engine ドキュメント
- Editor Utility Widget | Unreal Engine ドキュメント
- [UE4]Pythonによるインスタンシング - Qiita
- [UE4]Editor Utility Widget内でレベル上にあるActorを指定できるようにする|株式会社ヒストリア
- ◽ CSV を出力|UE5 ブループリントでツールをつくる本
- 【UE4】【Python】4.26からPythonPluginがPython3.7.7に対応したので色々まとめてみた 【★★~★★★★】 | キンアジのブログ
コード
余談
Unreal Editor で Git の設定をさせると lfs の設定で Content/** filter=lfs diff=lfs merge=lfs -text
が .gitattributes
に入るので、Python ファイルまで対象になってしまうの気づいてませんでした。次からはちゃんとカスタマイズしないといけないですね。
UE の PIE で Play From Here が機能しない場合
よく忘れてしまうのでメモ書き。
上記リンクにある通りなのですが、すでにレベル上に操作対象の Pawn が配置されていると Play From Here や、Play メニューの 「Spawn player at Current Camera Locaiton」が機能しません。
Pawn を消してあげれば機能するようになります。
Voxel Plugin PRO を UE5 Preview 1 で利用する
公式の Discord Channel で日々情報は更新されているので、そちらを見れば済むのですがメモとして。
Windows
OS: Windows 10
Quick Start - Voxel Plugin Documentation
上記の Using the Beta / Building from source が基本的な方法になります。
簡単には
- C++ プロジェクトで作成するか、C++ プロジェクトに変換
- プロジェクトの
Plugins
ディレクトリに、VoxelPro
のProBetaLTS
branch を checkout (デフォルト branch がProBetaLTS
なので、単に checkout するだけで OK です) - ここで注意ですが、上記の wiki だと、
VoxelPro.uplugin
のように rename しているように見えますが、ここで rename はせず、checkout したままの、Voxel.uplugin
にします uproject
をダブルクリックしてビルドを行います
私は 3 のステップで
VoxelPro.uplugin
に rename してしまい、数時間を無駄にしました 😅
macOS
OS: macOS Monterey (チップは M1 です)
基本的には Windows の方と同じです。ただし、4 で uproject
をダブルクリックし、ビルドを実行しても失敗します 🥲
コードの編集
以下の二つの issue を参考にコードを編集します。(ここも discord から拾ったのですが、どのスレッドからだったか見失ってしまいました。。)
[Fix] Linux (21.04) Building Issue · Issue #435 · Phyronnaz/VoxelPlugin · GitHub
Fix build on macOS/clang by holvonixAdvay · Pull Request #388 · Phyronnaz/VoxelPlugin · GitHub
私の場合の Plugins/VoxelPro
以下の diff は以下です。
diff --git a/Source/VoxelEditor/Private/Details/VoxelPaintMaterialCustomization.cpp b/Source/VoxelEditor/Private/Details/VoxelPaintMaterialCustomization.cpp index 852950e05..fed87c116 100644 --- a/Source/VoxelEditor/Private/Details/VoxelPaintMaterialCustomization.cpp +++ b/Source/VoxelEditor/Private/Details/VoxelPaintMaterialCustomization.cpp @@ -360,7 +360,7 @@ void FVoxelPaintMaterial_MaterialCollectionChannelCustomization::CustomizeHeader Material->SetScalarParameterValue("EditorPreviewSingleIndex", Index); - const FVoxelMaterialCollectionMaterialInfo MaterialInfo{ Index, Material, *FString::Printf(TEXT("Index %03d"), Index) }; + const FVoxelMaterialCollectionMaterialInfo MaterialInfo{ static_cast<uint8>(Index), Material, *FString::Printf(TEXT("Index %03d"), Index) }; AssetsToMaterials->Add(Material, MaterialInfo); IndicesToMaterials->Add(Index, MaterialInfo); } diff --git a/Source/VoxelGraphEditor/Private/VoxelLandscapeGrassGraph.cpp b/Source/VoxelGraphEditor/Private/VoxelLandscapeGrassGraph.cpp index 1c63607e8..64a0b3981 100644 --- a/Source/VoxelGraphEditor/Private/VoxelLandscapeGrassGraph.cpp +++ b/Source/VoxelGraphEditor/Private/VoxelLandscapeGrassGraph.cpp @@ -33,10 +33,10 @@ void FVoxelLandscapeGrassGraph::Register() { - MakeInstance = [](TFunction<UObject* (UClass* Class)> CreateGraph) + MakeInstance = [](TFunction<UObject* (UClass* Class)> _CreateGraph) { auto Instance = MakeVoxelShared<FVoxelLandscapeGrassGraph>(); - Instance->CreateGraph = CreateGraph; + Instance->CreateGraph = _CreateGraph; return Instance; }; }
以下のように UE5 Preview 1 で Voxel Pro が動作します。
余談
UE5 Preview 1 で Xcode にPlugins
ディレクトリを認識させるために Finder で uproject
ファイルに対して Generate Xcode Project
を実行したのですが、うまくいかず結局 VSCode で編集しました。
OnlineSubsystemEOS の動作確認: 認証の確認
前回 EOS_Platform_Tick
が呼び出されている箇所を確認したので、SDK の Sample で確認したように Callback が受け取れることが分かりました。
続いて認証について確認します。
EOS_Auth_Login の呼び出し箇所
SDK の Sample で確認した際は EOS_Auth_Login
を利用していたので、今回もそこを起点に確認します。
FUserManagerEOS::Login
と FUserManagerEOS::LoginViaExternalAuth
の二箇所で使われていますが、今回は前者のみ確認します。
bool FUserManagerEOS::Login(int32 LocalUserNum, const FOnlineAccountCredentials& AccountCredentials) { LocalUserNumToLastLoginCredentials.Emplace(LocalUserNum, MakeShared<FOnlineAccountCredentials>(AccountCredentials)); FEOSSettings Settings = UEOSSettings::GetSettings(); // 略 EOS_Auth_LoginOptions LoginOptions = { }; LoginOptions.ApiVersion = EOS_AUTH_LOGIN_API_LATEST; LoginOptions.ScopeFlags = EOS_EAuthScopeFlags::EOS_AS_BasicProfile | EOS_EAuthScopeFlags::EOS_AS_FriendsList | EOS_EAuthScopeFlags::EOS_AS_Presence; FPlatformEOSHelpersPtr EOSHelpers = EOSSubsystem->GetEOSHelpers(); FAuthCredentials Credentials; LoginOptions.Credentials = &Credentials; EOSHelpers->PlatformAuthCredentials(Credentials); if (AccountCredentials.Type == TEXT("exchangecode")) { // This is how the Epic launcher will pass credentials to you FCStringAnsi::Strncpy(Credentials.TokenAnsi, TCHAR_TO_UTF8(*AccountCredentials.Token), EOS_MAX_TOKEN_SIZE); Credentials.Type = EOS_ELoginCredentialType::EOS_LCT_ExchangeCode; } else if (AccountCredentials.Type == TEXT("developer")) { // This is auth via the EOS auth tool Credentials.Type = EOS_ELoginCredentialType::EOS_LCT_Developer; FCStringAnsi::Strncpy(Credentials.IdAnsi, TCHAR_TO_UTF8(*AccountCredentials.Id), EOS_OSS_STRING_BUFFER_LENGTH); FCStringAnsi::Strncpy(Credentials.TokenAnsi, TCHAR_TO_UTF8(*AccountCredentials.Token), EOS_MAX_TOKEN_SIZE); } else if (AccountCredentials.Type == TEXT("accountportal")) { // This is auth via the EOS Account Portal Credentials.Type = EOS_ELoginCredentialType::EOS_LCT_AccountPortal; } else { UE_LOG_ONLINE(Warning, TEXT("Unable to Login() user (%d) due to missing auth parameters"), LocalUserNum); TriggerOnLoginCompleteDelegates(LocalUserNum, false, *FUniqueNetIdEOS::EmptyId(), FString(TEXT("Missing auth parameters"))); return false; } FLoginCallback* CallbackObj = new FLoginCallback(); CallbackObj->CallbackLambda = [this, LocalUserNum](const EOS_Auth_LoginCallbackInfo* Data) { if (Data->ResultCode == EOS_EResult::EOS_Success) { // Continue the login process by getting the product user id for EAS only ConnectLoginEAS(LocalUserNum, Data->LocalUserId); } else { FString ErrorString = FString::Printf(TEXT("Login(%d) failed with EOS result code (%s)"), LocalUserNum, ANSI_TO_TCHAR(EOS_EResult_ToString(Data->ResultCode))); UE_LOG_ONLINE(Warning, TEXT("%s"), *ErrorString); TriggerOnLoginCompleteDelegates(LocalUserNum, false, *FUniqueNetIdEOS::EmptyId(), ErrorString); } }; // Perform the auth call EOS_Auth_Login(EOSSubsystem->AuthHandle, &LoginOptions, (void*)CallbackObj, CallbackObj->GetCallbackPtr()); return true; }
まず EOS_Auth_LoginOptions
の ApiVersion
と ScopeFlags
は SDK の Sample と同じものが指定されています。
続いて Credentials ですが、EOS_Auth_Credentials
が型になるはずですが、ここでは FAuthCredentials
となっています。ただ、以下のように定義されているため同じです。
struct FAuthCredentials : public EOS_Auth_Credentials FAuthCredentials() : EOS_Auth_Credentials() { ApiVersion = EOS_AUTH_CREDENTIALS_API_LATEST; Id = IdAnsi; // 空 Token = TokenAnsi; // 空 }
その後で EOSHelpers->PlatformAuthCredentials(Credentials);
が呼び出されていますが、実装が空なので結局は SDK の Sample と同じく ApiVersion
のみ指定されたと考えてよさそうです。
続いて Callback ですが、成功時には SDK の Sample と同様に EOS_Connect_Login
へと続けています。
よって、だいたいの流れは同じと考えて良さそうです。また、この辺のコードは FUserManagerEOS
にまとまっているようです。
FUserManagerEOS
は以下のように実装しているインターフェースが多いです。
class FUserManagerEOS : public IOnlineIdentity , public IOnlineExternalUI , public IOnlineFriends , public IOnlinePresence , public IOnlineUser
FUserManager を使う方法
前回見た FOnlineSubsystemEOS::Init
内で初期化されており FOnlineSubsystemEOS
から呼び出すことができます。
また、上述したように実装しているインターフェースが多いので、 FOnlineSubsystemEOS
の多くの Get メソッドから返される実態も UserManager
になっています。
IOnlineFriendsPtr FOnlineSubsystemEOS::GetFriendsInterface() const { return UserManager; } IOnlineExternalUIPtr FOnlineSubsystemEOS::GetExternalUIInterface() const { return UserManager; } IOnlineIdentityPtr FOnlineSubsystemEOS::GetIdentityInterface() const { return UserManager; } IOnlineUserPtr FOnlineSubsystemEOS::GetUserInterface() const { return UserManager; } IOnlinePresencePtr FOnlineSubsystemEOS::GetPresenceInterface() const { return UserManager; }
ここまで見てきたように FOnlineSubsystemEOS
は OnlineSubsystemEOS を使う際の公開部分なので、直接 OnlineSubsystem として取得することができます。
[UE4] 複数のOnlineSubsystemを併用する - Qiita
今回はデフォルトを EOS にしてあるので、
[OnlineSubsystem] DefaultPlatformService=EOS
以下で取得することができます。
IOnlineSubsystem::Get();
FUserManagerEOS::Login が他に呼び出される箇所
UOnlineEngineInterfaceImpl::LoginPIEInstance
で呼び出されることが確認できました。
以下から Credential 情報を取得し PIE 起動時に渡しているようです。後日ちゃんと試してみようと思います。
まとめ
今回は、認証の簡単な流れを SDK の Sample と比較しつつ眺めました。当たり前かもしれませんが、SDK で説明されていることと同じように実装されているように見えました。
いくつかマクロでわからない部分があったりしたので、そこは別途拾ってみようと思います。