unity3d Human skin real time rendering 真實模擬人皮實時渲染


先放出結果圖片。。。由於網上下的模型是拼的,所以眼皮,臉頰,嘴唇看起來像存在裂痕,解決方式是加入曲面細分和置換貼圖 進行一定隆起,但是博主試了一下fragment shader的曲面細分,雖然細分成功了但是着色效果變的很奇怪,這里就不用曲面細分了,大家如果有在fragment shader上用曲面細分的好辦法,可以的話請告訴我

參數設置1

參數設置2

細致到毛孔的高光

 

 

次表面散射的耳朵

 

 

人皮渲染是十多年的課題了,人們想盡一切辦法想讓其變得真實可信,大型3A級次時代游戲近來做的又來越真實了如《羅馬之子》,他們的皮膚自稱已經超過了NVIDIA的例子

 

 

 

這是在2005年SIGGRAPH的多層皮膚渲染,他們的參數都是經過精密的醫學上的測量的,而且渲染花費了5分鍾的時間。。。

 

 

研究這個找了許多資料,在結合之前的知識弄出了一個看起來還算入眼的人皮
本例做到了以下幾點
1.    次表面散射

2.    基於物理的渲染

包括specular和brdf等等,brdf我用了一張貼圖調整曲率來代替,specular在之前這篇文章有詳細講解 鏈接在此

3.    法線模糊

等等之類。。。

 

 

為什么皮膚渲染這么難?
1.    大多數的漫反射光來自次表面散射
2.    皮膚顏色主要來自上表皮
3.    粉或紅色主要為真皮中的血液
此圖為人皮的組成模擬,人有好多層表皮,這就說明在真實情況下要進行數次折射與反射,這就更難達到真實

 

 

光的折射與反射
 


上圖直觀的表明了光線“被怎么樣了”
光線接觸到皮膚時,有大約96%被皮膚各層散射了,只有大約4%被反射

再說specular,人的皮膚會出油,所以就會有反射,但是人的皮膚不能像鏡子那樣反射,因為人的皮膚是粗糙的,在這種情況下用基於物理的(physically based)方法就最好不過了,沒了解過physically based的看官們可以先了解下這篇文章和上面的一樣
 
使用了之前試出效果最好的的方法,也就是使命召喚2中用到的的方法,同時試了下beckmann的方法,但是效果並不好,phong等方法也沒有試
這里,我們的實現方法是這樣的:

<span style="font-size:14px;">            /*
            *this part is compute Physically-Based Rendering 
            *the method is in the ppt about "ops2"
            */

            float _SP = pow(8192, _GL);
            float d = (_SP + 2) / (8 * PIE) * pow(dot(n2, H), _SP);
            float f = _SC + (1 - _SC)*pow(2, -10 * dot(H, lightDir));
            float k = min(1, _GL + 0.545);
            float v = 1 / (k* dot(viewDir, H)*dot(viewDir, H) + (1 - k));

            float all = d*f*v;
            float3 refDir = reflect(-viewDir, n2);
            float3 ref = texCUBElod(_Cubemap, float4(refDir, _nMips - _GL*_nMips)).rgb;</span>

 

 

然后發現盡管gloss調到最大也沒有達到我們預期的那種效果,
又進行了 “智能補光”
也就是常規的求高光的方式,我們在此加入了高光貼圖,不讓不該高光的地方(如眉毛)產生高光

			float specBase = max(0, dot(n2, H));
			float spec = pow(specBase, 10) *(_GL + 0.2);
			spec = lerp(0, 1.2, spec);
			float3 spec3 = spec * (tex2D(_SpecularTex, i.uv_MainTex) - 0.1);
			spec3 *= Luminance(diff);
			spec3 = saturate(spec3);
			spec3 *= _SpecularPower;

光經過哪,就帶一部分那里的顏色可以發現光從入射到出射,位置和方向都變了

光走的路徑數量是無窮大,光反射回來的都為漫反射,油脂表面的透明度也都是不一樣的,
這就產生了次表面散射

 
NVIDIA在GDC2007年的演講中提到把圖像blur個六遍達到柔和的次表面散射效果
每次blur都是在不同的顏色通道以不同的范圍和程度進行blur
 
由於我們的貼圖是這樣的“高配”
 
用在本例上會丟失少許本來貼圖上的細節,但是確實有一定的次表面散射效果,各位看官自行取舍,而且千萬不要只做高斯模糊,這樣的話細節會丟失更多,而且沒有什么次表面散射的感覺

