Unreal Engine 4のShading Modelを拡張して独自のライティングマテリアルを追加します。
この記事は2018年10月14日に開催される、Unreal Fest 2018の講演、**「UE4のライティング解体新書~効果的なNPRのためにライティングの仕組みを理解しよう~」**のスライドの補足記事です。
スライドはこちら
スライドでコードの変更部分などを説明しようとするとかなりのページじなってしまうので、別記事として作成します。
ベースとなるUE4のバージョンは4.20です。
Shading Model
マテリアルのライティングの種類です。エンジンにもとから用意されているものには
- Unlit
- Default Lit
- Subsurface
- Eye
- Hair
などがありますが、基本的にはPBRなライティング用のモデルです。これらにない独自のシェーディングを実装したい場合に自分でShading Modelを追加する手法を解説します。
追加するShading Model
サンプルとしてシンプルなハーフランバートシェーディングを作成します。
ハーフランバートとは、通常、法線と光源の向きが90度以上になると明るさが0になり、真っ黒になるのを180度で真っ黒になるようにする手法です。物理的には正しくないですが、陰影が柔らかになります。
最近のゲームエンジンでは真っ黒になった部分には間接光が加算されるので不要なのですが、間接光の計算が実装されていない大昔はアンビエントカラーを指定したり、ハーフランバートにしたり工夫していたものでした。
仕様、簡単です。
- ピクセルの明るさを法線と光源ベクトルの内積に1を足して2で割ったものを使用する。
通常は法線と光源ベクトルの内積を0でクリップしたものを使用します。シェーダーコードでは
saturate(dot(N, L))
になりますが、これを
(dot(N,L)+1) * 0.5
にします。内積は1から-1の結果を返すので、それを1-0の範囲に変換して使います。
エンジンコードの変更
まずはエンジン側です。新しいShading Modelを定義します。
Shading ModelはEngineTypes.hで定義されています。
UENUM()
enum EMaterialShadingModel
{
MSM_Unlit UMETA(DisplayName = "Unlit"),
MSM_DefaultLit UMETA(DisplayName = "Default Lit"),
MSM_Subsurface UMETA(DisplayName = "Subsurface"),
MSM_PreintegratedSkin UMETA(DisplayName = "Preintegrated Skin"),
MSM_ClearCoat UMETA(DisplayName = "Clear Coat"),
MSM_SubsurfaceProfile UMETA(DisplayName = "Subsurface Profile"),
MSM_TwoSidedFoliage UMETA(DisplayName = "Two Sided Foliage"),
MSM_Hair UMETA(DisplayName = "Hair"),
MSM_Cloth UMETA(DisplayName = "Cloth"),
MSM_Eye UMETA(DisplayName = "Eye"),
// @UEFEST2018 BEGIN
MSM_HalfLambert UMETA(DisplayName = "Half Lambert"),
// @UEFEST2018 END
MSM_MAX,
};
ソースを改変するときは、このように検索しやすいコメントで囲っておくとエンジンのアップデート時のマージ作業が楽になります。
次はShading ModelのIDから文字列を返す部分。
FString GetShadingModelString(EMaterialShadingModel ShadingModel)
{
FString ShadingModelName;
switch(ShadingModel)
{
case MSM_Unlit: ShadingModelName = TEXT("MSM_Unlit"); break;
case MSM_DefaultLit: ShadingModelName = TEXT("MSM_DefaultLit"); break;
case MSM_Subsurface: ShadingModelName = TEXT("MSM_Subsurface"); break;
case MSM_PreintegratedSkin: ShadingModelName = TEXT("MSM_PreintegratedSkin"); break;
case MSM_ClearCoat: ShadingModelName = TEXT("MSM_ClearCoat"); break;
case MSM_SubsurfaceProfile: ShadingModelName = TEXT("MSM_SubsurfaceProfile"); break;
case MSM_TwoSidedFoliage: ShadingModelName = TEXT("MSM_TwoSidedFoliage"); break;
case MSM_Cloth: ShadingModelName = TEXT("MSM_Cloth"); break;
case MSM_Eye: ShadingModelName = TEXT("MSM_Eye"); break;
// @UEFEST2018 BEGIN
case MSM_HalfLambert: ShadingModelName = TEXT("MSM_HalfLambert"); break;
// @UEFEST2018 END
default: ShadingModelName = TEXT("Unknown"); break;
}
return ShadingModelName;
}
Shading Modelの数を返す部分
void UpdateMaterialShaderCompilingStats(const FMaterial* Material)
{
/* 中略 */
switch(Material->GetShadingModel())
{
case MSM_Unlit:
INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumUnlitMaterialShaders,1);
break;
case MSM_DefaultLit:
case MSM_Subsurface:
case MSM_PreintegratedSkin:
case MSM_ClearCoat:
case MSM_Cloth:
case MSM_SubsurfaceProfile:
case MSM_TwoSidedFoliage:
// @UEFEST2018 BEGIN
case MSM_HalfLambert:
// @UEFEST2018 END
INC_DWORD_STAT_BY(STAT_ShaderCompiling_NumLitMaterialShaders,1);
break;
default: break;
};
Shader Codeに設定するdefineを生成する部分
void FMaterial::SetupMaterialEnvironment(
EShaderPlatform Platform,
const FUniformExpressionSet& InUniformExpressionSet,
FShaderCompilerEnvironment& OutEnvironment
) const
{
/* 中略 */
switch(GetShadingModel())
{
case MSM_Unlit: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_UNLIT"), TEXT("1")); break;
case MSM_DefaultLit: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_DEFAULT_LIT"), TEXT("1")); break;
case MSM_Subsurface: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_SUBSURFACE"), TEXT("1")); break;
case MSM_PreintegratedSkin: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN"), TEXT("1")); break;
case MSM_SubsurfaceProfile: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE"), TEXT("1")); break;
case MSM_ClearCoat: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_CLEAR_COAT"), TEXT("1")); break;
case MSM_TwoSidedFoliage: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE"), TEXT("1")); break;
case MSM_Hair: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_HAIR"), TEXT("1")); break;
case MSM_Cloth: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_CLOTH"), TEXT("1")); break;
case MSM_Eye: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_EYE"), TEXT("1")); break;
// @UEFEST2018 BEGIN
case MSM_HalfLambert: OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_HALF_LAMBERT"), TEXT("1")); break;
// @UEFEST2018 END
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"));
};
実はこれだけです。ただ単純にShading Modelを追加するだけなら簡単です。
もう少し凝り始めると、マテリアル入力ピンの名前を変えたり増やしたりという作業が必要になりますが、そのへんはまたの機会に。難しくはありません。
シェーダーコードの変更
次は実際の描画を担うシェーダーコードに手を入れます。
まずはShading Model IDを追加。
#define SHADINGMODELID_UNLIT 0
#define SHADINGMODELID_DEFAULT_LIT 1
#define SHADINGMODELID_SUBSURFACE 2
#define SHADINGMODELID_PREINTEGRATED_SKIN 3
#define SHADINGMODELID_CLEAR_COAT 4
#define SHADINGMODELID_SUBSURFACE_PROFILE 5
#define SHADINGMODELID_TWOSIDED_FOLIAGE 6
#define SHADINGMODELID_HAIR 7
#define SHADINGMODELID_CLOTH 8
#define SHADINGMODELID_EYE 9
// @UEFEST2018 BEGIN
// ORG>#define SHADINGMODELID_NUM 10
#define SHADINGMODELID_HALF_LAMBERT 10
#define SHADINGMODELID_NUM 11
// @UEFEST2018 BEGIN
#define SHADINGMODELID_MASK 0xF
これを見てわかるとおり、Shading Modelは15個までしか持てません。GBufferに4bitの領域を確保して書き込んでいる都合です。
次にエディタでGBuffer表示する場合のShading Modelごとの色を返す部分。適当な色を設定します。
float3 GetShadingModelColor(uint ShadingModelID)
{
/* 中略 */
switch(ShadingModelID)
{
case SHADINGMODELID_UNLIT: return float3(0.1f, 0.1f, 0.2f); // Dark Blue
case SHADINGMODELID_DEFAULT_LIT: return float3(0.1f, 1.0f, 0.1f); // Green
case SHADINGMODELID_SUBSURFACE: return float3(1.0f, 0.1f, 0.1f); // Red
case SHADINGMODELID_PREINTEGRATED_SKIN: return float3(0.6f, 0.4f, 0.1f); // Brown
case SHADINGMODELID_CLEAR_COAT: return float3(0.1f, 0.4f, 0.4f); // Brown
case SHADINGMODELID_SUBSURFACE_PROFILE: return float3(0.2f, 0.6f, 0.5f); // Cyan
case SHADINGMODELID_TWOSIDED_FOLIAGE: return float3(0.2f, 0.2f, 0.8f); // Cyan
case SHADINGMODELID_HAIR: return float3(0.6f, 0.1f, 0.5f);
case SHADINGMODELID_CLOTH: return float3(0.7f, 1.0f, 1.0f);
case SHADINGMODELID_EYE: return float3(0.3f, 1.0f, 1.0f);
// @UEFEST2018 BEGIN
case SHADINGMODELID_HALF_LAMBERT: return float3(0.5f, 0.5f, 0.5f);
// @UEFEST2018 END
default: return float3(1.0f, 1.0f, 1.0f); // White
}
GBufferにShading Model IDを書き込みます。
/* ファイルの最後の部分 */
#else
GBuffer.Metallic = saturate(GetMaterialCustomData1(MaterialParameters)); // Iris Distance
#if NUM_MATERIAL_OUTPUTS_GETTANGENTOUTPUT > 0
float3 Tangent = GetTangentOutput0(MaterialParameters);
GBuffer.CustomData.yz = UnitVectorToOctahedron( normalize(Tangent) ) * 0.5 + 0.5;
#endif
#endif
// @UEFEST2018 BEGIN
#elif MATERIAL_SHADINGMODEL_HALF_LAMBERT
GBuffer.ShadingModelID = SHADINGMODELID_HALF_LAMBERT;
// @UEFEST2018 END
#else
// missing shading model, compiler should report ShadingModelID is not set
#endif
}
そして、最も重要な部分、実際に拡張したShading Modelで描画する部分。
下記の関数をShadingModels.ushに追加。
// @UEFEST2018 BEGIN
FDirectLighting HalfLambertBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
NoL = (dot(N, L) + 1.f) * 0.5f;
Lighting.Diffuse = AreaLight.FalloffColor * (Falloff * NoL) * Diffuse_Lambert( GBuffer.DiffuseColor );
return Lighting;
}
// @UEFEST2018 END
呼び出し部分
FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
switch( GBuffer.ShadingModelID )
{
case SHADINGMODELID_DEFAULT_LIT:
return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE:
return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_PREINTEGRATED_SKIN:
return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLEAR_COAT:
return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_SUBSURFACE_PROFILE:
return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_TWOSIDED_FOLIAGE:
return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_HAIR:
return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_CLOTH:
return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
case SHADINGMODELID_EYE:
return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
// @UEFEST2018 BEGIN
case SHADINGMODELID_HALF_LAMBERT:
return HalfLambertBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
// @UEFEST2018 END
default:
return (FDirectLighting)0;
}
}
結果確認
では実際に描画してみましょう。
左がハーフランバート、右が標準のDefault Litです。
Shading Modelの拡張自体はとても簡単です。
実際には影の扱い、独自のスペキュラ、サブサーフェスカラーの使用、独自の情報をGBufferに格納など、ここからやることが沢山ありますが、まずは入り口です。
さらに改造する場合のヒント
マテリアルノードの入力ピンの名前を変えたい場合は
Source/Editor/UnrealEd/Private/MaterialGraph.cppを修正します。
マテリアルノードの入力ピンの有効無効は
Source/Runtime/Engine/Private/Materials/Material.cppを修正。
サブサーフェスカラーを使う場合は、
Source/Runtime/Engine/Public/MaterialShared.hのIsSubsurfaceShadingModel()を修正。
Shaders/Private/BasePassCommon.ushの#define WRITES_CUSTOMDATA_TO_GBUFFERに定義を追加。