本节我们开始分析shader代码。
分析目标:Toony Colors Pro 2/Examples/Cat Demo/UnityChan/Style 1 Skin
项目在:https://gitee.com/yichichunshui/ToonyColors.git
void surf(Input IN, inout SurfaceOutputCustom o)
{
这是个表面着色器函数,直接处理的是像素,输入结构体为Input,定义如下:
struct Input
{
half2 uv_MainTex;
half2 uv_BumpMap;
float3 viewDir;
};
三个成员:主纹理uv、法线纹理uv,眼睛的方向。
输出结构体:SurfaceOutputCustom ,定义如下:
struct SurfaceOutputCustom
{
half atten;
fixed3 Albedo;
fixed3 Normal;
fixed3 Emission;
half Specular;
fixed Gloss;
fixed Alpha;
fixed3 ShadowColorTex;
fixed Rim;
};
参数依次为:衰减系数、漫反射颜色、法线、自发光、镜面反射系数、粗糙度、透明度、阴影纹理、边缘光系数。
完整代码如下:
void surf(Input IN, inout SurfaceOutputCustom o)
{
fixed4 mainTex = tex2D(_MainTex, IN.UV_MAINTEX);
//Shadow Color Texture
fixed4 shadowTex = tex2D(_STexture, IN.UV_MAINTEX);
o.ShadowColorTex = shadowTex.rgb;
o.Albedo = mainTex.rgb * _Color.rgb;
o.Alpha = mainTex.a * _Color.a;
//Normal map
half4 normalMap = tex2D(_BumpMap, IN.uv_BumpMap.xy);
o.Normal = UnpackScaleNormal(normalMap, _BumpScale);
//Rim
float3 viewDir = normalize(IN.viewDir);
half rim = 1.0f - saturate( dot(viewDir, o.Normal) );
rim = smoothstep(_RimMin, _RimMax, rim);
o.Rim = rim;
}
surf函数依次对输出结构体SurfaceOutputCustom 的成员进行赋值。
代码解释为:
采样主纹理;
采样阴影纹理;
赋值阴影颜色成员;
主纹理颜色主颜色混合赋值给漫反射颜色;
主纹理的alpha主颜色的alpha赋值给输出结构体成员透明度。
采样法线纹理;
解压法线纹理;此函数在UntyStandardUtils.cginc中;
规格化眼睛朝向;
计算边缘因子;
smoothstep处理边缘因子;
赋值边缘因子;
搞清楚每句代码的意义是啥,它对最终的显示效果又什么影响。
Shadow Color Texture
的样子是什么?
我们在unity里面看到这个图片只有RGB三个通道,没有A通道。
下面我们要安装ps,在ps中看看其图层分布,在ps中显示的这个图片也是只有三个通道:
而主纹理呢?_MainTex
这个是有A通道的图片,在unity中可以只看a通道的图,点击下面的红色区域按钮即可在RGB通道和A通道之间切换了:
也就是上面的贴图对于主纹理只显示脸的部分。其余部分都是透明的。
我们的如果只对这个主纹理进行采样应该这样显示:
void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb;
}
同时使用unity自己的光照模型Lambert,然后把输入变量改为SurfaceOutput即可。
然后再回到之前的函数:
o.Albedo = mainTex.rgb * _Color.rgb;
这里采样完主纹理之后,又乘以了一个颜色,也就是说,我们还可以对主纹理进行颜色的修改,不是说采样完就可以了,那么_Color就是为我们提供了更改主纹理的颜色的方法了。
比如:
我们可以将主纹理的RGB都改为200,稍微降低点原主纹理的颜色,这样整体的感觉就会变得暗一些。
这样也就学到一个知识点,如果调整原来贴图的颜色。
我们接着分析,注释掉掉法线、注释掉边缘光因子计算。就看看阴影贴图是如何使用的。
我们看到这句话:
s.Albedo = lerp(s.ShadowColorTex.rgb, s.Albedo, ramp);
使用lerp函数在主纹理和阴影纹理之间做插值,插值因子为ramp。ramp=0,则为ShadowColorTex,ramp为1,则为albedo。
哦,这样我就明白了,其实在光照函数中,就是要计算一个因子ramp,然后用ramp在两个纹理之间做融合。
所以现在的问题就看看ramp是如何计算的?
找到代码:
fixed3 ramp = smoothstep(RAMP_THRESHOLD - RAMP_SMOOTH*0.5, RAMP_THRESHOLD + RAMP_SMOOTH*0.5, NDL);
我们知道smoothstep的圆形为:smoothstep(a,b,x),返回的值在0到1之间。
大致的函数图像为:
而这里的两个边缘a和b是宏定义的:
#define RAMP_THRESHOLD _RampThreshold
#define RAMP_SMOOTH _RampSmooth
其中_RampThreshold和_RampSmooth是在properties中声明的,可以slider的变量:
_RampThreshold ("Ramp Threshold", Range(0,1)) = 0.5
_RampSmooth ("Ramp Smoothing", Range(0.001,1)) = 0.1
所以,看看NDL在什么范围:
fixed3 ramp = smoothstep(RAMP_THRESHOLD - RAMP_SMOOTH*0.5, RAMP_THRESHOLD + RAMP_SMOOTH*0.5, NDL);
#define IN_NORMAL s.Normal //法线,有surf函数传入
half3 lightDir = gi.light.dir; //gi的灯光方向
fixed ndl = max(0, dot(IN_NORMAL, lightDir) * 0.5 + 0.5);
#define NDL ndl
dot(IN_NORMAL, lightDir) 值在-1到1之间,max(0,dot),在0到1之间,乘以0.5,在0到0.5之间,加上0.5,在0.5到1之间。
所以最终ndl的值在0.5到1之间。
所以要想有平滑效果,还是要把区间限定在0.5到1之间。
想一下,人家为什么这么做呢?
如果是单张纹理,那么是没有光照效果的,也就是明暗变换只能在原始的单张贴图上体现,如下:
而如果使用ndl作为因子进行插值,那么则为:
你可能看不出明显的变化,但是如果你改变灯光的方向,那么则能够看到人脸上颜色的变化了。
综上,其实就是想让灯光的变化体现在人的脸上而已,所做的就是用ndl因子,在阴影贴图和主纹理之间做插值。
那么其实这个效果不明显,更好的是直接用ndl值乘以主纹理颜色,更能体现灯光对人脸的影响,有明显的明暗效果。
然后继续:
fixed4 c;
c.rgb = s.Albedo * lightColor.rgb * ramp;
return c;
定义一个颜色,最后返回的是这个颜色,那么s.Albedo * lightColor.rgb,其实就混合了灯光的颜色,让灯光的颜色的改变也能体现到人的脸上。
这个ramp因子计算的有点复杂:
_SColor = lerp(_HColor, _SColor, _SColor.a); //Shadows intensity through alpha
ramp = lerp(_SColor.rgb, _HColor.rgb, ramp);
fixed3 wrappedLight = saturate(_DiffTint.rgb + saturate(dot(IN_NORMAL, lightDir)));
ramp *= wrappedLight;
fixed4 c;
c.rgb = s.Albedo * lightColor.rgb * ramp;
我觉得上面的思路是什么呢?
首先主纹理有个颜色了,这是第一个;
然后考虑阴影贴图,这个是第二个;
第一次融合是用这个主纹理和影子贴图进行融合,融合因子是ndl的平滑因子;
ok,这里其实有已经有阴影的效果了,而且还考虑的灯光方向。
接着我们想灯光的颜色也能影响最终颜色,所以直接乘以灯光颜色即可。这是第二次颜色的混合。
最后我们还想着三个颜色都是能不能让明暗更加明显呢?让暗的地方更暗一点呢?
这里首先是给定两个颜色,一个是高亮颜色_HColor,一个是黑暗的颜色_SColor,他们直接用一个黑暗颜色的a通道做lerp得到一个颜色。
他们的定义在:
_HColor ("Highlight Color", Color) = (0.785,0.785,0.785,1.0)
_SColor ("Shadow Color", Color) = (0.195,0.195,0.195,1.0)
然后用:
ramp = lerp(_SColor.rgb, _HColor.rgb, ramp);
还记得lerp吗?ramp=1的时候,完全是_HColor,而ramp为0的时候,则全是_SColor,而ramp是啥呢?是ndl因子平滑后的值。ndl越大,说明越是灯光直射的地方,所以我们把_HColor放在lerp的第二个参数,这样计算的结果和ndl的值成正比关系。
接下来人家是如何进一步拉开明暗分界的:
fixed3 wrappedLight = saturate(_DiffTint.rgb + saturate(dot(IN_NORMAL, lightDir)));
ramp *= wrappedLight;
saturate(dot(IN_NORMAL, lightDir)就是ndl值;
而_DiffTint.rgb+上这个值,注意fixed3 a=1,其实就是fixed3 a = fixed3(1,1,1);这是自动补全的。
这里的意思是,外加了一个颜色提示,并且rgb分量都要加上ndl的值。最后ramp乘以这个权重。
所以你看这里的ramp已经经过:ndl的一次平滑,再加上一个颜色偏移,防止过暗,因为乘法是颜色变暗。
ramp=ramp*(color+ramp);
最后用这个ramp做计算:
c.rgb = s.Albedo * lightColor.rgb * ramp;
这一切都是为了计算漫反射颜色,最后考虑gi的间接光照:
#ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT
c.rgb += s.Albedo * gi.indirect.diffuse;
#endif
最后在添加一个边缘光效果:
c.rgb += ndl * lightColor.rgb * atten * s.Rim * _RimColor.rgb * _RimColor.a;
rim因子在surf中计算:
float3 viewDir = normalize(IN.viewDir);
half rim = 1.0f - saturate( dot(viewDir, o.Normal) );
rim = smoothstep(_RimMin, _RimMax, rim);
o.Rim = rim;
ndl值作为因子,可以在越靠近灯光越直射的地方边缘光越大,这个我觉得是不对的,应该去除掉。
c.rgb += lightColor.rgb * atten * s.Rim * _RimColor.rgb * _RimColor.a;
其他的应该好理解,灯光的颜色参与混合,边缘光的颜色参与混合,衰减因子也可以忽略,边缘因子参与混合,边缘颜色的a可以参与混合。其实最简单的就是:
c.rgb += s.Rim * _RimColor.rgb;
这样结果为:
至此光照函数分析完毕。
下面分析的是surface的声明函数部分的一些特性,如使用何种光照:
#pragma surface surf ToonyColorsCustom addshadow fullforwardshadows exclude_path:deferred exclude_path:prepass
首先光照函数为自定义的ToonyColorsCustom,函数的声明如下:
inline half4 LightingToonyColorsCustom (inout SurfaceOutputCustom s, half3 viewDir, UnityGI gi)
{
三个参数:
surf函数的输出结构体;
眼睛方向;
Unity的全局光照函数;
#define IN_NORMAL s.Normal
定义宏IN_NORMAL等于输出结构体的法线。
half3 lightDir = gi.light.dir;
灯光方向为gi中的灯光方向;
#if defined(UNITY_PASS_FORWARDBASE)
half3 lightColor = _LightColor0.rgb;
half atten = s.atten;
#else
half3 lightColor = gi.light.color.rgb;
half atten = 1;
#endif
如果定义了UNITY_PASS_FORWARDBASE宏;
则灯光的颜色等于_LightColor的颜色;衰减等于输出结构体的衰减,但其实输出结构体在surf函数中,没有对atten赋值,所以它是0。
否则,灯光的颜色等于gi的灯光的颜色;衰减为1。
IN_NORMAL = normalize(IN_NORMAL);
fixed ndl = max(0, dot(IN_NORMAL, lightDir) * 0.5 + 0.5);
#define NDL ndl
计算ndl的值,并做半角处理。
定义了宏,定义ndl值进行平滑处理:
#if defined(UNITY_PASS_FORWARDBASE)
#define RAMP_THRESHOLD _RampThreshold
#define RAMP_SMOOTH _RampSmooth
#else
#define RAMP_THRESHOLD _RampThresholdOtherLights
#define RAMP_SMOOTH _RampSmoothOtherLights
#endif
fixed3 ramp = smoothstep(RAMP_THRESHOLD - RAMP_SMOOTH*0.5, RAMP_THRESHOLD + RAMP_SMOOTH*0.5, NDL);
#if !(POINT) && !(SPOT)
ramp *= atten;
#endif
对ramp进行乘以衰减。
s.Albedo = lerp(s.ShadowColorTex.rgb, s.Albedo, ramp);
在阴影颜色和漫反射颜色进行lerp操作。
这里还有一个全局的照明函数:
void LightingToonyColorsCustom_GI(inout SurfaceOutputCustom s, UnityGIInput data, inout UnityGI gi)
{
gi = UnityGlobalIllumination(data, 1.0, IN_NORMAL);
s.atten = data.atten; //transfer attenuation to lighting function
gi.light.color = _LightColor0.rgb; //remove attenuation
}
我们试着去除或者是改个名字,都会报错。
这是为啥呢?
因为在光照函数中,我们使用到了一个gi:
inline half4 LightingToonyColorsCustom (inout SurfaceOutputCustom s, half3 viewDir, UnityGI gi)
{
……
}
也就是说,这个LightingToonyColorsCustom_GI函数,负责给LightingToonyColorsCustom 提供输入变量gi。
那么这里的执行顺序是:surf-》全局光照函数-》自定义光照函数
那么是不是说,所有的自定义光照函数都必须有要给gi呢?
很显然不是的,由于自定义光照函数的输入可以是不同的,当需要有gi的时候,我们才需要自定义个全局光照函数。如果自定义光照压根没有需要gi,那么也无需自定义光照函数。
比如下面的例子,我们自定义一个光照函数,用于返回简单的颜色试试:
Shader "Unlit/MyLightModel"
{
SubShader
{
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf MyLight
struct Input
{
float2 uv;
};
half4 LightingMyLight(SurfaceOutput s, half3 lightDir, half atten)
{
half3 rgb = half3(0, 1, 0);
rgb += s.Albedo;
return half4(rgb, 1);
}
void surf(Input IN, inout SurfaceOutput o)
{
o.Albedo = fixed4(1, 0, 0, 1);
}
ENDCG
}
}
上面的surface代码,主要是介绍最简单的处理的光照模型的方法,在surf中返回红色;在自定义光照模型MyLight中,进行合绿色进行混合,最后返回的是黄色。
而对于自定义的光照模型和gi,请参考:https://docs.unity3d.com/Manual/SL-SurfaceShaderLightingExamples.html
下面再看下自定义的全局光照函数中的输入参数:UnityGI
struct UnityGI
{
UnityLight light;
UnityIndirect indirect;
};
它包含两个成员:光照和间接光照,这个在UnityCommonLighting.cginc中:
struct UnityLight
{
half3 color;
half3 dir;
half ndotl; // Deprecated: Ndotl is now calculated on the fly and is no longer stored. Do not used it.
};
struct UnityIndirect
{
half3 diffuse;
half3 specular;
};
也就是说unity提供的全局光包含两个部分:直接光照和讲解光照。
直接光照包含了颜色、方向,ndotl(此变量不建议使用了);
间接光照包含了漫反射和镜面反射两个。
另外一个输入:UnityGIInput
struct UnityGIInput
{
UnityLight light; // pixel light, sent from the engine
float3 worldPos;
half3 worldViewDir;
half atten;
half3 ambient;
// interpolated lightmap UVs are passed as full float precision data to fragment shaders
// so lightmapUV (which is used as a tmp inside of lightmap fragment shaders) should
// also be full float precision to avoid data loss before sampling a texture.
float4 lightmapUV; // .xy = static lightmap UV, .zw = dynamic lightmap UV
#if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION) || defined(UNITY_ENABLE_REFLECTION_BUFFERS)
float4 boxMin[2];
#endif
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
float4 boxMax[2];
float4 probePosition[2];
#endif
// HDR cubemap properties, use to decompress HDR texture
float4 probeHDR[2];
};
包含了:直接光照、世界坐标位置、世界眼睛方向、衰减、环境光、光照贴图uv、其他未知变量。
而上面的自定义的全局光照函数中,使用unity自己提供的:UnityGlobalIllumination
void LightingToonyColorsCustom_GI(inout SurfaceOutputCustom s, UnityGIInput data, inout UnityGI gi)
{
gi = UnityGlobalIllumination(data, 1.0, IN_NORMAL);
s.atten = data.atten; //transfer attenuation to lighting function
gi.light.color = _LightColor0.rgb; //remove attenuation
}
原型如下:
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld)
{
return UnityGI_Base(data, occlusion, normalWorld);
}
这个就不再继续往下深究了,有空我们再看其实现。
ok,这里的分析已经完成。等待完善。