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 の作成

f:id:you1dan:20220319073416p:plain

今回は WBP_AssetLocationStore としました。

UI

必要最低限だけ配置しています。

f:id:you1dan:20220319073940p:plain

ひとまず実行・配置

ここまでで、Editor Utility Widget を実行し、レベルエディタに配置しておくことにします。 f:id:you1dan:20220319074124p:plain

f:id:you1dan:20220319074133p:plain

イベントハンドルとログ出力

単純にハンドルしておきます。

f:id:you1dan:20220319074905p:plain

Log String でもいいですが、ここでは Print String で。

f:id:you1dan:20220319074920p:plain

Output Log ウィンドウで一旦 Clear しておくと確認しやすいです。ログの状況を確認したい場合は Window メニューから Output Log をもう一つだしておいてもいいかもしれません。

f:id:you1dan:20220319075150p:plain

それぞれのボタンをクリックするとログが流れることが確認できました。

f:id:you1dan:20220319075354p:plain

Python を呼び出す

Python を使用したエディタのスクリプティング | Unreal Engine ドキュメント

プロジェクトのフォルダの下にある「Content/Python」サブフォルダ。 が読み込まれるとなっているので、そこにスクリプトを配置することにします。

f:id:you1dan:20220319080028p:plain

ひとまず内容は以下のようにして、save.py, load.py命名します

import unreal

unreal.log("save")

Python Script を Blueprint から呼出す

f:id:you1dan:20220319080338p:plain

どうやら失敗しました。読み込めていないようです。

f:id:you1dan:20220319080448p:plain

ここでは前に進めるためにプロジェクト固有でロードパスを追加します。
ドキュメントにもありますが、エディタの再起動が必要です。

f:id:you1dan:20220319080635p:plain

Python Script のログも表示されるようになりました。

f:id:you1dan:20220319081119p:plain

Actor の Location を CSV で読み書きする

対象の ActorBP_Cube として作成します。
Static Mesh を持つだけの単純なものです。

f:id:you1dan:20220319083912p:plain

雑にレベルに配置します。

f:id:you1dan:20220319084245p:plain

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 の使い方あってるのかどうかなど)のでいろいろ試してみたいです。

参考にさせていただいたページ 🙏

コード

GitHub - dany1468/UE_EUW_CSV

余談

Unreal Editor で Git の設定をさせると lfs の設定で Content/** filter=lfs diff=lfs merge=lfs -text.gitattributes に入るので、Python ファイルまで対象になってしまうの気づいてませんでした。次からはちゃんとカスタマイズしないといけないですね。

UE の PIE で Play From Here が機能しない場合

よく忘れてしまうのでメモ書き。

'Play from here' doesn't work in any project, even default templates. - AnswerHub - Unreal Engine Forums

上記リンクにある通りなのですが、すでにレベル上に操作対象の 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 が基本的な方法になります。

簡単には

  1. C++ プロジェクトで作成するか、C++ プロジェクトに変換
  2. プロジェクトの Plugins ディレクトリに、VoxelProProBetaLTS branch を checkout (デフォルト branch が ProBetaLTS なので、単に checkout するだけで OK です)
  3. ここで注意ですが、上記の wiki だと、VoxelPro.uplugin のように rename しているように見えますが、ここで rename はせず、checkout したままの、Voxel.uplugin にします
  4. uproject をダブルクリックしてビルドを行います

私は 3 のステップで VoxelPro.uplugin に rename してしまい、数時間を無駄にしました 😅

macOS

OS: macOS Monterey (チップは M1 です)

基本的には Windows の方と同じです。ただし、4 で uproject をダブルクリックし、ビルドを実行しても失敗します 🥲

コードの編集

以下の二つの issue を参考にコードを編集します。(ここも discord から拾ったのですが、どのスレッドからだったか見失ってしまいました。。)

私の場合の 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 が動作します。

f:id:you1dan:20220304231040p:plain

余談

UE5 Preview 1 で XcodePlugins ディレクトリを認識させるために Finder で uproject ファイルに対して Generate Xcode Project を実行したのですが、うまくいかず結局 VSCode で編集しました。

OnlineSubsystemEOS の動作確認: 認証の確認

前回 EOS_Platform_Tick が呼び出されている箇所を確認したので、SDK の Sample で確認したように Callback が受け取れることが分かりました。
続いて認証について確認します。

EOS_Auth_Login の呼び出し箇所

SDK の Sample で確認した際は EOS_Auth_Login を利用していたので、今回もそこを起点に確認します。

FUserManagerEOS::LoginFUserManagerEOS::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_LoginOptionsApiVersionScopeFlagsSDK の 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 起動時に渡しているようです。後日ちゃんと試してみようと思います。

f:id:you1dan:20220127175900p:plain

まとめ

今回は、認証の簡単な流れを SDK の Sample と比較しつつ眺めました。当たり前かもしれませんが、SDK で説明されていることと同じように実装されているように見えました。

いくつかマクロでわからない部分があったりしたので、そこは別途拾ってみようと思います。