【图形学】《ShaderLab新手宝典》笔记
聪头 游戏开发萌新

ShaderLab新手宝典

时间:2021年11月30日09:12:27

本笔记仅作为《Shader入门精要》的部分要点回顾和补充,重点放在表面着色器和ASE的使用上

Ch7.透明效果

P125 模板测试

  • 不管通过还是未通过测试,在测试结束之后都可以对缓存中的模板值做操作

语法:

1
2
3
4
5
6
7
8
9
10
Stencil
{
Ref referenceValue
ReadMask readMask
WriteMask writeMask
Comp comparisonFunction
Pass stencilOperation
Fail stencilOperation
ZFail stencilOperation
}

Ch8.表面着色器的基础概念

P134 组织结构

表面着色器编译指令的语法结构为:

#pragma surface surfaceFunction lightModel [optionalparams]

1)surface:声明所使用的Shader是表面着色器

2)surfaceFunction:声明表面着色器的函数名称,被称为表面函数,一般使用surf作为表面函数的名称

3)lightModel:声明所使用的的光照模型。Unity内置四种光照模型,分别为:

  • 非物理光照模型:Lambert和BlinnPhong
  • 物理光照模型:Standard和StandardSpecular

4)[optionalparams]:其他可选参数

P134 编译指令中的可选参数

自行查询书籍了解

P137 表面函数的语法结构

1
2
3
4
void surf(Input IN, inout SurfaceOutput o)
{
//表面函数代码
}

1.表面函数输入结构体

使用者可以通过下表中的变量直接获取到相关数据,然后传入表面函数中进行计算

变量 说明
float2 uv_texName、float2 uv2_texName uv关键词后接纹理名称,获取模型某套uv坐标
float3 viewDir 摄像机视角方向(世界空间),没有被标准化
使用COLOR语义定义的float4变量 插值后的逐顶点颜色
float4 screenPos 屏幕空间坐标,可用于反射或屏幕空间特效,但不适用于GrabPass,需要使用ComputeGrabScreenPos函数单独计算uv
float3 worldPos 世界空间坐标
float3 worldRefl 世界空间反射向量,前提是没有修改表面法线 o.Normal
float3 worldNormal 世界空间法线线路,前提是没有修改表面法线 o.Normal
float3 worldRefl; INTERNAL_DATA 如果表面法线 o.Normal 进行了修改,在表面函数中通过WorldReflectionVector(IN, o.Normal)得到基于法线贴图的世界空间反射向量
float3 worldNormal; INTERNAL_DATA 如果表面法线 o.Normal 进行了修改,在表面函数中通过 WorldNormalVector(IN, o.Normal)得到基于法线贴图的世界空间法线向量

2.表面函数输出结构体

Lambert和BlinnPhong使用SurfaceOutput结构体输出

1
2
3
4
5
6
7
8
9
struct SurfaceOutput
{
fixed3 Albedo; //漫反射颜色
fixed3 Normal; //切线空间法线
fixed3 Emission; //自发光
half Specular; //镜面反射指数,范围0~1
fixed Gloss; //镜面反射强度
fixed Alpha; //透明通道
};

金属工作流输出结构体

1
2
3
4
5
6
7
8
9
10
struct SurfaceOutputStandard
{
fixed3 Albedo; //漫反射颜色
fixed3 Normal; //切线空间法线
fixed3 Emission; //自发光
half Metallic; //0表示非金属,1表示金属
half Smoothness; //0表示非常粗糙,1表示非常光滑
half Occlusion; //环境光遮蔽,默认为1
fixed Alpha; //透明通道
};

高光工作流输出结构体

1
2
3
4
5
6
7
8
9
10
struct SurfaceOutputStandard
{
fixed3 Albedo; //漫反射颜色
fixed3 Normal; //切线空间法线
fixed3 Emission; //自发光
//half Metallic; //0表示非金属,1表示金属
half Smoothness; //0表示非常粗糙,1表示非常光滑
half Occlusion; //环境光遮蔽,默认为1
fixed Alpha; //透明通道
};

Ch9.编写表面着色器

P141 使用法线贴图

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
Shader "Surface Shader/Normal Map"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
_Normal ("Normal Map", 2D) = (1,1,1,1)
_Bumpiness ("Bumpiness", Range(0, 1)) = 0
}
SubShader
{
CGPROGRAM
#pragma surface surf Lambert

struct Input
{
float2 uv_MainTex;
float2 uv_Normal;
};

sampler2D _MainTex;
fixed4 _Color;
sampler2D _Normal;
fixed _Bumpiness;

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
//采样法线贴图并解包
fixed3 n = UnpackNormal(tex2D(_Normal, IN.uv_Normal));
n *= float3(_Bumpiness, _Bumpiness, 1);
o.Normal = n;
}
ENDCG
}
FallBack "Diffuse"
}

P146 使用顶点修改函数

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
Shader "Surface Shader/Vertex Modify"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_Expansion ("Expansion", Range(0, 0.1)) = 0
}
SubShader
{
CGPROGRAM
#pragma surface surf Lambert vertex:vert

struct Input
{
float2 uv_MainTex;
};

sampler2D _MainTex;
fixed _Expansion;

//顶点修改函数,输入/输出 appdata_full 结构体
void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Expansion;
}

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}

P148 自定义光照函数

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
Shader "Surface Shader/Custom Lambert"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
SubShader
{
CGPROGRAM
#pragma surface surf CustomLambert

struct Input
{
float2 uv_MainTex;
};

sampler2D _MainTex;

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
}

//自定义光照函数
half4 LightingCustomLambert(SurfaceOutput s, half3 lightDir, half atten)
{
fixed NdotL = saturate(dot(s.Normal, lightDir));
half4 c;
c.rgb = s.Albedo * _LightColor0 * NdotL * atten;
c.a = s.Alpha;
return c;
}

ENDCG
}
FallBack "Diffuse"
}

P149 最终颜色修改函数

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
Shader "Surface Shader/Final Color Modify"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_ColorTint ("Color Tint", Color) = (1,1,1,1)
}
SubShader
{
CGPROGRAM
//声明最终颜色修改函数为 ModifyColor
#pragma surface surf Lambert finalcolor:ModifyColor

struct Input
{
float2 uv_MainTex;
};

sampler2D _MainTex;
fixed4 _ColorTint;

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
}

//最终颜色修改函数
void ModifyColor (Input IN, SurfaceOutput o, inout fixed4 color)
{
color *= _ColorTint;
}

ENDCG
}
FallBack "Diffuse"
}

P151 使用曲面细分函数

曲线细分语法:

添加编译指令:tessellate:FunctionName

1
2
3
4
float4 FunctionName()
{
return tess; //曲面细分等级
}

固定数量的曲面细分

1
2
3
4
5
6
7
8
9
#pragma surface surf Lambert tesellate:tessellation vertex:height addshadow

half _Tessellation; //参考值 1 ~ 32

//曲面细分函数
float4 tessellation()
{
return _Tessellation;
}

基于边长的曲面细分

会根据不同长度的边分别对应不同的曲面细分等级,边越长细分等级越高,从而使模型的边长在屏幕上看起来一致

1
2
3
4
5
6
7
8
9
10
#pragma surface surf Lambert tesellate:tessellateEdge vertex:height addshadow

half _EdgeLength; //边的平均长度,越小细分程度越高 参考值 1 ~ 32

//曲面细分函数
float4 tessellateEdge(appdata_full v0, appdata_full v1, appdata_full v2)
{
//调用基于边长的曲面细分函数
return UnityEdgeLengthBasedTess(v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
}

视锥剔除曲面细分

在基于边长的基础上,额外判断顶点是否在摄像机的视锥体内,超出视锥体范围的顶点将会被剔除

1
2
3
4
5
6
7
8
9
10
11
#pragma surface surf Lambert tesellate:tessellateCull vertex:height addshadow

half _EdgeLength; //边的平均长度,越小细分程度越高 参考值 1 ~ 32
float _MaxHeight; //参考值 0 ~ 0.5

//曲面细分函数
float4 tessellateCull(appdata_full v0, appdata_full v1, appdata_full v2)
{
//调用基于边长的曲面细分函数
return UnityEdgeLengthBasedTessCull(v0.vertex, v1.vertex, v2.vertex, _EdgeLength, _MaxHeight);
}

基于距离的曲面细分

  • d < minDist,细分等级为tess
  • minDist < d < maxDist:逐渐降低
  • d > maxDist:细分等级保持为1
1
2
3
4
5
6
7
8
9
10
11
12
#pragma surface surf Lambert tesellate:tessellateDistance vertex:height addshadow

half _MinDistance;
half _MaxDistance;
half _Tessellation; //参考值 1 ~ 32

//曲面细分函数
float4 tessellateDistance(appdata_full v0, appdata_full v1, appdata_full v2)
{
//基于距离的曲线细分
return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, _MinDistance, _MaxDistance, _Tessellation);
}

Phong 曲面细分

Phong细分会使新生成的顶点沿着原始顶点的平均法线方向偏移一段距离,使模型表面变得更加光滑

  • Phong曲面细分单独使用是没有效果的,需要结合曲面细分函数才会生效
1
2
3
4
5
6
7
8
9
10
#pragma surface surf Lambert tesellate:tessellation tessphong:_Phong

half _Tessellation; //参考值 1 ~ 32
fixed _Phong; //不加会报错,值越大,越光滑 0~1

//曲面细分函数
float4 tessellation()
{
return _Tessellation;
}

P164 透明度混合

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
Shader "Surface Shader/Transparent"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_Color ("Color(RGB-A)", Color) = (1,1,1,1)
}
SubShader
{
Tags {"Queue" = "Transparent"}

CGPROGRAM
//使用alpha指令开启透明效果
#pragma surface surf Lambert alpha

struct Input
{
float2 uv_MainTex;
};

sampler2D _MainTex;
fixed4 Color;

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a * _Color.a;
}
ENDCG
}
FallBack "Diffuse"
//个人感觉 Fallback "Transparent/VertexLit" 更合适
}

P166 透明度测试

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
Shader "Surface Shader/Transparent"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_AlphaTest ("Alpha Test", Range(0,1)) = 0
}
SubShader
{
Tags {"Queue" = "AlphaTest"}

CGPROGRAM
//添加alphatest指令开启透明测试
//添加addshadow 指令开启
#pragma surface surf Lambert alphatest:_AlphaTest addshadow

struct Input
{
float2 uv_MainTex;
};

sampler2D _MainTex;
//不用再次声明_AlphaTest照样没问题(和Phong曲面细分不同)

void surf (Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a * _Color.a;
}
ENDCG
}
FallBack "Diffuse"
//个人感觉 Fallback "Transparent/Cutout/VertexLit" 更合适
}

Ch10.Image Effect

P168 GrabPass

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
Shader "Custom/GrabPass"
{
Properties
{
_GrayScale ("Gray Scale", Range(0, 1)) = 0
}
SubShader
{
Tags {"Queue" = "Transparent"}

//调用GrabPass,并定义抓取图像的名称
GrabPass {"_ScreenTex"}

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

struct v2f
{
float4 pos : SV_POSITION;
float4 grabPos : TEXCOORD0;
};

v2f vert(float4 vertex : POSITION)
{
v2f o;
o.pos = UnityObjectToClipPos(vertex);
//计算抓取图像在屏幕上的位置
o.grabPos = ComputeGrabScreenPos(o.pos);

return o;
}

fixed _GrayScale;

//声明抓取图像
sampler2D _ScreenTex;

half4 frag (v2f i) : SV_TARGET
{
//采样抓取图像
//tex2Dproj相当于将纹理的xy分量除以w分量之后再对纹理贴图进行采样
half4 src = tex2Dproj(_ScreenTex, i.grabPos);
half grayscale = Luminance(src.rgb);
half4 dst = half4(grayscale, grayscale, grayscale, 1);

return lerp(src, dst, _GrayScale);
}
}
}
}

P187 自定义后处理堆栈

