GBufferを拡張する

UE4 Advent Calendar 2015、6日目の記事です。

今回は誰得な感じはありますが、エンジンコードを改変してGBufferを拡張してしまえ!という記事です。

正直、個人製作でこれをやることはほとんどないかと思いますが、なんかすげぇ映像表現を思いついたのにGBufferに情報がなくて断念せざるを得ない…というかなり奇特な方には有用かもしれません。

…まあ、そういう人は勝手に調べて勝手に改造してしまうんでしょうけどね。

まず、GBufferについて簡単に解説を行います。

まだDirectXが8くらいの頃はマルチレンダーターゲット(MRT)と呼ばれる機能が存在していませんでした。

この頃は1回のドローコールで描画できるバッファはカラーバッファと深度バッファの2つだけで、深度バッファについては深度とステンシル値以外は描画することが出来ませんでした。

そのため、カラーバッファに描画する情報はマテリアルにライティングを行った結果を描画していました。

これがフォワードレンダリングです。

さて、DirectXが9.0cとなり、ShaderModelが3.0となると深度バッファを除いて最大4枚までのバッファに一度に情報を描画できるMRTが実装されました。

MRTの描画条件はビューポートが同一であり、バッファのサイズが同一であり、描画するポリゴンの位置が同一である必要がありました。

これだけ聞くと、ライティングしたマテリアルを描画する以外に何を描画するんだ?と思われるかもしれませんが、そこで登場したのがディファードレンダリングというレンダリング手法でした。

ディファードレンダリング自体の考え方は結構昔からあったらしいのですが(20世紀には論文が出ていた、という話を聞いたことがあります)、ハードウェアの制約上実現が難しかったわけです。

しかしMRTの実現によって現実的な手法となったわけです。

その具体的な手法はいろいろな場所で語られていますが、要はライティングに必要な情報をバッファに書き込んでおき、ライティング計算を遅延(ディファード)させるという手法です。

この場合、バッファに書き込む情報はテクスチャカラーだけではなく、メッシュの法線情報であったりその他のカラーとは関係ない情報だったりします。

そのため、これらのバッファはカラーバッファとは呼びにくく、カラーも書き込むけどカラーじゃないものも書き込むグラフィクス用の汎用的なバッファ、という意味でGBufferと名付けられました。

GBufferのGはGraphicsのGらしいです。Generalかと思ってたんですが、違うらしい…

今回やるGBufferの拡張とは、GBufferを増やして新しい情報を格納し、それをライティングで使用するというものです。

GBufferを増やすことのメリットはとにかく情報を格納できる点です。

マテリアルやモデル形状に関する情報を多く格納できるのであれば、ライティング時やポストプロセスなんかで使用することが出来るようになります。

デメリットはバッファを確保するメモリが増えることと、描画するバッファが多くなればパフォーマンスに影響を与えるという点です。

特にパフォーマンスに与える影響は大きくなるかと思いますので、VRアプリを作る際には気をつけるべきでしょう。

というわけでGBufferの解説は簡単ですが終了です。

続きからではGBufferの拡張方法について書いていきますが、ほとんどコードの提示になります。

また、単純に拡張するだけでもつまらないので、拡張したGBufferに接線情報を保存し、異方性スペキュラをやってみます。

こちらは次回のエントリーとなります。

解説も最小限になると思うのですが、どうしても詳しく解説してほしい部分がありましたらTwitterなりコメントなりでおねがいします。

・GBufferを増やす

最初に.h, .cppを修正してGBufferを増やしましょう。

今回増設するGBufferに格納するのはワールド空間中のタンジェントベクトルです。

同じようなものとしてワールド空間中の法線ベクトルが格納されているので、これと同じフォーマットのバッファを作成しましょう。

変更するのは以下の2つです。

Engine/Source/Rungime/Renderer/Private/PostProcess/SceneRenderTargets.h

Engine/Source/Rungime/Renderer/Private/PostProcess/SceneRenderTargets.cpp

ソースコードは一部省略(...と書かれている部分)されています。

ハイライト部分が追加、修正した部分です。

SceneRenderTargets.h

79行目付近

BEGIN_UNIFORM_BUFFER_STRUCT(FGBufferResourceStruct, )

...

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2D, GBufferVelocityTexture )

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2D, GBufferTangentTexture)

