標籤:

基於屏幕空間次表面散射預積分方法的實時皮膚渲染

基於屏幕空間次表面散射預積分方法的實時皮膚渲染

48 人贊了文章

寫作初衷

最近在研究真實感皮膚渲染,渲染的效果還說的過去,故在此和大家分享一下。參考了很多論文與大牛們的想法,技術鄙陋,歡迎指正。

皮膚渲染原理

由於是真實感渲染,就需要明白光線與皮膚交互的原理。這部分就會涉及到光線傳播性質與皮膚結構。

首先來介紹下光線傳播的性質。初中學過物理的都知道光線從一種介質射向另一種介質時會發生兩種現象,一部分光線在介質交界處發生了反射, 並未進入另外一種介質,另外一部分光線則進入了另一種介質就會發生折射(透射)。光線的兩種現象合稱為光線的散射,如下圖:

光的散射(scattering)——反射(reflection)和折射(refraction)

反射(reflected light)和透射光(transmitted light)的相互作用

反射部分光照的輻射亮度(radiance)和入射光照的輻射照度(irradiance)之比是一個關於入射角度、出射角度的函數,這個函數就被稱之為雙向反射分布函數(BRDF)。穿越介質的那部分光照的輻射亮度和輻射照度的函數就被稱為雙向透射分布函數(BTDF)。這兩部分出射光的輻射亮度總和和入射光的輻射照度的比例就被叫做雙向散射分布函數(BSDF),即BSDF = BRDF + BTDF

皮膚渲染基石——次表面散射(SSS-Subsurface Scatter)

如果我們把光線行進的路線分為反射和透射,反射用R表示,透射用T表示,那麼光線從一個點到另外一個點之間行進的路線就可以用R和T表示,比如BRDF描述的路徑就是R,BTDF描述的路徑就是TT,除此之外可能還會出現TRT,TRRRT等光照路線,由此我們可以想見,在光線入射點的附近應該有許多的出射光線。實際渲染中,如果光線出射點的位置和入射點相距不足一個像素,我們就認為入射點和出射點位置相同,這時候當前像素的光照只受其自身影響;如果入射點和出射點相距超過一個像素,則表示某個像素的光照結果不僅僅受當前像素影響,同時還受附近其他像素的光照影響,這就是我們常說的次表面散射效果了。

《Real-Time Rendering 3rd》一書中對次表面散射的闡釋。紅色區域表示一個像素的大小,當出射光線集中分布在紅色區域內時,則認為次表面散射效果可以忽略,當出射光線較為均勻地分布在綠色區域內時,則需要單獨考慮次表面散射效果。

基於路徑追蹤實現的次表面散射渲染效果圖 ?Photorealizer

接下來說明下皮膚的構成

皮膚是一個多層結構,其表面油脂層貢獻了皮膚光照的主要反射部分,而油脂層下面的表皮層和真皮層則貢獻了主要的次表面散射部分。

次表面散射光線密度分布是一個各向同性函數,也就是說一個像素受周邊像素的光照影響的比例只和兩個像素間的距離有關。這個密度分布函數在有些地方稱為diffusion profile,用R(r)來表示。實際上所有材質都存在次表面散射現象,區別只在於其密度分布函數R(r)的集中程度,如果該函數的絕大部分能量都集中在入射點附近(r=0),就表示附近像素對當前像素的光照貢獻不明顯,可以忽略,則在渲染時我們就用漫反射代替,如果該函數分布比較均勻,附近像素對當前像素的光照貢獻明顯,則需要單獨計算次表面散射。據此次表面散射的計算可以分為兩個部分:

(1)對每個像素進行一般的漫反射計算。

(2)根據diffusion profile和(1)中的漫反射結果,加權計算周圍若干個像素對當前像素的次表面散射貢獻。

不同的皮膚渲染方法,通常就是對diffusion profile的不同近似,根據加權計算所在的空間,將皮膚的渲染方法分為圖像空間的方法屏幕空間的方法兩類。由於圖像空間內一般像素計算負擔較大(計算複雜度和模型個數正相關),並且針對每一個次表面散射效果的模型都需要若干張貼圖,顯存開銷也較大。而屏幕空間的計算複雜度和模型個數無關,且只需要一張屏幕大小的貼圖,因此目前主流方案均採用屏幕空間的次表面散射,因此只說明下基於屏幕空間方法,圖像空間方法請自行搜索。

