前言
感觉好久没更新博客了,这段时间决定重新把写博客的习惯捡起来!前段时间学习研究了一下次表面散射相关的知识,这次我们就在Unity中简单实现一下该效果。如果哪里有错误的地方,希望大家能够指出,多多讨论。
次表面散射(Subsurface scattering)
次表面散射是光在传播时的一种现象,表现为光在穿过透明物体表面后,与材料之间发生交互作用而导致光被散射开来,光路也在其他的位置穿出物体。光一般会穿透物体的表面,在物体内部在不同的角度被反射若干次,最终穿出物体。次表面散射在三维计算机图形中十分重要,可用来渲染大理石、皮肤、树叶、蜡、牛奶等多种不同材料。
例如:
当然为了能在游戏中实时渲染,我们只能近似模拟次表面散射现象。本篇文章实现原理主要参考了这篇文章 Fast Subsurface Scattering
话不多说,下面我们一步一步的来实现伪次表面散射,在本篇文章中只贴出关键的Shader代码,基础的Shader代码就不再一一解释了。
实现
在自然界中,光线的传播一般包含三种情况,即:
-
反射: 入射光与反射光在表面的同一侧,且入射点与反射点相同 -
次表面散射:入射光与反射光在表面的同一侧,且入射点与反射点不同 -
透射:入射光与反射光在表面的不同侧,即光线投过了物体
为了模拟这种背面透光的效果,我们可以把法线向光源方向偏移一定程度后,然后取反,再去和视线方向做运算。
模拟背光反射率的方程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlRyTesr-1631352827920)(https://www.alanzucconi.com/wp-content/ql-cache/quicklatex.com-577e07757a85aaf42b85235abb657979_l3.svg)]
公式转换为Shader代码:
float3 H = L + N * distortion;
float sss = pow(saturate(dot(V, -H)), power) * scale;
return sss;
在平行光下的渲染效果如图:
环绕照明(Warp Lighting)
其实还有一种简单模拟次表面的技巧:环绕照明(Warp Lighting),正常情况下,当表面的法线对于光源方向垂直的时候,Lambert漫反射提供的照明度是0。而环绕光照修改漫反射函数,使得光照环绕在物体的周围,越过那些正常时会变黑变暗的点。这减少了漫反射光照明的对比度,从而减少了环境光和所要求的填充光的量。
下图和代码片段显示了如何将漫反射光照函数进行改造,使其包含环绕效果。
其中,wrap变量为环绕值,是一个范围为0到1之间的浮点数,用于控制光照环绕物体周围距离。
代码:
float diffuse = max(0, dot(L, N));
float wrap_diffuse = max(0, (dot(L, N) + _WrapValue) / (1 + _WrapValue));
return wrap_diffuse;
渲染效果:
然后,我们把前两种合成看看效果
代码:
float3 H = L + N * distortion;
float sss = pow(saturate(dot(V, -H)), power) * scale;
float diffuse = max(0, dot(L, N));
float wrap_diffuse = max(0, (dot(L, N) + _WrapValue) / (1 + _WrapValue));
return sss + wrap_diffuse;
现在看来是不是有点散射那味了
叠加上自定义的颜色再看看效果如何:
现在还只是考虑了平行光,下面我们把点光源也考虑进去看看效果如何
说到关于点光源相关的计算,那么一般都是在 ForwardAdd 的Pass 中去计算,但是这样会造成每多一盏灯,DrawCall就会翻一倍,所以这里我就直接在 ForwardBase 里面计算点光源了。
在Unity中 有一个内置函数用来计算点光源 Shade4PointLights ,
使用如下:
float3 pointColor = 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, i.worldPos, -N);
return fixed4(pointColor,1);
然后我去看看该方法的源码,分析一下大概意思, 源码可以在 UnityCG.cginc 文件中找到:
float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal)
{
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
lengthSq = max(lengthSq, 0.000001);
float4 ndotl = 0;
ndotl += toLightX * normal.x;
ndotl += toLightY * normal.y;
ndotl += toLightZ * normal.z;
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl * atten;
float3 col = 0;
col += lightColor0 * diff.x;
col += lightColor1 * diff.y;
col += lightColor2 * diff.z;
col += lightColor3 * diff.w;
return col;
}
具体分析可以参考这篇文章:Unity3D ShaderLab 之 Shade4PointLights 解读
以上代码可以很明显的发现,引擎会把四盏点光源的x, y, z坐标,分别存储到lightPosX, lightPosY, lightPosZ中, 换句话说: light0 的位置是 float3(lightPosX[0], lightPosY[0], lightPosZ[0]) light1 的位置是 float3(lightPosX[1], lightPosY[1], lightPosZ[1]) light2 的位置是 float3(lightPosX[2], lightPosY[2], lightPosZ[2]) light3 的位置是 float3(lightPosX[3], lightPosY[3], lightPosZ[3])
unity_LightColor数组就是点光源颜色。
有了以上信息就好办了,我们来魔改一下,改成我们需要的次表面散射。
代码如下:
inline float SubsurfaceScattering (float3 V, float3 L, float3 N, float distortion,float power,float scale)
{
float3 H = L + N * distortion;
float I = pow(saturate(dot(V, -H)), power) * scale;
return I;
}
float3 CalculatePointLightSSS (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,float3 pos,float3 N,float3 V)
{
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
lengthSq = max(lengthSq, 0.000001);
float4 ndotl = 0;
ndotl += toLightX * N.x;
ndotl += toLightY * N.y;
ndotl += toLightZ * N.z;
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float3 pointLightDir0 = normalize(float3(toLightX[0],toLightY[0],toLightZ[0]));
float pointSSS0 = SubsurfaceScattering(V,pointLightDir0,N,_DistortionBack,_PowerBack,_ScaleBack);
float3 pointLightDir1 = normalize(float3(toLightX[1],toLightY[1],toLightZ[1]));
float pointSSS1 = SubsurfaceScattering(V,pointLightDir1,N,_DistortionBack,_PowerBack,_ScaleBack);
float3 pointLightDir2 = normalize(float3(toLightX[2],toLightY[2],toLightZ[2]));
float pointSSS2 = SubsurfaceScattering(V,pointLightDir2,N,_DistortionBack,_PowerBack,_ScaleBack);
float3 pointLightDir3 = normalize(float3(toLightX[3],toLightY[3],toLightZ[3]));
float pointSSS3 = SubsurfaceScattering(V,pointLightDir3,N,_DistortionBack,_PowerBack,_ScaleBack);
float3 col = 0;
col += lightColor0 * atten.x * (pointSSS0+ndotl.x);
col += lightColor1 * atten.y * (pointSSS1+ndotl.y);
col += lightColor2 * atten.z * (pointSSS2+ndotl.z);
col += lightColor3 * atten.w * (pointSSS3+ndotl.w);
return col;
}
float3 pointColor = CalculatePointLightSSS(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0,i.worldPos,N,V);
return fixed4(pointColor,1);
然后我们在场景中放3盏点光源看看效果如何:
叠加上之前计算的平行光:
最后我们把 Wrap-Diffuse、Specular-BlinnPhong 等效果叠加上去看看整体效果。 当然,散射颜色也可以定义一个变量来控制,方便美术调整效果。
在有平行光、无点光源的情况下的背面和正面:
在无平行光、有点光源的情况下:
在有平行光、有点光源的情况下(颜色太杂乱了…):
厚度图
吸收(Absorption)是模拟半透明材质的最重要特性之一。 光线在物质中传播得越远,它被散射和吸收得就越厉害。 为了模拟这种效果,我们需要测量光在物质中传播的距离,并相应地对其进行衰减。
可以在下图中看到具有相同入射角的三种不同光线,穿过物体的长度却截然不同。
这里我们就采用外部局部厚度图来模拟该现象,当然,该方法在物理上来说并不准确,但是可以比较简单快速的模拟出这种效果。
烘焙厚度图可以用Substance Painter 或者用Unity的插件:Mesh Materializer把厚度信息存储在顶点色里面。
厚度图输出来是这样(这里换了个简单的模型,之前那个模型厚度烘焙有点问题-.-):
最后用厚度值乘上次表面散射值,就能得到最终效果:
最后奉上完整代码:
Shader "lcl/SubsurfaceScattering/FastSSSTutorial" {
Properties{
_MainTex ("Texture", 2D) = "white" {}
_BaseColor("Base Color",Color) = (1,1,1,1)
_Specular("Specular Color",Color) = (1,1,1,1)
[PowerSlider()]_Gloss("Gloss",Range(1,200)) = 10
[Main(sss,_,3)] _group ("SubsurfaceScattering", float) = 1
[Tex(sss)]_ThicknessTex ("Thickness Tex", 2D) = "white" {}
[Sub(sss)]_ThicknessPower ("ThicknessPower", Range(0,10)) = 1
[Sub(sss)][HDR]_ScatterColor ("Scatter Color", Color) = (1,1,1,1)
[Sub(sss)]_WrapValue ("WrapValue", Range(0,1)) = 0.0
[Title(sss, Back SSS Factor)]
[Sub(sss)]_DistortionBack ("Back Distortion", Range(0,1)) = 1.0
[Sub(sss)]_PowerBack ("Back Power", Range(0,10)) = 1.0
[Sub(sss)]_ScaleBack ("Back Scale", Range(0,1)) = 1.0
[SubToggle(sss, __)] _CALCULATE_POINTLIGHT ("Calculate Point Light", float) = 0
}
SubShader {
Pass{
Tags { "LightMode"="Forwardbase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#pragma multi_compile _ _CALCULATE_POINTLIGHT_ON
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex,_ThicknessTex;
float4 _MainTex_ST;
fixed4 _BaseColor;
half _Gloss;
float3 _Specular;
float4 _ScatterColor;
float _DistortionBack;
float _PowerBack;
float _ScaleBack;
float _ThicknessPower;
float _WrapValue;
float _ScatterWidth;
struct a2v {
float4 vertex : POSITION;
float3 normal: NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f{
float4 position:SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalDir: TEXCOORD1;
float3 worldPos: TEXCOORD2;
float3 viewDir: TEXCOORD3;
float3 lightDir: TEXCOORD4;
};
v2f vert(a2v v){
v2f o;
o.position = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldPos = mul (unity_ObjectToWorld, v.vertex);
o.normalDir = UnityObjectToWorldNormal (v.normal);
o.viewDir = UnityWorldSpaceViewDir(o.worldPos);
o.lightDir = UnityWorldSpaceLightDir(o.worldPos);
return o;
};
inline float SubsurfaceScattering (float3 V, float3 L, float3 N, float distortion,float power,float scale)
{
float3 H = L + N * distortion;
float I = pow(saturate(dot(V, -H)), power) * scale;
return I;
}
float3 CalculatePointLightSSS (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,float3 pos,float3 N,float3 V)
{
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
lengthSq = max(lengthSq, 0.000001);
float4 ndotl = 0;
ndotl += toLightX * N.x;
ndotl += toLightY * N.y;
ndotl += toLightZ * N.z;
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl * atten;
float3 pointLightDir0 = normalize(float3(toLightX[0],toLightY[0],toLightZ[0]));
float pointSSS0 = SubsurfaceScattering(V,pointLightDir0,N,_DistortionBack,_PowerBack,_ScaleBack)*3;
float3 pointLightDir1 = normalize(float3(toLightX[1],toLightY[1],toLightZ[1]));
float pointSSS1 = SubsurfaceScattering(V,pointLightDir1,N,_DistortionBack,_PowerBack,_ScaleBack)*3;
float3 pointLightDir2 = normalize(float3(toLightX[2],toLightY[2],toLightZ[2]));
float pointSSS2 = SubsurfaceScattering(V,pointLightDir2,N,_DistortionBack,_PowerBack,_ScaleBack)*3;
float3 pointLightDir3 = normalize(float3(toLightX[3],toLightY[3],toLightZ[3]));
float pointSSS3 = SubsurfaceScattering(V,pointLightDir3,N,_DistortionBack,_PowerBack,_ScaleBack)*3;
float3 col = 0;
col += lightColor0 * atten.x * (pointSSS0+ndotl.x);
col += lightColor1 * atten.y * (pointSSS1+ndotl.y);
col += lightColor2 * atten.z * (pointSSS2+ndotl.z);
col += lightColor3 * atten.w * (pointSSS3+ndotl.w);
return col;
}
fixed4 frag(v2f i): SV_TARGET{
fixed4 col = tex2D(_MainTex, i.uv) * _BaseColor;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
fixed3 lightColor = _LightColor0.rgb;
float3 N = normalize(i.normalDir);
float3 V = normalize(i.viewDir);
float3 L = normalize(i.lightDir);
float NdotL = dot(N, L);
float3 H = normalize(L + V);
float NdotH = dot(N, H);
float NdotV = dot(N, V);
float thickness = tex2D(_ThicknessTex, i.uv).r * _ThicknessPower;
float3 sss = SubsurfaceScattering(V,L,N,_DistortionBack,_PowerBack,_ScaleBack) * lightColor * _ScatterColor * thickness;
float wrap_diffuse = max(0, (NdotL + _WrapValue) / (1 + _WrapValue));
float3 diffuse = lightColor * wrap_diffuse * col;
fixed3 specular = lightColor * pow(max(0,NdotH),_Gloss) * _Specular;
fixed3 pointColor = fixed3(0,0,0);
#ifdef _CALCULATE_POINTLIGHT_ON
pointColor = CalculatePointLightSSS(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0,i.worldPos,N,V) * thickness;
#endif
float3 resCol = diffuse + sss + pointColor + specular;
return fixed4(resCol,1);
};
ENDCG
}
}
FallBack "Diffuse"
}
参考
fast-subsurface-scattering https://www.patreon.com/posts/subsurface-write-20905461 https://zhuanlan.zhihu.com/p/42433792 https://zhuanlan.zhihu.com/p/21247702 https://zhuanlan.zhihu.com/p/36499291 http://walkingfat.com https://zhuanlan.zhihu.com/p/27842876
|