拡張したGBufferを使って異方性スペキュラをやってみる

UE4 Advent Calendar 2015 その2の7日目の記事です。

前回、UE4 Advent Calendarの記事としてGBufferの拡張を行いました

しかし、GBufferを拡張しただけでは何も起こりません。ただGPUが重くなるだけです。

そもそもGBufferを拡張したのだから、何かに使わないともったいない。

ということで、拡張したGBufferにはワールド空間タンジェントを格納していますので、これを使って異方性スペキュラをやってみます。

まず、異方性スペキュラ(異方性反射)とは何か?

異方性反射で画像検索するとたくさん出てきますが、よくあるのはディスク状の金属板に扇型の光の反射が出ているものではないでしょうか。

もしくは球に髪の毛に出るような天使の輪が入っている映像ですね。

これらは傷ついた金属やヘアライン加工された金属、CD/DVDの盤面、人間の髪の毛などに出てくる特殊なスペキュラです。

細いものが束になっているようなものの場合、その形状の関係から反射光が接線方向(傷の方向に対して垂直方向)に光が伸びたりします。

これをUE4で実装する方法は2種類あります。

1つはContent Exampleにあるように、法線方向を加工することで擬似的にそれっぽい反射をさせる方法です。

擬似ではありますが、UE4のエンジンを改造せずに実装できるので、どうしてもという人はこちらを参考にした方がいいのではないかと思います。

ただ、ちょっと特殊なので特定の異方性スペキュラを実現するのは難しいかもしれません。

もう1つはマテリアル内でライティング計算を行い、Emissiveの出力として異方性スペキュラを実現する方法です。

多光源に対応できないという問題はありますが、各種計算式やテクスチャの使用などをかなり自由に行うことが可能です。

そもそも多光源が不要なトゥーン系のゲームを作る場合はUE4のディファードシェーディングを無理に使うよりマテリアル内で計算してしまった方が有利かもしれません。

ただしシャドウマップが使用できないなどの問題もありますので何でもかんでもうまくいくというわけではないです。

なお、後者の実装については『Unreal Engine 4 マテリアルデザイン入門』の付録にWardの異方性スペキュラでの実装を書いておきました。

気になる人は買ってね!

…よし、自然に宣伝できたぞ

今回やるのはこの2つの実装のどれでもなく、エンジンを改造してしまえば好き勝手出来るよね!という実装です。

実装しておきながらなんですが、はっきり言ってオススメできません。

オススメできない理由としては、やはりエンジン改造のリスクです。

UE4ソースコードを公開してエンジンの改造もOKというスタンスを採っていますが、もちろんEpic Gamesでも日々エンジンの修正は行われています。

エンジンを改造したあと、正式版のUE4で入った新しい機能を使いたいとなったらどうすればいいでしょう?

それだけを持ってくる、というのはそれほど簡単ではないので、改造した分を新しいUE4にマージする必要が出てきます。

が、これだけ巨大なエンジンですので、マージコストは相当高くなるはずです。

そもそも自分が改造した部分に大幅な改良が行われていたりすると目も当てられません。

とは言っても、どうしてもやらなければならない場合というのが特にコンシューマゲーム開発では出てくることもあります。

そういう状況があった場合は…がんばって!

というわけで(どういうわけだ?)、続きからで異方性スペキュラの実装をやっていきます。

・ShadingModelIDを追加する

まずはShadingModelIDを追加します。

DefaultLit、Unlit、Subsurfaceなどを選択するあれです。そこに異方性スペキュラ用のShadingModelIDを追加しましょう。

Engine/Source/Runtime/Engine/Classes/Engine/EngineTypes.h

200行目付近

UENUM()

enum EMaterialShadingModel

{

MSM_Unlit UMETA(DisplayName="Unlit"),

...

MSM_TwoSidedFoliage UMETA(DisplayName="Two Sided Foliage"),

MSM_Anisotropy UMETA(DisplayName="Anisotropy"),

MSM_MAX,

};

ShadingModelIDの列挙型にMSM_Anisotoropyという異方性スペキュラ用のものを追加します。

Engine/Source/Runtime/Engine/Private/Materials/MaterialShader.cpp

30行目付近

FString GetShadingModelString(EMaterialShadingModel ShadingModel)

{

FString ShadingModelName;

switch(ShadingModel)

{

case MSM_Unlit: ShadingModelName = TEXT("MSM_Unlit"); break;

...

case MSM_TwoSidedFoliage: ShadingModelName = TEXT("MSM_TwoSidedFoliage"); break;

case MSM_Anisotropy: ShadingModelName = TEXT("MSM_Anisotropy"); break;

default: ShadingModelName = TEXT("Unknown"); break;

}

return ShadingModelName;

}

