TA志望学生の備忘録

テクニカルアーティストを志望する学生の備忘録

【お知らせ】更新について

お久しぶりです。最後にブログ書いたの70日前らしいです。。。

 

早速ですが、お知らせということでしばらく記事の更新ができそうにないです

 

理由としては、就職活動のため面接やらで忙しいのと、改めてHLSLやC++などの基礎を勉強し直し中のため、何かを開発、作成することがなく更新することもないためです。

もっと早くこのお知らせを書くべきでしたね。。。

 

 

次回の更新は完全に未定です。

もしかしたら思いついたものを更新する可能性もありますし、内定のお知らせの可能性もあります。

 

というわけで、しばらくブログ更新はお休みになります。

気長にお待ちください。

UEのツールを作る【2】

前回(一か月前)の続きです、、、。

いろいろ詰まったりして思うように進まず遅れました。

だいぶムキになってやっていたので、途中の画像とかコードがあまりなく文字ばかりになってしまいますが、ご了承ください。

 

はじめに

前回は友人の効率改善のためにツールを作成し始めましたが、既存の機能では解決できそうにないのでプラグインを作成することにしました。

そのためにソースコードのダウンロードやc++の勉強をしたので、それらのまとめみたいな感じになると思います。

下記リンクでソースコード、zipファイルを確認できます。

github.com

※このブログ内で記載されているコードは一部となります。

結果

完成品の動作になります。

youtu.be

 

下記リンクで詳細が見れます。

https://www.vivivit.com/works/918410

 

制作過程

こちらを主に参考にしながら始めました。

historia.co.jp

 

「エディタツールバーボタン」で作成したらクリック後の動作を考えるだけで済みますが、初回かつ後学のためと思い、「空白」から制作を始めました。(コードは参考にした)

 

空っぽスタートなので呼び出しできるようにエディタを拡張していきます。

StartupModuleでモジュールロード時の処理、ShutdownModuleでアンロード時の処理を記述しました。

StartupModule

    if(IsRunningCommandlet()){return;}
    Extender = MakeShareable(new FExtender);
    Extender -> AddMenuExtension
    (
        "LevelEditor",
        EExtensionHook::After,
        nullptr,
        FMenuExtensionDelegate::CreateRaw(this, &FMaterialSetModule::OnWindowMenuExtension)
    );

    FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
    LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(Extender);

    IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
    MainFrameModule.OnMainFrameCreationFinished().AddRaw(this, &FMaterialSetModule::OnMainFrameLoad);

 

ShutdownModule

    if(Extender.IsValid() && FModuleManager::Get().IsModuleLoaded("LevelEditor"))
    {
        FLevelEditorModule &LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
        LevelEditorModule.GetMenuExtensibilityManager()->RemoveExtender(Extender);
    }
   
    if(FModuleManager::Get().IsModuleLoaded("MainFrame"))
    {
        IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
        MainFrameModule.OnMainFrameCreationFinished().RemoveAll(this);
    }
OnWindowMenuExtensionとOnMainFrameLoadは後述します。
ここまで書く時点でなにがc++の書き方でどれがUEのモジュールなのかもわからず頭ぐちゃぐちゃでした。
この前にヘッダで宣言を書いてますが、TSharedPtrやらTArrayやらもよくわかんないし、<>で囲われてるものが出てきたりして何の何の何って感じでした。
 
なんとなくの理解ですが、TSharedPtrはスマートポインタと呼ばれるもので、intとかfloatみたいな型宣言の複雑なデータが扱えるものっぽい?

Smart Pointers in Unreal Engine | Unreal Engine 5.5 ドキュメンテーション | Epic Developer Community

<>の中はオブジェクトの型が入るみたいですね。

他にも、"*"や"&"は演算子として使用するだけでなくポインタに関係する際に出てくるんですね。

2.7. 演算子 - ゼロから学ぶ C++

このあたりがなんとなく分かってくると、ある程度は何をしているのか感じるくらいにはなってきました。

後はこのなんとなくをより具体的にしていきながら、モジュールとメソッドに何があるのか調べて何を与えるとどうなるかをドキュメントを見てメモしていきました。

感想みたいなので脱線しましたが、ここからはコードのメモです。解説できるほど理解していないのでメモです。

 

AddMenuExtesionでメニューを登録して、LoadModuleCheckedでエディタ拡張に必要なモジュールをロード。

CreateRawはメソッドをコールバックとして登録。

AddExtenderで拡張メニュー(Extender)の登録。

ShutdownModuleはExtenderの削除を行うので事前に有効であるか確認している。

 

ここからOnWindowMenuExtensionとOnMainFrameLoadです。

 

OnMyToolMenuExtension

void FMaterialSetModule::OnWindowMenuExtension(FMenuBuilder & MenuBuilder)
{
    MenuBuilder.BeginSection("MyMenuHook", LOCTEXT("MyMenu", "CustomPlugin"));
    MenuBuilder.AddMenuEntry(
        LOCTEXT("MyMenuTitle", "SetMaterial"),
        LOCTEXT("MyMenuToolTip", "マテリアルにテクスチャを自動で割り当てます"),
        FSlateIcon(),
        FUIAction(FExecuteAction::CreateRaw(this, &FMaterialSetModule::OnMyToolMenu))
    );
    MenuBuilder.EndSection();
}

 

BeginSectionで新しいメニューセクションを開始。

AddMenuEntryでセクション内に新しいメニュー項目を追加。

FUIActionで選択された際に実行するアクションを指定。これでOnMyToolMenuを呼び出し。

 

 

OnMainFrameLoad

void FMaterialSetModule::OnMainFrameLoad(TSharedPtr<SWindow> InRootWindow, bool bIsNewProjectWindow)
{
    if(
(!bIsNewProjectWindow) && (InRootWindow.IsValid())
)
    {
        RootWindow = InRootWindow;
    }
}

新しいウィンドウじゃないかつ、InRootWindowが有効なポインタであるか。

初期化を行っている(多分)。

 

 

    TWeakObjectPtr<UMaterialInterface> SelectedMaterial;
 
    UPROPERTY()
    TObjectPtr<UTexture2D> SelectedAlbedo;
    UPROPERTY()
    TObjectPtr<UTexture2D> SelectedNormal;
    UPROPERTY()
    TObjectPtr<UTexture2D> SelectedRoughness;

ガベージコレクションで適切に管理をしないとクラッシュの原因になりました。

なのでUPROPERTYマクロとTWeakObjectPtrを使用しています。

 

ガベージコレクション:メモリ上の不要なデータを自動的に削除する仕組み

[UE4] GC(ガベージコレクション)の基本 #UE4 - Qiita

UEに存在する機能で、便利な仕組みっぽいです。内部的なデータの流れをすべて理解していないので、詳しくはわかっていません。勉強中です…。

 

        Window -> SetOnWindowClosed(FOnWindowClosed::CreateLambda([this](const TSharedRef<SWindow>&){
            MyWindow.Reset();
        }));

ウィンドウが閉じたときに、MyWindowをリセットしてメモリリークなどを防ぐ。

 

      SNew()

ウィジェットの作成。

SVerticalBoxSHorizontalBoxSTextBlockなどいろいろある。

 

            + SVerticalBox::Slot()

スロットの追加

親元の箱みたいなイメージで、ここにPaddingなどで見た目の調整を行う。

 

だいたいこんな感じで作ってきました。

ここからは考え方とか参考サイトなど、いろいろのまとめです。

 

 

ーーーーーーーーーーーーー

UIのこと

 

マテリアルを指定>テクスチャを入力>グラフ内にノードを作成。という流れを想定。

処理を考えるにも、まずは入力ができないと始まらないので、UIの作成から。

モックアップ

編集/エディタの環境設定/未分類からデベロッパーツール/UIの拡張点の表示を有効にするとUIを作成する際に助かります。

