游戏蛮牛学习群(纯技术交流,不闲聊):159852603
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2353

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1212|回复: 13

[士郎] 基于屏幕空间次表面散射预积分方法的实时皮肤渲染

[复制链接]  [移动端链接]
排名
3
昨日变化

6973

主题

7498

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
10467
威望
30
注册时间
2013-7-29
在线时间
3583 小时
最后登录
2018-11-16

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-10-26 14:43:34 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
写作初衷
最近在研究真实感皮肤渲染,渲染的效果还说的过去,故在此和大家分享一下。参考了很多论文与大牛们的想法,技术鄙陋,欢迎指正。





皮肤渲染原理


由于是真实感渲染,就需要明白光线与皮肤交互的原理。这部分就会涉及到光线传播性质与皮肤结构。
首先来介绍下光线传播的性质。初中学过物理的都知道光线从一种介质射向另一种介质时会发生两种现象,一部分光线在介质交界处发生了反射, 并未进入另外一种介质,另外一部分光线则进入了另一种介质就会发生折射(透射)。光线的两种现象合称为光线的散射,如下图:


1.jpg


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

反射(reflected light)和透射光(transmitted light)的相互作用
反射部分光照的辐射亮度(radiance)和入射光照的辐射照度(irradiance)之比是一个关于入射角度、出射角度的函数,这个函数就被称之为双向反射分布函数(BRDF)。穿越介质的那部分光照的辐射亮度和辐射照度的函数就被称为双向透射分布函数(BTDF)。这两部分出射光的辐射亮度总和和入射光的辐射照度的比例就被叫做双向散射分布函数(BSDF),即BSDF = BRDF + BTDF。
3.jpg

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



如果我们把光线行进的路线分为反射和透射,反射用R表示,透射用T表示,那么光线从一个点到另外一个点之间行进的路线就可以用R和T表示,比如BRDF描述的路径就是R,BTDF描述的路径就是TT,除此之外可能还会出现TRT,TRRRT等光照路线,由此我们可以想见,在光线入射点的附近应该有许多的出射光线。实际渲染中,如果光线出射点的位置和入射点相距不足一个像素,我们就认为入射点和出射点位置相同,这时候当前像素的光照只受其自身影响;如果入射点和出射点相距超过一个像素,则表示某个像素的光照结果不仅仅受当前像素影响,同时还受附近其他像素的光照影响,这就是我们常说的次表面散射效果了。



4.jpg



《Real-Time Rendering 3rd》一书中对次表面散射的阐释。红色区域表示一个像素的大小,当出射光线集中分布在红色区域内时,则认为次表面散射效果可以忽略,当出射光线较为均匀地分布在绿色区域内时,则需要单独考虑次表面散射效果。


6.png


基于路径追踪实现的次表面散射渲染效果图
©Photorealizer




接下来说明下皮肤的构成


皮肤是一个多层结构,其表面油脂层贡献了皮肤光照的主要反射部分,而油脂层下面的表皮层和真皮层则贡献了主要的次表面散射部分。
7.jpg




次表面散射光线密度分布是一个各向同性函数,也就是说一个像素受周边像素的光照影响的比例只和两个像素间的距离有关。这个密度分布函数在有些地方称为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)))来确定。



8.jpg

屏幕空间的算法示意图预积分的方法
图像空间的方法和屏幕空间的方法很大程度上都是通过周边像素对当前像素的光照贡献来实现次表面散射的效果,从原理上区别不大,方法之间的区别通常只是在于如何去近似diffusion profile,在性能和效果上有一个较好的平衡。Pre-Integrated Skin Shading的方法则不同于上述方法,是一个从结果反推实现的方案。观察次表面散射效果可以发现:


(1)次表面散射的效果主要发生在曲率较大的位置(或者说光照情况变化陡峭的位置),而在比较平坦的位置则不容易显现出次表面散射的效果(比如鼻梁处的次表面散射就比额头处的次表面散射效果要强)

