調べものリンク集

読みたいもの,あとで読み返したいものの雑多なメモ

カテゴリは結構適当. 適宜更新と整理をする予定.

UE4 ComputeShader Write, Material Read (GPGPU)

エンジンバージョン:4.24 (4.25がリリースされたら対応予定)

ComputeShaderで計算した結果を、RWテクスチャを利用してダイレクトにマテリアルで利用する。↓はサンプルコードの実行の様子。

はじめに

前回の記事ではComputeShaderでメッシュ情報を直接生成する方法を紹介した。前回記事の手法では頂点カラー等を利用することでComputeShaderからマテリアルへ追加情報を渡すことができる。しかし頂点データの一部を間借りすることから渡せる情報に制限がある。
当記事ではComputeShaderが書き込みをしたRWテクスチャをそのままマテリアルで利用することで頂点データ利用に比べてより自由に情報を受け渡す方法を検証してうまくいったので公開。そもそもこんな面倒なことしなくても可能な方法があるかもしれないので注意。
(GlobalShader利用, エンジン改造無し)

サンプルコード

uprojectファイルを右クリック→Generate Visual Studio project files でプロジェクトコード生成.
github.com

説明

RWテクスチャ定義

マテリアルで利用可能且つComputeShaderで書き込みが可能なUAVリソースとしてのテクスチャが必要であるため、UTextureRenderTarget2Dを継承したRWテクスチャを定義する。
bCanCreateUAVにtrueを指定することでリソース自体がUAV利用可能な設定で作成される。ただしこの設定だけではUAVが作成されないため(メンバ変数uav_)、RWテクスチャとして利用するComputeShader処理側で必要なときに生成する(後述)。
また、UAVの破棄はRenderThreadで処理する必要があるのでデストラクタでRenderThreadのコマンドとして発行している。

// UavTextureSampleComponent.h
UCLASS(BlueprintType)
class UNglRWTextureRenderTarget2D : public UTextureRenderTarget2D
{
	GENERATED_UCLASS_BODY()

public:
	virtual ~UNglRWTextureRenderTarget2D();

	FUnorderedAccessViewRHIRef uav_;
};
// UavTextureSampleComponent.cpp
UNglRWTextureRenderTarget2D::UNglRWTextureRenderTarget2D(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bNeedsTwoCopies = false;
	// リソース生成時にUAV利用可能な設定で作成することを指示
	bCanCreateUAV = true;
}

UNglRWTextureRenderTarget2D::~UNglRWTextureRenderTarget2D()
{
	if (uav_.IsValid())
	{
		// UAVの破棄
		// 破棄は描画スレッドで実行する必要があるのでコマンドをキューに積む.
		auto rhi_resource = uav_;
		ENQUEUE_RENDER_COMMAND(UpdateResourceImmediate)(
			[rhi_resource](FRHICommandListImmediate& RHICmdList)
		{
			rhi_resource->Release();
		}
		);
	}
}

RWテクスチャのUAV生成

本来ならUTextureRenderTarget2Dの初期化フローの中で生成するのが理想だが、今回はUAV利用する側が生成することにした。

// UavTextureSampleComponent.cpp 252行目
if (!rw_tex->uav_.IsValid())
{
	rw_tex->uav_ = RHICreateUnorderedAccessView(rw_tex->Resource->TextureRHI);
}

ComputeShaderによるRWテクスチャへの書き込み

生成したUAVはComputeShaderパラメータに設定することで利用可能。

// UavTextureSampleComponent.cpp 271行目
SetUAVParameter(RHICmdList, rhi_cs, cs->rw_tex, rw_tex->uav_);

RWテクスチャのマテリアルでの利用

UNglRWTextureRenderTarget2Dは通常のUTextureオブジェクトと同様にマテリアルパラメータに設定可能。これでComputeShaderで書き込みをしたRWテクスチャをそのままマテリアルで利用できる。
f:id:nagakagachi:20200319232634p:plain

サンプルコードの概要

サンプルではComputeShaderで簡易な波の計算をして128x128のRWテクスチャに書き込み、マテリアルでその情報を利用して多数配置した三角錐メッシュを揺らしている。サンプルではComputeShaderは2つあり、一つは波の計算をして構造化バッファを更新するもの、もう一つは構造化バッファをもとにRWテクスチャへマテリアルで利用する情報を書き込むもの。マテリアルではRWテクスチャの値を読み取って頂点を動かしている。

f:id:nagakagachi:20200320001630p:plain

テクスチャから取得した波の高さの可視化

ComputeShaderとMaterial連携サンプル動画1

テクスチャから取得した波の高さの勾配ベクトルの可視化

ComputeShaderとMaterial連携サンプル動画2

テクスチャから取得した波の高さと勾配ベクトルを利用して頂点オフセットで揺れ表現をした様子

ComputeShaderとMaterial連携サンプル動画0


手記はここで途切れている

UE4 ComputeShaderでMeshの頂点バッファをリアルタイム書き換えする (GPGPU)

エンジンバージョン:4.22, 4.23
諸事情によりサンプルプロジェクト無し

UE4のProceduralMeshのようなことをComputeShaderでできたら、大量の頂点をリアルタイムに操作できて面白いのではないかという思い付き。
今回はたくさんの草をリアルタイムに生成する目的で実装してある程度うまくいったのでメモ。
この方法を応用すれば頂点カラーとかにInstanceID的なものやそのほか特殊な情報を埋め込んでおいてマテリアルBPで利用するといったこともできる。

この記事はC++とGlobalShaderを利用したもので、且つエンジン改造はしない。
また、自前で用意した頂点バッファをComputeShaderで書き換えてUE4のメッシュ描画に流す方法がメインであるため、実装したComputeShader自体については深く説明しない。

www.youtube.com
↑プレイヤーからの影響を草一本一本が受ける例。
草の位置や基準方向などを構造化バッファで保持しているのでそれらをComputeShaderで更新すればこのようなことも可能。
www.youtube.com
↑さらにマテリアルBP側で頂点オフセットによる風揺れをつけた例。
描画自体はUE4のメッシュ描画に乗っかっているのでStaticMesh等と同じようにマテリアルが使えるのが強みの一つ。

基本の考え

自分で作成した頂点バッファやインデックスバッファをUE4のメッシュ描画に流し込む方法は意外と簡単で、FPrimitiveSceneProxy派生クラスでGetDynamicMeshElements()関数をoverrideして適切にバッファ情報を渡せば可能になる。
そこでこれらのバッファをUnorderedAccess可能な設定で作成しておき、毎フレームComputeShaderで書き換えるようにすることで、形状をリアルタイムに変更しつつマテリアル等を含めた描画部分はUE4の仕組みをそのまま利用できると考えた。

f:id:nagakagachi:20190922201851p:plain
フローのイメージ

必要な要素の概要

  • 自作ComputeShader用GlobalShader
    • 自作MeshProxyが生成した頂点バッファUAVやインデックスバッファUAVへ読み書きして草メッシュを作る
    • 草メッシュの位置や向きは別途StructuredBufferで与える
  • 自作MeshComponentクラス
    • 対になる自作MeshProxyを生成してシステムに登録し、ゲーム側からの情報を通知する役割
  • 自作MeshProxyクラス
    • 自作MeshComponentの描画スレッド側における分身のような存在
    • UnorderedAccess可能な各種頂点バッファとインデックスバッファおよびそれらのUAVを作成する
    • UE4メッシュ描画システムのメッシュ情報収集に適切に頂点バッファなどを設定する
    • ComputeShaderのDispatchコマンド発行もする

自作ComputeShader用GlobalShader

自作MeshProxyの各バッファのUAVを設定して書き換えるComputeShaderのGlobalShader。
GlobalShader自体については公式のドキュメントを参考。
https://docs.unrealengine.com/ja/Programming/Rendering/ShaderInPlugin/QuickStart/index.html
ComputeShaderをGlobalShaderとして作成してUAV他パラメータを設定する流れについては以下のブログが参考になる。
[UE4][ComputeShader][HLSL][C++]Unreal Engine 4で(RW)StructuredBufferを用いたComputeShaderを利用する(その2:C++側シェーダクラス) – サイアメント技術メモ

今回はComputeShaderのスレッド一つが草一本を担当することとし、スレッドIDと草一つに割り当てられた頂点数からスレッドが書き込む頂点バッファ内の位置を決めている。あまり特殊なことはしていないが、接線法線頂点バッファ等がPackedフォーマットになっている場合はUAVから読み取った値をUnpackしたり書き戻す際に再度Packしたりといった処理が必要な点に注意。

f:id:nagakagachi:20190922220711p:plain
ComputeShaderの処理イメージ