同、80行目付近

switch(Material->GetShadingModel())

{

...

case MSM_TwoSidedFoliage:

case MSM_Anisotropy:

INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumLitMaterialShaders,1);

break;

default: break;

};

Engine/Source/Runtime/Engine/Private/Materials/MaterialShared.cpp

1250行目付近

switch(GetShadingModel())

{

case MSM_Unlit: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_UNLIT"), TEXT("1")); break;

...

case MSM_TwoSidedFoliage: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE"), TEXT("1")); break;

case MSM_Anisotropy: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_ANISOTROPY"), TEXT("1")); break;

default:

UE_LOG(LogMaterial, Warning, TEXT("Unknown material shading model: %u Setting to MSM_DefaultLit"),(int32)GetShadingModel());

OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_DEFAULT_LIT"),TEXT("1"));

};

ShadingModelIDを増やした場合に追加すべき箇所はこれくらいです。

ただ、今回は異方性スペキュラの度合いを指定する必要があり、これを実現するために入力ピンをマテリアル入力に追加するのは骨が折れます。

そこで、SubsurfaceColorのR値を用いることにします。間借りさせてもらうわけですね。

そのためにはAnisotropyが選択された場合にSubsurfaceColorが有効になってくれないと困ります。

Engine/Source/Runtime/Engine/Public/MaterialShared.h

55行目付近

inline bool IsSubsurfaceShadingModel(EMaterialShadingModel ShadingModel)

{

return ShadingModel == MSM_Subsurface || ShadingModel == MSM_PreintegratedSkin || ShadingModel == MSM_SubsurfaceProfile || ShadingModel == MSM_TwoSidedFoliage || ShadingModel == MSM_Anisotropy;

}

Engine/Source/Runtime/Engine/Private/Materials/Material.cpp

4086行目付近

case MP_SubsurfaceColor:

Active = ShadingModel == MSM_Subsurface || ShadingModel == MSM_PreintegratedSkin || ShadingModel == MSM_TwoSidedFoliage || ShadingModel == MSM_Anisotropy;

break;

この段階でビルドも出来ますし、ShadingModelIDでAnisotropyが選択できるようになります。

SubsurfaceColorが有効になるのも確認しておいてください。

・異方性ライティングの処理をシェーダに追加する

ここからが本命の処理を入れる部分です。

いじるのはシェーダのみなので、エンジンのビルドは不要です。UE4を立ち上げた状態でも変更可能です。

まずはマテリアルでAnisotropyが指定された場合に適切なSheingModelIDがGBufferに書き込まれるようにします。

Engine/Shaders/DeferredShadingCommon.usf

227行目付近

#define SHADINGMODELID_UNLIT 0

...

#define SHADINGMODELID_TWOSIDED_FOLIAGE 6

#define SHADINGMODELID_ANISOTROPY 7

#define SHADINGMODELID_NUM 8

AnisotropyのShadingModelIDの番号を割り当てます。7番ですね。

Engine/Shaders/BasePassPixelShader.usf

880行目付近

#elif MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE

GBuffer.ShadingModelID = SHADINGMODELID_TWOSIDED_FOLIAGE;

GBuffer.CustomData = EncodeSubsurfaceColor(SubsurfaceColor);

#elif MATERIAL_SHADINGMODEL_ANISOTROPY

GBuffer.ShadingModelID = SHADINGMODELID_ANISOTROPY;

GBuffer.CustomData = GetMaterialSubsurfaceData(MaterialParameters).rgb;

#else

// missing shading model, compiler should report ShadingModelID is not set

#endif

マテリアルでShadingModelIDを切り替えると、それに応じた適切なdefineが設定されます。

これはMaterialShader.cppで追加した部分に記述されています。

これで、ShadingModelIDがAnisotropyの場合にGBufferに適切なIDとSubsurfaceColorが設定されるようになります。

最後にライティング計算の実装を行います。

UE4のシェーダには異方性ライティングの処理は入っていませんが、ライティング計算式が記述されているBRDF.usfにはGGXの異方性ライティングの計算式が入っています。

D_GGXanisoという関数がそれです。今回はこれを使います。

2014年に発表されたトライエースの五反田さんによるDiffuseの計算式も入ってた…

Engine/Shaders/ShadingModels.usf

242行目付近

float3 GGXAnisoShading( FGBufferData GBuffer, float3 LobeRoughness, float3 LobeEnergy, float3 L, float3 V, half3 N, float2 DiffSpecMask )