(2)在有凹凸细节的部位也容易出现次表面散射,这一点其实和(1)说的是一回事,只是(1)中的较大曲率是由几何形状产生的,而(2)中的凹凸细节则一般是通过法线贴图来补充。

结合以上两个观察,预积分的方法是把次表面散射的效果预计算成一张二维查找表,查找表的参数分别是dot(N, L)和曲率,因为这两者结合就能够反映出光照随着曲率的变化。

9.jpg

上图中1/r表示的就是曲率,相应的计算方法:

10.png
11.jpg

上图右边就是曲率显示出来的效果,可以看出类似额头这样的位置曲率是比较小的,而鼻子等位置的曲率就比较大。

上述方法解决了(1)的问题,但对于(2)提到的一些凹凸起伏的细节,由于它不是由几何造型产生的,因此无法用上述曲率计算的方法确认其是否有明显的次表面散射效果,因此作者进一步结合了bent normal的方法来实现这些细节处的次表面散射效果。


Bent Normal其实不是专门用来处理次表面散射专有的方法,可以应用于很多预计算复杂光照的情况,简单的说就是把包含AO,阴影,次表面散射之类的复杂光照信息pre-bake到法线里面,然后计算光照时使用pre-bake得到的法线,结合正常的光照计算方法,就能得到比较复杂的光照效果。


12.jpg

bent normal的方案大致来说就是对法线贴图进行模糊的操作,以实现类似次表面散射的泛光效果,然后在计算最终光照的时候,使用原始的法线贴图和模糊后的法线贴图的线性插值结果作为最终的bent normal。

照明计算
由于是在unity2018引擎下实现,所以直接调用了LightingStandardSpecular函数进行光照计算,unity2018下的全局光照也是采用BRDF模型来实现的,所以整体的光照效果很好。

自定义函数

[AppleScript] 纯文本查看 复制代码
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
[AppleScript] 纯文本查看 复制代码
 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
[AppleScript] 纯文本查看 复制代码
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贴图等。
13.jpg

角色身体部分材质球展示
最终渲染效果
14.jpg


知乎@Rufus



2.jpg

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

6蛮牛粉丝
1352/1500
排名
1867
昨日变化

7

主题

210

帖子

1352

积分

Rank: 6Rank: 6Rank: 6

UID
220815
好友
7
蛮牛币
2148
威望
0
注册时间
2017-5-5
在线时间
409 小时
最后登录
2018-11-16
QQ
发表于 2018-10-26 16:35:49 | 显示全部楼层
大大的一个赞

回复

使用道具 举报

排名
390
昨日变化
2

73

主题

597

帖子

3492

积分

Rank: 9Rank: 9Rank: 9

UID
93357
好友
6
蛮牛币
10280
威望
0
注册时间
2015-4-18
在线时间
1164 小时
最后登录
2018-11-18

专栏作家认证开发者

QQ
发表于 2018-10-26 17:24:33 | 显示全部楼层
要怎么学习才能到达这种地步哇。膜拜了

回复 支持 反对

使用道具 举报

3偶尔光临
178/300
排名
16810
昨日变化
2

0

主题

97

帖子

178

积分

Rank: 3Rank: 3Rank: 3

UID
211102
好友
0
蛮牛币
123
威望
0
注册时间
2017-3-10
在线时间
43 小时
最后登录
2018-10-26
发表于 2018-10-26 19:59:36 | 显示全部楼层
{:88:}{:88:}{:88:}{:88:}{:88:}

回复 支持 反对

使用道具 举报

5熟悉之中
597/1000
排名
3680
昨日变化
2

0

主题

51

帖子

597

积分

Rank: 5Rank: 5

UID
197052
好友
0
蛮牛币
2392
威望
0
注册时间
2016-12-29
在线时间
160 小时
最后登录
2018-11-17
发表于 2018-10-27 16:01:21 | 显示全部楼层
厉害了

