【UE5】 エンジンを改造してSubstrateにカスタムシェーディングを追加する

モデル 沙花叉クロヱ © 2016 cover corp. / マップ Goddess Temple
はじめに
Unreal Engine 5.7がリリースされSubstrateも正式版になりました。
いずれSubstrateに一本化するという話もあるようです。
Substrateでもトゥーンがしたい!ということでエンジンとシェーダーをカスタマイズしてトゥーンシェーダーを追加しようと思います。
エンジンはバージョンは5.7.1を使用しました。
前提知識
- エンジンのビルド環境
- レンダリングフローの理解
- Substrateの基本的な知識
エンジンの改造(C++)
マテリアルグラフ上で独自のパラメーターを渡せるようにSubstrateEyeBSDFノードを改造します。
エンジンのソースを開きSubstrateEyeBSDFを検索すると以下のファイルで使用されていることがわかります。
これらを修正していきます。
- \Engine\Source\Runtime\Engine\Public\Materials\MaterialExpressionSubstrate.h
- \Engine\Source\Runtime\Engine\Public\MaterialCompiler.h
- \Engine\Source\Runtime\Engine\Private\Materials\MaterialExpressionSubstrate.cpp
MaterialExpressionSubstrate.h
UMaterialExpressionSubstrateEyeBSDFクラスでSubstrate Eye BSDFノードが定義されています。
必要なパラメーターを設定するためのピンをクラスに定義します。
UCLASS(MinimalAPI, collapsecategories, hidecategories = Object, DisplayName = "Substrate Toon BSDF")
class UMaterialExpressionSubstrateEyeBSDF : public UMaterialExpressionSubstrateBSDF
{
GENERATED_UCLASS_BODY()
UPROPERTY()
FExpressionInput DiffuseColor;
UPROPERTY()
FExpressionInput Roughness;
UPROPERTY()
FExpressionInput Normal;
UPROPERTY()
FExpressionInput Tangent;
UPROPERTY()
FExpressionInput EmissiveColor;
// ... (以下略)
}
MaterialExpressionSubstrate.cpp
ここでは、マテリアルノードのピンの型定義や、コンパイラへのデータ受け渡しの処理を記述します。
UMaterialExpressionSubstrateEyeBSDF::Compile
パラメーターの整合性の確認やデフォルト値の設定を行います。
NormalやTangentがSharedLocalBasisで使われていることに注意して書き換えます。
int32 UMaterialExpressionSubstrateEyeBSDF::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
int32 NormalCodeChunk = CompileWithDefaultNormalWS(Compiler, Normal);
int32 TangentCodeChunk = CompileWithDefaultTangentWS(Compiler, Tangent);
const FSubstrateRegisteredSharedLocalBasis NewRegisteredSharedLocalBasis = SubstrateCompilationInfoCreateSharedLocalBasis(Compiler, NormalCodeChunk, TangentCodeChunk);
FSubstrateOperator& SubstrateOperator = Compiler->SubstrateCompilationGetOperator(Compiler->SubstrateTreeStackGetPathUniqueId());
SubstrateOperator.BSDFRegisteredSharedLocalBasis = NewRegisteredSharedLocalBasis;
if (SubstrateOperator.bUseParameterBlending)
{
return Compiler->Errorf(TEXT("Substrate Eye BSDF node cannot be used with parameter blending."));
}
else if (SubstrateOperator.bRootOfParameterBlendingSubTree)
{
return Compiler->Errorf(TEXT("Substrate Eye BSDF node cannot be the root of a parameter blending sub tree."));
}
int32 OutputCodeChunk = Compiler->SubstrateEyeBSDF(
CompileWithDefaultFloat3(Compiler, DiffuseColor, 0.0f, 0.0f, 0.0f),
CompileWithDefaultFloat1(Compiler, Roughness, 0.5f),
NormalCodeChunk,
TangentCodeChunk,
CompileWithDefaultFloat3(Compiler, EmissiveColor, 0.0f, 0.0f, 0.0f),
Compiler->GetSubstrateSharedLocalBasisIndexMacro(NewRegisteredSharedLocalBasis),
&SubstrateOperator);
return OutputCodeChunk;
}
UMaterialExpressionSubstrateEyeBSDF::SubstrateGenerateMaterialTopologyTree
SubstrateOperatorを設定します。
BSDFFeaturesはFSubstrateCompilationContext::SubstrateGenerateDerivedMaterialOperatorDataでピクセルあたりに必要なバイト数を計算するのに使われます。
SubstrateEyeBSDFは固定長ですが、ピンが接続状況に応じて追加のバッファを使用するか決定したい場合は、ここでフラグを設定します。
FSubstrateOperator* UMaterialExpressionSubstrateEyeBSDF::SubstrateGenerateMaterialTopologyTree(class FMaterialCompiler* Compiler, class UMaterialExpression* Parent, int32 OutputIndex)
{
FSubstrateOperator& SubstrateOperator = Compiler->SubstrateCompilationRegisterOperator(SUBSTRATE_OPERATOR_BSDF, Compiler->SubstrateTreeStackGetPathUniqueId(), this->MaterialExpressionGuid, Parent, Compiler->SubstrateTreeStackGetParentPathUniqueId());
SubstrateOperator.BSDFType = SUBSTRATE_BSDF_TYPE_EYE;
SubstrateOperator.BSDFFeatures = ESubstrateBsdfFeature::Eye;
SubstrateOperator.ThicknessIndex = Compiler->SubstrateThicknessStackGetThicknessIndex();
SubstrateOperator.bBSDFWritesEmissive = EmissiveColor.IsConnected();
return &SubstrateOperator;
}
使用するGBufferのサイズを変更する場合:
case SUBSTRATE_BSDF_TYPE_EYE:
{
// Custom encoding
SubstrateMaterialRequestedSizeByte += UintByteSize;
SubstrateMaterialRequestedSizeByte += UintByteSize;
// ESubstrateBsdfFeatureの値によって可変長にしてもよい
if (It.Has(ESubstrateBsdfFeature::ExCustomParameter))
{
SubstrateMaterialRequestedSizeByte += UintByteSize;
}
break;
}
MaterialCompiler.h / HLSLTranslator.h
パラメーターに対応した引数に変更します。
HLSLTranslator.h も同様に変更しておきます。
virtual int32 SubstrateEyeBSDF(
int32 DiffuseAlbedo, int32 Roughness, int32 Normal, int32 Tangent,
int32 EmissiveColor, const FString& SharedLocalBasisIndexMacro, FSubstrateOperator* PromoteToOperator) = 0;
HLSLTranslator.cpp
GetSubstrateEyeBSDF関数を呼び出すシェーダーコードを生成します。
int32 FHLSLMaterialTranslator::SubstrateEyeBSDF(
int32 DiffuseAlbedo, int32 Roughness, int32 EmissiveColor,
int32 Normal, int32 Tangent, const FString& SharedLocalBasisIndexMacro,
FSubstrateOperator* PromoteToOperator)
{
return AddCodeChunk(
MCT_Substrate, TEXT("Parameters.%s.PromoteParameterBlendedBSDFToOperator(GetSubstrateEyeBSDF(\n\
/*DiffuseAlbedo*/ %s,\n\
/*Roughness*/ %s,\n\
/*EmissiveColor*/ %s,\n\
/*SharedLocalBasisIndexMacro*/ %s),\n\
%u, %u, %u, %u) /* Normal = %s ; Tangent = %s */"),
*GetParametersSubstrateTreeName(CurrentSubstrateCompilationContext),
*SubstrateGetCastParameterCode(DiffuseAlbedo, MCT_Float3),
*SubstrateGetCastParameterCode(Roughness, MCT_Float),
*SubstrateGetCastParameterCode(EmissiveColor, MCT_Float),
*SharedLocalBasisIndexMacro,
PromoteToOperator->Index,
PromoteToOperator->BSDFIndex,
PromoteToOperator->LayerDepth,
PromoteToOperator->bIsBottom ? 1 : 0,
*GetParameterCode(Normal),
*GetParameterCode(Tangent)
);
}
パラメーターの読み書き(HLSL)
Substrateで独自のパラメーターを追加する場合、以下の3箇所の処理を書き換える必要があります。
- マテリアルノードからBSDFコンテキストへの変換
- BSDFコンテキストからGBufferへのパッキング
- GBufferからBSDFコンテキストへのアンパッキング
GetSubstrateEyeBSDF
マテリアルノードから渡されたパラメーターをFSubstrateBSDF構造体に設定する関数です。
FSubstrateBSDFは以下のようにフラグとパラメーターを持っています。
struct FSubstrateBSDF
{
uint State;
float4 VGPRs[5];
// ... (以下略)
}
パラメーターの格納順は自分で定義し、マクロを使用してアクセスします。
#define EYE_DIFFUSEALBEDO(X) X.VGPRs[0].xyz
#define EYE_ROUGHNESS(X) X.VGPRs[0].w
// 今回の例では使いませんが、様々なパラメーターを持つことができます。
#define EYE_F0(X) X.VGPRs[1].xyz
#define EYE_F90(X) X.VGPRs[2].xyz
#define EYE_SSSMFP(X) X.VGPRs[3].xyz
#define EYE_SSSPHASEANISOTROPY(X) X.VGPRs[3].w
#define EYE_HASSPECULAR(X) false
// 一部のシェーダーで呼ばれるので定義だけしておく
#define EYE_IRISDISTANCE(X) 0.f
#define EYE_IRISMASK(X) 0.f
#define EYE_IRISNORMAL(X) float3(0.f, 0.f, 0.f)
#define EYE_IRISPLANENORMAL(X) float3(0.f, 0.f, 0.f)
GetSubstrateEyeBSDF関数内でフラグやパラメーターを設定します。
FSubstrateData GetSubstrateEyeBSDF(float3 DiffuseAlbedo, float Roughness, float3 Emissive, uint SharedLocalBasisIndex)
{
FSubstrateData SubstrateData = GetInitialisedSubstrateData();
BSDF_SETTYPE (SubstrateData.InlinedBSDF, SUBSTRATE_BSDF_TYPE_EYE);
BSDF_SETEMISSIVE (SubstrateData.InlinedBSDF, Emissive);
EYE_DIFFUSEALBEDO (SubstrateData.InlinedBSDF) = DiffuseAlbedo;
EYE_ROUGHNESS (SubstrateData.InlinedBSDF) = Roughness;
// 必要に応じてフラグの設定を行います
// const bool bHasAnisotropy = Anisotropy != 0.f;
// BSDF_SETHASANISOTROPY (SubstrateData.InlinedBSDF, bHasAnisotropy);
// const bool bHasSSS = any(SSSMFP > 0.f);
// BSDF_SETSSSTYPE(SubstrateData.InlinedBSDF, bHasSSS ? SSS_TYPE_DIFFUSION : SSS_TYPE_NONE);
#if SUBSTRATE_INLINE_SHADING
SubstrateData.InlinedBSDF.Coverage = 1.0f;
#endif
#if USE_DEVELOPMENT_SHADERS
SubstrateData.PreviewColor = DiffuseAlbedo;
#endif
return SubstrateData;
}
PackSubstrateOut
マテリアルのステートやパラメーターをGBufferにパッキングします。
プロジェクトの設定でAdaptive GBufferを指定した場合GBufferの構成は以下のようになります。
- PixelHeader
- UINT32型テクスチャ
- 8ビットのフラグ
- 8ビットの追加のフラグ(Slabの場合)、あるいはAO値
- 16ビットの自由に使える領域
- TopLayerData
- UINT32型テクスチャ
- 2ビットのフラグ (
Invalid/Valid/SingleLayeredWater) - 22ビットのワールド法線
- 8ビットのラフネス値
- SubsrfaceData
- UINT32型テクスチャ
- 3ビットのサブサーフェスの種類を表すフラグ
- 29ビットのサブサーフェスデータ(
SubsurfaceProfileId/SSSMFP)
- マテリアルのパラメーターに応じた追加のバッファ
-
BSDF_TYPE_EYEの場合は UINT32型テクスチャ x2 -
FSubstrateCompilationContext::SubstrateGenerateDerivedMaterialOperatorDataのSubstrateMaterialRequestedSizeByteで決定されます
-
case SUBSTRATE_BSDF_TYPE_EYE:
{
bIsOnlySlab = false;
bIsSimpleMaterial = false;
bIsSingleMaterial = false;
TopLayerTotalWeight += BSDF.TopLayerDataWeight;
TopLayerData.Roughness += BSDF.TopLayerDataWeight * EYE_ROUGHNESS(BSDF);
TopLayerData.UnlitViewBaseColor += BSDF.TopLayerDataWeight * SubstrateGetBSDFBaseColor(BSDF);
TopLayerData.Material = TOP_LAYER_MATERIAL_VALID;
const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;
SubstratePixelHeader.SetHasSubsurface(bHasSSS);
SubstratePixelHeader.SetMaterialMode(HEADER_MATERIALMODE_EYE);
SubstratePixelHeader.SetCastContactShadow(false); // Eye shading disables contact shadow. See CastContactShadow(...)
bSubstrateSubsurfaceEnable = bHasSSS;
break;
}
PixelHeaderを追加のデータとともに書き出します。
else if (SubstratePixelHeader.IsEye())
{
// Data0 (Header_State|Header_AO|Data)
uint Out = 0;
HEADER_SETCOMMONSTATES(Out, SubstratePixelHeader.State);
Out = Out & HEADER_EYEENCODING_MASK;
SUBSTRATE_STORE_UINT1(Out);
#if (HEADER_EYEENCODING_BIT_COUNT) > 32
#error Substrate eye path header is > 32bits
#endif
}
独自のパラメーターを決定したバッファサイズに合うように書き出します。
サブサーフェスが有効な場合、設定しておきます。
case SUBSTRATE_BSDF_TYPE_EYE:
{
const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;
float3 DiffuseAlbedo = EYE_DIFFUSEALBEDO(BSDF);
float Alpha = 0.f;
uint EyeData1 = PackColorLinearToGamma2AlphaLinear(float4(DiffuseAlbedo, Alpha));
SUBSTRATE_STORE_UINT1(EyeData1);
const uint EyeData2 = 0;
SUBSTRATE_STORE_UINT1(EyeData2);
// const bool bHasSSS = BSDF_GETSSSTYPE(BSDF) != SSS_TYPE_NONE;
// if (bHasSSS)
// {
// SSSDataType = BSDF_GETSSSTYPE(BSDF);
// SSSDataAniso = EYE_SSSPHASEANISOTROPY(BSDF);
// SSSDataMFP = EYE_SSSMFP(BSDF);
// SSSDataBaseColor = EYE_DIFFUSEALBEDO(BSDF);
// }
}
break;
UnpackSubstrateBSDFIn
GBufferからデータを読み取りFSubstrateBSDFを設定して返します。
else if (SubstrateHeader.IsEye())
{
OutBSDF.State = Data0 & HEADER_EYEENCODING_MASK;
OutBSDF.LuminanceWeightV = 1.0f;
BSDF_SETTYPE(OutBSDF, SUBSTRATE_BSDF_TYPE_EYE);
BSDF_SETSHAREDLOCALBASISID(OutBSDF, 0);
BSDF_SETHASANISOTROPY(OutBSDF, 0);
BSDF_SETISTOPLAYER(OutBSDF, 1);
BSDF_SETSSSTYPE(OutBSDF, SSS_TYPE_NONE);
const float4 EyeData1 = UnpackRGBA8(Data1);
EYE_DIFFUSEALBEDO(OutBSDF) = SubstrateSrgbToLinear(EyeData1.xyz);
EYE_ROUGHNESS(OutBSDF) = SubstrateUnpackTopLayerData(SubstrateHeader.PackedTopLayerData).Roughness;
// Data2や追加のバッファのデータを読み取ることもできます
// EYE_CUSTOMDATA(OutBSDF) = UnpackR11G11B10F(Data2);
// EYE_EXDATA(OutBSDF) = UnpackRGBA8(SubstrateLoadUint1(SubstrateBuffer, SubstrateAddressing));
}
ライティング(HLSL)
最後に、ライティング計算を行う箇所を修正します。
SubstrateEvaluateBSDFCommon
直接光の計算パスで呼び出されます。
ここで独自のトゥーン調のシェーディングを実装します。
Sample.IntegratedDiffuseValueとSample.IntegratedSpecularValueに計算結果を代入します。
case SUBSTRATE_BSDF_TYPE_EYE:
{
const float3 DiffuseColor = EYE_DIFFUSEALBEDO(BSDFContext.BSDF);
// ビルボード風のライティング
const float LightingIntensity = saturate(BSDFContext.Context.VoL);
Sample.DiffuseColor = DiffuseColor;
Sample.DiffusePathValue = Diffuse_Lambert(DiffuseColor);
Sample.IntegratedDiffuseValue += (LightingIntensity * AreaLight.Falloff) * AreaLight.FalloffColor * Sample.DiffusePathValue;
Sample.IntegratedSpecularValue = 0.f;
Sample.bPostProcessSubsurface = false;
break;
}
SubstrateImportanceSampleBSDF
反射の計算パスで呼ばれます。
反射を使用しない場合は処理をスキップしても構いません。
case SUBSTRATE_BSDF_TYPE_EYE:
{
if (Out.Term & SHADING_TERM_SPECULAR && EYE_HASSPECULAR(BSDFContext.BSDF))
{
float SpecularRoughness = MakeRoughnessSafe(EYE_ROUGHNESS(BSDFContext.BSDF));
float2 Alpha = Pow2(SpecularRoughness);
float4 GGXSample = ImportanceSampleVisibleGGX(E, Alpha, BSDFContext.TangentV);
const float HPDF = GGXSample.w;
const float3 H = mul(GGXSample.xyz, BSDFContext.TangentBasis);
const float VoH = saturate(dot(BSDFContext.V, H));
Out.L = 2 * dot(BSDFContext.V, H) * H - BSDFContext.V;
Out.PDF = RayPDFToReflectionRayPDF(VoH, HPDF);
Out.Term = SHADING_TERM_SPECULAR;
Out.Weight = 1.f;
}
break;
}
SubstrateEvaluateForEnvLight
間接光の計算パスや反射光の合成パスで呼ばれます。
case SUBSTRATE_BSDF_TYPE_EYE:
{
float3 DiffuseColor = EYE_DIFFUSEALBEDO(BSDFContext.BSDF);
SubstrateEnvLightResult.DiffuseNormal = -View.ViewForward;
SubstrateEnvLightResult.DiffuseColor = DiffuseColor;
SubstrateEnvLightResult.DiffuseWeight = DiffuseColor;
if (bEnableSpecular && EYE_HASSPECULAR(BSDFContext.BSDF))
{
float3 F0 = EYE_F0(BSDFContext.BSDF);
float3 F90 = EYE_F90(BSDFContext.BSDF);
F90 *= F0RGBToMicroOcclusion(F0);
float SafeRoughness = MakeRoughnessSafe(EYE_ROUGHNESS(BSDFContext.BSDF));
FBxDFEnergyTermsRGB SpecularEnergyTerms = ComputeGGXSpecEnergyTermsRGB_Env(SafeRoughness, BSDFContext.Context.NoV, F0, F90);
FBxDFEnergyTermsRGB DiffuseEnergyTerms = ComputeDiffuseEnergyTermsRGB(SafeRoughness, BSDFContext.Context.NoV);
float3 EvalEnvBRDF = SpecularEnergyTerms.E;
float DirectionalAlbedo_SpecularTransmission = ComputeEnergyPreservation(SpecularEnergyTerms);
SubstrateEnvLightResult.DiffuseWeight *= DiffuseEnergyTerms.E;
SubstrateEnvLightResult.SpecularDirection = SubstrateGetOffSpecularPeakReflectionDir(BSDFContext.N, BSDFContext.R, SafeRoughness);
SubstrateEnvLightResult.SpecularWeight = EvalEnvBRDF;
SubstrateEnvLightResult.SpecularSafeRoughness = SafeRoughness;
SubstrateEnvLightResult.SpecularColor = F0;
SubstrateEnvLightResult.DiffuseWeight *= DirectionalAlbedo_SpecularTransmission;
}
break;
}
SubstrateDeferredLighting
デフォルトだとシャドウマップで影だと判定されるとライティングの計算がスキップされます。
このままだと一部が不自然に真っ暗になるので強制的にライティングを行うようにします。
const bool bNeedLighting = (BSDFShadowTerms.SurfaceShadow + BSDFShadowTerms.TransmissionShadow) > 0 || BSDF_GETTYPE(BSDF) == SUBSTRATE_BSDF_TYPE_EYE;
if (bNeedLighting)
{
// ...
InternalReadMaterialData_Substrate_Regular
よりフラットなライティングを行うためFLumenMaterialDataを生成する部分も変更します。
Out.WorldNormal = Out.TangentBasis[2];
if (BSDFType == SUBSTRATE_BSDF_TYPE_EYE)
{
Out.WorldNormal = -View.ViewForward;
}
Out.WorldNormalForPositionBias = Out.TangentBasis[2];
確認
エンジンのコンパイル後、ConsoleVariables.iniを編集してシェーダーのデバッグを有効にしてエンジンを起動し、プロジェクト設定でSubstrate GBuffer FormatをAdaptive GBufferに変更して描画を確認します。
r.ShaderDevelopmentMode=1

Lumen(Emissiveマテリアルからのライティング)の確認
結び
お疲れ様でした。
Substrateの改造やレンダリングパスのイメージを掴むヒントになれば幸いです。
おまけ
Slab BSDFのフラグを確認するとSingle LayoutやComplex Layoutの場合使われていないビットがあることがわかります。
カスタムシェーダーフラグを立てて各ライティングパスに介入する方針にするとマテリアルレイヤーを使えたりFuzzやGlintなど移植が大変な機能が使えるかもしれません。