Shader

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
Shader "Hidden/BSC - HLSL"
{
HLSLINCLUDE
#include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"
//属性声明
TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
half _Brightness;
half _Saturation;
half _Contrast;

float4 Frag(VaryingsDefault i) : SV_TARGET
{
//采样 RenderTexture
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);
//亮度
color.rgb *= _Brightness;
//饱和度
float luminance = dot(color.rgb, float3(0.2126729, 0.7151522, 0.0721750));
color.rgb = lerp(luminance, color.rgb, _Saturation);
//对比度
half3 grayColor = half3(0.5, 0.5, 0.5);
color.rgb = lerp(grayColor, color.rgb, _Contrast);

return color;
}
ENDHLSL

SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
HLSLPROGRAM
#pragma vertex VertDefault
#pragma fragment Frag
ENDHLSL
}
}
}

C#脚本

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
using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

//参数模块:继承PostProcessEffectSettings
//参数需序列化
[Serializable]
//参数①:关联脚本的渲染模块
//参数②:后期处理操作应用的对象类型或所处时间阶段
// - BeforeTransparent:后处理效果只会应用于不透明物体,会在透明物体渲染之前执行
// - BeforeStack:后处理效果会在内置的堆栈之前执行,如抗锯齿、景深、色调映射等前面
// - AfterStack:后处理效果会在内置的堆栈之后执行,如果有抗锯齿效果,会在其之前执行
//参数③:后期处理效果在下拉列表中的名称,使用"/"可以创建子目录
[PostProcess(typeof(BSCRenderer), PostProcessEvent.AfterStack, "Custom/BSC")]
public sealed class BSC : PostProcessEffectSettings
{
//开放属性
[Range(0f, 2f), Tooltip("Brightness effect intensity.")]
public FloatParameter Brightness = new FloatParameter { value = 1f };

[Range(0f, 2f), Tooltip("Saturation effect intensity.")]
public FloatParameter Saturation = new FloatParameter { value = 1f };

[Range(0f, 2f), Tooltip("Contrast effect intensity.")]
public FloatParameter Contrast = new FloatParameter { value = 1f };
}

//渲染模块:继承PostProcessEffectRenderer<T>
public sealed class BSCRenderer : PostProcessEffectRenderer<BSC>
{
public override void Render(PostProcessRenderContext context)
{
// 查找Shader文件
var sheet = context.propertySheets.Get(Shader.Find("Hidden/BSC-HLSL"));

//传递属性到Shader
sheet.properties.SetFloat("_Brightness", settings.Brightness);
sheet.properties.SetFloat("_Saturation", settings.Saturation);
sheet.properties.SetFloat("_Contrast", settings.Contrast);

context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
}
}

Setting中常用变量类型

类型 描述
FloatParameter 浮点型
BoolParameter 布尔型
ColorParameter 颜色
Vector2Parameter 二维向量
Vector3Parameter 三维向量
Vector4Parameter 四维向量
TextureParameter 纹理贴图,常用的默认值有:None、Black、White、Transparent

最终效果

image

Ch11.自定义材质面板

P197 Toggle

默认关键词

  • 例句:[Toggle] _Invert color?", Float) = 0
    • 以上例句会被Unity默认设置为_INVERT_ON

自定义关键词

  • 例句:[Toggle(ENABLE_FANCY)] _Fancy ("Fancy?", Float) = 0
    • 关键词:ENABLE_FANCY

P197 Enum

内置枚举类型

  • 例句:[Enum(UnityEngine.Rendering.BlendMode)] _Blend ("Blend mode", Float) = 1

自定义枚举类型

  • 一个枚举最多只能自定义7个名称/数值对
  • 例句:[Enum(Off, 0, On, 1)] _ZWrite ("ZWrite", Float) = 0

P198 KeywordEnum

和Enum类似,只不过关键词枚举会有与之对应的Shader关键词

  • 例句:[KeywordEnum(None, Add, Multiply)] _Overlay ("Overlay mode", Float) = 0
    • 这三个选项对应的Shader关键词分别为:_OVERLAY_NONE_OVERLAY_ADD_OVERLAY_MULTIPLY

P198 编译指令定义关键词

定义了ToggleDrawer或KeywordEnumDrawer之后,如果想要正常使用,还需要在编译指令中声明Shader关键词

两种编译指令,不同关键词之间用空格隔开

  • shader_feature:只会为材质使用到的关键词生成变体,没有使用到的关键词不会生成变体,因此无法在运行的时候通过脚本切换效果
    • 例如,#pragma shader_feature _OVERLAY_NONE _OVERLAY_ADD _OVERLAY_MULTIPLY
    • 例如,#pragma shader_feature _INVERT_ON
  • multi_compile:会为所有关键词生成变体,因此可以在运行的时候通过脚本切换效果
    • 例如,#pragma multi_compile _OVERLAY_NONE _OVERLAY_ADD _OVERLAY_MULTIPLY

P199 PowerSlider

指数滑动条

  • 例子:[PowerSlider(3.0)] _Brightness ("Birghtness", Range(0.01, 1)) = 0.1
    • 这是一个以3为指数的滑动条

P200 IntRange

整数滑动条

  • 例子:[IntRange] _Alpha ("Alpha", Range(0, 255)) = 100

Ch13.初级案例

P228 1.流光效果

原理:通过引入_Time对uv坐标进行偏移,实现流光效果

image
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
Shader "Samples/Light Flow"
{
Properties
{
_Tex("Texture", 2D) = "white" {}
_Color("Color", Color) = (0, 1, 1, 1)

// 关键词枚举,0为x方向,1为y方向
[KeywordEnum(X,Y)] _DIRECTION("Flow Direction", float) = 0
_Speed("Flow Speed", float) = 1
}
SubShader
{
Tags {"RenderType" = "Transparent" "Queue" = "Transparent"}

Blend One One
Cull Off

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 定义枚举关键词
#pragma shader_feature _DIRECTION_X _DIRECTION_Y

#include "unityCG.cginc"

struct v2f
{
float2 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _Tex;
float4 _Tex_ST;
fixed4 _Color;
float _Speed;

v2f vert (appdata_base v)
{
v2f o;

o.texcoord = TRANSFORM_TEX(v.texcoord, _Tex);
o.vertex = UnityObjectToClipPos(v.vertex);

return o;
}

float4 frag (v2f i) : SV_Target
{
float2 texcoord;

// 判断流动方向
#if _DIRECTION_X
texcoord = float2(i.texcoord.x + _Time.x * _Speed,
i.texcoord.y);
#elif _DIRECTION_Y
texcoord = float2(i.texcoord.x,
i.texcoord.y + _Time.x * _Speed);
#endif

return tex2D(_Tex, texcoord) * _Color;
}
ENDCG
}
}
}