屏幕空間的方法

屏幕空間的方法類似於圖像空間的方法,只是計算irradiance時輸出的位置不是UV坐標而是模型的投影坐標,此外還需要將屏幕空間中屬於皮膚的材質的像素用stencil buffer標記出來,然後對標記出的皮膚材質進行若干次卷積操作,卷積核的權重由diffusion profile確定,卷積核的大小則需要根據當前像素的深度(d(x,y))及其導數(dFdx(d(x,y))和dFdy(d(x,y)))來確定。

屏幕空間的演算法示意圖

預積分的方法

圖像空間的方法和屏幕空間的方法很大程度上都是通過周邊像素對當前像素的光照貢獻來實現次表面散射的效果,從原理上區別不大,方法之間的區別通常只是在於如何去近似diffusion profile,在性能和效果上有一個較好的平衡。Pre-Integrated Skin Shading的方法則不同於上述方法,是一個從結果反推實現的方案。觀察次表面散射效果可以發現:

(1)次表面散射的效果主要發生在曲率較大的位置(或者說光照情況變化陡峭的位置),而在比較平坦的位置則不容易顯現出次表面散射的效果(比如鼻樑處的次表面散射就比額頭處的次表面散射效果要強)

(2)在有凹凸細節的部位也容易出現次表面散射,這一點其實和(1)說的是一回事,只是(1)中的較大麴率是由幾何形狀產生的,而(2)中的凹凸細節則一般是通過法線貼圖來補充。

結合以上兩個觀察,預積分的方法是把次表面散射的效果預計算成一張二維查找表,查找表的參數分別是dot(N, L)和曲率,因為這兩者結合就能夠反映出光照隨著曲率的變化。

上圖中1/r表示的就是曲率,相應的計算方法:

frac{1}{r} =frac{Delta N}{Delta p}=frac{left| fwidth(normal) 
ight| }{left| fwidth(position) 
ight| }

上圖右邊就是曲率顯示出來的效果,可以看出類似額頭這樣的位置曲率是比較小的,而鼻子等位置的曲率就比較大。

上述方法解決了(1)的問題,但對於(2)提到的一些凹凸起伏的細節,由於它不是由幾何造型產生的,因此無法用上述曲率計算的方法確認其是否有明顯的次表面散射效果,因此作者進一步結合了bent normal的方法來實現這些細節處的次表面散射效果。

Bent Normal其實不是專門用來處理次表面散射專有的方法,可以應用於很多預計算複雜光照的情況,簡單的說就是把包含AO,陰影,次表面散射之類的複雜光照信息pre-bake到法線裡面,然後計算光照時使用pre-bake得到的法線,結合正常的光照計算方法,就能得到比較複雜的光照效果。

bent normal的方案大致來說就是對法線貼圖進行模糊的操作,以實現類似次表面散射的泛光效果,然後在計算最終光照的時候,使用原始的法線貼圖和模糊後的法線貼圖的線性插值結果作為最終的bent normal。

照明計算

由於是在unity2018引擎下實現,所以直接調用了LightingStandardSpecular函數進行光照計算,unity2018下的全局光照也是採用BRDF模型來實現的,所以整體的光照效果很好。

自定義函數