自作MeshComponentクラス

UPrimitiveComponentを継承
コンポーネントとしてゲームスレッド側の情報を管理する。UProceduralMeshComponentを参考にした。
コンポーネントの描画スレッド側の情報を管理するカスタムMeshProxyクラスを生成して保持し、適切に情報を渡したりするのが主な役目。
適切なタイミングで自作MeshProxyへComputeShaderのDispatchコマンド生成をリクエストをする。

virtual void UComputeMeshComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override
{
    // SendRenderDynamicData_Concurrent の呼び出しをリクエスト
    MarkRenderDynamicDataDirty();
}
virtual void UComputeMeshComponent::SendRenderDynamicData_Concurrent() override
{
    // カスタムMeshProxyにComputeShaderのDispatchコマンド生成を要求
    カスタムMeshProxy->EnqueueDispatchComputeShader();
}

自作MeshProxyクラス

FPrimitiveSceneProxyを継承
今回の主役。FProceduralMeshSceneProxyを参考にした。

バッファ生成時の設定

ComputeShaderで読み書きをするバッファはUnorderedAccessView(UAV)として使用可能な設定で生成する必要がある。
そのため各種頂点バッファおよびインデックスバッファの生成時のusageフラグに BUF_UnorderedAccess を追加する。

FRHIResourceCreateInfo CreateInfo;
uint32 usage = BUF_Static | BUF_ShaderResource;
    if (need_uav_)
        usage |= BUF_UnorderedAccess;
VertexBufferRHI = RHICreateVertexBuffer( 頂点要素サイズ * 頂点数, usage, CreateInfo);

そのうえで RHICreateUnorderedAccessView() 関数でUAVを生成する。このUAVに対してComputeShaderで読み書きすることで頂点位置などを書き換えることができる。

if (need_uav_)
    uav_ = RHICreateUnorderedAccessView(VertexBufferRHI, PF_R32_FLOAT);

今回の草は1本につき6頂点、4トライアングルで構成されるものとして生成時に最大草数分のサイズを確保している。
各種頂点バッファの生成とVertexFactoryへのBindは以下のソースコードを参考に。
Engine/Source/Runtime/Engine/Public/Rendering/PositionVertexBuffer.h
Engine/Source/Runtime/Engine/Public/Rendering/ColorVertexBuffer.h
Engine/Source/Runtime/Engine/Public/Rendering/StaticMeshVertexBuffer.h
インデックスバッファについては以下を参考に。
Engine/Source/Runtime/Engine/Public/DynamicMeshBuilder.h

今回は頂点バッファのフォーマットとして

Position : Float3
Color : FColor
TangentNormal :PackedNormal ( R8G8B8A8_SNORMにパックしたフォーマット )
Texcoord : Float2

を採用した。TangentNormalとTexcoordをHalfFloatとすることもできたが、うまくComputeShaderで読み書きができなかったので要調査。

描画メッシュとして登録

上記のように作成した頂点バッファやインデックスバッファを実際にメッシュ描画に登録するために GetDynamicMeshElements() 関数をoverrideする。
具体的な設定はProceduralMeshComponent等のエンジン側のソースコードが参考になる。

// 描画メッシュ収集時に呼ばれる関数
virtual void FComputeMeshProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
{
    // TODO
    // Collector に描画したい頂点バッファ群を設定したVertexFactoryやインデックスバッファを登録する
    // ProceduralMeshComponent.cpp 等を参考
}

最後にComputeShaderをDispatchして頂点バッファを書き換える処理をRHICmdListに積み込む必要がある。RHICmdListへの操作はRenderThread側で実行する必要があるので、ENQUEUE_RENDER_COMMANDマクロを使って描画スレッドで実行されるようにリクエストする。

void FComputeMeshProxy::EnqueueDispatchComputeShader()
{
    // RenderThreadで実行してほしい処理をマクロで登録
    ERHIFeatureLevel::Type FeatureLevel = component_->GetWorld()->Scene->GetFeatureLevel();
    ENQUEUE_RENDER_COMMAND(CaptureCommand)(
        [this, FeatureLevel](FRHICommandListImmediate& RHICmdList)
    {
        // -- この中がRenderThreadで実行される --
        // ComputeShaderのDispatch処理をRHICmdListに積み込む関数
        DispatchComputeShader_RenderThread(
            RHICmdList,
            this,
            FeatureLevel);
    }
    );
}

