Toony Colors Pro 2项目分析——眼睛shader

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wodownload2/article/details/89671990

本节我们开始分析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,这里的分析已经完成。等待完善。

猜你喜欢

转载自blog.csdn.net/wodownload2/article/details/89671990