——内容源自唐老狮的shader课程
目录
1.原理
利用时间变化来改变数据,从而导致渲染结果改变,带来画面变化
2.Shader中内置的时间变量
1.float4 _TIme:四个分量分别是(t/20,t,2t,3t),t代表游戏场景从加载开始所经过的时间
2.float4 _SinTime:四个分量分别是(t/8,t/4,t/2,t),t代表游戏运行时间的正弦值
3.float4 _CosTime:四个分量分别是(t/8,t/4,t/2,t),t代表游戏运行时间的余弦值
4.float4 unity_DeltaTime:(dt,1/dt,smootDt,1/smootDt),dt代表帧间隔时间(上一帧到当前帧间隔时间),smootDt是平滑处理过的时间间隔,对帧间隔时间进行了某种平滑算法处理后得到的结果
3.Shader中经常会改变的数据
1.颜色:通过时间控制颜色的变化,如 渐变,闪烁 等
2.位置:利用时间使顶点在某个方向上移动,如 波动 等
3.纹理坐标:利用时间变化来改变纹理坐标,如 水流,云彩,序列帧动画 等
4.法线:利用时间动态修改法线方向,如 风吹草动 等
5.缩放:利用时间改变物体缩放比例,如 脉动,跳动 等
6.透明度:利用时间控制物体透明度,如 淡入淡出,闪烁 等
4.纹理动画
4.1.背景滚动
4.1.1.补充知识
frac(x):内部计算规则为frac(x) = 1 - floor(x),它能保留一个数的小数部分,负数保留的是小数部分+1的结果。它能保证uv坐标在0-1之间。
4.1.2.基本原理
不停地利用时间变量对uv坐标进行偏移运算,超过1的部分从0开始采样,小于1同理
Shader "Models_4/RollingBackground"
{
Properties
{
_MainTex("Texture", 2D) = ""{}
//控制流速
_RollingSpeedU("RollingSpeedU", Float) = 1
_RollingSpeedV("RollingSpeedV", Float) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float _RollingSpeedU;
float _RollingSpeedV;
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
v2f vert (appdata_full v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy;
return data;
}
fixed4 frag (v2f f) : SV_Target
{
float2 uv = frac(float2(_Time.y * _RollingSpeedU, _Time.y * _RollingSpeedV) + f.uv);
fixed4 backTex = tex2D(_MainTex, uv);
return backTex;
}
ENDCG
}
}
}

4.2.帧动画
4.2.1.基本原理
通过_Time.y确认当前具体应该是哪一帧,然后算出在图中的几行几列,即确认采样范围,然后将采样范围缩放到 0-1 之间,但由于uv采样是从左下角开始,故采样范围要经过一点变化。
Shader "Models_4/SequenceFrame"
{
Properties
{
_MainTex("MainTex", 2D) = ""{}
//图集行列
_Rows("Rows", Int) = 8
_Columns("Columns", Int) = 8
_SequenceFrameSpeed("SequenceFrameSpeed", Float) = 1
}
SubShader
{
Tags { "RenderType" = "Transparent" "IgnoreProjector" = "True" "Queue" = "Transparent" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Tags {}
CGPROGRAM
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
float _Rows;
float _Columns;
float _SequenceFrameSpeed;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_full v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy;
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
//当前帧的索引
float frameIndex = floor(_Time.y * _SequenceFrameSpeed) % (_Rows * _Columns);
//图片采样起始位置的运算,除以对应的行和列的目的是 将其转换到0-1的坐标范围内
//这里1 - ,是因为要把图片坐标系(左上为原点)转换为 uv坐标系(左下为原点)
//+ 1 也差不多这个原因
float2 frameUV = float2((frameIndex % _Columns) / _Columns, 1 - ((floor(frameIndex / _Columns) + 1) / _Rows));
float2 size = float2(1 / _Columns, 1 / _Rows);
//* size 相当于把0 - 1范围 缩放到了0 - 1/8的范围内
//+ frameUV 是把起始的采样位置 移到了 对应帧(小格子)的起始采样位置
float2 uv = f.uv * size + frameUV;
fixed4 frameColor = tex2D(_MainTex, uv);
return frameColor;
}
ENDCG
}
}
}

5.流动的2D河流
5.1.基本原理
让我们的顶点在对应的轴上产生偏移,主要运用的是Shader中的内置函数sin以及内置时间变量_Time.y
波浪感的关键因素:
1.波长: 其越大,波动越缓慢,周期越长
2.波长的倒数: 其越大,波动越频繁,周期越短
3.频率: 单位时间内波动发生的次数
4.幅度: 波峰或波谷相对于中线的最大偏移位置
5.2.关键步骤
1.让顶点上下动起来:让sin参与计算,如sin(_Time.y),可使其不断返回-1~1之间的值,而为了控制波动频率,可以用sin(_Time.y * 波动频率)
问题是所有顶点的偏移都一样,会出现整体移动的效果
2.让顶点有差异地动起来:以不同地坐标制造差异性,可以使用sin(_Time.y * 波动频率 + 顶点某轴的坐标),然后用得到的返回值作为顶点在某一轴向的偏移值,便可以让顶点有差异性的动起来
问题是无法体现波长和波动幅度(振幅,或者说幅度)
3.体现波长和幅度:使用
波动幅度 * sin(_Time.y * 波动频率 + 顶点某轴坐标 * 波长的倒数)
倒数越大,波形周期越短
具体轴向根据模型空间决定
5.3.补充知识
渲染标签DisableBatching:其作用是是否对SubShader关闭批处理,原因是我们在制作顶点动画的时候,有时需要使用模型空间下的数据,而批处理会合并所有相关的模型,这些模型各自的模型空间会丢失,导致我们无法正确使用模型空间下相关数据。在实现2d河流效果时,我们就需要让顶点在模型空间下进行偏移,因此需要使用该标签,为Shader关闭批处理。
同时,对于导入的模型资源,要观察其符不符合unity轴向标准(左右x,上下y,前后z)
Shader "Models_4/Water_2D"
{
Properties
{
_MainTex("MainTex", 2D) = ""{}
//类似漫反射颜色(大概)
_Color("Color", Color) = (1, 1, 1, 1)
//振幅
_WaveAmplitude("WaveAmplitude", Float) = 1
//波动频率
_WaveFrequency("WaveFrequency", Float) = 1
//波长的倒数
_InvWaveLength("InvWaveLength", Float) = 1
//纹理变化速度
_Speed("Speed", Float) = 1
}
SubShader
{
Tags { "Queue" = "Transparent" "IgnorProjector" = "True" "RenderType" = "Transparent" "DisableBatching" = "True" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
float _Speed;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_full v)
{
v2f data;
float4 offset = float4(0, 0, 0, 0);
//在世界空间下看,模型的x是y轴,我们要对模型的x轴进行改变,但不能直接改变世界空间下的y轴,因为世界空间下改变不会作用到模型空间下
//直接改模型空间的点,并且对其原始状态上的轴做判断
offset.x = _WaveAmplitude * sin(_WaveFrequency * _Time.y + v.vertex.z * _InvWaveLength);
float4 vertex = v.vertex + offset;
data.pos = UnityObjectToClipPos(vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
//让主纹理也动
data.uv += float2(0, _Time.y * _Speed);
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
//float2 uv = frac(f.uv + float2(0, _Time.y * _Speed));
fixed4 mainColor = tex2D(_MainTex, f.uv) * _Color;
return mainColor;
}
ENDCG
}
}
}

6.广告牌效果
6.1.概念
是一种图形技术,用于确保对象始终面对摄像机,同时在某些轴上保持固定的方向(一般分为全向广告牌和轴对齐广告牌)
全向广告牌:任何视角下,对象在所有轴上始终面向 摄像机
轴对齐广告牌:对象在一个轴上保持固定方向,而在其他轴上面向摄像机。其中垂直广告牌尤为特殊,他在水平面(XZ)平面上旋转,但在垂直方向上始终保持不变
6.2.基本原理
核心是旋转模型空间坐标系让其始终面向摄像机,故而需要构建一个基于模型空间的新坐标系。改坐标系有两个关键因素构成:
1.原点:基于模型空间的,可以自定义,但一般还是用000
2.三个轴向(x轴,y轴,z轴):通常情况下这仨由 右方向,垂直向上方向,视角方向 构成。
轴的计算:获得视角向量(新z轴)后,将其与 旧y轴(垂直向上的轴,(0, 1, 0))叉乘得到 右方向 轴(即 新x轴),然后将 视角方向 与 右方向 叉乘得到 新y轴。
最后,新顶点的位置如下:
偏移位置 = 顶点坐标 - Center
新顶点位置 = Center + X轴 * 偏移位置.x + Y轴 * 偏移位置.y + Z轴 * 偏移位置.z
垂直广告牌只需要在计算视角方向(新x轴)的时候,让该轴的y分量为0即可
Shader "Models_4/BillboardEffect"
{
Properties
{
_MainTex("MainTex", 2D) = ""{}
_Color("Color", Color) = (1, 1, 1, 1)
_VerticalAmount("VerticalAmount", Range(0, 1)) = 0
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" "DisableBatching" = "True" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _VerticalAmount;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_full v)
{
v2f data;
float3 center = float3(0, 0, 0);
float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
//z
float3 normalDir = cameraInObjectPos - center;
//全向或是垂直
normalDir.y *= _VerticalAmount;
normalDir = normalize(normalDir);
float3 oldUpDir = normalDir.y > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
//float3 oldUpDir = float3(0, 1, 0);
//x
//x
float3 rightDir = normalize(cross(oldUpDir, normalDir));
//y
float3 newUpDir = normalize(cross(normalDir, rightDir));
float3 centerOffset = v.vertex.xyz - center;
float3 newVertex = center + rightDir * centerOffset.x + newUpDir * centerOffset.y + normalDir * centerOffset.z;
data.pos = UnityObjectToClipPos(newVertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
fixed4 mainColor = tex2D(_MainTex, f.uv) * _Color;
return mainColor;
}
ENDCG
}
}
}

7.顶点动画注意事项
7.1.批处理
7.1.1.为什么批处理会影响顶点动画
unity中默认有静态批处理和动态批处理,而批处理的主要作用是合并多个对象,将它们作为一个DrawCall来处理。之所以批处理会对顶点动画带来影响,是因为 不同的对象拥有不同的变换矩阵(平移,旋转,缩放)。而进行批处理之后,它们的变换矩阵将会进行统一处理,进而令其失去独立性。
举例子就是两个魔尺(颜色一样),分开各拼各的时候,你能分辨出它们各自的顶点(就当是每个三角衔接处),而如果把这俩魔尺合一块拼起来,那么某一个点究竟是哪把魔尺的就无法辨别了。
7.1.2.关闭批处理的问题
DrawCall的提升,进而导致性能的问题,而如果因为关闭批处理带来了性能问题,并且必须优化带有定点动画的Shader,该如何解决呢
7.1.3.如何解决问题
提前将独立的模型顶点存储起来:
1.通过c#代码存储到网格的颜色属性中:在Shader中通过颜色属性获取顶点信息。我们可以在appdata_full中点出color成员来使用这些顶点
private void SaveToMeshColor()
{
MeshFilter meshFilter = GetComponent<MeshFilter>();
if (meshFilter != null)
{
Mesh mesh = meshFilter.mesh;
Vector3[] vertices = mesh.vertices;
Color[] colors = new Color[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
{
colors[i] = new Color(vertices[i].x, vertices[i].y, vertices[i].z, 1);
}
mesh.colors = colors;
}
}
2.通过c#代码存到uv中:与存储到color类似,但是一般只在存储两个值的时候使用。
7.2.阴影
7.2.1.问题的产生
顶点动画通过Fallback所产生的阴影是根据 没有变形过的顶点 来的,所以对于2d河流这种,直接使用Fallback产生的阴影会跟实际不符
7.2.2.解决
自己实现阴影,并在该Pass中及逆行顶点偏移的计算即可。无需进行裁剪空间坐标变换以及uv相关计算
Pass
{
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(appdata_full v)
{
v2f data;
float4 offset = float4(0, 0, 0, 0);
offset.x = _WaveAmplitude * sin(_WaveFrequency * _Time.y + v.vertex.z * _InvWaveLength);
v.vertex += offset;
//这个会自动调用v的数据
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data);
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
SHADOW_CASTER_FRAGMENT(f);
return fixed4(1, 1, 1, 1);
}
ENDCG
}