次表面散射指的是光线射入半透明材质,在内部发生散射后再透射出来的光线传播过程,考虑到有些项目会需要使用次表面散射,下面就给大家介绍下在Unity3D中次表面散射的简单实现,希望可以帮到大家。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
以下内容参考了GPU精粹1中关于次表面散射一节的内容。
二、概述
次表面散射,英文全称为Subsurface Scattering,简称SSS。指的是光线射入半透明材质,在内部发生散射后再透射出来的光线传播过程。真实世界中拥有次表面散射的材质有蜡烛、大理石、玉石以及人的皮肤等。要模拟物理上真实的次表面散射是很复杂的一件事,比较经典的次表面散射模型有BSSRDF,全称叫双向次表面散射反射分布函数。本文中并未使用BSSRDF模型,而是简单的使用了exp指数函数对深度进行计算以此模拟散射。
以下为实现后的最终效果:
三、原理
首先我们来对次表面散射物体的光照做一个分解,也算是一个简单的建模过程。
Color = Diffuse * Scattering + SpecularColor;
Diffuse指的是物体表面的漫反射颜色,Scattering是散射的颜色,SpecularColor是高光颜色。
1、漫反射的计算
漫反射的计算公式,在这里我们使用的是环绕光照计算公式,即:
?
1 |
diff = ( dot(normal, lightDir) + wrap )/( 1+wrap )
|
其中normal为顶点的法线,lightDir是光照方向,wrap为环绕参数。传统的Lambert光照中,当物体表面的法线与光源方向垂直的时候,其产生的光照结果为0,但是在次表面散射物体中,由于内部散射光线的传播,导致其在上述情形下光照结果不会完全为0。所以为了减少传统Lambert光照中的黑暗区域,我们使用环绕光照公式,当dot(normal, lightDir)结果为0的时候,我们强制其至少有一点点亮度,即wrap/(1+wrap)。
2、散射的计算
我们假设物体表面的散射是均匀分布的,并且无视光源位置以及光照方向对散射的影响,取物体在视线方向上的深度值作为参数,带入exp指数函数中进行计算。当然上述假设并不符合物理规律,但是考虑到效果以及效率的问题,我们只好先这么干了。
为了获取物体在视线方向上的深度值,我们需要先以cull front模式渲染一遍物体,保存物体背面的顶点的深度值信息,然后再回到正常的cull back模式下渲染物体,使用(backDepth-frontDepth)来求出深度值,最后带入公式exp(-C*depth)中。C为外部传入的参数,用于调节物体的透光率。
3、高光的计算
高光的计算我们使用经典的BlinnPhong光照公式,即:
?
1 2 |
Specular = pow((dot(normal, half)), shiness);
Half = normalize(lightDir + viewDir);
|
其中normal为顶点法线向量,half为半角向量,是入射光向量与视点向量的角平分线向量,shiness为高光指数。
BlinnPhong光照模型相比较于Phong光照模型,其高光区域更平滑柔和,这也是为什么我们使用它。
4、半透明
由于在散射计算中,需要使用到物体表面顶点的深度值信息,导致我们在渲染时不 能关闭ZWrite,这就使得我们不能通过Unity3D中设置RenderType=Transparent、Queue=Transparent来实现半透明混合效果。在Unity3D中,要实现半透明,一般的做法是:
?1 2 3 |
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite off
|
所以,我们需要寻找另外一种方法来实现半透明,通过GrabPass操作来获取除物体本身以外的屏幕渲染结果,然后我们在片段着色器中手动进行混合计算,以此达到半透明效果。当然需要注意的是,GrabPass本身的操作比Alpha混合要昂贵的多,需要牺牲更多的计算性能,另外GrabPass在某些手机平台上可能不被支持。
四、实现
下面给出顶点着色器以及片段着色器的实现代码:
?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal:NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 screenUV:TEXCOORD1;
float3 lightDir:TEXCOORD2;
float3 viewDir:TEXCOORD3;
float3 normal:TEXCOORD4;
float4 grabUV:TEXCOORD5;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _GrabTexture;
sampler2D _BackDepthTex;
float4 _AttenuationC;
float4 _Color;
float _Shininess;
float _ScatteringFactor;
float _Wrap;
struct LightingInput {
float3 Albedo;
float3 Normal;
float Gloss;
float Specular;
float Alpha;
};
float4 CalculateLighting (LightingInput i, float3 lightDir, float3 viewDir, float atten, float3 scattering)
{
float3 h = normalize (lightDir + viewDir);
float diff = (dot (i.Normal, lightDir)+_Wrap)/(1+_Wrap);
diff = saturate (diff);
float nh = (dot (i.Normal, h)+_Wrap)/(1+_Wrap);
nh = saturate(nh);
float spec = pow (nh, i.Specular*128.0) * i.Gloss;
float4 c;
c.rgb = (i.Albedo * _LightColor0.rgb * diff *scattering + _LightColor0.rgb * _SpecColor.rgb * spec) * (atten * 2);
c.a = i.Alpha + _SpecColor.a * spec * atten;
return c;
}
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.screenUV = ComputeScreenPos(o.vertex);
o.lightDir = ObjSpaceLightDir(v.vertex);
o.viewDir = ObjSpaceViewDir(v.vertex);
o.normal = v.normal;
o.grabUV = ComputeGrabScreenPos(o.vertex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
float4 frag (v2f i) : SV_Target
{
// sample the texture
float4 col = tex2D(_MainTex, i.uv);
//
float frontDepth = LinearEyeDepth( i.screenUV.z/i.screenUV.w );
//
float2 backDepthUV = i.screenUV.xy/i.screenUV.w;
float4 backDepthColor = tex2D(_BackDepthTex, backDepthUV);
float backDepth = LinearEyeDepth(DecodeFloatRGBA(backDepthColor));
//do scattering
float depth = backDepth-frontDepth;
float3 scattering = exp(-_AttenuationC.xyz*depth);
//do lighting
LightingInput lightVar;
lightVar.Albedo = col.rgb * _Color.rgb;
lightVar.Gloss = col.a;
lightVar.Alpha = col.a * _Color.a;
lightVar.Specular = _Shininess;
lightVar.Normal = i.normal;
col = CalculateLighting (lightVar, i.lightDir, i.viewDir, _LightColor0.a, scattering);
//blend
//col.xyz = col.a*col.rgb + (1-col.a)*tex2D(_GrabTexture, i.grabUV.xy/i.grabUV.w);
col.xyz = lerp(tex2D(_GrabTexture, i.grabUV.xy/i.grabUV.w), col.rgb, col.a);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
|
照例,我们对上述代码中某些函数进行说明:
1、ComputeScreenPos函数是将经过透视投影的顶点变换到屏幕坐标系中,然后就可以使用xy/w的值作为UV取屏幕坐标系下的深度图的值。
具体细节可以参看UnityCG.cginc文件,这里也将代码贴出来:
?1 2 3 4 5 6 7 8 9 10 11 |
inline float4 ComputeScreenPos (float4 pos) {
float4 o = pos * 0.5f;
#if defined(UNITY_HALF_TEXEL_OFFSET)
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w * _ScreenParams.zw;
#else
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
#endif
o.zw = pos.zw;
return o;
}
|
2、ComputeGrabScreenPos函数做的事情跟上述ComputeScreenPos函数是一样的,只不过对于GrabPass取到的渲染结果与屏幕空间不太一致,这里也列出代码:
?1 2 3 4 5 6 7 8 9 10 11 |
inline float4 ComputeGrabScreenPos (float4 pos) {
#if UNITY_UV_STARTS_AT_TOP
float scale = -1.0;
#else
float scale = 1.0;
#endif
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*scale) + o.w;
o.zw = pos.zw;
return o;
}
|
3、LinearEyeDepth函数是将经过透视投影变换的深度值还原成其在View坐标系中的值。具体细节读者可以参考此链接:
http://blog.sina.com.cn/s/blog_70f96aa90102v0wd.html
这篇博客是本人早期写的,大体意思有说明到,如果要穷究细节,则需要对透视投影做深入了解了。
4、DecodeFloatRGBA与EncodeFloatRGBA是一对函数。EncodeFloatRGBA用于将float值编码到RGBA四个通道上,Decode则是相应的解码过程。这两个函数是为了提高深度值的精度,以便于进行深度值计算时不会产生太大误差。
5、_BackDepthTex是物体背面的深度图,由Camera.RenderWithShader()产生,这部分的代码在脚本中实现,读者可以参考完整示例。
下面给出完整示例程序:
张明 2020-02-25 23楼 源码在GAD改版后丢失了,这里放百度网盘,需要的自取:链接:https://pan.baidu.com/s/1LBPycNqmlsmFFbLWzvkA7Q 提取码:tqar 标签:Unity3D,教程,lightDir,float,散射,float4,col,float3 From: https://www.cnblogs.com/gangtie/p/18497979