あとはWidgetReflerctorとUEのソースコード、後は公式ドキュメントを参考にしました。

dev.epicgames.com

他にも、Starship Galleryも便利です。

 

SlateUIはc++で書いてる感じがしないくらい直感的に書けていいですね。

でもネストばっかでちょっとHTML感があるのは見づらくてややこしいなって感じました。

 

UIで苦戦したのは、サムネ付きのプルダウンの選択でした。

↑この選択するやつ

 

                        SNew(SObjectPropertyEntryBox)
                        .AllowedClass(UMaterialInterface::StaticClass())
                        .ObjectPath_Lambda([this]() -> FString {
                            return SelectedMaterial.IsValid() ? SelectedMaterial -> GetPathName() : FString();
                        })
                        .OnObjectChanged_Lambda([this](const FAssetData& AssetData) {
                            UMaterialInterface* NewMaterial = Cast<UMaterialInterface>(AssetData.GetAsset());
                            if (NewMaterial)
                            {
                                SelectedMaterial = NewMaterial;
                            }
                        })
                        .AllowClear(true)
                        .ThumbnailPool(ThumbnailPool.ToSharedRef())
 

こんな感じで実装しています。

SObjectPropertyEntryBoxが大事で、そこからはフィルタ、パス、クリアボタンなどの設定です。

参考↓

SObjectPropertyEntryBoxでActorアセットを選択する話 - crssnkyの。

これで選択できるようになってちょっとUEのツールって感じがしてテンション上がりました。

 

ここからボタンを二つ追加します。