為了節省花銷,省去了ppt中的rendering時blur,直接在ps上做了6張高斯模糊的貼圖放入material,並線性混合


			float3 c = tex2D(_MainTex, i.uv_MainTex) * 128;
			c += tex2D(_BlurTex1, i.uv_MainTex) * 64;
			c += tex2D(_BlurTex2, i.uv_MainTex) * 32;
			c += tex2D(_BlurTex3, i.uv_MainTex) * 16;
			c += tex2D(_BlurTex4, i.uv_MainTex) * 8;
			c += tex2D(_BlurTex5, i.uv_MainTex) * 4;
			c += tex2D(_BlurTex6, i.uv_MainTex) * 2;
			c /= 256;




我們同時也起到重要作用的是邊緣光rim和brdf,
使用了BRDF最明顯的好處是,Brdf貼圖間接控制了明暗交界線的顏色,可通過曲率控制,模擬了光與陰影交界處光對皮膚的反射與折射,如果全黑的話說明光只是普通的漫反射。

而且使人皮有了次表面散射的質感

 

			/*
			*this part is to add the sss
			*used front rim,back rim and BRDF
			*/

			float3 rim = (1 - dot(viewDir, n2))*_RimPower * _RimColor *tex2D(_RimTex, i.uv_MainTex);
			float3 frontrim = (dot(viewDir, n2))*_FrontRimPower * _FrontRimColor *tex2D(_FrontRimTex, i.uv_MainTex);

			float3 sss = (1 - dot(viewDir, n2)) / 50 * _SSSPower;
			sss = lerp(tex2D(_SSSFrontTex, i.uv_MainTex), tex2D(_SSSBackTex, i.uv_MainTex), sss * 20)*sss;

			fixed atten = LIGHT_ATTENUATION(i);
			float curvature = length(fwidth(mul(_Object2World, float4(normalize(i.normal), 0)))) /
				length(fwidth(i.worldpos)) * _CurveScale;  

			float3 brdf = tex2D(_BRDFTex, float2((dot(normalize(i.normal), lightDir) * 0.5 + 0.5)* atten, curvature)).rgb;



對於rim的話添加了前向rim和后向rim其實本質上還是rim,后向rim使用了白色的圖片產生一種玉質的感覺(好吧其實更像羊羹),前向rim使用了紅色的圖片,相當於添加了光線在血液層的散射,讓人的臉蛋有了真實的血色



這是次表面散射的結果:

光源在嘴里


像不像把手指或者耳朵放在手電筒前面的那種效果?那就是次表面散射
需要一張Intense strips貼圖來混合原有顏色,方法就是在點光源情況下,求出當前點與點光源的距離,距離越近就越亮

關於法線,
用了一種新的混合方式,這樣能保有更多法線細節,
這里簡單講解一下法線混合,

float3 n1 = tex2D(texBase,   uv).xyz*2 - 1;
float3 n2 = tex2D(texDetail, uv).xyz*2 - 1;
float3 r  = normalize(n1 + n2);
return r*0.5 + 0.5;


大家可能用過這種方式來混合兩個法線貼圖,這種線性的方式折中了兩個貼圖,得到的細節權重是平均的,效果並不好,得到的是這樣的結果
 

改進了一下,變成了覆蓋混合

float3 n1 = tex2D(texBase,   uv).xyz;
float3 n2 = tex2D(texDetail, uv).xyz;
float3 r  = n1 < 0.5 ? 2*n1*n2 : 1 - 2*(1 - n1)*(1 - n2);
r = normalize(r*2 - 1);
return r*0.5 + 0.5;

就是法線1的法線比較深的地方,就多一些權重,比較淺的地方就被法線2適當覆蓋,但是這樣效果還是不夠真實
 

在GDC2012的Mastering DX11 with unity中講到了一種官方的辦法如下:

float3x3 nBasis = float3x3(
    float3(n1.z, n1.y, -n1.x), //繞着y軸+90度旋轉
float3(n1.x, n1.z, -n1.y),// 繞着x軸-90度旋轉
float3 (n1.x, n1.y, n1.z ));

 n = normalize (n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);

得到的結果是這樣的,是不是好了許多?


 

雙方的細節程度都有很多提升,

他們用了一個basis來變換第二法線。具體可以看 這篇文章—鏈接
 

用 AutoLight.cginc里定義的一個函數LIGHT_ATTENUATION求出光的衰減atten,atten在directional light中固定是1,在點光源中才有衰減效果,因為directional light在unity中是沒有位置區別的,在哪里都一樣。

fixed atten = LIGHT_ATTENUATION(i);



對於細節方面,如毛孔,在本例的貼圖和法線貼圖都很細致,已經包括毛孔和皮膚的紋路,如果貼圖精度低還想要高細節的話,可以再貼上細節
 




全部可設置變量:

 
 
 
全部代碼已共享至GitHub鏈接
                                                       ---- by wolf96


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM