UE4 Engine Fix-Hair ShadingModel 在 不投影的 光源下曝光的問題
起因問題
又是一個光照爆掉的問題,重現的話,只要拖一個 點光源在頭髮周圍,然後關閉 cast shadow,就可以重現這個問題,整個頭髮會直接爆掉,類似這樣,欣賞一下 Mike 老師新染的頭髮,喜不喜歡~
可以看到整個背光面曝光都會非常嚴重,這個問題在目前最新的 UE版本 4。25。1 以及 4。24 都會存在,不止點光源,包括粒子光源,只有不投影,都會有這個問題
定位原因
找原因就是看程式碼了,先看一遍完整的流程
計算光照的入口在 DeferredLightingCommon。ush 裡面的
GetDynamicLightingSplit
(4。25之前的版本是叫GetDynamicLighting,這個版本累加 Lighting 的時候拆分了 DiffuseColor 和 SpecularColor,所以後面加了 Split 字尾)。
這個方法裡會先呼叫
GetShadowTerms
計算陰影項,根據光源衰減,計算賦值 Shadow。SurfaceShadow 和 Shadow。TransmissionShadow
比如點光源
Shadow。SurfaceShadow = LightAttenuation。z * StaticShadowing;
Shadow。TransmissionShadow = LightAttenuation。w * StaticShadowing;
然後基於螢幕空間的 RayMarching 計算 contact shadow,
會根據得到 的 ContactShdow 縮放 Shadow。TransmissionShadow
得到陰影項後,呼叫
IntegrateBxDF
分發到不同的 ShadingModel 的 BxDF 計算光照
具體到 Hair 的 shadingModel ,是呼叫到
HairBxDF
清空 Diffuse 和 Specular,呼叫 HairShading 計算 Lighting。Transmission
Lighting。Diffuse = 0;
Lighting。Specular = 0;
在
HairShading
裡面就是 UE目前使用的 Marschner + KajiyaKay 的 Hair shading model ,具體的這兩個演算法已經有很多文章介紹了,這裡就不贅述了
簡單來說,就是 利用基於物理的 Marschner 模型,計算光打在頭髮上之後的 R 項(第一次反射),TT 項(第一次穿透),RTR 項(第一次穿透再反射回來的),然後再用 KajiyaKay 的經驗模型, 簡單模擬一下散射項,尤其是背光面沒有 散射項的話,會稍微不太真實,和受光區域差別比較明顯,這部分距離邏輯在 KajiyaKayDiffuseAttenuation
float3 KajiyaKayDiffuseAttenuation(FGBufferData GBuffer, float3 L, float3 V, half3 N, float Shadow)
{
// Use soft Kajiya Kay diffuse attenuation
float KajiyaDiffuse = 1 - abs(dot(N, L));
float3 FakeNormal = normalize(V - N * dot(V, N));
//N = normalize( DiffuseN + FakeNormal * 2 );
N = FakeNormal;
// Hack approximation for multiple scattering。
float Wrap = 1;
float NoL = saturate((dot(N, L) + Wrap) / Square(1 + Wrap));
float DiffuseScatter = (1 / PI) * lerp(NoL, KajiyaDiffuse, 0。33) * GBuffer。Metallic;
float Luma = Luminance(GBuffer。BaseColor);
float3 ScatterTint = pow(GBuffer。BaseColor / Luma, 1 - Shadow);
return sqrt(GBuffer。BaseColor) * DiffuseScatter * ScatterTint;
}
這部分程式碼也是後面解決問題,可能需要用到的
Shading計算完成後,就是出來的累加光照了
LightAccumulator_AddSplit( LightAccumulator, Lighting。Diffuse, Lighting。Specular, Lighting。Diffuse, LightColor * LightMask * Shadow。SurfaceShadow, bNeedsSeparateSubsurfaceLightAccumulation );
LightAccumulator_AddSplit( LightAccumulator, Lighting。Transmission, 0。0f, Lighting。Transmission, LightColor * LightMask * Shadow。TransmissionShadow, bNeedsSeparateSubsurfaceLightAccumulation );
第一行的LightAccumulator 由於上面 HairBxdf 清空了 diffuse 和 specular,所以是0
第二行就是關鍵了,頭髮之所以爆掉,也是因為發現這裡的 Shadow。TransmissionShadow 永遠是 1,導致背光面也完整疊加了上面模擬的光照結果
問題解決
Shadow。TransmissionShadow 項的計算是在
GetShadowTerms
裡面,細看一下程式碼,首先 它由於不投影,不會經過光源衰減,所以是預設值1,之後計算 contact shadow,發現又有這麼一段
if( GBuffer。ShadingModelID == SHADINGMODELID_HAIR || GBuffer。ShadingModelID == SHADINGMODELID_EYE )
{
// If hair transmittance is enabled, sharp shadowing should already be handled by
// the dedicated deep shadow maps。 Thus no need for contact shadow
const bool bUseComplexTransmittance = (LightData。HairTransmittance。ScatteringComponent & HAIR_COMPONENT_MULTISCATTER) > 0;
if (!bUseComplexTransmittance)
{
Shadow。TransmissionShadow *= ContactShadow;
}
}
預設不使用 Hair strands (會有multi scatter)的話,LightData。HairTransmittance。ScatteringComponent 是隻有 R,TT,RTR的
o。ScatteringComponent = HAIR_COMPONENT_R | HAIR_COMPONENT_TT | HAIR_COMPONENT_TRT;
沒有HAIR_COMPONENT_MULTISCATTER 標記,這也就導致這裡的 contact shadow 也是白算的,這裡的分支一個都不會走,所以就導致最終輸出了預設值 1。
最簡單的改法
就是在上面判斷的地方加一個或就解決了
if (!bUseComplexTransmittance || LightData。ShadowedBits < 1)
如果沒有光源不產生陰影的話,就使用 contact shadow 的結果。(ShadowedBits 在文底有解釋)
這是修改後的結果
Non Shadow 的 點光源光照
允許 Cast Shadow 有投影的話,會多一層過渡出來,因為根據光源型別計算的陰影遮蔽項的過渡,效果會自然一些
對於效果要求沒有那麼高的直接這麼改應該就好了,但如圖這麼背面完全沒有散射,如果希望提升一下效果的,可以要麼換用 Hair Strands,它會計算一個完整的自己的 deep shadow map,並且有 GlobalScattering 和 LocalScattering,當然,效果和複雜度都會提升
float3 EvaluateHairMultipleScattering(
const FHairTransmittanceData TransmittanceData,
const float Roughness,
const float3 Fs)
{
return TransmittanceData。GlobalScattering * (Fs + TransmittanceData。LocalScattering) * TransmittanceData。OpaqueVisibility;
}
要麼上面不那麼改shadowTerm,到BxDF的時候,先調整一下普通 Hair shading model 的 那個 稍微 tricky 的 KajiyaKay 的散射模型,不能像現在這麼一路 += 過來,再比如可以暴露它的wrap項,目前寫了定值1
這個其實就是 wrap lighting 的計算公式,一種最簡化的計算 area light 的公式,這裡使用這個,顯然是為了效率,但我們可以調整 wrap項,wrap=0 就當作一個點光源,wrap=1表示一個完全覆蓋的area light
然後 Marschner model裡修改傳入目前預設只有 1 的 Backlit 的有關的程式碼,稍微增強背光面的光照
我有嘗試,但由於沒什麼物理依據,所以就不貼具體的改法了。
那大概這樣~
附:
ShadowedBits 的填充
SimpleLight ,沒有shadow map,填充很簡單
// No shadowmap channels for simple lights
uint32 ShadowMapChannelMask = 0;
ShadowMapChannelMask |= SimpleLightLightingChannelMask << 8;
LightData。LightDirectionAndShadowMapChannelMask = FVector4(FVector(1, 0, 0), *((float*)&ShadowMapChannelMask));
普通光源多一些
uint32 LightTypeAndShadowMapChannelMaskPacked =
(ShadowMapChannel == 0 ? 1 : 0) |
(ShadowMapChannel == 1 ? 2 : 0) |
(ShadowMapChannel == 2 ? 4 : 0) |
(ShadowMapChannel == 3 ? 8 : 0) |
(DynamicShadowMapChannel == 0 ? 16 : 0) |
(DynamicShadowMapChannel == 1 ? 32 : 0) |
(DynamicShadowMapChannel == 2 ? 64 : 0) |
(DynamicShadowMapChannel == 3 ? 128 : 0);
LightTypeAndShadowMapChannelMaskPacked |= LightProxy->GetLightingChannelMask() << 8;
// pack light type in this uint32 as well
LightTypeAndShadowMapChannelMaskPacked |= SortedLightInfo。SortKey。Fields。LightType << 16;