一つは確定させてウィンドウを閉じるボタンで、もう一つは連続して設定するためのボタンです。

 

                    SNew(SButton)
                    .Text(LOCTEXT("Execute", "実行"))
                    .OnClicked_Lambda([this]() -> FReply {
                        FMakeNode::CreateTextureSampleNode(
                            Cast<UMaterial>(SelectedMaterial.Get()),
                            SelectedAlbedo.Get(),
                            SelectedNormal.Get(),
                            SelectedRoughness.Get()
                        );

次に中身を考えて作っていきます。

可用性と可読性、拡張性が上がると思ったので、内部処理は別ファイルで行っていきます。

内部の処理を行うMakeNode.cppは次のブログで書きます。

詰まったとことか

まず、いきなり5.4でc++のプロジェクトが作成できない状態になりました。

エラー画面のスクショやログが残っていないのですが、試行錯誤の記録を残しておきます。

解決策1

UEの再インストール

理由

5.5で作成できたので環境が問題ではないと判断したため。

結果

変わらず作成できない。

 

解決策2

Visual Studio Installerでとにかくコンポーネントをインストール。

理由

5.4用のコンポーネントが不足していると考えたため。

結果

作成できた。

 

まとめ

おそらく、最新のコンポーネントにエンジンが対応していない?と思われるのでなるべく多くのバージョンのMSVCなどをインストールしておけば再発防止できるかも。

 

次にビルド環境構築で詰まりました。

これが非常に気づきにくくて時間がかかりました。が、解決法はシンプルでVisual Studio Build Toolをインストールしていなかったからでした。

【UE5】VSCodeでプロジェクトのビルドを行う。 - アルゴンUE4/UE5&アプリ開発日記

こちらのサイトを見つけてようやく気付きました。

 

UEのライブコーディングでエラーが出ていないのに結果が反映されず、ずっと自分のコードを疑い続けて一歩も進まない状態が続きました。

反省:環境構築や初心者向けの基本的な情報を流し見せず、しっかり読み込む。

 

 

 

 

 

 

 

あとがき

またまた更新が遅くなりました。

上記の詰まったところで、進まなかったり、できな過ぎて心折れてたりして制作自体に時間がかかってしまい、更新できませんでした…。

一区切りつくとこまですすんだらこまめにブログを書くべきか…。

どうしてもある程度の成果がでないと書く内容が薄くなるんですよね~。

ちょっといろいろ検討してみます。

ただ、また次も遅くなりそうです。(ES書いたり就活でやることががが)

今回はメインの実行ファイルまでの内容で、次は処理を担うコードの解説?メモ?になります。

二重括弧を勝手に脚注にするな()

 

その他参考サイト様's

Unreal Engine Plugins | プラグイン制作 | UE5攻略リンク

一週間で身につくC++言語の基本|トップページ~C++言語の初心者でも、簡単にプログラミングが気軽に学習できるサイトです。

ゼロから学ぶ C++

その他の演算子 - cppreference.com

UnrealC++でレベルエディタに独自メニューを追加する #UE4 - Qiita

【UE4】エディタ拡張(インプットボックス編) – 株式会社ロジカルビート

独自のタブをエディタのメニューから呼び出す方法 #UE4 - Qiita

Grid Layout - TAColor

【UE4:エディタ拡張】左右にレイアウトを揃える #UnrealEngine4 - Qiita

Materials C++ | UE5攻略リンク

[UE4]プラグインをパッケージ化しよう!|株式会社ヒストリア

【UE5.4】 プラグインの単体ビルドに関するあれこれ | Alche, Inc

他にも公式ドキュメントのページなどありましたが、あまりにも多すぎるため一部となります。

 

UEのツールを作る【1】

友人の作業効率化のためのツールをつくることにしたので、その記録です。

今回作るツールはマテリアルの自動設定を目標にしています。

 

 

はじめに

友人の困りごと

  • マテリアルをいちいちベースカラーやノーマルマップなどの作成が面倒
  • srgbとリニアの設定が面倒

このあたりを解決できるようにツールを作りたいと思います。

 

環境設定

UEのバージョンは5.5.3です。

 

 

作成する前に、マテリアルインスタンスを使えばマテリアルグラフの画面でノードを作成したりする手間が省けるのではと思い聞きましたが、機能をよくわかっていないとのことでした。なのでマテリアルインスタンスを作成するツールと、パラメータの設定を簡単に行えるツールの二つに分けて考えたいと思います。

 

UEのツール作成はエディタユーティリティウィジェット(EUW)やエディタユーティリティブループリント(EUB)があるのでそれらで作成できないか試してみます。

 

 

その一

こちらの公式を参考にやってみました。

dev.epicgames.com

 

こちらのブループリントは対象のスタティックメッシュに特定のマテリアルを割り当てるものです。

このキューブに対して

コンテンツドロワー上で右クリック→スクリプト化されたアセットアクションから実行すると

マテリアルが変更できました。

しかし、これを複数のマテリアルを持つメッシュに対して行うと、

オブジェクトの持つすべてのマテリアルが変更されてしまいました。

 

 

このままではブループリントを編集しないと割り当てるマテリアルを変更できないかつ、メッシュの持つすべてのマテリアルを変更してしまいます。

したいことはマテリアルグラフに指定ノードの作成なので違いますね。これはこれで何か別のツールを作成する際に使えそうですが。

 

というわけで他の情報を探してみます。

 

 

その二

qiita.com

こちらの記事を見つけたのでやってみます。

どうやらマテリアルインスタンスダイナミック(MID)というのを利用するようです。

dev.epicgames.com

こちらが公式ドキュメントです。

ぱっと見はエディタ用の機能ではなさそうですが、一度作成してみます。

こちらが参照されるパラメータをもつマテリアルです。

これでBaseColorとNormalのパラメーターの変更ができました。

しかしこの方法では事前にインスタンス化するマテリアルの設定をしないといけない、さらにMIDで作成されたマテリアルのパラメータのプレビューや後から割り当ての確認などもできませんでした。

やはりランタイム用の機能であり、エディタ上で使うものではありませんでした。

 

おわりに

他に調べても、ブループリントでマテリアルグラフのノードを作成することはできなそうです。

となると、c++で直接コードを記入して作っていくしかなさそうです。

次回はc++でコードを書いていく予定です。(現在勉強中)

結局更新遅くなりましたね。。。

あと誰かblueprintUEの上手な張り方知りませんか()

 

SRP作成(3)

三回目(実質二回目)引き続きやっていきます。

前回はこちら

SRP作成(2) - TA志望学生の備忘録

 

 

はじめに

こちらのサイト様を参考にすすめています。

catlikecoding.com

今回はこのドローコールのページです。

シェーダ

描画にはCPUがGPUに描画指示をしなければならない。どのように描画するかはGPUの命令セットであるシェーダーによって定義される。

シェーダーはメッシュのほかにオブジェクトの変換行列やマテリアルプロパティなどの追加情報を必要とする。

 

Shader "Custom RP/Unlit" {
   
    Properties {}
   
    SubShader {

        Pass {}
    }
}

コンパイルできる最小のシェーダー

 

            HLSLPROGRAM
            ENDHLSL

Passブロックに記述する。

この間にHLSLを記述する。

 

メッシュを描画するために、GPUはすべての三角形をラスタライズしピクセルデータに変換する必要がある。

頂点座標を3D空間から2D可視化空間に変換し、その結果の三角形で該当するピクセルを塗りつぶす。

前者は頂点シェーダ。

後者はフラグメントシェーダ。

各シェーダを識別するのはプラグマ指示文で行われる。

 

            #pragma vertex UnlitPassVertex
            #pragma fragment UnlitPassFragment

VertexかFragmentのどちらかと関連する名前が続く。

 

HLSLにはクラスの概念がない。コードブロックのローカルスコープ以外に、単一のグローバルスコープしかない。なのですべてのものがどこからでもアクセス可能。

ファイルをインクルードするとファイルの内容すべてをインクルードディレクティブの位置に挿入する。

同じファイルを複数回インクルードするとコードが重複するので、インクルードガードを追加することで、コンパイラエラーを防ぐ。

#define CUSTOM_UNLIT_PASS_INCLUDED

defineディレクティブで任意の識別子を定義できる。

 

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#endif

ifndefディレクティブで識別子の定義を確認する。

もしマクロが定義済みならコンパイルされないようにスキップする必要がある。

ので、#endifを追加してスコープを終了させる。

 

フラグメントシェーダーが色を返す。

float4 UnlitPassFragment () {
    return 0.0;
}

positionとtexture座標のみにfloatを使い、それ以外にはhalfを使ったほうがいい?(モバイル用)

モバイルをターゲットにしていない場合、大抵のGPUはfloatを使うのでhalfでも精度は問題ない。

 

float4 UnlitPassFragment () : SV_TARGET {
    return 0.0;
}

セマンティクスでパラメータの使用目的の指定。

 

float4 UnlitPassVertex (float3 positionOS) : SV_POSITION {
    return float4(positionOS, 1.0);
}

positionOS オブジェクト空間(Object Space)

位置座標はx,y,zと常に1に設定されるwで構成される。wは位置と方向の区別に使う。これにより同じ変換行列(マトリクス)を使って位置と方向を正しく変換できる。

w = 1 位置(点)

w = 0 方向(ベクトル)

位置は移動回転スケールの影響を受ける。

ベクトルは移動の影響を受けず、回転スケールのみの影響を受ける。

 

(float3 positionOS : POSITION)

頂点データには位置以外も含まれることがあるので、セマンティクスを追加する。

 

空間変換には行列が必要。これは何かが描画されるときにGPUに送られる。

行列は常に同じで、Unityが提供する標準入力がある。

 

UnityInput.hlsl

float4x4 unity_ObjectToWorld;

インクルードガード内で記入する。

オブジェクト座標をワールド座標に変換するための行列。

c#ではフィールド変数と呼ばれる。

シェーダではユニフォーム変数と呼ぶ。(uniform = 一定)

描画中は値が変わらない変数で、GPUが一回の描画ごとに一回だけ設定する。全ての頂点・ピクセルで同じ値を使う。

 

Common.hlsl

float3 TransformObjectToWorld (float3 positionOS) {
    return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}

mul : 行列とベクトルの掛け算(マトリクス変換)

計算のためにfloat3のpositionOSのwに1を入れてfloat4にする。

ローカル座標をワールド座標に変換して.xyzで出力をfloat3にする。

 

#include "../ShaderLibrary/Common.hlsl"

関数の上にCommon.hlslをインクルードする。(相対パス

 

    float3 positionWS = TransformObjectToWorld(positionOS.xyz);

TransformObjectToWorldの入出力はfloat3なので、positionOSのxyzのみを渡して、ワールド座標に変換。positionWSに代入。

 

ワールドスペースからクリップスペースに変換する必要がある。

→プロジェクション行列

クリップスペース:カメラの視界に収まる形に変換した座標。

 

float4x4 unity_MatrixVP;

View-Projection行列

 

float4 TransformWorldToHClip (float3 positionWS) {
    return mul(unity_MatrixVP, float4(positionWS, 1.0));
}

クリップ座標:HClip (Homogeneous Clip Space)

WSからHClipに変換。

 

    return TransformWorldToHClip(positionWS);

頂点シェーダで使用する。

上記の変換用関数二つはCore RP Pipelineパッケージに含まれている。

 

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

変換関係ファイル。

unity_ObjectToWorldの存在を仮定していない。が、関連する行列がマクロによってUNITY_MATRIX_Mとして定義されている。

#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_MATRIX_P glstate_matrix_projection

これでUNITY_MATRIX_Mをunity_ObjectToWorldで置き換える。

他の行列も置き換える。

 

#define UNITY_MATRIX_I_V unity_MatrixInvV
#define UNITY_PREV_MATRIX_M unity_prev_MatrixM
#define UNITY_PREV_MATRIX_I_M unity_prev_MatrixIM

Unity2022はこれら三つの行列が必要。

 

float4x4 unity_WorldToObject;
float4x4 unity_MatrixV;
float4x4 unity_MatrixInvV;
float4x4 unity_prev_MatrixM;
float4x4 unity_prev_MatrixIM;
float4x4 glstate_matrix_projection;

UnityInputにも追加する。

 

real4 unity_WorldTransformParams;

これも追加する必要がある。

real4と定義されたベクトル。プラットフォームによってfloat4かhalf4になる。

 

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"

ここに基本的なマクロが入っている。

 

float4 _BaseColor;

先頭にアンダースコアをつけるのはマテリアル・プロパティを表す標準的な方法。

インクルード下、Vertex関数前で記述。

 

    Properties {
        _BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
    }

プロパティブロックで記述することでマテリアルとリンクする。

 

バッチング

ドローコールを実行する度にCPUとGPUの通信が必要になる。

バッチング(バッチ処理):ドローコールをまとめる処理。

CPUとGPU間の通信に費やす時間を短縮する。

 

SRPBacher:インスペクターで有効か確認できる。ドローコールを減らすのではなく無駄のないようにする。マテリアルプロパティをGPUにキャッシュして、ドローコールの度に送信する必要がないようにする。シェーダが一様なデータの厳密な構造を順守している場合のみ機能する。

 

すべてのマテリアルプロパティは、グローバルレベルではなく、具体的なメモリバッファ内で定義する必要がある。

cBufferブロックでラップすることで行う。構造体宣言のように機能するが、セミコロンで終了する必要がある。

特定の定数メモリバッファで分離するが、グローバルレベルでアクセスは可能なまま。

 

CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;
CBUFFER_END

cBufferはすべてのプラットフォームでサポートされているわけではないので、CoreRPLibraryに含まれるCBUFFERマクロを使う。バッファ名を引数として取る。

 

CBUFFER_START(UnityPerDraw)
    float4x4 unity_ObjectToWorld;
    float4x4 unity_WorldToObject;
    float4 unity_LODFade;
    real4 unity_WorldTransformParams;
CBUFFER_END

これらもバッファに含む必要がある。

これでシェーダのインスペクターにSRP Compatibleが表示される。

 

SRP Bacher Compatible

あくまでシェーダに互換性があるだけで、SRPBacherを有効化するにはGraphicsSettings.useScriptableRenderPipelineBatchingをtrueにする必要がある。

 

    public CustomRenderPipeline () {
        GraphicsSettings.useScriptableRenderPipelineBatching = true;
    }

CustomRenderPipelineに追加して、パイプラインインスタンスが作成されるときに実行する。

 

マテリアルが増えても同じシェーダならバッチは一回になる。マテリアルの中身ではなく同じシェーダを使っているかで判断している。唯一の制限はマテリアルごとにメモリ配置が同じでなければならない。(単一カラーのプロパティのみを含むなど)

 

カスタムコンポーネントを作成することでオブジェクトごとに色を設定できる。

→色ごとにマテリアルを作成する必要がなくなる。

 

[DisallowMultipleComponent]

一つのゲームオブジェクトに2つ以上アタッチできないようにする。

 

static int baseColorId = Shader.PropertyToID("_BaseColor");

_BaseColorプロパティにアクセス。

 

[SerializeField]
Color baseColor = Color.white;

インスペクターで編集可能にする。

 

        MaterialPropertyBlock

オブジェクトごとにマテリアルのプロパティを変更するためのクラス。

 

    void OnValidate () {}

インスペクターの値を変更したときに自動で実行。

 

GetComponent<Renderer>().SetPropertyBlock(block);

Rendererコンポーネントを取得。オブジェクトのマテリアルにblockを適用。

 

    void Awake () {
        OnValidate();
    }

エディタ上のみでなく実行時にも適用するようにする。

 

GPUインスタンシング

同じメッシュを持つ複数のオブジェクトに対して一度に単一のドローコールを発行することで動作する。

CPU オブジェクトごとのトランスフォーメーションとマテリアルのプロパティを配列に入れてGPUに送信

GPU すべてのエントリを反復処理し、提供された順番にレンダリングする。

 

            #pragma multi_compile_instancing

GPUインスタンシングを有効にするプリプロセッサディレクティブ。

 

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"

GPUインスタンシングをサポートするためにインクルードする必要があるファイル。

 

struct Attributes {
    float3 positionOS : POSITION;
};

構造体でセマンティクスをラップする。

 

    UNITY_VERTEX_INPUT_INSTANCE_ID

インスタンシング用のIDを定義するマクロ。

 

    UNITY_SETUP_INSTANCE_ID(input);

入力からインデックスを取り出し、他のインスタンス化マクロが依存するグローバルなstatic変数に格納する。

 

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

CBUFFER_START,ENDから置き換えることで、_BaseColorを配列参照に置き換える。

 

    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)

GPUインスタンシング対応のプロパティ。

 

インスタンスインデックスもPassFragmentで利用できるようにする必要がある。

PassVertexに位置とインデックスの両方を出力させて、それをコピーするために構造体を使う。

 

    UNITY_TRANSFER_INSTANCE_ID(input, output);

inputからoutputへインスタンスIDをコピー

 

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
    UNITY_SETUP_INSTANCE_ID(input);
    return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}

オブジェクトのインスタンスIDを取得し、マテリアルごとのバッファから_BaseColorを取得して返す。

 

GPUインスタンシングは同じマテリアルを共有するオブジェクトに対してのみ機能する。

バッチサイズには、データ量に基づく制限がある。この制限を超えると複数のバッチになる。

 

ダイナミックバッチング

同じマテリアルを共有する複数の小さなメッシュを一つの大きなメッシュにまとめて代わりに描画する古い手法。小さなメッシュにしか使えない。

 

            enableDynamicBatching = true,
            enableInstancing = false

GPUインスタンシングを無効にし、enableDynamicBatchingを有効化。

GraphicsSettings.useScriptableRenderPipelineBatching = false;

同じくSRPバッチャーも無効。

 

ダイナミックバッチングは複数のメッシュを一つのメッシュにするため描画の順序が変わる。

 

透明

透明描画はソースとディスティネーションのブレンドモードを設定することでできる。

ソース:今描画されるもの。

ディスティネーション:先に描画されたもの。その結果が最終的に到達する場所。

ソース1は完全に追加。ディスティネーション0は無視される。

標準的な透明度のソースブレンドモードはSrcAlphaで、レンダリングカラーのRGBにアルファが乗算されることを意味する。つまり、アルファ値が低いほど弱くなる。

ディスティネーションは逆のOneMinusSrcAlphaに設定されて合計ウェイトが1になる。

 

        Pass {
            Blend [_SrcBlend] [_DstBlend]
        }

Blendステートメントのあとに2つのモードを続けることでPassブロックで定義できる。

シェーダーのプロパティを使いたいので[]に入れてアクセス。

 

透明レンダリングは普通、depthバッファに書き込まない。

ZWriteステートメントでdepthを書き込むかどうかを制御できる。

 

        _BaseMap("Texture", 2D) = "white" {}

Textureプロパティは、変なエラーを防ぐために空のコードブロックで終わらせる必要がある。

 

テクスチャはGPUメモリにアップロードする必要がある。これはUnityがやってくれる。

 

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

シェーダには関連するテクスチャへのハンドルが必要で、TEXTURE2Dマクロを引数として名前を使う。また、テクスチャのサンプラーステートを定義する必要がある。これはテクスチャのラップモードとフィルタモードを考慮してどのようにサンプリングするかを制御する。SAMPLERマクロで行う。

テクスチャとサンプラーステートはシェーダリソース。インスタンスごとに提供はできず、グローバルスコープで宣言する必要がある。UnlitPass.hlslのシェーダプロパティの前に行う。

 

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

Unityはテクスチャのタイリングとオフセットもテクスチャプロパティと同じ名前だが、スケールとtranslationのようなものを表す_STが付加されたfloat4を介して利用できるようにしている。このプロパティはUnityPerMaterialバッファの一部である必要があるためインスタンスごとに設定可能。

 

    float2 baseUV : TEXCOORD0;

テクスチャをサンプリングするには、頂点属性の一部であるテクスチャ座標が必要。

具体的には座標の最初のペアが必要。AttributesにTEXCOORD0の意味を持つfloat2フィールドを追加することで追加することで行う。これはベースマップのためのもの。

 

    float2 baseUV : VAR_BASE_UV;

テクスチャがサンプリングされるので、座標をfragment関数に渡す。

特別な意味を追加する必要はなく、GPUが特別な注意を払う必要のないデータを渡すだけ。そこでVAR_BASE_UVを使用する。

 

    float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
    output.baseUV = input.baseUV * baseST.xy + baseST.zw;

UnlitPassVertexで座標をコピーするときに、_BaseMap_STに保存されているスケールとオフセットも適用できる。この方法ではフラグメント単位ではなく頂点単位で行う。

スケールはXY、オフセットはZWに保存されて、swizzle propertiesでアクセスできる。

 

    float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
    float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
    return baseMap * baseColor;

UV座標がUnlitPassFragmentで利用可能になり、三角形全体で補間される。

テクスチャ、サンプラーの状態、座標を引数として、SAMPLE_TEXTURE2Dマクロを使用して、テクスチャをサンプリングする。

 

アルファクリッピング

レンダリングされるフラグメントの一部を破棄することで、アルファクリッピングをすることができる。カットオフのしきい値を定義することで、下回るアルファ値を持つフラグメントを破棄し、それ以外を保持できる。

 

    float4 base = baseMap * baseColor;
    clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
    return base;

UnlitPassFragmentのclip関数を呼び出すことでフラグメントを破棄することができる。

渡された値がゼロ以下であればフラグメントは中断され、破棄される。そこで、aまたはwプロパティからアクセスできる最終的なアルファ値からカットオフしきい値を引いた値を渡す。

 

マテリアルは透明ブレンディングかアルファクリッピングのどちらかを使うが、両方を同時に使うことはない。アルファクリッピングマテリアルは破棄されたフラグメント以外は完全に不透明でdepthバッファに書き込まれる。これはAlphaTestレンダーキューを使用するため、不透明オブジェクトのあとにレンダリングされる。これは、三角形がその後ろにあるものを完全にカバーすることを想定できなくなるため、フラグメントを破棄するとGPU最適化が不可能になることがあるため。なので、最適化するためにアルファクリッピングをトグルによって切り替えられるようにしなければならない。

 

            #pragma shader_feature _CLIPPING

トグルを有効にしたときに異なるバージョンのシェーダがコンパイルされるようにUnityに指示する。

 

    #if defined(_CLIPPING)
    #endif

#ifdef_CLIPPINGも使うことができるが、好みで機能は変わらない。

 

カットオフはUnityPerMaterialバッファの一部なのでインスタンスごとに設定できる。

 

あとがき

ポートフォリオの作成や、学校のイベントが重なって更新が遅くなりました。

グラフィックスエンジニア寄りのTAを募集してる会社ってなかなかないので大変です。。。

次の更新内容はツールになると思います。UEのマテリアルに関する便利ツール予定。

週一の更新を安定してできるようになりたいですねぇー。

参考

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

 

 

 

SRP作成(2)

前回SRPのブログを書いてから約二か月空いてしまいましたが、ようやく第二回です。

書き方などを探りながらなので安定していないかもしれません。

 

 

はじめに

https://shaurasan.hatenablog.com/entry/2024/11/28/153258

上記リンクが前回ですが、時間が空いてしまったので無いものとしてください。

なので、今回が実質第一回です。

 

下記のサイトの内容に沿って作成していきます。

非常にわかりやすくて、助かります。

catlikecoding.com

 

つくるよ

メモのような感じに書いていきます。

 

CameraRenderer.cs

    ScriptableRenderContext context;
    Camera camra;

ScriptableRenderContext カスタムレンダリングパイプラインが使用する状態と描画コマンドを定義

Camera カメラ情報

 

CustomRenderPipeline.cs

    protected override void Render (
        ScriptableRenderContext context, Camera cameras
    ) {}
   
    protected override void Render (
        ScriptableRenderContext context, List<Camera> cameras
    ) {}

異なる型に対応するメソッドの定義

 

    protected override void Render (
        ScriptableRenderContext context, List<Camera> cameras
    ) {
        for (int i = 0; i < cameras.Count; i++) {
            renderer.Render(context, cameras[i]);
        }
    }

すべてのカメラをループでレンダリング

 

CameraRenderer.cs

        ScriptableRenderContext.DrawSkybox(camera);

スカイボックスの描画

 

    void Submit () {
        context.Submit();
    }

コマンドをGPUに送信

無いと描画がされない。

呼び出すとScriptableRenderContextの状態がリセットされる。

 

    void Setup () {
        context.SetupCameraProperties(camera);
    }

カメラのプロパティ(ビュー行列、プロジェクション行列、カメラパラメータなど)をセットする。

 

    const string bufferName = "Render Camera";

    CommandBuffer buffer = new CommandBuffer {
        name = bufferName
    };

コマンドバッファの作成

コマンドバッファは描画コマンドを一時記録して、必要なタイミングでまとめて実行することができる。

 

        buffer.BeginSample(bufferName);
        buffer.EndSample(bufferName);

Profilerの計測開始、終了の指示。

負荷の確認用。

 

        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();

コマンドバッファの実行とクリア。

適切なタイミングで実行する必要がある。

 

        buffer.ClearRenderTarget(true, true, Color.clear);

現在のレンダーターゲットのクリア。

深度バッファ、色バッファ、クリアカラーの三つの引数。

前フレームの描画内容をリセットする。

 

        context.SetupCameraProperties(camera);
        buffer.ClearRenderTarget(true, true, Color.clear);

カメラプロパティを先に設定してからレンダーターゲットをクリアすると、より高速になる。

全体に透明なシェーダーでクリアする方法からGPUハードウェアクリアに代わる。

 

深度バッファとステンシルバッファは同じメモリを共有している。

 

        ScriptableCullingParameters p

SRPのカリング操作を設定するパラメータ

 

    bool Cull () {
        if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
            return true;
        }
        return false;
    }

カリングパラメータの取得

取得したらtrue できなかったらfalse。

outはメソッドに渡されるときに初期化されていなくていい。

メソッド内で値を設定する必要がある。

 

        if (!Cull()) {
            return;
        }

falseの場合に処理をスキップする。

 

    CullingResults cullingResults;

カリング操作の結果を含む構造体。

 

refは呼び出し元が初期化した値を渡す必要がある。

入力と出力の両方に使える。

大きな構造体をメソッドに渡すときに使うとコピーを避けることができる。

構造体はオブジェクトのようにメモリを割り当てないので効率的。

 

        var drawingSettings = new DrawingSettings();
        var filteringSettings = new FilteringSettings();

context.DrawRenderersの設定。

オブジェクトの描画設定と、描画オブジェクトのフィルタリングの設定。

var 型推論 コンパイラが変数の型を自動的に決定する。

 

        context.DrawRenderers(
            cullingResults, ref drawingSettings, ref filteringSettings
        );

レンダリングコンテキストに基づいて、オブジェクトを描画するメソッド。

 

    static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");

シェーダのタグを識別するためのクラス。

static 変数がクラス全体で共有される。

 

        var sortingSettings = new SortingSettings(camera);

オブジェクトをどの順番で描画するかを設定する。

 

var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);