...

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2D<float4>, GBufferVelocityTextureNonMS )

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2D<float4>, GBufferTangentTextureNonMS)

...

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2DMS<float4>, GBufferVelocityTextureMS )

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_TEXTURE( Texture2DMS<float4>, GBufferTangentTextureMS)

...

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_SAMPLER( SamplerState, GBufferVelocityTextureSampler)

DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER_SAMPLER( SamplerState, GBufferTangentTextureSampler)

END_UNIFORM_BUFFER_STRUCT( FGBufferResourceStruct )

同、370行目付近

int32 GetGBufferEIndex() const

{

return bAllowStaticLighting ? 6 : -1;

}

同、520行目付近

TRefCountPtr<IPooledRenderTarget> GBufferVelocity;

TRefCountPtr<IPooledRenderTarget> GBufferTangent;

SceneRenderTargets.cpp

186行目付近

, GBufferVelocity(GRenderTargetPool.MakeSnapshot(SnapshotSource.GBufferVelocity))

, GBufferTangent(GRenderTargetPool.MakeSnapshot(SnapshotSource.GBufferTangent))

同、475行目付近

OutRenderTargets[4] = FRHIRenderTargetView(GBufferD->GetRenderTargetItem().TargetableTexture, 0, -1, ColorLoadAction, ERenderTargetStoreAction::EStore);

OutRenderTargets[5] = FRHIRenderTargetView(GBufferTangent->GetRenderTargetItem().TargetableTexture, 0, -1, ColorLoadAction, ERenderTargetStoreAction::EStore);

int32 MRTCount = 6; // Derived from previously used RenderTargets[]; would have been nice to keep a pointer while filling it but it's more confusing to use

同、795行目付近

GBasePassOutputsVelocityDebug = CVarBasePassOutputsVelocityDebug.GetValueOnRenderThread();

if (bAllocateVelocityGBuffer)

{

FPooledRenderTargetDesc VelocityRTDesc = FVelocityRendering::GetRenderTargetDesc();

GRenderTargetPool.FindFreeElement(VelocityRTDesc, GBufferVelocity, TEXT("GBufferVelocity"));

}

// Create the world-space tangent g-buffer.

{

EPixelFormat NormalGBufferFormat = bHighPrecisionGBuffers ? PF_FloatRGBA : PF_A2B10G10R10;

if (bEnforce8BitPerChannel)

{

NormalGBufferFormat = PF_B8G8R8A8;

}

FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(BufferSize, NormalGBufferFormat, FClearValueBinding::Transparent, TexCreate_None, TexCreate_RenderTargetable, false));

GRenderTargetPool.FindFreeElement(Desc, GBufferTangent, TEXT("GBufferTangent"));

}

同、820行目付近

const FSceneRenderTargetItem& GBufferVelocityToUse = GBufferVelocity ? GBufferVelocity->GetRenderTargetItem() : GSystemTextures.BlackDummy->GetRenderTargetItem();

const FSceneRenderTargetItem& GBufferTangentToUse = GBufferTangent ? GBufferTangent->GetRenderTargetItem() : GSystemTextures.BlackDummy->GetRenderTargetItem();

...

GBufferResourceStruct.GBufferVelocityTexture = GBufferVelocityToUse.ShaderResourceTexture;

GBufferResourceStruct.GBufferTangentTexture = GBufferTangentToUse.ShaderResourceTexture;

...

GBufferResourceStruct.GBufferVelocityTextureNonMS = GBufferVelocityToUse.ShaderResourceTexture;

GBufferResourceStruct.GBufferTangentTextureNonMS = GBufferTangentToUse.ShaderResourceTexture;

...

GBufferResourceStruct.GBufferVelocityTextureMS = GBufferVelocityToUse.TargetableTexture;

GBufferResourceStruct.GBufferTangentTextureMS = GBufferTangentToUse.TargetableTexture;

...

GBufferResourceStruct.GBufferVelocityTextureSampler = TStaticSamplerState<>::GetRHI();

GBufferResourceStruct.GBufferTangentTextureSampler = TStaticSamplerState<>::GetRHI();

同、1100行目付近

RHICmdList.CopyToResolveTarget(GBufferD->GetRenderTargetItem().TargetableTexture, GBufferD->GetRenderTargetItem().ShaderResourceTexture, true, FResolveParams(ResolveRect));

RHICmdList.CopyToResolveTarget(GBufferTangent->GetRenderTargetItem().TargetableTexture, GBufferTangent->GetRenderTargetItem().ShaderResourceTexture, true, FResolveParams(ResolveRect));

ここでやっているのは描画に使用するGBufferを増やし、それらを取り扱うための各種オブジェクトを生成しています。

GBufferのフォーマットは法線用のGBufferと同じものを使用しています。

また、UE4.10ではGBufferは最高で7枚使用され、内5枚(GBufferA~E)は必ず使用されます。

この5枚はMRTの0~4番に割り当てられ、GBufferFとVelocity用GBufferは使用される場合に5,6番に割り当てられます。

今回は必ず使用される5枚のあとに、やはり必ず使用される6枚目のGBufferとして登録するようにしています。

そのため、MRTCountは通常は5のところ、改造したソースでは6となります。

・GBufferに書き込む

次はGBufferにワールド空間のタンジェントベクトルを書き込みます。

ここではシェーダファイル(.usf)のみ修正を行います。

まずはマテリアルからワールド空間タンジェントを取得できるようにします。

Engine/Shaders/MaterialTemplate.usf

94行目付近

struct FMaterialPixelParameters

{

...

half3 WorldNormal;

half3 WorldTangent;

同、1570行目付近

void CalcMaterialParameters(

...

#else

// Here we don't supoport two sided materials

Parameters.TangentNormal = Parameters.WorldNormal = normalize(Parameters.TangentNormal);

#endif

Parameters.WorldTangent = normalize(Parameters.TangentToWorld._11_12_13);

FMaterialPixelParameters構造体にWorldTangentという変数を追加し、CalcMaterialParameters関数でこの変数にワールド空間タンジェントを格納しています。

タンジェント空間のタンジェントは(1, 0, 0)なので、タンジェント空間からワールド空間に変換する行列から簡単に求まります。

次にGBuffer構造体とGBufferへのエンコード、デコード処理を変更します。

Engine/Shaders/DeferredShadingCommon.usf

250行目付近

struct FGBufferData

{

...

float4 Velocity;

float3 WorldTangent;

};

同、330行目付近

void EncodeGBuffer(

...

out float4 OutGBufferVelocity,

out float4 OutGBufferTangent,

float QuantizationBias = 0 // -0.5 to 0.5 random float. Used to bias quantization.

)

{

OutGBufferA.rgb = EncodeNormal( GBuffer.WorldNormal );

OutGBufferTangent.rgb = EncodeNormal( GBuffer.WorldTangent );

OutGBufferTangent.a = 0.0f;

同、390行目付近

FGBufferData DecodeGBufferData(

...

float4 InGBufferVelocity,

float4 InGBufferTangent,

float CustomNativeDepth,

...

{

FGBufferData GBuffer;

GBuffer.WorldNormal = DecodeNormal( InGBufferA.xyz );

GBuffer.WorldTangent = DecodeNormal( InGBufferTangent.xyz );

同、575行目付近

FGBufferData GetGBufferDataUint(uint2 PixelPos, bool bGetNormalizedNormal = true)

{

...

float4 GBufferD = GBuffers.GBufferDTextureNonMS.Load(int3(PixelPos, 0));

float4 GBufferTangent = GBuffers.GBufferTangentTextureNonMS.Load(int3(PixelPos, 0));

float CustomNativeDepth = CustomDepthTextureNonMS.Load(int3(PixelPos, 0)).r;

...

float SceneDepth = CalcSceneDepth(PixelPos);

return DecodeGBufferData(GBufferA, GBufferB, GBufferC, GBufferD, GBufferE, GBufferVelocity, GBufferTangent, CustomNativeDepth, CustomStencil, SceneDepth, bGetNormalizedNormal);

}

同、650行目付近

FGBufferData GetGBufferData(float2 UV, bool bGetNormalizedNormal = true)

{

...

float4 GBufferD = Texture2DSampleLevel(GBuffers.GBufferDTexture, GBuffers.GBufferDTextureSampler, UV, 0);

float4 GBufferTangent = Texture2DSampleLevel(GBuffers.GBufferTangentTexture, GBuffers.GBufferTangentTextureSampler, UV, 0);

float CustomNativeDepth = Texture2DSampleLevel(CustomDepthTexture, CustomDepthTextureSampler, UV, 0).r;

...

float SceneDepth = CalcSceneDepth(UV);

return DecodeGBufferData(GBufferA, GBufferB, GBufferC, GBufferD, GBufferE, GBufferVelocity, GBufferTangent, CustomNativeDepth, CustomStencil, SceneDepth, bGetNormalizedNormal);

}

同、740行目付近

FGBufferData GetGBufferDataMS(int2 IntUV, uint SampleIndex, bool bGetNormalizedNormal = true)

{

...

float4 GBufferD = GBuffers.GBufferDTextureMS.Load(IntUV, SampleIndex);

float4 GBufferTangent = GBuffers.GBufferTangentTextureMS.Load(IntUV, SampleIndex);

float CustomNativeDepth = CustomDepthTextureNonMS.Load(int3(IntUV, 0)).r;

...

#else

float4 GBufferVelocity = 0;

#endif

return DecodeGBufferData(GBufferA, GBufferB, GBufferC, GBufferD, GBufferE, GBufferVelocity, GBufferTangent, CustomNativeDepth, CustomStencil, SceneDepth, bGetNormalizedNormal);

}

GBufferに格納される情報はFGBufferData構造体に一旦格納され、これをEncodeGBuffer関数で実際のGBufferに書き込む値に変換します。

逆に、GBufferから情報を取り出す場合はDecodeGBuffer関数を使ってFGBufferData構造体に復元されます。

最後に実際にGBufferに書き込みを行う部分を修正します。

先に修正しているEncodeGBuffer関数はデカールでも使用されているので忘れずに修正しましょう。

Engine/Shaders/BasePassPixelShader.usf

604行目付近

void Main(

...

,out float4 OutGBufferD : SV_Target4

,out float4 OutGBufferTangent : SV_Target5

#if ALLOW_STATIC_LIGHTING

,out float4 OutGBufferE : SV_Target6

#if WRITES_VELOCITY_TO_GBUFFER

, out float4 OutGBufferVelocity : SV_Target7

#endif

#else

#if WRITES_VELOCITY_TO_GBUFFER

, out float4 OutGBufferVelocity : SV_Target6

#endif

#endif

#endif

同、825行目付近

FGBufferData GBuffer = (FGBufferData)0;

GBuffer.WorldNormal = MaterialParameters.WorldNormal;

GBuffer.WorldTangent = MaterialParameters.WorldTangent;

同、910行目付近

float QuantizationBias = PseudoRandom( MaterialParameters.SVPosition.xy ) * 0.5 - 0.5;

EncodeGBuffer(GBuffer, OutGBufferA, OutGBufferB, OutGBufferC, OutGBufferD, OutGBufferE, OutGBufferVelocity, OutGBufferTangent, QuantizationBias);

Engine/Shaders/DeferredDecal.usf

371行目付近

#elif DECAL_RENDERSTAGE == 1

{

...

float4 OutTarget6 = 0;

float4 OutTarget7 = 0;

EncodeGBuffer(Data, OutTarget1, OutTarget2, OutTarget3, OutTarget4, OutTarget5, OutTarget6, OutTarget7);

}

#elif DECAL_RENDERSTAGE == 2

{

...

float4 OutTarget6 = 0;

float4 OutTarget7 = 0;

EncodeGBuffer(Data, OutTarget1, OutTarget2, OutTarget3, OutTarget4, OutTarget5, OutTarget6, OutTarget7);

#if DECAL_BLEND_MODE == DECALBLENDMODEID_STAIN

以上となります。

ここまでやればGBufferにワールド空間タンジェントを格納できるはずです。

多分エラーも出ないはず。

この段階までの修正では絵的な変化はまったくありません。

ほんとに描画できてるの?と思う方は、VisualStudioのグラフィクス診断を利用するとGBufferが増えていること、書き込みが行われていることを確認できるはずです。

ue390.jpg

この画像はあるモデルを描画する際のMRTの状況です。

赤枠で囲った部分がワールド空間タンジェントを書き込んだGBufferとなります。

VSではこのバッファの描画状況をビジュアルで確認することも出来ますが、ここでは以下のような描画結果が得られます。

ue391.jpg

先程も言ったように、これだけでは描画結果に何ら影響を与えません。

影響をあたえるようにするためにはシェーディングを変更する必要がありますが、こちらは次回のエントリーで解説します。

というわけで今回はここまで!

明日は@yuujiiさんによるテストファーストな開発手順についてです。