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);
		}
	);
}