SSS(sub-Surface Scattering,3S)的中文意思是次表面散射或称半透明材质。它适用于表现各种有次表面反射的材质,如透明橡胶、有机玻璃或玉石等。因为实时渲染是不可能做大量的运算来计算光的折射和反射的,所以SSS属于比较高级的材质范畴。
要实现半透明材质,许多方法都进行了大量的运算,比如正弦、余弦的计算、指数的运算等,外加各种贴图和通道的混合,非常复杂。
首先要知道,因为光在半透明物体中沿着各个方向进行折射和反射,所以就会产生一个必然的结果,那就是光的方向性消失了。光在物体内部所能前进的深度依然和物体的密度、物体到光源的距离密切相关。基于此特点,可以方便地写出一个着色器来表现半透明材质的效果。
为了便于控制不同物体的表现,在着色器中提供两个变量:一个用于控制物体到光源距离的偏差,这样就可以表达出从哪个距离光进入了物体内部并开始衰减;另外一个用来控制光在物体中的衰减速度,也就是对物体密度和透明度的控制。主要代码如下:
Shader "Tut/Shader/SSS/SSS_1" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BaseColor("Base Color of Object",color)=(1,1,1,1)
_DistAdjust("Distance Adjust",float)=0
_Atten("Control the Density of Object",float)=1
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
pass{
Tags{ "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#pragma target 3.0
#include "UnityCG.cginc"
struct v2f{
float4 pos:SV_POSITION;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
//
return 0;
}
ENDCG
}//end pass
pass{
Blend One One
Tags{ "LightMode"="ForwardAdd"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
struct v2f{
float4 pos:SV_POSITION;
float3 N:TEXCOORD0;
float3 litDir:TEXCOORD1;
//float4 vp:TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.N=v.normal;
o.litDir=ObjSpaceLightDir(v.vertex);
//o.vp=v.vertex;
return o;
}
float4 _BaseColor;
float _DistAdjust;
float _Atten;
float4 frag(v2f i):COLOR
{
float3 N=normalize(i.N);
//float3 litDir=ObjSpaceLightDir(i.vp);
float3 litDir=i.litDir;//光源方向
float dist=length(litDir);//到光源的原始距离
dist=max(0,dist-_DistAdjust);//对原始距离进行一个偏移
float att=1/(1+dist*dist);
att=pow(att,_Atten);//计算光的衰减速度
float4 c= _BaseColor* att*2;
//c.a=1-c.a;
return c;
}
ENDCG
}//end pass
}
FallBack "Diffuse"
}
然后在场景加入两个点光源,调整一下参数,效果如下:
代码很简单,连一个通道图都没有用,效果如上图。
对于透明物体,其表面有反射,内部物体的光线又通过折射到达我们的眼中,因此,对于透明物体,我们想要把这两种现象都表现出来。一种比较直接的办法就是分别渲染物体的正面和背面来达到表现透明物体表面的镜面反射与深度的。
首先看效果图:
这里面有2种shader,WaterBox这个shader表现的是一种类似水体的效果,这个着色器首先正常渲染物体,也就是Cull Back,并且写入Z缓冲区,为后续的渲染在Z缓冲区中占位,其余代码则是对照明比较简单的计算。在第二个通道渲染物体背面的过程中,则使用了一种混合模式,将Z测试的条件设为Greater,从而利用第一个通道在Z缓冲区中的占位,使得Z能够通过测试。
从分层或者画面的角度而言,先渲染的是物体的底面,然后是表面。但是为了使水体在前后都可能存在其他物体的情况下,使前后两个面都能通过深度测试,我们不得不首先渲染物体的正面,并且在Z缓冲区中占位。比如,首先渲染的物体的底面,当Z的条件为Greater时,在存在背景物体的条件下,则无法通过;如果Z的条件设为Less,又因为正面存在,则无法通过;如果Z的条件为Always,并且是正面渲染,则Z测试无法通过。
渲染水体的着色器代码如下:
Shader "Tut/Shader/SSS/WaterBox" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Deep("Deep Color of Liquid",Color)=(0,0,0,0)
_Shallow("Shallow Color of Liquid",Color)=(1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry+1"}
//首先正常渲染,并写入到Z Buffer,这将成为水的表面层
//首先渲染正面,是为了使渲染背面的Z测试条件Greater能通过
Pass {
Blend One Zero
Cull Back
ZTest Less
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float4 diff:TEXCOORD1;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord.xy;
float3 L= ObjSpaceLightDir(v.vertex);
o.diff=max(0,dot(L,v.normal))*_LightColor0;
return o;
}
float4 _Shallow;
float4 frag(v2f i):COLOR
{
return i.diff*_Shallow;
}
ENDCG
}
Pass {
//然后我们渲染背面,有分层概念的人可能会说,我们不是应该渲染底面,再显然表面么
//是的,如果这是一幅水彩画的话,的确应该这么做,但是我们为了能够使渲染底面的Z测试条件
//Greater能在GPU中通过,恐怕不得不首先渲染物体的正面,并且写入到Z缓冲,从而能够使背面正确的被渲染
Blend OneMinusDstAlpha DstAlpha
Cull Front
ZTest Greater
ZWrite off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float4 diff:TEXCOORD1;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord.xy;
float3 L= ObjSpaceLightDir(v.vertex);
//因为渲染的是背面,所以需要翻转一下法线
o.diff=max(0,dot(L,-v.normal))*_LightColor0;
return o;
}
float4 _Deep;
sampler2D _MainTex;
float4 frag(v2f i):COLOR
{
float4 c=tex2D(_MainTex,i.uv);
return c*i.diff*_Deep;
}
ENDCG
}
}
FallBack "Diffuse"
}
对于可能出现在水中的物体,则使用了一个有两个通道的着色器。对于这个名为FloatObject的材质,其第一个通道负责对物体正面的渲染。第二个通道的Z测试条件为Greater,从而使其处于水体中的部分也能够渲染,并且使用了一种适当的混合方式写入到颜色缓冲区中,代码如下:
Shader "Tut/Shader/SSS/FloatObject" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_refVal("Stencil Ref Value",int)=0
_Deep("Deep Color of Liquid",Color)=(0,0,0,0)
_Front("Shallow Color of Liquid",Color)=(1,1,1,1)
}
SubShader {
//这是一个为了和WaterBox配合使用,描述漂浮在水面的物体而写的Shader
Tags { "RenderType"="Opaque" "Queue"="Geometry+2"}
Pass {
Blend One Zero
Cull Back
ZTest Less
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float4 diff:TEXCOORD1;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord.xy;
float3 L= ObjSpaceLightDir(v.vertex);
o.diff=max(0,dot(L,v.normal))*_LightColor0;
return o;
}
float4 _Front;
sampler2D _MainTex;
float4 frag(v2f i):COLOR
{
float4 c=tex2D(_MainTex,i.uv);
return c*i.diff*_Front*2;
}
ENDCG
}
Pass {
Blend SrcAlpha OneMinusSrcAlpha
Cull Back
ZTest Greater
ZWrite off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float4 diff:TEXCOORD1;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord.xy;
float3 L= ObjSpaceLightDir(v.vertex);
o.diff=max(0,dot(L,v.normal))*_LightColor0;
return o;
}
float4 _Deep;
sampler2D _MainTex;
float4 frag(v2f i):COLOR
{
float4 c=tex2D(_MainTex,i.uv);
return c*i.diff*_Deep*2;
}
ENDCG
}
}
FallBack "Diffuse"
}