inline void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float2 uv = IN.uv_MainTex; #if USE_ALBEDO float4 c = tex2D (_MainTex, uv) * _Color; #if USE_DETAILALBEDO float4 dA = tex2D(_DetailAlbedo, IN.uv_DetailAlbedo); c.rgb = lerp(c.rgb, dA.rgb, _AlbedoBlend); #endif o.Albedo = c.rgb; o.Alpha = c.a; #else #if USE_DETAILALBEDO float4 dA = tex2D(_DetailAlbedo, IN.uv_DetailAlbedo); o.Albedo.rgb = lerp(1, dA.rgb, _AlbedoBlend) * _Color; #else o.Albedo = _Color.rgb; o.Alpha = _Color.a; #endif #endif #if USE_OCCLUSION o.Occlusion = lerp(1, tex2D(_OcclusionMap, IN.uv_MainTex).r, _Occlusion); #else o.Occlusion = 1; #endif #if USE_SPECULAR float4 spec = tex2D(_SpecularMap, IN.uv_MainTex); o.Specular = _SpecularColor * spec.rgb; o.Smoothness = _Glossiness * spec.a; #else o.Specular = _SpecularColor; o.Smoothness = _Glossiness; #endif #if USE_NORMAL o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex)); #if USE_DETAILNORMAL float4 dN = tex2D(_DetailBump,IN.uv_DetailNormal); o.Normal = lerp(o.Normal, UnpackNormal(dN), _BumpBlend); #endif o.Normal.xy *= _NormalScale; #else o.Normal = float3(0,0,1); #endif #if UNITY_PASS_FORWARDBASE o.Emission = _EmissionColor; #endif}inline float3 SubTransparentColor(float3 lightDir, float3 viewDir, float3 lightColor, float3 pointDepth){ float VdotH = pow(saturate(dot(viewDir, -lightDir) + 0.5), _Power); return lightColor * VdotH * _SSColor.rgb * pointDepth;}inline void vert(inout appdata_full v){ v.vertex.xyz += v.normal *( (tex2Dlod(_HeightMap, v.texcoord).r - 0.5) * _VertexScale + _VertexOffset);}inline float3 BloodColor(float3 normal, float3 lightDir){ float NdotL = dot(normal, lightDir) * 0.5 + 0.5; return tex2D(_RampTex, float2(NdotL - 0.0, _BloodValue));}

頂點shader

inline v2f_surf vert_surf (appdata_full v) { UNITY_SETUP_INSTANCE_ID(v); v2f_surf o; UNITY_INITIALIZE_OUTPUT(v2f_surf,o); o.pos = UnityObjectToClipPos(v.vertex); o.screenPos = ComputeScreenPos(o.pos); o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex); float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldViewDir = (UnityWorldSpaceViewDir(worldPos)); float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); float tangentSign = v.tangent.w * unity_WorldTransformParams.w; float3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign; #if USE_DETAILALBEDO o.pack1 = TRANSFORM_TEX(v.texcoord,_DetailAlbedo); #endif #if USE_DETAILNORMAL o.pack2 = TRANSFORM_TEX(v.texcoord, _DetailBump); #endif o.tSpace0 = (float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x)); o.tSpace1 = (float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y)); o.tSpace2 = (float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z)); #ifdef DYNAMICLIGHTMAP_ON o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; #endif #ifdef LIGHTMAP_ON o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw; #endif // SH/ambient and vertex lights #ifndef LIGHTMAP_ON #if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL o.sh = 0; // Approximated illumination from non-important point lights #ifdef VERTEXLIGHT_ON o.sh += Shade4PointLights ( unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, unity_4LightAtten0, worldPos, worldNormal); #endif o.sh = ShadeSHPerVertex (worldNormal, o.sh); #endif #endif // !LIGHTMAP_ON UNITY_TRANSFER_SHADOW(o,v.texcoord1.xy); // pass shadow coordinates to pixel shader UNITY_TRANSFER_FOG(o,o.pos); // pass fog coordinates to pixel shader #ifndef USING_DIRECTIONAL_LIGHT o.lightDir = (UnityWorldSpaceLightDir(worldPos)); #else o.lightDir = _WorldSpaceLightPos0.xyz; #endif return o;}

片段shader