回复

使用道具 举报

7日久生情
2226/5000
排名
2698
昨日变化
12

1

主题

1430

帖子

2226

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
119154
好友
0
蛮牛币
2430
威望
0
注册时间
2015-8-21
在线时间
281 小时
最后登录
2018-11-18
发表于 2018-10-28 23:12:18 | 显示全部楼层
谢谢楼主大大。

回复

使用道具 举报

7日久生情
1565/5000
排名
2487
昨日变化

12

主题

632

帖子

1565

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
214924
好友
3
蛮牛币
4047
威望
0
注册时间
2017-3-28
在线时间
367 小时
最后登录
2018-11-16
发表于 2018-10-29 09:10:31 | 显示全部楼层
666666666到起飞

回复 支持 反对

使用道具 举报

5熟悉之中
509/1000
排名
5413
昨日变化

1

主题

161

帖子

509

积分

Rank: 5Rank: 5

UID
245227
好友
0
蛮牛币
597
威望
0
注册时间
2017-9-21
在线时间
97 小时
最后登录
2018-11-16
发表于 2018-10-29 09:27:51 | 显示全部楼层
好古老的书了
不过使用surface shader写就是简短方便啊

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1022/1500
排名
3124
昨日变化
2

1

主题

166

帖子

1022

积分

Rank: 6Rank: 6Rank: 6

UID
139214
好友
0
蛮牛币
1534
威望
0
注册时间
2016-3-12
在线时间
405 小时
最后登录
2018-11-16
发表于 2018-10-29 09:45:55 | 显示全部楼层
666

回复

使用道具 举报

5熟悉之中
530/1000
排名
5480
昨日变化
40

0

主题

87

帖子

530

积分

Rank: 5Rank: 5

UID
246100
好友
0
蛮牛币
350
威望
0
注册时间
2017-9-26
在线时间
197 小时
最后登录
2018-11-18
发表于 2018-10-29 16:08:30 | 显示全部楼层
虽然看不懂,但是感觉好厉害的样子

回复 支持 反对

使用道具 举报

4四处流浪
477/500
排名
17674
昨日变化
6

1

主题

182

帖子

477

积分

Rank: 4

UID
213085
好友
5
蛮牛币
326
威望
0
注册时间
2017-3-20
在线时间
261 小时
最后登录
2018-11-16
发表于 2018-10-30 08:54:07 | 显示全部楼层

回复

使用道具 举报

3偶尔光临
283/300
排名
7575
昨日变化
73

0

主题

32

帖子

283

积分

Rank: 3Rank: 3Rank: 3

UID
255447
好友
1
蛮牛币
383
威望
0
注册时间
2017-11-21
在线时间
95 小时
最后登录
2018-11-18
发表于 2018-10-31 10:28:28 | 显示全部楼层
哇,大佬
[发帖际遇]: 一个袋子砸在了 未来~ 头上,未来~ 赚了 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

3偶尔光临
169/300
排名
15053
昨日变化
3

0

主题

65

帖子

169

积分

Rank: 3Rank: 3Rank: 3

UID
20502
好友
0
蛮牛币
1
威望
0
注册时间
2014-4-8
在线时间
56 小时
最后登录
2018-11-12
QQ
发表于 2018-10-31 21:40:32 | 显示全部楼层
谢谢分享!!!!!!!!!!!!!!!!!!!!!!!!!!!

回复

使用道具 举报

5熟悉之中
540/1000
排名
5735
昨日变化

0

主题

163

帖子

540

积分

Rank: 5Rank: 5

UID
251353
好友
0
蛮牛币
1471
威望
0
注册时间
2017-10-29
在线时间
145 小时
最后登录
2018-11-16
发表于 2018-11-1 14:18:38 | 显示全部楼层
{:107:}

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

关闭

站长推荐 上一条 /1 下一条

快速回复 返回顶部 返回列表