描画設定。第一引数は使用シェーダ。第二引数は描画順。

 

        var filteringSettings = new FilteringSettings(RenderQueueRange.all);

レンダリングキューを対象にフィルタリング。

 

        var sortingSettings = new SortingSettings(camera) {
            criteria = SortingCriteria.CommonOpaque
        };

描画順序の指定。CommonOpaqueは不透明オブジェクト。

 

        var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

不透明だけを先に描画する。

 

        context.DrawSkybox(camera);

        sortingSettings.criteria = SortingCriteria.CommonTransparent;
        drawingSettings.sortingSettings = sortingSettings;
        filteringSettings.renderQueueRange = RenderQueueRange.transparent;

        context.DrawRenderers(
            cullingResults, ref drawingSettings, ref filteringSettings
        );

スカイボックスの描画後に描画順を変更し、drawSettingに適用。

fliteringSettingをtransparentにすることで、透明オブジェクトのみを描画対象にする。

 

 

一回目のDrawで不透明描画。

スカイボックスを挟んでから二回目のDraw

ここでは透明のみ描画。

 

    static ShaderTagId legacyShaderTagIds = {
        new ShaderTagId("Always"),
        new ShaderTagId("ForwardBase"),
        new ShaderTagId("PrepassBase"),
        new ShaderTagId("Vertex"),
        new ShaderTagId("VertexLMRGBM"),
        new ShaderTagId("VertexLM")
    };

レガシーシェーダを特定するShaderPassId

 

    void DrawUnsupportedShaders () {
        var drawingSettings = new DrawingSettings(
            legacyShaderTagIds[0], new SortingSettings(camera)
        );
        for (int i = 1; i < legacyShaderTagIds.Length; i++) {
            drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
        }
        var filteringSettings = FilteringSettings.defaultValue;
        context.DrawRenderers(
            cullingResults, ref drawingSettings, ref filteringSettings
        );
    }

legacyShaderTagIdの配列をループでdrawingSettingに登録

SetShaderPassName 第一引数:シェーダーパス 第二引数:シェーダータグ

FlieteringSettings.defaultValue:すべてのオブジェクトを描画対象にする。

これでレガシーシェーダーが描画されるが、シェーダープロパティを設定していないので正しく描画されない。

 

                new Material(Shader.Find("Hidden/InternalErrorShader"));

エラーマテリアルの作成

 

public partial class CameraRenderer {}

