前言

借由 Unity Shader 实战学习 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
Shader "Unlit/AloeaShader03"
{
Properties
{
_Color ("Color", Color) = (0,0,0,0) // 参数声明

}
SubShader
{
// 指定渲染顺序
Tags {
"RenderType"="Transparent"
"Queue"="Transparent"
}

Pass
{
Cull Off // 因为做的半透明效果,关闭剔除
ZWrite off // 因为做的半透明效果,关闭写入深度缓存
Blend One One // 混合相加实现半透明效果

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

#define TAU 6.283185 // 圆周常数

float4 _Color;

struct appdata
{
float4 vertex : POSITION;
float3 normal: NORMAL;
float2 uv : TEXCOORD0;
};

struct v2f
{
float4 vertex : SV_POSITION;
float3 normal: TEXCOORD0;
float2 uv : TEXCOORD1;
};


v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.uv = v.uv;
return o;
}

// 这部分重点,看下文详解
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * TAU * 8) * 0.02;
float t = cos((i.uv.y + offset - _Time.x * 2) * 30) * 0.5 + 0.5;
float topBottomRemover = (abs(i.normal.y) < 0.999);
float4 outColor = _Color * t * topBottomRemover;
return outColor;
}
ENDCG
}
}
}


细节

🤔效果调试过程

1
2
3
4
5
6
// S1
fixed4 frag (v2f i) : SV_Target
{
float t = cos(i.uv.y * 30); // 做出横向条纹
return float4(t, 0, 0, 1);
}






1
2
3
4
5
6
7
// S2
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * 40) * 0.02; // 叠加另一个方向
float t = cos((i.uv.y + offset) * 30); // 形成波纹
return float4(t, 0, 0, 1);
}






1
2
3
4
5
6
7
8
// S3
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * 40) * 0.02;
float t = cos((i.uv.y + offset) * 30);
t *= 1 - i.uv.y; // 叠加一个 y 轴方向的渐变,形成一个渐渐消失的过渡
return float4(t, 0, 0, 1);
}






1
2
3
4
5
6
// S4
fixed4 frag (v2f i) : SV_Target
{
float topBottomRemover = abs(i.normal.y); // 用法线方向计算出顶部和底部,用于剔除
return float4(topBottomRemover, 0, 0, 1);
}






1
2
3
4
5
6
7
8
9
10
11
// S5
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * 40) * 0.02;
// 将 cos 结果的 -1 ~ 1 修正到期望的 0 ~ 1 范围。x * 0.5 + 0.5
float t = cos((i.uv.y + offset) * 30) * 0.5 + 0.5;
t *= 1 - i.uv.y;
// 顶部面的法向量 y 为 1, 所以结果为 0 ,相乘后就可以实现出去掉顶部和底部的面
float topBottomRemover = (abs(i.normal.y) < 0.999);
return float4(t * topBottomRemover, 0, 0, 1);
}






1
2
3
4
5
6
7
8
9
10
11
// S6
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * 40) * 0.02;
float t = cos((i.uv.y + offset) * 30) * 0.5 + 0.5;
t *= 1 - i.uv.y;
float topBottomRemover = (abs(i.normal.y) < 0.999);
// 相乘上目标颜色,t * topBottomRemover 是一个 0 ~ 1 范围的 Alpha
float4 outColor = _Color * t * topBottomRemover;
return outColor;
}






1
2
3
4
5
6
7
8
9
10
11
// S7
fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * 40) * 0.02;
// cos 函数内加上一个时间差,模拟实现动画效果,用加法或者减法改变动画形成的方向
float t = cos((i.uv.y + offset - _Time.x * 2) * 30) * 0.5 + 0.5;
t *= 1 - i.uv.y;
float topBottomRemover = (abs(i.normal.y) < 0.999);
float4 outColor = _Color * t * topBottomRemover;
return outColor;
}