DispatchComputeShader_RenderThread()ではComputeShaderのGlobalShaderに対して書き込み対象の頂点バッファUAVを設定してDispatchコマンドを発行する。頂点バッファはComputeShaderで読み書きされた後にメッシュ描画で頂点バッファとして利用されるので、TransitionResourceによるトランジションコマンドも発行する必要があると思われる。

static void DispatchComputeShader_RenderThread(FRHICommandListImmediate& RHICmdList,FNglComputeMeshProxy* proxy,ERHIFeatureLevel::Type FeatureLevel)
{
    // 頂点バッファを書き換える自作ComputeShaderのGlobalShaderを取得
    TShaderMap<FGlobalShaderType>* GlobalShaderMap = GetGlobalShaderMap(FeatureLevel);
    TShaderMapRef<FComputeMeshShaderCS> cs(GlobalShaderMap);
    
    // Position頂点バッファリソースをGfxからComputeへトランジション
    RHICmdList.TransitionResource(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EGfxToCompute, Position頂点バッファUAV);
    // TODO ほかのバッファも同様に

    {
        // Position頂点バッファUAVをRWリソースとして設定
        SetUAVParameter(RHICmdList, cs->GetComputeShader(), cs->vtx_pos_buffer_, Position頂点バッファUAV);
        // TODO ほかのバッファも同様に

        // ComputeShaderのDispatch
        DispatchComputeShader(RHICmdList, *cs, ディスパッチグループ数, 1, 1);

        // Position頂点バッファUAVをRWリソースから外す
        SetUAVParameter(RHICmdList, cs->GetComputeShader(), cs->vtx_pos_buffer_, nullptr);
        // TODO ほかのバッファも同様に
    }

    // Position頂点バッファリソースをComputeからGfxへトランジション
    RHICmdList.TransitionResource(EResourceTransitionAccess::EReadable, EResourceTransitionPipeline::EComputeToGfx, Position頂点バッファUAV);
    // TODO ほかのバッファも同様に
}

ここでの注意点としては、RWリソースとして設定したバッファを使用後に外しておかないと、メッシュ描画時のバッファ利用方法と競合して正常に描画されなくなる点。

future work

インデックスバッファに同一インデックスを書き込むことでトライアングルを潰すことができるので、最大限のバッファサイズを確保しておいて必要に応じてトライアングルを増減させてTessellationのようなことができるかもしれない。
以下はプレイヤーの近く以外の草を単純にスケールで縮退させてみた例.


Shell方式の毛皮表現のように多数のポリゴンを重ねる表現などでも、カメラからの距離時応じて重ねる数を動的に変更したりすることでパフォーマンスを稼ぐことができるかもしれない。
メッシュを作っているだけなのでGPUパーティクル的なこともできそう。
最初にも書いたがカラーやUVなどの独自の情報を埋め込んでマテリアルBPで利用することでもいろいろなことができる気がする。
そのほかアイデア募集中。

UE4 C++プラグインでPostProcess検証

エンジン改造をせずにC++プラグインとGlobalShaderでPostProcess的なことをできないか調べてみた。

結論としては

  • UE4は動的解像度関係で複雑なUV計算などを隠蔽しているのでそのあたりを再現しないと正常に動かない
  • UI描画と同じフル解像度に対して処理するので動的解像度の恩恵を受けられない

まぁ、ダメでしたね。

概要

基本的にゲームスレッドからの描画命令は ENQUEUE_RENDER_COMMANDマクロによってキューに積み込んで描画スレッドが処理する。
描画スレッドはキューに積まれた順番に処理をしていく。
ActorのTickで積み込むとScene描画(ベースパスやPostProcess)の前に積み込まれるので、PostProcessでよく使うSceneColorテクスチャなどがまだ出来上がっていない。
ActorにはUI描画の直前(PostProcess後)のタイミングでコールされる関数を設定できるのでそれを利用すればいい感じのタイミングに描画コマンドを差し込めるのでは?

結果

というわけでActorのPostRenderFor()を利用して描画コマンドを積んでみたところをRenderDocで確認してみた。

f:id:nagakagachi:20190420212602p:plain

一応いい感じの場所で自前の描画コマンドが実行されていることが確認できる。

ただし以下の実行結果のようにSceneColorの領域とRendetTargetのViewportのマッチング計算がうまくいっていなかったりしていろいろ問題がある。
(ウィンドウサイズを変更すると描画領域が変わっておかしくなる)