P234 2.描边效果

原理:通过两个Pass,第一个Pass关闭深度写入,绘制一遍沿法线扩张的描边色模型;第二个Pass正常绘制。由于第一个Pass关闭了深度写入,如果渲染队列为Geometry,可能会导致在模型身后的几何体将描边颜色覆盖住,进而产生错误的效果,因此将整个Shader的渲染队列延迟到Transparent

亮点亦是缺点:第二个Pass会完全覆盖第一个Pass非描边部分,这会导致描边细节丢失,而仅仅绘制模型外围一圈描边,模型内部没有任何描边

image
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
Shader "Samples/Outline"
{
Properties
{
[Header(Texture Group)]
[Space(10)]
_Albedo ("Albedo", 2D) = "white" {}
[NoScaleOffset]_Specular ("Specular (RGB-A)", 2D) = "black" {}
[NoScaleOffset]_Normal ("Normal", 2D) = "bump" {}
[NoScaleOffset]_AO ("Ambient Occlusion", 2D) = "white" {}

[Header(Outline Properties)]
[Space(10)]
_OutlineColor ("Outline Color", Color) = (1,0,1,1)
_OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.01
}

SubShader
{
Tags { "RenderType"="Opaque" "Queue" = "Transparent"}

//---------- Outline Layer ----------
Pass
{
ZWrite Off

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

struct v2f
{
float4 vertex : SV_POSITION;
};

fixed4 _OutlineColor;
fixed _OutlineWidth;

v2f vert(appdata_base v)
{
v2f o;
v.vertex.xyz += v.normal * _OutlineWidth;
o.vertex = UnityObjectToClipPos(v.vertex);

return o;
}

fixed4 frag(v2f i) : SV_Target
{
return _OutlineColor;
}

ENDCG
}

//---------- Regular Layer ----------
CGPROGRAM
#pragma surface surf StandardSpecular fullforwardshadows

struct Input
{
float2 uv_Albedo;
};

sampler2D _Albedo;
sampler2D _Specular;
sampler2D _Normal;
sampler2D _AO;

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
fixed4 c = tex2D (_Albedo, IN.uv_Albedo);
o.Albedo = c.rgb;

fixed4 specular = tex2D (_Specular, IN.uv_Albedo);
o.Specular = specular.rgb;
o.Smoothness = specular.a;

o.Normal = UnpackNormal(tex2D (_Normal, IN.uv_Albedo));
o.Occlusion = tex2D (_AO, IN.uv_Albedo);
}

ENDCG
}
}

P241 3.遮挡半透明

原理:两个Pass渲染,被遮挡部分深度测试设为Greater,中间半透,边缘发亮;未被遮挡部分正常绘制

  • 遮挡半透明不需要显示后面的物体,就不必更改渲染队列了
image
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
Shader "Samples/X-Ray"
{
Properties
{
[Header(The Blocked Part)]
[Space(10)]
_Color ("X-Ray Color", Color) = (0,1,1,1)
_Width ("X-Ray Width", Range(1, 2)) = 1
_Brightness ("X-Ray Brightness",Range(0, 2)) = 1

[Header(The Normal Part)]
[Space(10)]
_Albedo("Albedo", 2D) = "white"{}
[NoScaleOffset]_Specular ("Specular (RGB-A)", 2D) = "black"{}
[NoScaleOffset]_Normal ("Nromal", 2D) = "bump"{}
[NoScaleOffset]_AO ("AO", 2D) = "white"{}
}

SubShader
{
Tags{"RenderType" = "Opaque" "Queue" = "Geometry"}

//---------- The Blocked Part ----------
Pass
{
//深度测试设为Greater,大于则通过
ZTest Greater
ZWrite Off

Blend SrcAlpha OneMinusSrcAlpha

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

struct v2f
{
float4 vertexPos : SV_POSITION;
float3 viewDir : TEXCOORD0;
float3 worldNor : TEXCOORD1;
};

v2f vert(appdata_base v)
{
v2f o;
o.vertexPos = UnityObjectToClipPos(v.vertex);
o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
o.worldNor = UnityObjectToWorldNormal(v.normal);

return o;
}

fixed4 _Color;
fixed _Width;
half _Brightness;

float4 frag(v2f i) : SV_Target
{
// Fresnel算法
half NDotV = saturate(dot(i.worldNor, i.viewDir));//中间1,边缘0
NDotV = pow(1 - NDotV, _Width) * _Brightness;//中间0,边缘1,0的范围大

fixed4 color;
color.rgb = _Color.rgb;
color.a = NDotV;
return color;
}
ENDCG
}

//---------- The Normal Part ----------
CGPROGRAM
#pragma surface surf StandardSpecular
#pragma target 3.0

struct Input
{
float2 uv_Albedo;
};

sampler2D _Albedo;
sampler2D _Specular;
sampler2D _Normal;
sampler2D _AO;

void surf(Input IN, inout SurfaceOutputStandardSpecular o)
{
o.Albedo = tex2D(_Albedo, IN.uv_Albedo).rgb;

fixed4 specular = tex2D(_Specular, IN.uv_Albedo);
o.Specular = specular.rgb;
o.Smoothness = specular.a;

o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_Albedo));
}
ENDCG
}
}

P249 4.三平面映射

原理:使用世界空间中X、Y、Z三个方向的坐标屏幕对纹理进行采样

  • X方向上显示colorYZ
  • Y方向上显示colorXZ
  • Z方向上显示colorXY