クラスや構造体を複数の部分に分割して、別々のファイルに格納する方法。

コードを整理するためにつかう

 

#if UNITY_EDITOR

#endif

エディター内のみで動作するコード

ビルドすると無視されるようになる。

#if UNITY_STANDALONE //PC用
#if UNITY_ANDROID //Android
#if UNITY_IOS //IOS

その他

 

Handles.ShouldRenderGizmos()

ギズモを描画すべきか判定

 

            context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
            context.DrawGizmos(camera, GizmoSubset.PostImageEffects);

ギズモの描画タイミングを指定

ポストプロセス前と後。

 

camera.cameraType == CameraType.SceneView

Sceneビュー専用のカメラを指定

 

EmitWorldGeometryForSceneView(camera)

指定されたカメラがSceneビューでワールドジオメトリを描画できるようにする。(3D モデルやメッシュ)

UIを明示的に追加

この処理を含むメソッドはカリングの実行前に行う。

 

デフォルトメインカメラのDepthは-1

カメラはDepthが深い順にレンダリングされる。

 

        buffer.name = camera.name;

カメラの名前とバッファの名前を合わせる。

 

    string SampleName { get; set; }

値の取得と設定をできるようにする。

 

buffer.name = SampleName = camera.name;

camera.nameの値をSampleNameに代入。

SampleNameの値をbuffer.nameに代入。

 

const string SampleName = bufferName;

const 定数として設定

SampleNameを定数bufferNameとして扱う。

 

        buffer.BeginSample(SampleName);
        buffer.EndSample(SampleName);

プロファイラーで表示する名前を指定。

これでBeginSampleとEndSampleのカウントが一致しなければならない。という警告が出なくなる。

 

#if UNITY_EDITOR
using UnityEngine.Profiling;
        Profiler.BeginSample("Editor Only");
        buffer.name = SampleName = camera.name;
        Profiler.EndSample();
#else

エディタのみでメモリを確保していることを明確にする。

UnityEngine.ProfilingからProfiler.BeginSampleとEndSampleを呼び出す。

名前を渡すのはBeginのみ。

"Editor Only":測定範囲の名前。

 

カメラは特定のレイヤー上のものだけを見るように設定できる。

リングマスクを調整することで行う。(インスペクター)

 

カメラのクリアフラグを調整することで、両方のカメラの結果を組み合わせることができる。

        CameraClearFlags flags = camera.clearFlags;

カメラが新しいフレームを描画する際に、前のフレームをどのようにクリアするか指定するプロパティ。

Skybox,Color.Depth,Nothingの1~4の値がある。

 

        buffer.ClearRenderTarget(
            flags <= CameraClearFlags.Depth, true, Color.clear
        );

3,4 Depth,Nothingの場合カラーバッファをクリアしない。

 

Unity2022はレンダーターゲットに数値でない値や無限大の値が含まれていると、ブレンドアーティファクトが発生する可能性がある。また、フレームデバッガがランダムなデータを表示する可能性がある。

        buffer.ClearRenderTarget(
            flags <= CameraClearFlags.Depth,
            flags == CameraClearFlags.Color,
            flags == CameraClearFlags.Color ?
                camera.backgroundColor.linear : Color.clear
        );

クリアフラグがcolorならクリア。

カラーバッファのクリアカラーをcolorなら背景色。それ以外なら透明色。

.linearはリニアカラースペースでレンダリングしているため、色をリニアスペースに変換する必要がある。

 

最初に描画されるメインカメラのクリアフラグはSkyboxまたはColorに設定する必要がある。フレームデバッガを有効にしても常にクリアされるわけではない。

セカンダリカメラのクリアフラグによって二つのカメラの描画結果がどうやって組み合わさるか決まる。

スカイボックス、カラーなら完全に置き換える。

depthなら背景が変化しない。(スカイボックスが描画されない。)

nothingならdepthが保持されるので、オブジェクトがそのまま表示される。透明オブジェクトは前景に重なって表示される。

 

複数のカメラで1フレームをレンダリングすると、カリングなどの処理が複数回発生するので、一つの視点ごとに一つのカメラを使用するほうが、効率的。

 

今回はここまでです。

 

あとがき

参考サイト様の一ページ分でした。

めっちゃ更新遅いので、ペース上げられるように頑張ります。

しかし、参照するドキュメントが多い、、、。

次はできれば来週までには更新したいですね~。

 

UnityDocunentations

Unity - Scripting API: ScriptableRenderContext

Unity - Scripting API: Camera

Unity - Scripting API: ScriptableCullingParameters

Unity - Scripting API: CullingResults

Unity - Scripting API: DrawingSettings

Unity - Scripting API: FilteringSettings

Unity - Scripting API: ShaderTagId

Unity - Scripting API: SortingSettings

Unity - Scripting API: RenderQueueRange

Unity - Scripting API: SortingCriteria

Unity - Scripting API: Material

Unity - Scripting API: Shader

Unity - Scripting API: Handles

Unity - Scripting API: GizmoSubset

Unity - Scripting API: SceneView

Unity - Scripting API: CommandBuffer

Unity - Scripting API: CameraType

Unity - Scripting API: Profiler

Unity - Scripting API: CameraClearFlags

Unreal Engineのリモート実行とPythonを試してみた

UEFEST 2024 TOKYOの下記の講演内容を実践した話です。

www.youtube.com

 

レンダリングパイプライン作成の息抜き程度に試してみました。

mayaなどのdccツールからunreal project内のpythonスクリプトをリモートで実行できるようです。

 

 

事前準備

まずはプラグインウィンドウから下のプラグインが有効化されているか確認します。

 

次にエディタの環境設定からコンテンツブラウザ統合を有効化します。

これでエディタ内でpythonスクリプトが表示されるようになり、実行も可能になりました。

 

最後に今回の本命であるリモート実行を有効にします。

プロジェクト設定からリモート実行を有効にするにチェックをつけます。

これで事前準備は完了です。

他にも、デベロッパーモードなども有効にすると関数の型情報なども生成されるようです。

 

リモート実行の検証

事前準備が済んだので早速dccツールからUE内のスクリプトを実行してみたいと思います。

シンプルなログを表示するスクリプトをUE側に用意します。

sample_call.py

import sys
import unreal

unreal.log(f"Hello,{sys.argv}")

 

次にこのスクリプトをリモート実行するスクリプトを用意します。

call_dcc.py

 
import time
import sys

#from モジュール import メソッド
from remote_execution import RemoteExecution

remote_execute = RemoteExecution()
remote_execute.start()

#接続待機
time.sleep(2)

if remote_execute.remote_nodes:
    remote_execute.open_command_connection(remote_execute.remote_nodes[0])
    #接続ノードの確認と最初のノードに接続

    remote_execute.run_command("sample_call.py World")

remote_execute.stop()

このスクリプトをmayaのスクリプトエディタから実行します。

 

無事にログが出力されました。

Hello World にならなかった…)

エラーでmaya側からできなかったりしたときは、UEのインストール先からバージョン/Engine/Plugin/Experiminal/PythonScriptPlugin/Content/Pythonをsys.path.appendで追加するとできたりしました。

 

リモート実行が確認できたので、オブジェクトのエクスポートもやってみます。

試したのはスタティックメッシュのみです。

 

UE側のコード

Py_Import_STMesh.py

import unreal

task = unreal.AssetImportTask()
source_file_path ='D:/maya/Script_Practice/data'
ue_file_dir = 'D:/UEProjects/PythonSample/Content/objects'

task.set_editor_property("automated", True)
task.set_editor_property("filename", source_file_path)
task.set_editor_property("destination_path", ue_file_dir)
task.set_editor_property("replace_existing", True)
task.set_editor_property("save", True)

import_ui = unreal.FbxImportUI()
import_ui.reset_to_default()