UE4 C++プラグインでPostProcess検証

UE4側でToneMap後にHookできるようになったり描画領域に応じたUV補正計算の自動化がサポートされないだろうか…

関連コード

シェーダ
#include "/Engine/Public/Platform.ush"

Texture2D       SceneColor;
SamplerState    SceneColorSampler;

// フルスクリーンVS
void MainVS(
    in uint GlobalVertexId : SV_VertexID,
    out float2 OutUV : TEXCOORD0,
    out float4 OutPosition : SV_POSITION
    )
{
    uint VertexId = GlobalVertexId;
    
    float2 CellVertexUV = float2(0x1 & ((VertexId + 1) / 3), VertexId & 0x1);

    // Output vertex position.
    OutPosition = float4(CellVertexUV * 2 - 1, 0, 1);

    // Output top left originated UV of the vertex.
    OutUV = float2(CellVertexUV.x, 1 - CellVertexUV.y);
}


// フレームバッファ使用PS
void MainPS(
    in noperspective float2 UV : TEXCOORD0,
    in float4 SvPosition : SV_POSITION,
    out float4 OutColor : SV_Target0
    )
{
    float4 c = SceneColor.SampleLevel(SceneColorSampler, UV, 0);

    OutColor = float4(c.x, UV.x, UV.y, 1);
}
Actor(.h)
UCLASS()
class ANglSampleRenderSystem : public AActor
{
	GENERATED_BODY()

public:
	void BeginPlay();

	static void RenderPostProcess(FRHICommandList& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, const FRenderTarget* viewRenderTarget);

	void PostRenderFor(class APlayerController* PC, class UCanvas* Canvas, FVector CameraPosition, FVector CameraDir);

private:
};
Acotr(.cpp)
class FSamplePostprocessShader : public FGlobalShader
{
public:
	static bool ShouldCache(EShaderPlatform Platform)
	{
		return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM4);
	}

	static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	{
		FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
	}

	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		// Useful when adding a permutation of a particular shader
		return true;
	}

	FSamplePostprocessShader() {}

	// シェーダパラメータと変数をバインド
	FSamplePostprocessShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		:FGlobalShader(Initializer)
	{
		// シェーダパラメータと変数をバインド
		SceneColor_.Bind(Initializer.ParameterMap, TEXT("SceneColor"));
		SceneColorSampler_.Bind(Initializer.ParameterMap, TEXT("SceneColorSampler"));
	}
	// シリアライズメソッド
	virtual bool Serialize(FArchive& Ar) override
	{
		// シェーダパラメータ変数のシリアライズ
		bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
		Ar << SceneColor_ << SceneColorSampler_;
		return bShaderHasOutdatedParameters;
	}

	// シェーダパラメータ設定をする固有メソッド
	template<typename TShaderRHIParamRef>
	void SetParameters(
		FRHICommandList& RHICmdList,
		const TShaderRHIParamRef ShaderRHI,
		FTextureRHIParamRef inTexture
	)
	{
		//SetTextureParameter(RHICmdList, ShaderRHI, SceneColor_, inTexture);
		SetTextureParameter( RHICmdList, ShaderRHI, 
			SceneColor_, SceneColorSampler_, 
			TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI(), inTexture);
	}
private:
	// シェーダパラメータ変数群
	FShaderResourceParameter SceneColor_;
	FShaderResourceParameter SceneColorSampler_;
};

class FSamplePostprocessShaderVS : public FSamplePostprocessShader
{
	DECLARE_SHADER_TYPE(FSamplePostprocessShaderVS, Global);

public:
	/** Default constructor. */
	FSamplePostprocessShaderVS() {}

	/** Initialization constructor. */
	FSamplePostprocessShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		:FSamplePostprocessShader(Initializer)
	{
	}
};

class FSamplePostprocessShaderPS : public FSamplePostprocessShader
{
	DECLARE_SHADER_TYPE(FSamplePostprocessShaderPS, Global);

public:

	/** Default constructor. */
	FSamplePostprocessShaderPS() {}

	/** Initialization constructor. */
	FSamplePostprocessShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
		:FSamplePostprocessShader(Initializer)
	{ }
};