image
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
Shader "Samples/Tri-Planar Mapping"
{
Properties
{
_Tiling ("Tiling", float) = 1
[NoScaleOffset]_Albedo ("Albedo", 2D) = "white" {}
[NoScaleOffset]_Normal ("Normal", 2D) = "bump" {}
_Bumpiness ("Bumpiness", Range(0.01, 10)) = 1
}
SubShader
{
CGPROGRAM
#pragma surface surf Lambert fullforwardshadows

struct Input
{
float3 worldPos;
float3 worldNormal;
INTERNAL_DATA
};

float _Tiling;
sampler2D _Albedo;
sampler2D _Normal;
half _Bumpiness;

void surf (Input IN, inout SurfaceOutput o)
{
//使用世界空间坐标进行采样
float3 texCoord = IN.worldPos * _Tiling;

//根绝法向量与坐标方向的契合度,来计算采样权重
// -------------------- Mask --------------------
float3 normal = abs(WorldNormalVector(IN, o.Normal));
//如果法向方向与(1,0,0)契合度越高,则maskX越大
fixed maskX = saturate(dot(normal, fixed3(1, 0, 0)));
fixed maskY = saturate(dot(normal, fixed3(0, 1, 0)));

// -------------------- Albedo --------------------
//用xy坐标采样(同xy,不同z的颜色会全部一致)
fixed4 colorXY = tex2D (_Albedo, texCoord.xy);
fixed4 colorYZ = tex2D (_Albedo, texCoord.yz);
fixed4 colorXZ = tex2D (_Albedo, texCoord.xz);

fixed4 c;
//maskX越大,说明法线方向越朝向X轴,此时采样的结果就越接近colorYZ
c = lerp(colorXY, colorYZ, maskX);
c = lerp(c, colorXZ, maskY);

o.Albedo = c.rgb;

// -------------------- Normal --------------------
fixed3 normalXY = UnpackNormal(tex2D(_Normal, texCoord.xy));
fixed3 normalYZ = UnpackNormal(tex2D(_Normal, texCoord.yz));
fixed3 normalXZ = UnpackNormal(tex2D(_Normal, texCoord.xz));

fixed3 n;
n = lerp(normalXY, normalYZ, maskX);
n = lerp(n, normalXZ, maskY);

o.Normal = n * half3(_Bumpiness, _Bumpiness, 1);
}
ENDCG
}
FallBack "Diffuse"
}

P258 5.MatCap效果

原理:使用法线向量对纹理进行采样

分析:模型某一点的单位化后的法线向量,投影到XY平面,用投影后的坐标进行采样。因为投影之前,所有法线向量最多构成一个单位球,因此投影后的XY坐标必不可能为(1,1)之类的点(单位圆以外的点),所以你会发现Matcap贴图长成这个样子

image

最终效果:

image
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
Shader "Samples/MatCap"
{
Properties
{
[NoScaleOffset]_MatCap ("MatCap", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue" = "Geometry"}

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float2 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MatCap;

v2f vert (appdata_base v)
{
v2f o;

// 使用UNITY_MATRIX_MV的逆转置矩阵
// 变换非统一缩放物体的法线向量
float4 normal = mul(UNITY_MATRIX_IT_MV, float4(v.normal, 0));
o.texcoord = normalize(normal.xyz).xy;

o.vertex = UnityObjectToClipPos(v.vertex);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 范围 [-1, 1] => [0, 1]
float2 texcoord = i.texcoord * 0.5 + 0.5;

return tex2D(_MatCap, texcoord);
}
ENDCG
}
}
FallBack "Diffuse"
}

P263 6.物体切割效果

原理:利用世界坐标配合透明度测试实现物体切割效果