{

float3 H = normalize(V + L);

float NoL = saturate( dot(N, L) );

float NoV = max( dot(N, V), 1e-5 );

float NoH = saturate( dot(N, H) );

float VoH = saturate( dot(V, H) );

float3 B = normalize(cross(N, GBuffer.WorldTangent));

float3 T = normalize(cross(B, N));

// Generalized microfacet specular

float aspect = sqrt(1.0f - GBuffer.CustomData.x * 0.9f);

float anisoXRoughness = max(0.01f, LobeRoughness[1] / aspect);

float anisoYRoughness = max(0.01f, LobeRoughness[1] * aspect);

float D = D_GGXaniso( anisoXRoughness, anisoYRoughness, NoH, H, T, B ) * LobeEnergy[1];

float Vis = Vis_SmithJointApprox( LobeRoughness[1], NoV, NoL );

float3 F = F_Schlick( GBuffer.SpecularColor, VoH );

float3 Diffuse = Diffuse_Lambert( GBuffer.DiffuseColor );

return Diffuse * (LobeEnergy[2] * DiffSpecMask.r) + (D * Vis * DiffSpecMask.g) * F;

}

// @param DiffSpecMask .r: diffuse, .g:specular e.g. float2(1,1) for both, float2(1,0) for diffuse only

float3 SurfaceShading( FGBufferData GBuffer, float3 LobeRoughness, float3 LobeEnergy, float3 L, float3 V, half3 N, float2 DiffSpecMask )

{

switch( GBuffer.ShadingModelID )

{

....

case SHADINGMODELID_CLEAR_COAT:

// this path does not support DiffSpecMask yet

return ClearCoatShading( GBuffer, LobeRoughness, LobeEnergy, L, V, N );

case SHADINGMODELID_ANISOTROPY:

return GGXAnisoShading( GBuffer, LobeRoughness, LobeEnergy, L, V, N, DiffSpecMask);

default:

return 0;

}

}

GGXAnisoShadingという関数で異方性GGXを使ったライティング計算を行っています。

基本的には通常のシェーディング関数(StandardShading関数)と同じですが、スペキュラのD項の計算式をD_GGXanisoに変更しています。

この関数には引数としてX, Yベクトルとその方向のラフネスが必要になります。

X, Yベクトルはタンジェントと従法線を使用し、ラフネスはSubsurfaceColorのR値(X値)を異方性の度合いとしてX,Y方向のラフネスを求めるようにしました。

異方性の度合いが0の場合は等方性反射となり、1.0に進むとタンジェント方向(Xベクトル方向)に光が伸びていくような感じになります。

今回は異方性GGXを使いましたが、マテリアルデザイン入門で紹介しているWardの異方性反射を利用することも出来ます。

同じようにShadingModelIDを増やしてやれば実装できますので、興味のある人は試してみてください。

・実装結果

実装結果は画像より動画の方がわかりやすいかな、と思ったので動画にしてみました。

最初に映っているのはDeultLitのマテリアルです。

映像からもわかると思いますが、ヘアライン加工を意識した法線マップを貼っています。

3種類のマテリアルはそれぞれ左からDefaultLit、異方性GGX、異方性Wardとなっています。

Emissiveによる実装でないことをわかりやすくするため、ライトを追加していますが、追加されるとそれに合わせて異方性スペキュラが出るのがわかりますね。

今回の実装では実用には耐えられません。

最大の理由は、タンジェントをマテリアル入力として指定することが出来ない点です。

あくまでも頂点に設定されているタンジェント空間のタンジェントを持ってきただけに過ぎません。

もしもタンジェントを指定することができるようになるのであればタンジェントマップ的なテクスチャを使ってピクセル単位で指定することが出来ます。

これで何が出来るかというと、円状に傷がつけられている場合の扇型のスペキュラや、タンジェント空間に依存しない傷の付け方が可能になるのです。

が、マテリアル入力ピンを増やすのはなかなか大変なので、今回は見送りました。

もし要望がありましたら別途記事を書きます。

キューブマップやSSRによる反射の異方性対応は残念ながら難しいです。

可能ではあるかもですが、なんとなくで思いつく実装は大変重くなってしまうのであまりオススメできないですね。

こうすればいいよ!ってネタがあったら教えて下さい。

というわけで、エンジン改造ネタでした。

まあ、普通の人はやらないでしょうけど、異方性反射は重要だ!という人はちょっと試してみてもいいかもしれません。

明日はおかずさんによるOculusの小ネタ集です。

あ、今回やったネタはVRでは重くなると思うのでオススメしないですよ。