import_ui.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH
import_ui.original_import_type = unreal.FBXImportType.FBXIT_STATIC_MESH
import_ui.import_animations = False
import_ui.import_as_skeletal = False
import_ui.import_materials = False
import_ui.import_textures = False
import_ui.import_mesh = True

task.options = import_ui

unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

 

dcc側のコード

 

autoExport.py

import time
import sys

from remote_execution import RemoteExecution

class UERemoteExecution:
    @classmethod
    def execute_file(clls, file_path: str, *args):
        remote_execute = RemoteExecution()
        remote_execute.start()

        time.sleep(2)
        if remote_execute.remote_nodes:
            remote_execute.open_command_connection(remote_execute.remote_nodes[0])
            remote_execute.run_command(f"{file_path} {' '.join(args)}")
        remote_execute.stop()

UERemoteExecution.execute_file(
    "Py_Import_STMesh.py",
    "D:/maya/Script_Practice/data",
    "D:/UEProjects/PythonSample/Content/objects"
)

 

これで一応自動エクスポートができました。

 

改造

現状の気になるところ

  • エクスポートは手動
  • 参照フォルダが固定
  • 読み込み設定が固定
  • フォルダ内のすべてを毎回参照する

これらを解消してすこしでも使いやすいものにします。

理想形は

  • mayaで選択したオブジェクトをエクスポート+UEにインポート
  • インポート先の指定
  • インポート設定を変更できるように

以上三点とします。

ではこれらを実現するコードを書いていきます。

 

まずはUIを想定します。

スタティックメッシュのみを想定して、インポート設定を考えます。

必要そうなのは、

  • 上書き保存するか
  • マテリアルも書き出すか
  • テクスチャも書き出すか

これらはオンオフのみでいいのでチェックボックスを三つ。

次にインポート先フォルダを指定するためにテキストボックスとエクスプローラーを開くボタンが必要そうです。

以上の要素を含むウィンドウをラフに設計します。


かなり字が汚いですがラフということで

だいたい形が想定できたのでコードを書いていきます。

 

from maya import cmds
from maya.common.ui import LayoutManager


if cmds.window("Sample", ex = True):
    cmds.deleteUI("Sample")
cmds.window("Sample", title = 'WindowSample', widthHeight = (400,250), sizeable = False)
       
tabs = cmds.tabLayout(innerMarginWidth=5, innerMarginHeight=5)
child1 = cmds.columnLayout(adjustableColumn = True, rowSpacing = 10)

cmds.text('インポート設定', font = 'boldLabelFont')

cmds.checkBox(label = '上書き保存', value = True)
cmds.checkBox(label = 'マテリアル書き出し', value = True)
cmds.checkBox(label = 'テクスチャ書き出し', value = True)

cmds.separator(height=10, style='in')

with LayoutManager(cmds.rowLayout(numberOfColumns = 3, adj = 2)) as field:
   
    #テキストボックスとボタン
    cmds.text('インポート先')
    cmds.textField()
    cmds.iconTextButton(image="browseFolder.png", st = 'iconOnly')

cmds.button(label = '実行')

   
cmds.tabLayout(tabs, e = True, tabLabel = [(child1, 'ImportOption')])

cmds.showWindow("Sample")

 

これをmayaのスクリプトエディタで実行します。

 

いろいろ気になりますが使う分には問題なさそうなのでokとします。

次は処理を書いていきます。

 

from maya import cmds
from maya.common.ui import LayoutManager
import os
import time
import sys


def create_ui():
    if cmds.window('ImportTools', ex = True):
        cmds.deleteUI('ImportTools')
    cmds.window('ImportTools', title = 'MayaToUnreal', widthHeight = (400,250), sizeable = False)
           
    tabs = cmds.tabLayout(innerMarginWidth = 5, innerMarginHeight = 5)
    child1 = cmds.columnLayout(adjustableColumn = True, rowSpacing = 10)

    cmds.text('インポート設定', font = 'boldLabelFont')

    cmds.checkBox('replace', label = '上書き保存', value = True)
    cmds.checkBox('material', label = 'マテリアル書き出し', value = True)
    cmds.checkBox('texture', label = 'テクスチャ書き出し', value = True)

    cmds.separator(height = 10, style='in')

    with LayoutManager(cmds.rowLayout(numberOfColumns = 3, adj = 2)) as field:
       
        #テキストボックスとボタン
        cmds.text('インポート先')
        cmds.textField('file_path')
        cmds.iconTextButton(image = 'browseFolder.png', st = 'iconOnly', command = lambda: browse_file_path('file_path'))

    cmds.button(label = '実行', command =lambda _: execute_action())
       
    cmds.tabLayout(tabs, e = True, tabLabel = [(child1, 'ImportOption')])

    cmds.showWindow('ImportTools')

def browse_file_path(text_field):
    file_path = cmds.fileDialog2(fileMode = 3, caption = 'インポート先を選択')
    if file_path:
        cmds.textField(text_field, edit = True, text = file_path[0])

create_ui()

#-----処理ライン-----



from remote_execution import RemoteExecution

class UERemoteExecution:
    @classmethod
    def execute_file(cls, file_path: str, *args):
        remote_execute = RemoteExecution()
        remote_execute.start()

        time.sleep(2)
        if remote_execute.remote_nodes:
            remote_execute.open_command_connection(remote_execute.remote_nodes[0])
            remote_execute.run_command(f"{file_path} {' '.join(args)}")
        remote_execute.stop()




# 書き出しプロセス
def export_selected_objects():
    # オブジェクト取得
    selected_objects = cmds.ls(selection = True)
    if not selected_objects:
        cmds.warning('オブジェクトが選択されていません')
        return

    # projectのルートディレクトリ取得
    project_directory = cmds.workspace(query = True, rootDirectory = True)
    export_directory = os.path.join(project_directory, 'data')

    # dataフォルダが存在しなければ作成する
    if not os.path.exists(export_directory):
        os.makedirs(export_directory)
        print(f'フォルダを作成しました: {export_directory}')

    # 選択オブジェクトをFBXに書き出し
    exported_files =
    for obj in selected_objects:
        export_path = os.path.join(export_directory, f'{obj}.fbx')
        try:
            cmds.select(obj, replace=True)
            cmds.file(
                export_path,
                force = True,
                options = 'mo = ; mt = 1; tf = 1; upAxis = z ; zAxis = y ; unitScale = 1;',
                type = 'FBX export',
                preserveReferences = True,
                exportSelected = True
            )
            print(f'書き出し先: {export_path}')
            exported_files.append(export_path)
        except Exception as e:
            print(f'書き出しに失敗: {obj}, エラー: {e}')

    if exported_files:
        print('書き出し完了。UEに送信中です。')

    return exported_files


# 引数の送信
def execute_action():
    toggle1_state = cmds.checkBox('replace', query = True, value = True)  
    toggle2_state = cmds.checkBox('material', query = True, value = True)  
    toggle3_state = cmds.checkBox('texture', query = True, value = True)  
    file_path = cmds.textField('file_path', query = True, text = True)  
    exported_files = export_selected_objects()

    if exported_files:
        UERemoteExecution.execute_file(
            'Py_ImportSample.py',
            *exported_files,  # リストからスペース区切りの文字列に変換
            file_path,
            str(int(toggle1_state)),
            str(int(toggle2_state)),
            str(int(toggle3_state)),
        )
    else:
        cmds.warning('ディレクトリが選択されていません')

 

一気にUIとまとめましたが、これでUE側に用意した'Py_ImportSample.py'をリモート実行されて、選択したオブジェクトをfbxとしてmayaプロジェクト内のdataフォルダに書き出し、それをUE内の指定したフォルダにインポートするようになりました。

 

Py_ImportSample.pyの中身は以下の通りです。

import unreal
import sys

unreal.log(f"引数リスト: {sys.argv}")