// シェーダコードとGlobalShaderクラスを関連付け
IMPLEMENT_SHADER_TYPE(, FSamplePostprocessShaderVS, TEXT("/Plugin/ShaderPlugin/Private/sample_postprocess.usf"), TEXT("MainVS"), SF_Vertex)
IMPLEMENT_SHADER_TYPE(, FSamplePostprocessShaderPS, TEXT("/Plugin/ShaderPlugin/Private/sample_postprocess.usf"), TEXT("MainPS"), SF_Pixel)


void ANglSampleRenderSystem::BeginPlay()
{
	Super::BeginPlay();
	
	AGameModeBase* Mode = UGameplayStatics::GetGameMode(this);
	APlayerController*PlayerController = UGameplayStatics::GetPlayerController(this, 0);
	AHUD* HUD = PlayerController->GetHUD();
	// このActorのPostRenderForをコールバック登録
	HUD->AddPostRenderedActor(this);
	// HUDオーバーレイを有効化
	HUD->bShowOverlays = true;
	
}

void ANglSampleRenderSystem::RenderPostProcess(FRHICommandList& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, const FRenderTarget* viewRenderTarget)
{
	SCOPED_DRAW_EVENTF(RHICmdList, SceneCapture, TEXT("ANglSampleRenderSystem::RenderTest"));
	
	auto finalRenderTarget = viewRenderTarget;
	if (!finalRenderTarget)
		return;

	auto ShaderMap = GetGlobalShaderMap(FeatureLevel);

	TShaderMapRef<FSamplePostprocessShaderVS> vs(ShaderMap);
	TShaderMapRef<FSamplePostprocessShaderPS> ps(ShaderMap);

	// PostProcessシステムからバッファを取得
	FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
	auto sceneColor = SceneContext.GetSceneColorTexture();

	auto finalRenderTargetSize = finalRenderTarget->GetSizeXY();

	// CanvasのViewFamilyから取得したターゲットへ書き込んでみる
	auto rt_action = ERenderTargetActions::DontLoad_Store;
	FRHIRenderPassInfo passInfo(finalRenderTarget->GetRenderTargetTexture(), rt_action, nullptr);
	RHICmdList.BeginRenderPass(passInfo, TEXT("NglSampleRender"));
	FIntPoint viewportSize(finalRenderTargetSize.X, finalRenderTargetSize.Y);
	// ターゲットに対してフルスクリーンビューポート
	RHICmdList.SetViewport(
		0, 0, 0.f,
		viewportSize.X, viewportSize.Y, 1.f);

	// PSO
	FGraphicsPipelineStateInitializer GraphicsPSOInit;
	RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
	GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();
	GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
	GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
	GraphicsPSOInit.PrimitiveType = PT_TriangleList;
	GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
	GraphicsPSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*vs);
	GraphicsPSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*ps);
	SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);


	// シェーダパラメータ更新
	vs->SetParameters(RHICmdList, vs->GetVertexShader(), nullptr);
	ps->SetParameters(RHICmdList, ps->GetPixelShader(), sceneColor.GetReference());// SceneColorをシェーダリソース利用

	// 矩形描画
	RHICmdList.DrawPrimitive(0, 2, 1);

	RHICmdList.EndRenderPass();
}

void ANglSampleRenderSystem::PostRenderFor(class APlayerController* PC, class UCanvas* Canvas, FVector CameraPosition, FVector CameraDir)
{
	Super::PostRenderFor(PC, Canvas, CameraPosition, CameraDir);

	ERHIFeatureLevel::Type FeatureLevel = GetWorld()->Scene->GetFeatureLevel();

	Canvas->SizeX;
	Canvas->SizeY;

	auto viewRenderTarget = Canvas->SceneView->Family->RenderTarget;

	ENQUEUE_RENDER_COMMAND(CaptureCommand)(
		[this, FeatureLevel, viewRenderTarget](FRHICommandListImmediate& RHICmdList)
		{
			RenderPostProcess(RHICmdList, FeatureLevel, viewRenderTarget);
		}
	);
}

UE4 翻訳メモ Mesh Drawing Pipeline Conversion Guide for Unreal Engine 4.22

4.22で大幅に変更されたらしいメッシュ描画パイプラインのドキュメントをGoogle翻訳先生の力を借りて翻訳しつつメモ

docs.unrealengine.com

より詳細は
docs.unrealengine.com

www.youtube.com
を参照


Introduction

