Unity卡通着色Shader实现

另外一篇已经被实现的边缘着色

Unity——Shader实现边缘发光效果 | mao的博客 (mao1mao2mao3mao4.github.io)

法线外拓的三种方式 来自文心一言的回答

要将模型空间下的法线转换到视角空间下,你需要使用模型到视图矩阵(UNITY_MATRIX_MV),但需要注意法线变换的特殊性。法线变换需要使用逆转置矩阵(Inverse Transpose Matrix)来保持其垂直性。Unity提供了一个宏UNITY_MATRIX_IT_MV(尽管你之前的代码中名称有误,但这里是为了说明正确的使用方式),它实际上已经包含了模型到视图矩阵的逆转置。

1.模型空间下

image-20241102171649400

2.视角空间下

image-20241102173012923

3.裁剪空间下

image-20241102173924641

基础阴影实现

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
 Shader "Unlit/07"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Diffuse("Diffuse", Color) = (1,1,1,1) //漫反射颜色
_OutLine("Outline",Range(0,1)) = 0.5
_OutLineColor("OutLineColo",Range(0,1)) = 0.1 //描边颜色
_Steps("Steps",Range(1,30)) = 1 //颜色区分的阶数
_ToonEffect("ToonEffect",Range(0,1)) = 0.5 //卡通化影响程度
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

pass
{
Cull Front

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work

float _OutLine;
float _OutLineColor;


float4 vert (appdata_base v) : SV_POSITION
{
v.vertex += v.normal * _OutLine;
return UnityObjectToClipPos(v.vertex);
}

fixed4 frag () : SV_Target
{
return _OutLineColor;
}
ENDCG
}

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc "



struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD3;
float3 worldPos : texcoord4;

};

sampler2D _MainTex;
float4 _MainTex_ST;


float4 _Diffuse; //漫反射颜色

float _OutLine;
float _OutLineColor;

float _Steps;

float _ToonEffect;

v2f vert (appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float4 texColor = tex2D(_MainTex,i.uv);

float3 environmentLightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;

float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightDir =normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 diffuseColor = _LightColor0.rbg * _Diffuse.rbg * (dot(lightDir,i.worldNormal) *0.5 + 0.5) * texColor.rbg;


//颜色卡通化
//将颜色平滑在【0,1】之间
diffuseColor = smoothstep(0,1,diffuseColor);

//颜色离散化
//floor向下取整,diffusColor的值因为Cos值限制在0到1 之间,不乘结果就只有0和1
//放大再缩小
float3 toon = floor(diffuseColor * _Steps) / _Steps;
diffuseColor = lerp(toon,diffuseColor,_ToonEffect);


float3 resColor = environmentLightColor + diffuseColor;

float col = float4(resColor,1);
return col;

}
ENDCG
}
}

FallBack "Diffuse"
}

边缘光

在世界空间下求得模型边缘

1
2
3
4
5
 //边缘光
//本身和视角平行的,也就是最边缘的cos值为0,但是这样不利于颜色控制,所以使用1-
float rim = 1- dot(viewDir,i.worldNormal);
//使用除法是为了方便在Unity操作窗口中对边缘光的大小效果进行一个直观的调节,数值越小,边缘光就越小
float3 rimColor = _RimColor * pow(rim , 1/_RimPower);

XRay

人物被物体遮挡,透过遮挡物看见人物的功能

实际上是调整深度和深度写入中的功能

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
Pass
{

//关闭深度写入,翻转深度测试
//A遮挡B,B是Xray物体
//翻转深度测试后,物体被遮挡后才会被渲染,也就是通过深度测试,紧接着就会进行深度写入操作
//这样的话深度缓冲中最新的数据就是该XRay物体,那么在A和B之间绘制一个物体C的时候,物体C会被绘制到A的前面
ZWrite Off
ZTest Greater
Blend SrcAlpha One //使用源alpha进行渲染

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

//透视颜色和透视强度
float4 _XRayColor;
float _XRayPower;

struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos: TEXCOORD2;
};


v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;

return o;

}

float4 frag(v2f i):SV_Target
{
float3 normal = normalize(i.worldNormal);
float3 pos = i.worldPos;

float3 viewDir = normalize(UnityWorldSpaceViewDir(pos));
float3 lightDir = normalize(UnityWorldSpaceLightDir(pos));

float rim = 1 - dot(viewDir,normal);

//当人物被遮挡的时候发出边缘光
return _XRayColor * pow(rim,1/_XRayPower);
}
ENDCG
}

请注意,所有透明有关的shader要正确设置自己的渲染顺序

这里通过固有关键字名称+数字的形式,不能有括号

1
Tags { "Queue"="Geometry+600" } //和半透明相关的都需要在透明度上下功夫

Shader优化

image-20241104095829434

卡通场景

杂项知识

  • 使用的内容和人物2DShader一致,新添加了对法线的使用

Unity —— Shader入门,法线贴图 | mao的博客 (mao1mao2mao3mao4.github.io)

  • 新增对UsePass的使用
1
2
UsePass "Unlit/07/OutLine" //直接使用别的Shader中的Pass,前提是该Pass中使用的参数要在Propreties中定义
//在Unity2018之后的版本中,对Pass的使用可以不大写
image-20241104110442625
  • Properties属性栏中,属性,例如:_MainTex,这样的属性名,如果在ShaderA,B中都有声明,A中首先引用一张图片,那么B在Unity操作窗口中同样会有这一张图片的引用

雪的制作

Properties属性添加

1
2
3
_Snow("Snow",Range(0,1))= 0.5//雪的大小
_SnowColor("SnowColor",Color) = (1,1,1,1)//雪的颜色
_SnowDir("SnowDir",vector) = (1,1,1,1) //雪的方向

Shader宏定义

1
2
3
4
5
6
7
8
9
         #pragma vertex vert
#pragma fragment frag

//对于一个宏变量的处理,表示开关状态,一般用 __ 表示未定义状态
//不然如果是一个有含义的变量去处理,在使用的时候需要对两个变量就行管理
//可能会出现同时有效的情况
//__代表的是默认不开的情况
//在命名上有一定规则
#pragma mutil_compile __ _SNOW_ON

Shader效果实现

1
2
3
4
5
6
7
8
9
10
11
 #if _SNOW_ON
if(dot(worldNormal,_SnowDir.xyz) > lerp(1-1,_Snow)) //lerp,前者是开始值,后置是目标值
{
resColor.rbg = _SnowColor.rbg;

}
else
{
resColor.rbg = resColor.rbg;
}
#endif

C#脚本宏控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShaderSnow : MonoBehaviour
{

private static string SNOW_ON = "SNOW_ON";

[MenuItem("Tools/Shader/打开或则关闭雪的宏")]
public static void OpenRimLight()
{
if(Shader.IsKeywordEnabled(SNOW_ON))
{
Shader.DisableKeyword(SNOW_ON);
}
else
{
Shader.EnableKeyword(SNOW_ON);
}
}
}

C#全局变量控制

1
2
3
4
//使用方式和在Shader的Properties定义的一样,只不过这里有,Properties就不能定义     
string Snow = "_Snow";

Shader.SetGlobalFloat(Snow, 1.0f);