您當前的位置:首頁 > 攝影

UE4 Engine Fix-Hair ShadingModel 在 不投影的 光源下曝光的問題

作者:由 繁弱 發表于 攝影時間:2020-07-12

起因問題

又是一個光照爆掉的問題,重現的話,只要拖一個 點光源在頭髮周圍,然後關閉 cast shadow,就可以重現這個問題,整個頭髮會直接爆掉,類似這樣,欣賞一下 Mike 老師新染的頭髮,喜不喜歡~

UE4 Engine Fix-Hair ShadingModel 在 不投影的 光源下曝光的問題

可以看到整個背光面曝光都會非常嚴重,這個問題在目前最新的 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 的 點光源光照

UE4 Engine Fix-Hair ShadingModel 在 不投影的 光源下曝光的問題

允許 Cast Shadow 有投影的話,會多一層過渡出來,因為根據光源型別計算的陰影遮蔽項的過渡,效果會自然一些

UE4 Engine Fix-Hair ShadingModel 在 不投影的 光源下曝光的問題

對於效果要求沒有那麼高的直接這麼改應該就好了,但如圖這麼背面完全沒有散射,如果希望提升一下效果的,可以要麼換用 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

Nol=saturate(\frac{dot(N,L)+Wrap}{1+Wrap})

這個其實就是 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;