_Time 是内置变量,_Time.xyzw 对应不同的时间分量。t 是开始渲染至今的时间,单位为秒,我们在上面约束在了 cos 函数内,这意味着采用 .x 的动画速度是慢于 .y 20倍的,但用哪个并不重要,因为都是根据效果调试出来的。
_Time.x = t / 20
_Time.y = t
_Time.z = t * 2
_Time.w = t * 4

现在,整个效果已经出来了! 😝







我们检查下细节发现,在圆柱一圈的连接处(uv 的边缘)上,出现了断横,就像我们廉价的条纹毛线衣一样不够优雅。

1
2
3
4
5
6
7
8
9
10
11
12
// S8
#define TAU 6.283185

fixed4 frag (v2f i) : SV_Target
{
float offset = cos(i.uv.x * TAU * 8) * 0.02; // 借助圆周常数的力量
float t = cos((i.uv.y + offset - _Time.x * 2) * 30) * 0.5 + 0.5;
float topBottomRemover = (abs(i.normal.y) < 0.999);
float4 outColor = _Color * t * topBottomRemover;
return outColor;
}

完美 ~ 😝😝



🤔剔除 Cull

引擎在处理剔除的时候,默认摄像机看不到的面是不会渲染的,这意味着物体的背面是不会被渲染的,这个场景下会产生我们想要的透明物体只剩下前面而看不到后面的问题,所以我们需要关闭剔除。

Cull 有三种选项,
Back:摄像机看不到的那一面剔除,默认值,常规情况下(非透明)确实应该这么处理以提高性能,毕竟看不到嘛
Front:摄像机看得到的那一面剔除,看不到的那一面展示,做一些特殊效果时可以用到
Off:不管摄像机看不看的面都展示,不需要计算剔除

注意这里的 Cull 指的是物体不同面在摄像机方位下的剔除规则,如果物体根本就超出了视锥体/视野范围,那么当然还是会直接剔除的,这不是 Shader 部分处理的事。

文档:https://docs.unity3d.com/Manual/SL-Cull.html


🤔混合透明 Alpha Blend

我们在这个案例中采用 Alpha Blend 这种方式实现的半透明效果。之后我可以单独整理下半透明的实现原理和方式。

1
Blend One One

指令使用上我们用到 Blend sourceFactor destinationFactor
表示最终颜色 = 物体颜色 * sourceFactor + 屏幕已有颜色 * destinationFactor,而 One 对应的是 1,这样就实现了透明效果。
注意这里不能用 Zero,因为除了 0,0,0,0 的部分要去除,正常的绿色波纹部分我们是不希望去除的,只需要用 0,0,0,0 去乘以 1 就可以实现去除了。

其他还有一些指令,下次用到再展开聊聊不同的效果
文档:https://docs.unity3d.com/Manual/SL-Blend.html


🤔渲染顺序 RenderQueue

默认情况下, Unity 根据对象距离摄像机的距离由远及近依次渲染,以保证我们看到的物体先后关系是正确。但一些情况则需要开发者特别设置,比如当对象是透明的..
我们可以通过设置 Queue Tag 改变渲染的顺序,当不同的实体(SubShader)采用不同的类就决定了它们的先后。
注意指定渲染顺序生效,需要声明关闭写入深度缓存 ZWrite Off, 否则依然以Unity的距离计算为准。

1
2
3
Tags { "Queue"="Overlay" }
Tags { "Queue"="Overlay-10" }
Tags { "Queue"="Overlay+10" }
Background 这个队列是最先渲染的,比如天空盒一类用它
Geometry 默认渲染队列,大部分实体几何体用它
AlphaTest 开启透明度测试的对象
GeometryLast 所有 Geometry 和 AlphaTest 渲染完成后
Transparent 所有 Geometry 和 AlphaTest 渲染完成后,再按照从远往近顺序进行渲染,透明对象用它
Overlay 所有叠加效果一类用它

文档:https://docs.unity3d.com/ScriptReference/Rendering.RenderQueue.html