exported_files = sys.argv[1:-4]  # FBXファイルのパスすべてをリストにする
file_path = sys.argv[-4]  
replace = bool(int(sys.argv[-3]))
import_materials = bool(int(sys.argv[-2]))
import_textures = bool(int(sys.argv[-1]))

unreal.log(f"FBXファイルのリスト: {exported_files}")
unreal.log(f"ファイルパス: {file_path}")
unreal.log(f"上書き: {replace}, マテリアル書き出し: {import_materials}, テクスチャ書き出し: {import_textures}")

for source_file_path in exported_files:
    unreal.log(f"インポート中: {source_file_path}")

    task = unreal.AssetImportTask()

    task.set_editor_property("automated", True)
    task.set_editor_property("filename", source_file_path)
    task.set_editor_property("destination_path", file_path)
    task.set_editor_property("replace_existing", replace)
    task.set_editor_property("save", True)

    import_ui = unreal.FbxImportUI()
    import_ui.reset_to_default()

    import_ui.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH
    import_ui.original_import_type = unreal.FBXImportType.FBXIT_STATIC_MESH
    import_ui.import_animations = False
    import_ui.import_as_skeletal = False
    import_ui.import_materials = import_materials
    import_ui.import_textures = import_textures
    import_ui.import_mesh = True

    task.options = import_ui

    unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

unreal.log("アセットインポートが完了しました。")

 

今回のコードはgitで公開しています。

https://github.com/shaurasan/MayaToUE

 

参考

mayaPythonヘルプ

http://me.autodesk.jp/wam/maya/docs/Maya2010/CommandsPython/cat_Windows.html

 

Layoutmanagerのこと

qiita.com

 

window自体の作成のこと

cgjishu.net

あとがき

Pythonの勉強とmayaスクリプトのいい勉強になりました。

ただ、これの作成に夢中になりちょっとc#やHLSLが抜けてそうでやばいです。

定期的にどの言語も使わないと忘れちゃいますね。

SRP作成(1)

SRPとは

SRP(Scriptable Render Pipeline)

Unityのc#を使用してレンダリングの制御ができる機能です。

スクリプタブルレンダーパイプラインの基礎 - Unity マニュアル

URPとHDRPもSRPの一種のようです。

カスタムSRPを利用するには、描画用のインスタンスレンダリングパイプラインアセットを作成し、Project Settingで適用する必要があるようです。

docs.unity3d.com

こちらを参考にインスタンスレンダリングパイプラインアセットを作成します。

SRP作成

 

using UnityEngine;
using UnityEngine.Rendering;

public class ExampleRenderPipelineInstance : RenderPipeline
{
    public ExampleRenderPipelineInstance() {
    }

    protected override void Render (ScriptableRenderContext context, Camera cameras) {
     
    }
}

こちらがレンダリングパイプラインアセットの中身になるコードです。

現在は何も処理を入れていません。このままでは使えないのでいったんアセット化できるようにします。

 

using UnityEngine;
using UnityEngine.Rendering;

// レンダリングパイプラインアセットの作成
[CreateAssetMenu(menuName = "Rendering/ExampleRenderPipelineAsset")]
public class ExampleRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline() {
 
        return new ExampleRenderPipelineInstance();
    }
}

これでProjectWindowから右クリック/作成/レンダリングにExampleRenderPipelineAssetが表示されるようになり、アセットの作成が可能になりました。

 

一度このまま適用してみます。

ProjectSettingから品質の中にあるレンダーパイプラインアセットを作成したものに変更します。

シーンビューに何も表示されなくなったら成功です。

 

オブジェクトも一切描画されませんし、スカイボックスなども表示されなくなりました。

中身を書いていないので正しい状態です。

ここから処理を書いて描画できるようにしていきます。

 

中身の作成

ここからは以下を参考に進みます。

learning.unity3d.jp

 

using UnityEngine;
using UnityEngine.Rendering;

public class ExampleRenderPipelineInstance : RenderPipeline
{
    public ExampleRenderPipelineInstance() {
    }


    //描画処理
    protected override void Render (ScriptableRenderContext context, Camera cameras) {

        //カメラごとに処理
        foreach ( var camera in cameras) //foreach (型 変数名 in コレクション)
        {
            //カメラプロパティ
            context.SetupCameraProperties( camera );

            //カリング設定
            CullingResults          cullingResults = new CullingResults();
            ScriptableCullingParameters cullingParameters;
            if ( !camera.TryGetCullingParameters( false, out cullingParameters ))continue;
            cullingResults = context.Cull( ref cullingParameters );

            //フィルタリング、ソート
            SortingSettings sortingSettings = new SortingSettings( camera ) { criteria = SortingCriteria.CommonOpaque };
            var settings  = new DrawingSettings( new ShaderTagId( "IdDeferred" ), sortingSettings);
            var filterSettings = new FilteringSettings(
                                    new RenderQueueRange( 0,(int)RenderQueue.GeometryLast ),
                                    camera.cullingMask
                                );
           
            //描画処理
            context.DrawRenderers( cullingResults, ref settings, ref filterSettings );

        }
        context.Submit();
    }    
}

カメラは一つずつ呼び出して、処理を行いそれをforeachでカメラの数だけ実行していますね。

 

カリングやフィルタリングのあたりは現状よくわからないです。

ScriptableRenderContextは以下の公式ドキュメントを参照して記述できそうですね。

Unity - Scripting API: ScriptableRenderContext

 

いったんこれでUnity側を確認してみます。

グリッドが表示されるようになりました。

オブジェクトは専用のシェーダーを記述する必要があるので、次回にやります。

最後にスカイボックスの描画までやって今回は終わりにします。

 

catlikecoding.com

ここからはこちらの記事を参考に進みます。

スカイボックスはScriptableRenderContextのDrawSkyboxで描画できるようなので、書き加えていきます。

using UnityEngine;
using UnityEngine.Rendering;

public class ExampleRenderPipelineInstance : RenderPipeline
{
    public ExampleRenderPipelineInstance() {
    }


    //描画処理
    protected override void Render (ScriptableRenderContext context, Camera[] cameras) {

        //カメラごとに処理
        foreach ( var camera in cameras) //foreach (型 変数名 in コレクション)
        {
            //カメラプロパティ
            context.SetupCameraProperties( camera );

            //カリング設定
            CullingResults          cullingResults = new CullingResults();
            ScriptableCullingParameters cullingParameters;
            if ( !camera.TryGetCullingParameters( false, out cullingParameters ))continue;
            cullingResults = context.Cull( ref cullingParameters );

            //フィルタリング、ソート
            SortingSettings sortingSettings = new SortingSettings( camera ) { criteria = SortingCriteria.CommonOpaque };
            var settings  = new DrawingSettings( new ShaderTagId( "IdDeferred" ), sortingSettings);
            var filterSettings = new FilteringSettings(
                                    new RenderQueueRange( 0,(int)RenderQueue.GeometryLast ),
                                    camera.cullingMask
                                );
           
            //描画処理
            context.DrawRenderers( cullingResults, ref settings, ref filterSettings );

            context.DrawSkybox(camera);

            context.SetupCameraProperties(camera);
        }
        context.Submit();
    }    
}

DrawSkyboxとSetUpCameraPropertiesを書き加えました。

SetUpCameraPropertiesはシーンを正しくレンダリングするため、カメラのプロパティをコンテキストに適用するために必要らしいです。

これでUnityを確認するとSkyBoxが描画されているはず。

 

いけました。

今回はここまでにします。

次はもう少し詳しいことやシェーダーを作成してオブジェクトを描画などを目標に進みます。

 

c#レンダリングあたりのことを同時に勉強しているのでなかなか大変ですね。

 


あとがき

CEDEC+KYUSYUに行ったりしていたので更新が遅くなりました。。。

UEFESTとかにも目を通していて、いろいろやりたいことが蓄積中です。

もう少し形になったものができたらgithubなんかも活用していきたいです。