image
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
Shader "Samples/Object Cutting"
{
Properties
{
[Header(Textures)] [Space(10)]
[NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {}
[NoScaleOffset] _Reflection ("Specular_Smoothness", 2D) = "black" {}
[NoScaleOffset] _Normal ("Normal", 2D) = "bump" {}
[NoScaleOffset] _Occlusion ("Ambient Occlusion", 2D) = "white" {}

[Header(Cutting)] [Space(10)]
[KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1
[Toggle] _Invert ("Invert Direction", Float) = 0
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" }
Cull Off

CGPROGRAM
#pragma surface surf StandardSpecular addshadow fullforwardshadows
#pragma target 3.0

#pragma multi_compile _DIRECTION_X _DIRECTION_Y _DIRECTION_Z

sampler2D _Albedo;
sampler2D _Reflection;
sampler2D _Normal;
sampler2D _Occlusion;

//外部传入模型的Position
float3 _Position;
fixed _Invert;

struct Input
{
float2 uv_Albedo;
float3 worldPos;
fixed face : VFACE; //VFACE语义,当正面朝向摄像机时返回正值,否则返回负值
};

void surf (Input i, inout SurfaceOutputStandardSpecular o)
{
fixed4 col = tex2D(_Albedo, i.uv_Albedo);
o.Albedo = i.face > 0 ? col.rgb : fixed3(0,0,0);//背面默认渲染成纯黑

// 判断切割方向
#if _DIRECTION_X
//参数①:参考值
//参数②:实际值
//实际值 >= 参考值,返回1;否则返回0
col.a = step(_Position.x, i.worldPos.x);
#elif _DIRECTION_Y
col.a = step(_Position.y, i.worldPos.y);
#else
col.a = step(_Position.z, i.worldPos.z);
#endif

// 判断是否反转切割方向
col.a = _Invert? 1 - col.a : col.a;

clip(col.a - 0.001);

//使用反射向量采用高光及粗糙度贴图
fixed4 reflection = tex2D(_Reflection, i.uv_Albedo);
o.Specular = i.face > 0 ? reflection.rgb : fixed3(0,0,0);
o.Smoothness = i.face > 0 ? reflection.a : 0;

o.Normal = UnpackNormal(tex2D(_Normal, i.uv_Albedo));

o.Occlusion = tex2D(_Occlusion, i.uv_Albedo);
}
ENDCG
}
}

C#脚本

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class GetPosition : MonoBehaviour
{
public GameObject CuttingPosition;

private Material Material;
private Vector3 Center = new Vector3(0, 0, 0);
void Start()
{
//获取当前物体材质
Material = this.GetComponent<Renderer>().sharedMaterial;
}

void Update()
{
if (CuttingPosition)
//获取CuttingPosition的坐标并传递给Shade
Material.SetVector("_Position", CuttingPosition.transform.position);
else
Material.SetVector("_Position", Center);
}
}

Ch14.进阶案例

P274 1.消融效果

原理:根据噪点图使用透明度测试

image
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
Shader "Samples/Dissolve"
{
Properties
{
// -------------------- PBS Textures --------------------
[Header(PBS Textures)]
[Space(10)]
[NoScaleOffset]_Albedo("Albedo", 2D) = "white" {}
[NoScaleOffset]_Specular("Specular_Smoothness", 2D) = "black" {}
[NoScaleOffset]_Normal("Normal", 2D) = "bump" {}
[NoScaleOffset]_AO("AO", 2D) = "white" {}

// -------------------- Dissolve Properties --------------------
[Header(Dissolve Properties)]
[Space(10)]
_Noise("Dissolve Noise", 2D) = "white" {}
_Dissolve("Dissolve", Range(0, 1)) = 0
[NoScaleOffset]_Gradient("Edge Gradient", 2D) = "black" {}
_Range("Edge Range", Range(2, 100)) = 6
_Brightness("Brightness", Range(0, 10)) = 1
}
SubShader
{
Tags
{
"RenderType"="TransparentCutout" "Queue" = "AlphaTest"
}

CGPROGRAM
#pragma surface surf StandardSpecular addshadow fullforwardshadows

struct Input
{
float2 uv_Albedo;
float2 uv_Noise;
};

sampler2D _Albedo;
sampler2D _Specular;
sampler2D _Normal;
sampler2D _AO;

sampler2D _Noise;
fixed _Dissolve;
sampler2D _Gradient;
float _Range;
float _Brightness;

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
// Clip Mask
//采样噪点图,r通道为alpha值 [0,1]
fixed noise = tex2D(_Noise, IN.uv_Noise).r;
//[0,1] -> [-1,1] 起始值 0:不溶解 1:完全溶解
fixed dissolve = _Dissolve * 2 - 1;
//dissolve为-1,则noise - dissolve >= 1 > 0.5,不溶解
//dissolve为0,则noise - dissolve = noise ∈ [0,1],可能溶解也可能不溶解
//dissove为1,则noise - dissolve <= 0,完全溶解
fixed mask = saturate(noise - dissolve);
clip(mask - 0.5);

// Burn Effect
//放大误差,倍率由_Range指定。这样做可以得到mask接近0.5的部分(以0~1渐变形式给出,越远值越为1)
fixed texcoord = saturate(mask * _Range - 0.5 * _Range);
o.Emission = tex2D(_Gradient, fixed2(texcoord, 0.5)) * _Brightness;

fixed4 c = tex2D (_Albedo, IN.uv_Albedo);
o.Albedo = c.rgb;

fixed4 specular = tex2D(_Specular, IN.uv_Albedo);
o.Specular = specular.rgb;
o.Smoothness = specular.a;

o.Normal = UnpackNormal(tex2D(_Normal, IN.uv_Albedo));

o.Occlusion = tex2D(_AO, IN.uv_Albedo);
}
ENDCG
}
}

P283 2.动态液体

原理:使用透明度测试,利用模型中心点,剔除上半部分。再使用sin实现波纹效果。

  • 瓶内液体的制作就是将瓶身缩小一点后即可,嵌套进瓶身即可

缺点:从瓶子底部看会穿帮

image
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
Shader "Samples/Dynamic Liquid"
{
Properties
{
_Color("Color", Color) = (1, 0, 0, 1)
_Specular("Specular_Smoothness", Color) = (0, 0, 0, 0)
_Level("Liquid Level", float) = 0 //液体等级,控制液体高度

// 定义关键词枚举的名称
[KeywordEnum(X,Z)] _Direction("Ripple Direction", float) = 0

_Speed("Ripple Speed", float) = 1 //时间倍率
_Height("Ripple Height", float) = 1 //控制正弦曲线幅度
}
SubShader
{
Tags{"RenderType" = "Transparent" "Queue" = "Transparent"}
Blend DstColor SrcColor
ZWrite Off

CGPROGRAM
#pragma surface surf StandardSpecular noshadow

// 定义关键词
#pragma shader_feature _DIRECTION_X _DIRECTION_Z

struct Input
{
float3 worldPos;
};

fixed4 _Color;
fixed4 _Specular;
half _Level;

half _Speed;
half _Height;

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
// 液面效果
//模型中心点,模型空间 -> 世界空间
float3 pivot = mul(unity_ObjectToWorld, float4(0, 0, 0, 1));
//pivot.y - IN.worldPos.y表示,如果点在模型中心上方返回负值,否则返回正值
//通过_Level进行值的修正
float liquid = pivot.y - IN.worldPos.y + _Level * 0.01;

// 波纹效果
//书上代码感觉不妥,如果xz很大(∞),ripple值∈(-∞,+∞)远超过[0,1],根本无法体现波纹效果
//float3 ripple = sin(_Time.y * _Speed) * _Height * IN.worldPos;
//正确的波纹效果应该要和中心点做差值,这样求得的结果是离中心点越远的点波纹效果越明显
float3 ripple = sin(_Time.y * _Speed) * _Height * (IN.worldPos - pivot);

// 根据波纹的不同方向进行判断
#if _DIRECTION_X
liquid += ripple.x;
#else
liquid += ripple.z;
#endif

// 像素剔除
liquid = step(0, liquid);
clip(liquid - 0.001);

o.Albedo = _Color.rgb;
o.Specular = _Specular.rgb;
o.Smoothness = _Specular.a;
}
ENDCG
}
}

P293 3.Billboard效果

原理:重建顶点的模型坐标系。z轴方向为观察方向,x轴为向右方向,y轴由叉乘得出,通过矩阵变换将所有顶点变换至新坐标系下

