Unity Shader:从基础使用到动画实现全解析
在Unity的精彩世界里,Shader(着色器)堪称打造独特视觉效果的魔法棒。无论是绚丽的光影、奇幻的材质,还是生动的动画,Shader都能助你一臂之力。今天,就让我们一同深入探索Unity中Shader的使用方法,以及如何调用Shader实现令人惊叹的动画效果。
一、Shader基础:开启图形渲染的大门
(一)Shader是什么
Shader本质上是运行在GPU上的小程序,专门负责控制渲染过程中每个像素或顶点的外观。想象一下,GPU就像一个高效的工厂,Shader则是工厂里的技术工人,精确地塑造着每个图形元素的模样。通过编写Shader,我们可以实现从简单的颜色变化到复杂的光照模拟、反射折射等各种效果。
(二)Shader的类型
- 顶点着色器(Vertex Shader):主要处理顶点数据,包括位置、法线、纹理坐标等。它如同一位空间魔法师,能够对顶点进行平移、旋转、缩放等变换,还能将顶点从模型空间巧妙地转换到世界空间、视空间等不同的坐标系统。
- 片段着色器(Fragment Shader):也叫像素着色器,在顶点着色器完成对顶点的处理后,片段着色器闪亮登场。它专注于计算每个像素的最终颜色,通过颜色混合、光照计算、纹理采样等操作,为画面赋予丰富的色彩和细节。
二、编写Shader:用代码描绘绚丽世界
(一)ShaderLab语法初窥
在Unity中,编写Shader通常借助ShaderLab语法。下面是一个简单Shader的基本结构:
// 定义一个名为Custom/MyShader的Shader
Shader "Custom/MyShader" {
// 定义属性,这些属性会在材质面板中显示,方便用户在不修改代码的情况下调整Shader效果
Properties {
// 定义一个名为_MainColor的颜色属性,在材质面板中显示名称为“Main Color”,默认值为白色(1, 1, 1, 1)
_MainColor ("Main Color", Color) = (1,1,1,1)
// 定义一个名为_FloatValue的浮点型属性,在材质面板中显示名称为“Float Value”,默认值为0.5
_FloatValue ("Float Value", Float) = 0.5
// 定义一个名为_RangeValue的范围型属性,在材质面板中显示名称为“Range Value”,范围是0到1,默认值为0.5
_RangeValue ("Range Value", Range(0, 1)) = 0.5
// 定义一个名为_MainTexture的二维纹理属性,在材质面板中显示名称为“Main Texture”,默认值为白色纹理
_MainTexture ("Main Texture", 2D) = "white" {}
// 定义一个名为_VectorValue的向量属性,在材质面板中显示名称为“Vector Value”,默认值为(1, 1, 1, 1)
_VectorValue ("Vector Value", Vector) = (1, 1, 1, 1)
// 定义一个名为_CubeMap的立方体贴图属性,在材质面板中显示名称为“Cube Map”,默认值为白色立方体贴图
_CubeMap ("Cube Map", Cube) = "white" {}
}
SubShader {
// 定义显卡支持的渲染代码区域,这里设置渲染类型为不透明
Tags { "RenderType" = "Opaque" }
Pass {
CGPROGRAM
// 声明顶点着色器函数为vert
#pragma vertex vert
// 声明片段着色器函数为frag
#pragma fragment frag
// 声明与Properties中定义的属性对应的变量,以便在着色器代码中使用
uniform fixed4 _MainColor;
uniform sampler2D _MainTexture;
// 输入结构体(appdata),用于存储从模型中读取的顶点数据
struct appdata {
// 顶点的位置信息,POSITION语义表示这是顶点位置
float4 vertex : POSITION;
// 顶点的纹理坐标,TEXCOORD0语义表示这是第一个纹理坐标集
float2 uv : TEXCOORD0;
};
// 输出结构体(v2f),用于将顶点着色器的计算结果传递给片段着色器
struct v2f {
// 经过变换后顶点在裁剪空间中的位置,SV_POSITION语义表示这是输出到裁剪空间的位置
float4 pos : SV_POSITION;
// 传递给片段着色器的纹理坐标
float2 uv : TEXCOORD0;
};
// 顶点函数(vert),处理顶点数据
v2f vert (appdata v) {
v2f o;
// 将顶点从模型空间转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 将顶点的纹理坐标传递给输出结构体
o.uv = v.uv;
return o;
}
// 片段着色器(Fragment Shader),计算每个像素的最终颜色
fixed4 frag (v2f i) : SV_Target {
// 通过纹理采样获取纹理颜色,并与主颜色相乘
fixed4 col = tex2D(_MainTexture, i.uv) * _MainColor;
return col;
}
ENDCG
}
}
// 备用Shader,当主SubShader在目标硬件上不被支持时使用,这里使用内置的Diffuse Shader
FallBack "Diffuse"
}
-
Properties块:这是我们与Shader交互的桥梁,通过
定义属性
,如颜色、纹理等,我们可以在材质的Inspector面板中轻松调整这些参数。
例如:`_MainColor`定义了一个颜色属性 _FloatValue定义了一个浮点属性 `_MainTexture`定义了一个纹理属性。 _RangeValue定义了一个范围 属性。 _VectorValue 定义了一个向量属性 _CubeMap定义了一个立方体贴图属性
-
SubShader块:这里面包含一个或多个Pass,每个Pass代表一次
渲染操作
。
Tags
用于设置渲染队列等重要信息,比如:RenderType:定义渲染类型,常见值有"Opaque"(不透明)、"Transparent"(透明)、"TransparentCutout"(透明裁剪)等。 Queue:指定渲染队列,决定物体的渲染顺序。常见队列有"Background"(背景队列,最早渲染)、"Geometry"(几何体队列,默认用于不透明物体)、"AlphaTest"(透明度测试队列)、"Transparent"(透明队列,后渲染以保证透明效果正确)等。
Pass块:核心的渲染代码部分,使用CGPROGRAM和ENDCG包围的是Cg/HLSL代码。
顶点着色器(Vertex Shader):处理顶点数据,如位置变换、纹理坐标传递等。
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
片段着色器(Fragment Shader):计算每个像素的最终颜色。
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTexture, i.uv) * _MainColor;
return col;
}
- FallBack:当当前SubShader在目标硬件上不被支持时,Unity会自动使用这里指定的备用Shader,比如
FallBack "Diffuse"
表示使用内置的Diffuse Shader。
三、使用Shader:让材质绽放光彩
(一)创建材质并应用Shader
- 在Unity的Project面板中,右键点击,选择
Create
->Material
,创建一个全新的材质。 - 选中新创建的材质,在Inspector面板的
Shader
下拉菜单中,找到我们刚刚编写的Shader(例如Custom/MyShader
)并选择它。此时,材质就如同被赋予了神奇的力量,开始按照Shader定义的方式展现独特的外观。 - 根据Shader中定义的属性,在材质的Inspector面板中进行调整。比如,对于上述Shader,我们可以轻松调整
Main Color
的颜色,选择心仪的Main Texture
纹理,从而打造出千变万化的材质效果。
(二)调试Shader
- 帧调试器(Frame Debugger):通过
Window
->Analysis
分析->Frame Debugger
调式器打开这个强大的工具。它就像一个时间放大镜,让我们能够逐帧查看渲染过程,包括每个Pass的详细渲染结果。通过分析这些信息,我们可以精准定位Shader在哪个环节出现问题,是顶点变换异常,还是纹理采样错误,一目了然。 - ShaderLab Profiler:在
Window
->Analysis
->ShaderLab Profiler
着色器实验室分析器中,我们可以获取Shader的性能数据,如绘制调用次数、GPU内存使用情况等。这些数据是优化Shader性能的关键依据,帮助我们发现性能瓶颈,进行针对性的优化。 - 添加调试输出:在Shader代码中,我们可以巧妙地使用
UnityEngine.Debug.Log
或Debug.LogWarning
等函数输出调试信息。在Cg/HLSL代码中,clip
函数不仅可以丢弃不符合条件的像素,还能输出调试信息,例如:
fixed4 frag (v2f i) : SV_Target {
if (i.uv.x < 0.5) {
clip(-1); // 丢弃左半部分像素
}
fixed4 col = tex2D(_MainTexture, i.uv) * _MainColor;
return col;
}
四、Shader动画:赋予场景灵动之美
(一)在Shader中创建可动画属性
假设我们要打造一个迷人的闪烁效果Shader,通过巧妙改变颜色的透明度来实现闪烁的梦幻效果。
Shader "Custom/FlickerShader" {
Properties {
_MainColor ("Main Color", Color) = (1,1,1,1)
_FlickerSpeed ("Flicker Speed", Range(0.1, 10)) = 1
_FlickerAmount ("Flicker Amount", Range(0, 1)) = 0.5
}
SubShader {
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
uniform fixed4 _MainColor;
uniform float _FlickerSpeed;
uniform float _FlickerAmount;
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target {
float flicker = sin(_Time.y * _FlickerSpeed) * _FlickerAmount;
fixed4 col = _MainColor;
col.a = col.a + flicker;
return col;
}
ENDCG
}
}
FallBack "Transparent/Diffuse"
}
在这个Shader中:
_FlickerSpeed
属性如同节奏大师,掌控着闪烁的速度。_FlickerAmount
属性则像一个调色盘,控制着闪烁的幅度。- 在
frag
函数中,我们利用sin(_Time.y * _FlickerSpeed) * _FlickerAmount
这个神奇的公式计算出闪烁值,并将其巧妙地应用到颜色的透明度上,从而实现迷人的闪烁效果。
(二)在脚本中动态调整Shader属性
为了让闪烁效果更加生动多变,我们创建一个C#脚本,在运行时动态调整Shader的属性。
using UnityEngine;
// 定义一个名为FlickerController的脚本类,继承自MonoBehaviour,以便该脚本可以挂载到游戏对象上
public class FlickerController : MonoBehaviour
{
// 定义最小闪烁速度,默认为1
public float minFlickerSpeed = 1;
// 定义最大闪烁速度,默认为5
public float maxFlickerSpeed = 5;
// 定义最小闪烁幅度,默认为0.2
public float minFlickerAmount = 0.2f;
// 定义最大闪烁幅度,默认为0.8
public float maxFlickerAmount = 0.8f;
// 声明一个Material类型的变量,用于存储游戏对象的材质
private Material material;
// Start方法在脚本实例化后的第一帧更新之前调用
void Start()
{
// 获取挂载该脚本的游戏对象的Renderer组件,并从中获取其材质,赋值给material变量
material = GetComponent<Renderer>().material;
}
// Update方法在每帧都会调用
void Update()
{
// 使用Mathf.PingPong函数,该函数会在0到1之间往返取值,随着时间的推移,
// 这个值会在0和1之间不断循环变化,Time.time是一个随时间不断增加的浮点数
// Mathf.Lerp函数则根据PingPong函数返回的值,在minFlickerSpeed和maxFlickerSpeed之间进行线性插值,
// 从而得到一个在最小和最大闪烁速度之间动态变化的新的闪烁速度
float newFlickerSpeed = Mathf.Lerp(minFlickerSpeed, maxFlickerSpeed, Mathf.PingPong(Time.time, 1));
// 将新的闪烁速度值传递给材质的_FlickerSpeed属性,该属性应该在对应的Shader中定义
material.SetFloat("_FlickerSpeed", newFlickerSpeed);
// 与上面计算闪烁速度类似,这里通过改变Time.time的倍数(乘以2),使得闪烁幅度的变化频率更快
// 然后在minFlickerAmount和maxFlickerAmount之间进行线性插值,得到新的闪烁幅度
float newFlickerAmount = Mathf.Lerp(minFlickerAmount, maxFlickerAmount, Mathf.PingPong(Time.time * 2, 1));
// 将新的闪烁幅度值传递给材质的_FlickerAmount属性,该属性同样应该在对应的Shader中定义
material.SetFloat("_FlickerAmount", newFlickerAmount);
}
}
在这个脚本中:
minFlickerSpeed
和maxFlickerSpeed
设定了闪烁速度的变化范围,就像为闪烁节奏设定了快慢边界。minFlickerAmount
和maxFlickerAmount
定义了闪烁幅度的区间,决定了闪烁效果的明显程度。- 在
Update
函数中,我们运用Mathf.Lerp
和Mathf.PingPong
这两个函数的魔法,动态地改变_FlickerSpeed
和_FlickerAmount
的值,并通过material.SetFloat
方法将新的值传递给Shader,从而让闪烁效果随着时间不断变化,充满生机。
(三)应用与测试
- 创建材质并应用Shader:在Project面板中创建一个新的材质,将上述
Custom/FlickerShader
应用到该材质上,赋予材质闪烁的潜力。 - 挂载脚本:将
FlickerController
脚本挂载到带有渲染器的游戏对象上,并将刚才创建的材质赋给该游戏对象的渲染器,建立起脚本与材质之间的紧密联系。 - 运行测试:激动人心的时刻到了,运行游戏,你将看到游戏对象的颜色如同夜空中闪烁的繁星,按照设定的方式欢快地闪烁起来,为场景增添独特的魅力。
(四)其他动画方式
- 顶点动画:在Shader的顶点函数中,我们可以创造出奇妙的顶点动画效果。比如,让物体表面的顶点像波浪一样起伏波动。通过三角函数或噪声函数,我们能够精准地修改顶点的位置,实现类似水面波光粼粼、旗帜随风飘扬等令人惊叹的效果。
v2f vert (appdata v) {
v2f o;
float wave = sin(v.vertex.x * 5 + _Time.y) * 0.1;
v.vertex.y += wave;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
- 纹理动画:通过巧妙地改变纹理的采样坐标,我们可以实现生动的纹理动画。想象一下,让纹理像潺潺流水一样滚动,模拟出水流的动态效果。
fixed4 frag (v2f i) : SV_Target {
float2 uv = i.uv + float2(_Time.y * 0.1, 0);
fixed4 col = tex2D(_MainTexture, uv);
return col;
}
希望通过这篇博客,你能对Unity中Shader的使用和动画实现有更深入的理解和掌握,在游戏开发的世界里创造出更加绚丽多彩的视觉效果!