Unreal EngineUE4)4.22リリースでは、Mesh Drawing Pipelineは完全に書き直されました。主な変更点は、フレーム毎に描画の準備をするImmediate Mode からすべてのシーン描画が事前に準備される Retained Mode に移行したことです。これは、シーン全体のシェーダバインディングテーブルを必要とするDirectX Raytracing(DXR)や、CPUが可視性の情報無しに描画の準備をする必要がある GPU-Driven-Rendering など、今後のテクノロジをサポートできるようになるための重要な変更です。

UE4 4.22でのMesh Drawing Pipelineの変更についての詳細は、Mesh Drawing PipelineのドキュメントおよびGame Developer's Conference(GDC)のプレゼンテーションUnreal Engine 4.22用Mesh Drawing Pipelineのリファクタリングを参照してください。

Mesh Draw Commands

古いパイプラインでは、mesh pass draw policiesはFMeshBatchに基づいてRendering Hardware Interface(RHI)コマンドを直接実行していました。新しいパイプラインでは、メッシュ描画コマンドFMeshDrawCommandの概念が導入されています。これは、FMeshBatchとRHIの間のインタフェースとして機能します。 Mesh drawコマンドは完全なスタンドアロンの描画記述です。FMeshDrawCommandはRHIが描画について知る必要があるすべての情報を格納します。これにより、描画ステート全体をそれらのシェーダバインディングと一緒にキャッシュして再利用することができます。

Static Draw Lists and Primitive Sets

4.22メッシュレンダリングパイプラインでは、静的描画リスト(TStaticMeshDrawList)とプリミティブセット(たとえば、FTranslucentPrimSet、FCustomDepthPrimSet)はFParallelMeshDrawCommandPassによって置き換えられました。 FParallelMeshDrawCommandPassはパスごとの可視メッシュ描画コマンドリストをカプセル化します。

新しいデザインには2つの重要な変更があります。まず、シーンごとのMesh ListsがVisible Mesh Listsに置き換えられました。以前のStatic Mesh Passではシーン内のパス毎のStatic Mesh List(TStaticMeshDrawList)をトラバースして個々のStatic MeshについてFViewInfo :: StaticMeshVisibilityMapをチェックすることで表示可能なMeshを選択していました。新しいデザインでは、描画は可視メッシュの描画コマンド配列(FMeshDrawCommandPassSetupTaskContext :: MeshDrawCommands)の単なるトラバースです。新しいアプローチは、シーンの複雑さに応じてスケールします。 2つ目の重要な変更は、静的および動的メッシュ描画リストをマージすることです。これにより、メッシュ描画パイプライン全体が簡素化され、レンダラーは静的描画と動的描画を並べ替えることもできます。

このパイプラインには、DrawDynamicMeshPass関数によるImmediate Modeのメッシュレンダリングエミュレーションもあります。これは非常に柔軟なレンダリングパスですが、キャッシング、自動インスタンス化、および複数の動的メモリ割り当てをサポートしていないため、パフォーマンスが重要でないメッシュパスにのみ使用してください。たとえばこの機能はエディタ専用ヘルパーメッシュのレンダリングを担当していたDrawViewElementsの置き換えとなります。

Drawing Policies

FDepthDrawingPolicyやFBasePassDrawingPolicyなどのdraw policiesは、FDepthPassMeshProcessorおよびFBasePassMeshProcessorに置き換えられました。特定のパスメッシュプロセッサはFMeshProcessor基本クラスから派生し、各FMeshBatchをパス用の一連のメッシュ描画コマンドに変換します。ここが最終的な描画フィルタリングが行われる場所です。シェーダの組み合わせ(Permutation)が選択され、シェーダバインディングが揃えられます。

Shader Bindings

以前はすべてのシェーダーパラメーターは適切なDrawing PoliciesによってRHICmdListに直接設定されていました。新たな仕組みではすべてのパラメータがFMeshDrawSingleShaderBindingsに集められ、後で描画中にSetOnCommandListを呼び出すことでRHICmdListに設定されます。これはシェーダバインディングを使用してすべての描画ステートをキャッシュできるようにするために必要な仕組みです。

古いパイプラインは、FDrawingPolicyRenderStateを使用して、common high-level mesh pass render stateを渡します(例えばuniform bufferを渡すのと同じように)。新しいパイプラインは、機能を大幅に変更することなくFDrawingPolicyRenderStateをFMeshPassProcessorRenderStateにリネームします。

