文/清风
写作初衷
最近在研究真实感皮肤渲染,渲染的效果还说的过去,故在此和大家分享一下。参考了很多论文与大牛们的想法,技术鄙陋,欢迎指正。
皮肤渲染原理
由于是真实感渲染,就需要明白光线与皮肤交互的原理。这部分就会涉及到光线传播性质与皮肤结构。
首先来介绍下光线传播的性质。初中学过物理的都知道光线从一种介质射向另一种介质时会发生两种现象,一部分光线在介质交界处发生了反射,并未进入另外一种介质,另外一部分光线则进入了另一种介质就会发生折射(透射)。光线的两种现象合称为光线的散射,如下图:
光的散射(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表示的就是曲率,相应的计算方法:
上图右边就是曲率显示出来的效果,可以看出类似额头这样的位置曲率是比较小的,而鼻子等位置的曲率就比较大。
上述方法解决了(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贴图等。
角色身体部分材质球展示
最终渲染效果
知乎@Rufus
来源:游戏蛮牛
原地址:http://www.manew.com/thread-141574-1-1.html
|