GBufferを拡張せずに異方性スペキュラをやってみる

前回前々回のAdvent Calendar用記事ではGBufferを拡張してワールド空間タンジェントを保存、異方性スペキュラを実装とやっていました。

しかし、GBuffer拡張時にも書いたように、GBufferが増えることによってGBuffer書き込み時の処理速度に影響を与えるという問題が浮上してきます。

昔のハードウェアはGPUの計算能力が低かったため、色々なものをテクスチャに保存してルックアップテーブルとして参照するということをやっていました。その方が速かったわけです。

しかし現代のハードウェアは計算能力が大変高くなったため、ルックアップテーブルを利用することで遅くなる事例も出ています。

GBufferにしても同様で、GBufferを増やして書き込み、読み出しを行うより、今あるGBufferに圧縮して書き込み、読みだしてデコードする方が高速な場合も存在します。

もちろん、実装方法や条件によっても変化はすると思いますが、GBufferを増やさなくて済むならそれに越したことはないはずです。

某ハードの場合だと特に、GBufferを増やしたらESRAMに載らなくなりました、なんてことになりかねませんからね!

というわけで、前回まではGBufferを増やして高品質な異方性スペキュラをやる方法を提示したわけですが、今回はGBufferを増やさずに対応する方法を提示していきます。

ついでにマテリアル入力を増やして、タンジェントと異方性の度合いをマテリアルエディタで計算、設定ができるようにします。

今回もまたソースコード量が多目です。

一部のコードは前回と被りますので、そちらも参照していただければと思います。

まず最初にやっぱりShadingModelIDを増やします。

こちらは前回記事の頭の4つ分のコードですので、こちらを参照してください。

シェーダで利用するShadingModelIDは後で増やしますので、今は放置しておいてください。

次にマテリアル入力にパラメータを増やします。これが地味に面倒です。

Engine/Source/Runtime/Engine/Public/SceneTypes.h

128行目付近

enum EMaterialProperty

{

...

MP_PixelDepthOffset,

MP_Tangent,

MP_Anisotropy,

//^^^ New material properties go above here ^^^^

...

まずはTangentとAnisotropyというパラメータを列挙型に追加します。

新しいプロパティはこの上に書け、というコメントが有りますのでその上に書いていきます。

あとはこのプロパティに対応する各種追加を行います。

Engine/Source/Runtime/Engine/Classes/Materials/Material.h

392行目付近

UPROPERTY()

FScalarMaterialInput PixelDepthOffset;

UPROPERTY()

FVectorMaterialInput Tangent;

UPROPERTY()

FScalarMaterialInput Anisotropy;

Engine/Source/Runtime/Engine/Classes/Materials/MaterialExpressionMakeMaterialAttributes.h

64行目付近

UPROPERTY()

FExpressionInput PixelDepthOffset;

UPROPERTY()

FExpressionInput Tangent;

UPROPERTY()

FExpressionInput Anisotropy;

マテリアル入力にTangentとAnisotropyの入れ物を用意しました。

Engine/Source/Editor/UnrealEd/Private/MaterialGraph.cpp

57行目付近

MaterialInputs.Add(FMaterialInputInfo(LOCTEXT("PixelDepthOffset", "Pixel Depth Offset"), MP_PixelDepthOffset));

MaterialInputs.Add(FMaterialInputInfo(LOCTEXT("Tangent", "Tangent"), MP_Tangent));

MaterialInputs.Add(FMaterialInputInfo(LOCTEXT("Anisotropy", "Anisotropy"), MP_Anisotropy));

Engine/Source/Runtime/Engine/Private/Materials/HLSLMaterialTranslator.h

341行目付近

Chunk[MP_PixelDepthOffset] = Material->CompilePropertyAndSetMaterialProperty(MP_PixelDepthOffset,this);

Chunk[MP_Tangent] = Material->CompilePropertyAndSetMaterialProperty(MP_Tangent, this);

Chunk[MP_Anisotropy] = Material->CompilePropertyAndSetMaterialProperty(MP_Anisotropy, this);

同、718行目付近

LazyPrintf.PushParam(*GenerateFunctionCode(MP_PixelDepthOffset));

LazyPrintf.PushParam(*GenerateFunctionCode(MP_Tangent));

LazyPrintf.PushParam(*GenerateFunctionCode(MP_Anisotropy));

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

2130行目付近

DoMaterialAttributeReorder(&PixelDepthOffset, Ar.UE4Ver());

DoMaterialAttributeReorder(&Tangent, Ar.UE4Ver());

DoMaterialAttributeReorder(&Anisotropy, Ar.UE4Ver());

static_assert(MP_MAX == 30, "New material properties must have DoMaterialAttributesReorder called on them to ensure that any future reordering of property pins is correctly applied.");

同、3490行目付近

FExpressionInput* UMaterial::GetExpressionInputForProperty(EMaterialProperty InProperty)

...

case MP_PixelDepthOffset: return &PixelDepthOffset;

case MP_Tangent: return &Tangent;

case MP_Anisotropy: return &Anisotropy;

同、3800行目付近

int32 UMaterial::CompilePropertyEx( FMaterialCompiler* Compiler, EMaterialProperty Property )

...

case MP_PixelDepthOffset: return PixelDepthOffset.CompileWithDefault(Compiler, Property);

case MP_Tangent: return Tangent.CompileWithDefault(Compiler, Property);

case MP_Anisotropy: return Anisotropy.CompileWithDefault(Compiler, Property);

同、4110行目付近

case MP_PixelDepthOffset:

Active = !IsTranslucentBlendMode((EBlendMode)BlendMode);

break;

case MP_Tangent:

case MP_Anisotropy:

Active = ShadingModel == MSM_Anisotropy;

break;

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

3260行目付近

int32 UMaterialExpressionMakeMaterialAttributes::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex, int32 MultiplexIndex)

...

static_assert(MP_MAX == 30,

"New material properties should be added to the end of the inputs for this expression. \

The order of properties here should match the material results pins, the make material attriubtes node inputs and the mapping of IO indices to properties in GetMaterialPropertyFromInputOutputIndex().\

Insertions into the middle of the properties or a change in the order of properties will also require that existing data is fixed up in DoMaterialAttriubtesReorder().\

");

EMaterialProperty Property = GetMaterialPropertyFromInputOutputIndex(MultiplexIndex);

switch (Property)

....

case MP_PixelDepthOffset: Ret = PixelDepthOffset.Compile(Compiler); Expression = PixelDepthOffset.Expression; break;

case MP_Tangent: Ret = Tangent.Compile(Compiler); Expression = Tangent.Expression; break;

case MP_Anisotropy: Ret = Anisotropy.Compile(Compiler); Expression = Anisotropy.Expression; break;

};

同、3320行目付近

UMaterialExpressionBreakMaterialAttributes::UMaterialExpressionBreakMaterialAttributes(const FObjectInitializer& ObjectInitializer)

...

static_assert(MP_MAX == 30,

"New material properties should be added to the end of the outputs for this expression. \

The order of properties here should match the material results pins, the make material attriubtes node inputs and the mapping of IO indices to properties in GetMaterialPropertyFromInputOutputIndex().\

Insertions into the middle of the properties or a change in the order of properties will also require that existing data is fixed up in DoMaterialAttriubtesReorder().\

");

...

Outputs.Add(FExpressionOutput(TEXT("PixelDepthOffset"), 1, 1, 1, 1, 0));

Outputs.Add(FExpressionOutput(TEXT("Tangent"), 1, 1, 1, 1, 0));

Outputs.Add(FExpressionOutput(TEXT("Anisotropy"), 1, 1, 1, 1, 0));

}

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

200行目付近

EMaterialValueType GetMaterialPropertyType(EMaterialProperty Property)

...

case MP_PixelDepthOffset: return MCT_Float;

case MP_Tangent: return MCT_Float3;

case MP_Anisotropy: return MCT_Float;

};

同、2170行目付近

const TMap CreatePropertyToIOIndexMap()

...

static_assert(MP_MAX == 30,

"New material properties should be added to the end of \"real\" properties in this map. Immediately before MP_MaterialAttributes . \

The order of properties here should match the material results pins, the inputs to MakeMaterialAttriubtes and the outputs of BreakMaterialAttriubtes.\

Insertions into the middle of the properties or a change in the order of properties will also require that existing data is fixed up in DoMaterialAttriubtesReorder().\

");

...

Ret.Add(MP_PixelDepthOffset, 24);

Ret.Add(MP_Tangent, 25);

Ret.Add(MP_Anisotropy, 26);

//^^^^ New properties go above here ^^^^

同、2230行目付近

int32 GetDefaultExpressionForMaterialProperty(FMaterialCompiler* Compiler, EMaterialProperty Property)

...

case MP_Refraction: return Compiler->Constant3(1, 0, 0);

case MP_Tangent: return Compiler->Constant3(1, 0, 0);

case MP_Anisotropy: return Compiler->Constant(0.0f);

同、2280行目付近

FString GetNameOfMaterialProperty(EMaterialProperty Property)

...

case MP_PixelDepthOffset: return TEXT("PixelDepthOffset");

case MP_Tangent: return TEXT("Tangent");

case MP_Anisotropy: return TEXT("Anisotropy");

};

C++コードの修正はこれくらいです。

次はシェーダの修正ですが、ここは少々気をつけなければなりません。

マテリアルエディタで作成されたマテリアルはMaterialTemplate.usfというファイルを元にしてシェーダコードに展開されます。

展開の仕方がかなりベタベタな書き方で、上から順番に%sと書かれている部分を置換していくだけという形式です。

ですので、MaterialTemplate.usfの%sの順番と、HLSLMaterialTranslator.hでコード生成している部分の順番が正しく合ってないとおかしなことになってしまいます。

前述のHLSLMaterialTranslator.hの修正項目では、TangentとAnisotropyのコード展開はPixelDepthOffsetの直下なので、MaterialTemplate.usfでも同様の場所に追加してやる必要がある、というわけです。

そのコードは以下のようになります。

Engine/Shaders/MaterialTemplate.usf

182行目付近

struct FMaterialPixelParameters

...

#if (ES2_PROFILE || ES3_1_PROFILE)

float4 LayerWeights;

#endif

half3 WorldTangent;

};

同、230行目付近

FMaterialPixelParameters MakeInitializedMaterialPixelParameters()

...

MPP.TangentToWorld = float3x3(1,0,0,0,1,0,0,0,1);

MPP.WorldTangent = 0;

return MPP;

}

同、1450行目付近

float GetMaterialPixelDepthOffset(FMaterialPixelParameters Parameters)

{

%s;

}

half3 GetMaterialTangentRaw(FMaterialPixelParameters Parameters)

{

%s;

}

half3 GetMaterialTangent(FMaterialPixelParameters Parameters)

{

half3 RetTangent;

RetTangent = GetMaterialTangentRaw(Parameters);

return RetTangent;

}

float GetMaterialAnisotropy(FMaterialPixelParameters Parameters)

{

%s;

}

問題のコードはここですね。

同、1670行目付近

void CalcMaterialParameters(

...

#if !PARTICLE_SPRITE_FACTORY

Parameters.Particle.MotionBlurFade = 1.0f;

#endif // !PARTICLE_SPRITE_FACTORY

half3 TangentTangent = GetMaterialTangent(Parameters);

#if MATERIAL_TANGENTSPACENORMAL

TangentTangent *= Parameters.TwoSidedSign;

#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4

// ES2 will rely on only the final normalize for performance

TangentTangent = normalize(TangentTangent);

#endif

// normalizing after the tangent space to world space conversion improves quality with sheared bases (UV layout to WS causes shrearing)

Parameters.WorldTangent = normalize(TransformTangentVectorToWorld(Parameters.TangentToWorld, TangentTangent));

#else

Parameters.WorldNormal = normalize(TangentTangent);

#endif

}

マテリアル入力はタンジェント空間のタンジェントも受け付けられるようにしています。

入力されたものがタンジェント空間かワールド空間かは法線のタンジェント空間ノーマルフラグを参照するようにしています。

なので、入力する法線と同じ空間のタンジェントを入力すればOKです。

今回の実装ではGBufferのCustomData(GBufferDのgbaチャンネル)にワールド空間タンジェントと異方性の度合いを圧縮して保存します。

この計算処理はちょっと面倒で、エンコードはGBufferに保存する際だけで済むのですが、デコードは複数箇所で利用することになります。

そこで、これらの処理を関数化しておきます。そうすると後でいろいろ楽できます。

Engine/Shaders/DeferredShadingCommon.usf

227行目付近

#define SHADINGMODELID_TWOSIDED_FOLIAGE 6

#define SHADINGMODELID_ANISOTROPY 7

#define SHADINGMODELID_NUM 8

float3 EncodeTangentAndAnisotropy(half3 WorldTangent, float Anisotropy)

{

float TangentXYLength = length(WorldTangent.xy);

float ZAngle = atan2(WorldTangent.y, WorldTangent.x); // -PI <= ZAngle <= PI

ZAngle = ZAngle / PI * 0.5f + 0.5f; // -> 0 <= ZAngle <= 1.0

float ZSign = WorldTangent.z >= 0.0f ? 0.0f : 128.0f;

float ZSign_and_Anisotropy = (ZSign + (Anisotropy * 127.0f)) / 255.0f;

return float3(TangentXYLength, ZAngle, ZSign_and_Anisotropy);

}

void DecodeTangentAndAnisotropy(out float3 WorldTangent, out float Anisotropy, float3 CustomData)