inline float4 frag_surf (v2f_surf IN) : SV_Target { UNITY_SETUP_INSTANCE_ID(IN); // prepare and unpack data Input surfIN; UNITY_INITIALIZE_OUTPUT(Input,surfIN); surfIN.uv_MainTex.x = 1.0; surfIN.uv_MainTex = IN.pack0.xy; #if USE_DETAILALBEDO surfIN.uv_DetailAlbedo = IN.pack1; #endif #if USE_DETAILNORMAL surfIN.uv_DetailNormal = IN.pack2; #endif float3 worldPos = float3(IN.tSpace0.w, IN.tSpace1.w, IN.tSpace2.w); float3 lightDir = normalize(IN.lightDir); float3 worldViewDir = normalize(IN.worldViewDir); #ifdef UNITY_COMPILER_HLSL SurfaceOutputStandardSpecular o = (SurfaceOutputStandardSpecular)0; #else SurfaceOutputStandardSpecular o; #endif float3x3 wdMatrix= float3x3( normalize(IN.tSpace0.xyz), normalize(IN.tSpace1.xyz), normalize(IN.tSpace2.xyz)); // call surface function surf (surfIN, o); // compute lighting & shadowing factor UNITY_LIGHT_ATTENUATION(atten, IN, worldPos) float4 c = 0; o.Normal = normalize(mul(wdMatrix, o.Normal)); float fragDepth = length(worldPos - _WorldSpaceCameraPos); float backDepth = DecodeFloatRGBA(tex2Dproj(_CullFrontDepthTex, IN.screenPos)) * 255; float thickness = saturate(1 - max(backDepth - fragDepth, _MinDistance) * _Thickness); // Setup lighting environment UnityGI gi; UNITY_INITIALIZE_OUTPUT(UnityGI, gi); gi.light.color = _LightColor0.rgb * atten; float3 bloodColor = BloodColor(o.Normal, lightDir) * o.Albedo * gi.light.color; o.Albedo *= 0; gi.light.dir = lightDir; float3 transparentColor = SubTransparentColor(lightDir, worldViewDir, _LightColor0.rgb, thickness); // Call GI (lightmaps/SH/reflections) lighting function UnityGIInput giInput; UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput); giInput.light = gi.light; giInput.worldPos = worldPos; giInput.worldViewDir = worldViewDir; giInput.atten = atten; #if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON) giInput.lightmapUV = IN.lmap; #else giInput.lightmapUV = 0.0; #endif #if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL giInput.ambient = IN.sh; #else giInput.ambient.rgb = 0.0; #endif giInput.probeHDR[0] = unity_SpecCube0_HDR; giInput.probeHDR[1] = unity_SpecCube1_HDR; #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION) giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending #endif #ifdef UNITY_SPECCUBE_BOX_PROJECTION giInput.boxMax[0] = unity_SpecCube0_BoxMax; giInput.probePosition[0] = unity_SpecCube0_ProbePosition; giInput.boxMax[1] = unity_SpecCube1_BoxMax; giInput.boxMin[1] = unity_SpecCube1_BoxMin; giInput.probePosition[1] = unity_SpecCube1_ProbePosition; #endif LightingStandardSpecular_GI(o, giInput, gi); // realtime lighting: call lighting function c += LightingStandardSpecular (o, worldViewDir, gi); c.rgb += o.Emission + bloodColor + transparentColor; UNITY_APPLY_FOG(IN.fogCoord, c); UNITY_OPAQUE_ALPHA(c.a); return c;}

渲染材質需要用到如下貼圖資源:Diffuse、Roughness、Specular、Normal、Scatter、Hight Map、AO貼圖等。

角色身體部分材質球展示

最終渲染效果

後期全部源碼會共享到github。

Refrence

[1] Henrik Wann Jensen, Stephen R. Marschner, Marc Levoy, Pat Hanrahan. A Practical Model for Subsurface Light Transport

[2] dEon, Eugene. NVIDIA Demo Team Secrets–Advanced Skin Rendering

[3] John Hable, George Borshukov, Jim Hejl. Fast Skin Shading

[4] John Hable. Uncharted 2: Character Lighting and Shading

[5] Jorge Jimenez, Veronica Sundstedt, Diego Gutierrez. Screen-Space Perceptual Rendering of Human Skin

[6] Jorge Jimenez, Károly Zsolnai, etc. Separable Subsurface Scattering

[7] Eric Penner. Pre-integrated Skin Shading

[8] Eric Penner, George Borshukov. GPU Pro 2, Pre-Integrated Skin Shading

[9] David Neubelt, Matt Pettineo. Crafting a Next-Gen Material Pipeline for The Order: 1886

[10] Eugene dEon, David Luebke. GPU Gems 3, Chapter 14. Advanced Techniques for Realistic Real-Time Skin Rendering

[11] Jorge Jimenez, David Whelan, etc. Real-Time Realistic Skin Translucency

[12] John Isidoro, Chris Oat, Jason Mitchell.Next Generation Skin Rendering

[13]洛城 zhuanlan.zhihu.com/p/27


推薦閱讀:

父親的角色到底有多重要?
宇宙最強甄子丹打10個已經是強翻天,她媽媽卻更是狠角色
《紐約時報》:伍迪·艾倫電影中的女性角色 來源:紐約時報
來自媽媽角色的自省

TAG:渲染 | 角色 |