分类:

  • Spherical:球体模式。能够在水平和垂直方向旋转,角度完全朝向摄像机。例如,粒子特效
  • Cylindrical:圆柱体模式。只在水平方向旋转,垂直方向固定。例如,远距离的树

本质就是z轴方向的y轴是否恒为0,恒为0就是圆柱体,否则为球体

image
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
Shader "Samples/Billboard"
{
Properties
{
[NoScaleOffset] _Tex ("Texture", 2D) = "white" {}
_Tint ("Tine", Color) = (1, 1, 1, 1)
[KeywordEnum(Spherical, Cylindrical)] _Type ("Type", float) = 0
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"DisableBatching" = "True"
}

Blend OneMinusDstColor One
ZWrite Off

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 声明枚举的关键词
#pragma shader_feature _TYPE_SPHERICAL _TYPE_CYLINDRICAL

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};

struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
};

sampler2D _Tex;
fixed4 _Tint;

v2f vert (appdata v)
{
v2f o;

// 计算面片朝向摄像机的前方向量
float3 forward = mul(unity_WorldToObject,
float4(_WorldSpaceCameraPos, 1)).xyz;

// 判断Billboard的类型
#if _TYPE_CYLINDRICAL
forward.y = 0;
#endif

forward = normalize(forward);

// 当摄像机完全在面片正上方或者正下方的时候,旋转临时的上方向量
float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);

float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));

// 将顶点在新的坐标系上移动位置
float3 vertex = v.vertex.x * right + v.vertex.y * up;

o.vertex = UnityObjectToClipPos(vertex);
o.texcoord = v.texcoord;
return o;
}

float4 frag (v2f i) : SV_Target
{
return tex2D(_Tex, i.texcoord) * _Tint;
}
ENDCG
}
}
}

P302 4.序列帧动画

原理:根据时间,计算uv,采样纹理

image
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
Shader "Samples/Sequence Animation"
{
Properties
{
[NoScaleOffset] _Tex ("Sequence Image", 2D) = "white" {}
_Tint ("Tint", Color) = (1, 1, 1, 1)
_Row ("Row Amount", float) = 1
_Column ("Column Amount", float) = 1
_Rate ("Animation Rate", float) = 1
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent"
"DisableBatching" = "True"
}
Blend OneMinusDstColor One
ZWrite Off

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};

struct v2f
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
};

sampler2D _Tex;
fixed4 _Tint;
float _Row;
float _Column;
float _Rate;

v2f vert (appdata v)
{
v2f o;

// ---------- Billboard 部分 ----------
float3 forward = mul(unity_WorldToObject,
float4(_WorldSpaceCameraPos, 1)).xyz;
forward.y = 0;
forward = normalize(forward);

float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
float3 right = normalize(cross(forward, up));
up = normalize(cross(right, forward));

float3 vertex = v.vertex.x * right + v.vertex.y * up;
o.vertex = UnityObjectToClipPos(vertex);

// ---------- 序列帧 部分 ----------

// 计算序列帧的行索引和列索引
float time = floor(_Time.y * _Rate);
float row = floor(time / _Column); //商为行索引
float column = fmod(time, _Column); //余数为列索引

// 计算序列帧的纹理坐标
float texcoordU = (v.texcoord.x + column) / _Column;
//因为纹理是从左上角开始的,实际采用的V值是从0开始的
//由于纹理设为Repeat,所求V初始值需要向负方向偏移 1 / _Row
float texcoordV = (v.texcoord.y - 1 - row) / _Row;
o.texcoord = float2(texcoordU, texcoordV);

return o;
}

float4 frag (v2f i) : SV_Target
{
return tex2D(_Tex, i.texcoord) * _Tint;
}
ENDCG
}
}
}

P312 5.卡通风格效果

原理:Half Lambert采样Ramp,描边,边缘高光,光照模型

image
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
Shader "Samples/Toon"
{
Properties
{
[Header(Diffuse)]
[Space(10)] _Albedo ("Albedo", 2D) = "white" {}
_Ramp ("Toon Ramp", 2D) = "white" {}

[Header(Rim)]
[Space(10)][HDR] _RimColor ("Rim Color", Color) = (0,2,2,1)
_RimWidth ("Rim Width", Range(0,1)) = 0
_RimFalloff ("Rim Falloff", Range(0.01,10)) = 1

[Header(Outline)]
[Space(10)] _OutlineColor ("Outline Color", Color) = (0,0,0,0)
_OutlineWidth ("Outline, Width", Float) = 0.02
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }

// ---------- Outline 部分----------
Pass
{
//使用先渲染背面的描边,描边细节会更丰富
Cull Front

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

fixed4 _OutlineColor;
half _OutlineWidth;

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

float4 frag () : SV_Target
{
return _OutlineColor;
}
ENDCG
}

// ---------- Surface 部分----------
CGPROGRAM
#pragma surface surf Toon

sampler2D _Albedo;

struct Input
{
float2 uv_Albedo;
float3 worldNormal;
float3 viewDir;
};

// 自定义的表面函数输出结构体
struct SurfaceOutputToon
{
//四个必选项
half3 Albedo;
half3 Normal;
half3 Emission;
fixed Alpha;

// 将Input结构体包含进来,为了获取worldNormal和viewDir
Input SurfaceInput;

// 内置的全局照明结构体,为了获取光照信息
UnityGIInput GIdata;
};

//UnityGIInput的定义
//struct UnityGIInput
//{
// float3 worldPos;
// half3 worldViewDir;
// half atten;
// half3 ambient;
// float4 probeHDR[2];
//};

void surf (Input i, inout SurfaceOutputToon o)
{
o.SurfaceInput = i;
o.Albedo = tex2D(_Albedo, i.uv_Albedo);
}

//GI函数:格式 "Lighting + 光照模型名称_GI"
void LightingToon_GI (inout SurfaceOutputToon s, UnityGIInput GIdata, UnityGI gi)
{
s.GIdata = GIdata;
}

sampler2D _Ramp;
half4 _RimColor;
fixed _RimWidth;
half _RimFalloff;

//UnityGI定义
//struct UnityGI
//{
// UnityLight light;
// UnityIndirect indirect;
//};
//直接光照信息
//struct UnityLight
//{
// half3 color;
// half3 dir;
//};
//间接光照信息
//struct UnityIndirect
//{
// half3 diffuse;
// half3 specular;
//};

//自定义光照函数
half4 LightingToon (SurfaceOutputToon s, UnityGI gi)
{
// 重新赋值,方便后续调用结构体内的变量
UnityGIInput GIdata = s.GIdata;
Input i = s.SurfaceInput;

// 使用内置的UnityGI_Base()函数计算GI
gi = UnityGI_Base(GIdata, GIdata.ambient, i.worldNormal);

// 将光照转为Ramp
fixed NdotL = dot(i.worldNormal, gi.light.dir);
fixed2 rampTexcoord = float2(NdotL * 0.5 + 0.5, 0.5);
fixed3 ramp = tex2D(_Ramp, rampTexcoord).rgb;

// 计算漫反射
half3 diffuse = s.Albedo * ramp * _LightColor0.rgb *
(GIdata.atten + gi.indirect.diffuse);

// 计算边缘高光
fixed NdotV = dot(i.worldNormal , i.viewDir);
fixed rimMask = pow((1.0 - saturate((NdotV + _RimWidth))), _RimFalloff);
half3 rim = saturate(rimMask * NdotL) * _RimColor *
_LightColor0.rgb * GIdata.atten;

// 输出漫反射与边缘高光的和
return half4(diffuse + rim, 1);
}
ENDCG
}
FallBack "Diffuse"
}