シェーダバインディングの他の部分は、シェーダのSetParametersとSetMesh関数に埋め込まれました。これらはGetShaderBindingsとGetElementShaderBindingsに置き換えられました。上記の関数はカスタマイズ可能なShaderElementDataTypeに描画単位のパラメータを渡します。

リファクタリングにより多くのLooseパラメータがパス毎、または他のuniform buffersに移動しました。Looseパラメータを使用すると自動インスタンス化が無効になるだけでなく、各描画の間で定数バッファ更新を実行する必要があるため、処理速度の低下の原因となります。

ViewUniformBufferやDepthPassUniformBufferのような以前の標準的なuniform buffersは、新しいデータで毎フレームを作り直されていました。新しいパイプラインではこれらは永続的かつグローバルです(FScene :: FPersistentUniformBuffers内に保持されます)。これによってメッシュ描画コマンドがキャッシュされていてもシェーダはフレームごとに適切なデータを受信できます。

FPrimitiveViewRelevance

FPrimitiveViewRelevanceは、2つの追加の関連性フラグで拡張されました。

  • Separate Velocity Pass にはbVelocityRelevanceフラグが必要です。
  • Translucency Self Shadow にはbTranslucentSelfShadowフラグが必要です。

さらに、すべての動的描画はview relevanceに依存するようになり、view relevanceで特定のパスを無効にすることでレンダリングも無効になります。

Shaders

新しいパイプラインではGPUSceneが導入されました。これはシーン内のすべてのプリミティブのprimitive uniform bufferデータを含む構造化バッファです。現在、local vertex factory(Static Mesh Component)とSM5 feature levelのみがこのレンダリングパスを利用できます。シェーダは、GPUSceneを有効にしてコンパイルするためにPrimitiveのuniform bufferに直接アクセスする代わりに、GetPrimitiveData(PrimitiveId)を使用する必要があります。

プリミティブデータアクセスは、[Custom Expression Material ]ノード内で度々使用されます。たとえば、プリミティブのバウンディングボックスにアクセスするためです。それらを変換するためには、Primitive.MemberをGetPrimitiveData(Parameters.PrimitiveId).Memberに置き換える必要があります。

UE4 周期的なリムエミッシブ

あけましておめでとうございます

視線ベクトルとピクセル法線の内積を利用して縁をエミッシブで光らせるリムライト的表現をすることがよくあると思います。今回はこのリムライト的表現で使われる値をそのまま使わずに周期関数へ入力してから使うことで、周波数を変えたり波にオフセットをかけたりできるヘンなリムライト?を作ってみます。

視線ベクトルvの逆向きベクトル-vと法線ベクトルnの内積は正面から縁に向かって1から0に変化します。


1 - dot(\vec{-v},\vec{n})
とすれば正面が0で縁が1になり、単純にこれをエミッシブに出力すれば縁が光ると思います。

これを周期関数である sin() に入力します。
sin(1 - dot(\vec{-v},\vec{n}))

このままだと -1 ~ +1 の値になるので、1を足して0.5を掛けることで 0 ~ 1 の値に変換します。
(sin(1 - dot(\vec{-v},\vec{n})) + 1)*0.5

さらに周波数を変えるための変数 freq をsin() の中にかけ合わせます。
(sin( (1-dot(\vec{-v},\vec{n}) )*freq)+1)*0.5

周期のオフセットのための変数 offset をsin() の中に足します。
(sin( (1-dot(\vec{-v},\vec{n}) )*freq+offset)+1)*0.5

最後に0から1への変化の急峻さをコントロールする変数 sharpness を使ってべき乗します。
(power(sin( (1-dot(\vec{-v},\vec{n}) )*freq+offset)+1)*0.5, sharpness)

この値を適当なカラーにかけてエミッシブ出力するなどすれば完成。

freqを大きくするとリムライトの波の数が増え、
offsetを増やすと波の位置が移動し、
sharpnessを大きくすると明るい部分が狭く(鋭く)なります。


プロジェクトはこちら
(サンプルプロジェクトではsin()ではなく自前で周期関数モドキ作っていますがsin()などに置き換えても同じような挙動になるはずです)
drive.google.com

それなりにコメントを書いているのでナニカの参考にしていただければ…

あと法線との内積に視線ベクトルではなくワールド空間ベクトルを使ったりすると面白いかも。
(サンプルプロジェクトのマテリアル M_PeriodicRim_WorldDir でやってます)