{

// Decompress WorldTanget and Anisotropy by polar coordination.

float polarLength = CustomData.x;

float polarAngle = (CustomData.y * 2.0f - 1.0f) * 3.1415926;

float s, c;

sincos(polarAngle, s, c);

float2 txy = float2(c, s) * polarLength;

float zSign = CustomData.z >= (127.5f / 255.0f) ? -1.0f : 1.0f;

WorldTangent = float3(txy, zSign * sqrt(max(1.0f - dot(txy.xy, txy.xy), 0.0f)));

Anisotropy = (zSign < 0.0f) ? CustomData.z - 0.5f : CustomData.z;

Anisotropy = Anisotropy * 255.0f / 127.0f;

}

浮動小数点4つは32ビット浮動小数点なら128ビット必要になるわけですが、保存可能なサイズはたったの24ビットです。

そこで、タンジェントは単位ベクトルであることを利用し、XY平面上の極座標とZ要素の符号を17ビットで保存、残りの7ビットで異方性の度合いを保存するようにしています。

計算量が多くなってしまうのは欠点ですが、こうしないとGBufferを増やさなければならないので致し方なし…

Engine/Shaders/BasePassPixelShader.usf

875行目付近

#elif MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE

GBuffer.ShadingModelID = SHADINGMODELID_TWOSIDED_FOLIAGE;

GBuffer.CustomData = EncodeSubsurfaceColor(SubsurfaceColor);

#elif MATERIAL_SHADINGMODEL_ANISOTROPY

GBuffer.ShadingModelID = SHADINGMODELID_ANISOTROPY;

half3 WorldTangent = MaterialParameters.WorldTangent;

float Anisotropy = GetMaterialAnisotropy(MaterialParameters);

GBuffer.CustomData = EncodeTangentAndAnisotropy(WorldTangent, Anisotropy);

エンコードはここで行います。

Engine/Shaders/ShadingModels.usf

243行目付近

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 WorldTangent;

float Anisotropy;

DecodeTangentAndAnisotropy(WorldTangent, Anisotropy, GBuffer.CustomData);

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

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

// Generalized microfacet specular

float aspect = sqrt(1.0f - Anisotropy * 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 )

...

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

ダイレクトライティングに対する処理はここです。

前回も提示した部分ですが、タンジェントと異方性の度合いの取得方法が変わってます。

今回はついでに環境マップとSSRも異方性を考慮するようにします。

こちらはUBIのFarCry4の実装を参考にしました。スライド資料はこちらです。

Engine/Shaders/ReflectionEnvironmentComputeShaders.usf

630行目付近

if( GBuffer.ShadingModelID != SHADINGMODELID_UNLIT )

{

float3 N = GBuffer.WorldNormal;

float3 V = -CameraToPixel;

if( GBuffer.ShadingModelID == SHADINGMODELID_ANISOTROPY )

{

float3 WorldTangent;

float Anisotropy;

DecodeTangentAndAnisotropy(WorldTangent, Anisotropy, GBuffer.CustomData);

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

float3 AnisoTangent = cross(B, V);

float3 AnisoNormal = cross(AnisoTangent, B);

N = normalize(lerp(N, AnisoNormal, Anisotropy));

}

ComputeShaderによる実装なので、SM5向けの実装と思われます。

PixelShaderを使ったバージョンもありますが、環境によってはそちらを修正する必要があるかもしれません。

Engine/Shaders/ScreenSpaceReflections.usf

125行目付近

float3 N = GBuffer.WorldNormal;

const float SceneDepth = CalcSceneDepth(UV);

const float3 PositionTranslatedWorld = mul( float4( ScreenPos * SceneDepth, SceneDepth, 1 ), View.ScreenToTranslatedWorld ).xyz;

const float3 V = normalize(View.TranslatedViewOrigin.xyz - PositionTranslatedWorld);

if( GBuffer.ShadingModelID == SHADINGMODELID_ANISOTROPY )

{

float3 WorldTangent;

float Anisotropy;

DecodeTangentAndAnisotropy(WorldTangent, Anisotropy, GBuffer.CustomData);

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

float3 AnisoTangent = cross(B, V);

float3 AnisoNormal = cross(AnisoTangent, B);

N = normalize(lerp(N, AnisoNormal, Anisotropy));

}

元のコードでは法線Nがconst扱いになっている点に注意です。

まあ、const外したところで困らないはずですが…

というわけで、長かったコードの提示はこれにて終了です。お疲れ様でした。

結果はまた動画にしてみたので参考にしてください。

平行光源が突然消えたり現れたりしてることがあるんですが、原因がわかりません。

不具合かもしれませんね…

精度はあまりよろしくないのは否定できませんが、十分使える場面はあるかと思います。

興味がある方は実装してみてはどうでしょう?