P326 6.夜视仪后期处理

原理:镜头扭曲(书上只给了公式)、调色、四周暗角效果、随机生成的噪点

image

Shader

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
Shader "Hidden/Night Vision"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
SubShader
{
Cull Off ZWrite Off ZTest Always

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float4 vertex : SV_POSITION;
half2 uv : TEXCOORD0;
half4 screenPos : TEXCOORD1;
};

v2f vert (appdata_img v)
{
v2f o;

o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;

// 通过裁切空间坐标得到屏幕空间坐标
o.screenPos = ComputeScreenPos(o.vertex);

return o;
}

sampler2D _MainTex;
half _Distortion;
half _Scale;

fixed _Brightness;
fixed _Saturation;
fixed _Contrast;

fixed4 _Tint;

// 暗角属性
half _VignetteFalloff;
half _VignetteIntensity;

// 噪点属性
sampler2D _Noise;
half _NoiseAmount;
half _RandomValue;

fixed4 frag (v2f i) : SV_Target
{
// 镜头扭曲
fixed2 center = i.uv - 0.5;
half radius2 = pow(center.x, 2) + pow(center.y, 2);
half distortion = 1 + sqrt(radius2) * radius2 * _Distortion;

half2 uvScreen = center * distortion * _Scale + 0.5;
fixed4 screen = tex2D(_MainTex, uvScreen);

// 亮度、饱和度、对比度
screen += _Brightness;

fixed4 luminance = Luminance(screen.rgb).xxxx;
screen = lerp(luminance, screen, _Saturation);

fixed4 gray = fixed4(0.5,0.5,0.5,1);
screen = lerp(gray, screen, _Contrast);

// 着色
screen *= _Tint;

// 暗角
// 得到屏幕中心到该点的距离,最终效果应该是中间黑,呈四周渐变逐渐到灰白(最大约为0.707)
half circle = distance(i.screenPos.xy, fixed2(0.5,0.5));
//利用pow扩大黑暗区域,利用1-结果,得到中间大面积白,四周灰
fixed vignette = 1 - saturate(pow(circle, _VignetteFalloff));
//整体压暗,四周值小,暗得更快。最终效果就是中间白,四周黑
screen *= pow(vignette, _VignetteIntensity);

// 噪点颗粒
float2 uvNoise = i.uv * _NoiseAmount;
//利用脚本传来的随机_RandomValue进行uv偏移
uvNoise.x += sin(_RandomValue);
uvNoise.y -= sin(_RandomValue + 1);

fixed noise = tex2D(_Noise, uvNoise).r;
screen *= noise;

return screen;
}
ENDCG
}
}
}

C#脚本

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]
public class NightVision : MonoBehaviour
{
public Shader EffectShader;

[Header("Basic Properties")]
[Range(-2, 2)] public float Distortion = 0.5f;
[Range(0.01f, 1)] public float Scale = 0.5f;
[Range(-1, 1)] public float Brightness = 0;
[Range(0, 2)] public float Saturation = 1;
[Range(0, 2)] public float Contrasrt = 1;

public Color Tint = Color.black;

[Header("Advanced Properties")]
[Range(0, 10)] public float VignetteFalloff = 1;
[Range(0, 100)] public float VignetteIntensity = 1;

public Texture2D Noise;
[Range(0, 10)] public float NoiseAmount = 1;
private float RandomValue;

private Material currentMaterial;

Material EffectMaterial
{
get
{
if (currentMaterial == null)
{
currentMaterial = new Material(EffectShader)
{
hideFlags = HideFlags.HideAndDontSave
};
}
return currentMaterial;
}
}

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (EffectMaterial != null)
{
EffectMaterial.SetFloat("_Distortion", Distortion);
EffectMaterial.SetFloat("_Scale", Scale);

EffectMaterial.SetFloat("_Brightness", Brightness);
EffectMaterial.SetFloat("_Saturation", Saturation);
EffectMaterial.SetFloat("_Contrast", Contrasrt);

EffectMaterial.SetColor("_Tint", Tint);

EffectMaterial.SetFloat("_VignetteFalloff", VignetteFalloff);
EffectMaterial.SetFloat("_VignetteIntensity", VignetteIntensity);

if (Noise != null)
{
EffectMaterial.SetTexture("_Noise", Noise);
EffectMaterial.SetFloat("_NoiseAmount", NoiseAmount);
EffectMaterial.SetFloat("_RandomValue", RandomValue);
}

Graphics.Blit(source, destination, EffectMaterial);
}
else Graphics.Blit(source, destination);
}

private void Update()
{
//随机生成范围中的数值
RandomValue = Random.Range(-3.14f, 3.14f);
}
}
 评论