由于shaderlab入门的内容实在太长,上面一篇打开已经卡的不行了,接下来的实战更新在这里进行
正好halo最近推出了文档功能,预计实战效果部分未来会进行文档化处理
第9节: Shader基础知识——动态效果
如何制作动态效果
知识回顾 游戏画面中为什么能看到动态效果
游戏画面中之所以能产生动态效果
主要的原因是因为 游戏循环 机制
即游戏画面每隔一个固定时间(每一帧)就会重新渲染
游戏运行时,每一帧都会更新屏幕,这种更新频率通常称为 帧率(Frames Per Second,FPS)
比如 30 FPS、60 FPS 代表的就是 1秒钟更新30次,1秒钟更新60次
而之所以看起来画面是变化的,
是因为我们在每一帧可能都会改变游戏中对象的位置、角度、缩放、颜色等等信息后重新渲染
一般情况下,只要帧率大于24FPS,人眼就认为一帧帧切换着的画面是流畅且连贯的了
知识点一 如何利用Shader制作动态效果
通过知识回顾可以知道
让画面动起来是因为每一帧对象的位置、角度、缩放、颜色等等信息的改变后重新渲染带来的
相当于就是间隔一定时间更新一些数据,从而带来了画面变化
那么想要利用Shader制作出动态效果,其实原理也是一样的
我们只需要间隔一定时间改变Shader中的数据,从而改变渲染的结果,最终达到画面变化的目的
这样就能够带来动态感了
总结:
利用Shader制作动态效果的关键就是 ―― 利用时间变化来改变数据,从而导致渲染结果改变,带来画面变化
知识点二 Shader中的内置时间变量
利用Shader制作动态效果的关键就是 ―― 利用时间变化来改变数据,从而导致渲染结果改变,带来画面变化
时间是关键数据,Shader中提供了对应的内置时间变量
1.float4 _Time
4个分量的值分别是(t/20, t, 2t, 3t)
其中t代表该游戏场景从加载开始缩经过的时间
2.float4 _SinTime
4个分量的值分别是(t/8, t/4, t/2, t)
其中t代表 游戏运行的时间的正弦值
3.float4 _CosTime
4个分量的值分别是(t/8, t/4, t/2, t)
其中t代表 游戏运行的时间的余弦值
4.float4 unity_DeltaTime
4个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt)
dt代表帧间隔时间(上一帧到当前帧间隔时间)
smoothDt是平滑处理过的时间间隔,对帧间隔时间进行某种平滑算法处理后的结果
知识点三 Shader中经常会改变的数据
利用Shader制作动态效果的关键就是 ―― 利用时间变化来改变数据,从而导致渲染结果改变,带来画面变化
我们已经知道,在Shader中如何获取时间变量
那么我们一般会利用时间和什么数据一起计算,来达到动态效果呢?
1.颜色
通过时间控制颜色的变化,比如 渐变、闪烁 等效果
2.位置
利用时间使顶点在某个方向上移动,比如 波动 等效果
3.纹理坐标
利用时间变化来动态改变纹理坐标,比如 水流、云彩、序列帧动画 等效果
4.法线
利用时间动态修改法线方向,比如 风吹草动 等效果
5.缩放
利用时间改变物体缩放比例,比如 脉动、跳动等效果
6.透明度
利用时间控制物体透明度,比如 淡入淡出、闪烁等效果
总结
利用Shader制作动态效果的关键就是
利用时间变化来改变数据,从而导致渲染结果改变,带来画面变化
Unity Shader中常用的时间变量有
_Time(t/20, t, 2t, 3t) ―― t代表该游戏场景从加载开始缩经过的时间
_SinTime(t/8, t/4, t/2, t) ―― t代表 游戏运行的时间的正弦值
_CosTime(t/8, t/4, t/2, t) ―― t代表 游戏运行的时间的余弦值
unity_DeltaTime(dt, 1/dt, smoothDt, 1/smoothDt) ―― dt代表帧间隔时间,smoothDt是平滑处理过的间隔时间
【纹理动画】序列帧动画
知识点一 分析利用纹理坐标制作序列帧动画的原理
关键点
1.UV坐标范围0~1,原点为图片左下角
2.图集序列帧动画播放顺序为从左到右,从上到下
分析问题
1.如何得到当前应该播放哪一帧动画?
2.如何将采样规则从0~1修改为在指定范围内采样?
问题解决思路
1.用内置时间参数 _Time.y 参与计算得到具体哪一帧
时间是不停增长的数值,用它对总帧数取余,便可以循环获取到当前帧数
2.利用时间得到当前应该绘制哪一帧后
我们只需要确认从当前小图片中,采样开始位置,采样范围即可
采样开始位置,可以利用当前帧和行列一起计算
采样范围可以将0~1范围 缩放转换到 小图范围内
知识点二 用Shader实现序列帧动画
1.新建Shader 删除无用代码
2.声明属性,进行属性映射
主纹理、图集行列、序列帧切换速度
3.透明Shader
设置渲染标签
Tags { "RenderType"="Opaque" "IgnoreProjector"="True" "Queue"="Transparent" }
关闭深度写入,开启混合
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
4.结构体
只需要顶点坐标和纹理坐标
5.顶点着色器
只需要进行坐标转换和纹理坐标赋值
6.片元着色器
6-1:利用时间计算帧数
6-2:利用帧数计算当前 uv采样起始位置(得到小图片uv起始位置)
6-3:计算uv缩放比例(将0~1 转换到 0~1/n)
6-4:进行uv偏移计算(在小图片格子中采样)
6-5:采样
实现
Shader "Unlit/SequentialFrameAnimation"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//图集行列
_Rows("Rows", int) = 8
_Columns("Columns", int) = 8
//切换动画速度变量
_Speed("Speed", float) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _Rows;
float _Columns;
float _Speed;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//得到当前帧 利用时间变量计算
float frameIndex = floor(_Time.y * _Speed) % (_Rows * _Columns);
//小格子(小图片)采样时的起始位置计算
//除以对应的行和列 目的是将行列值 转换到 0~1的坐标范围内
//1 - (floor(frameIndex / _Columns) + 1)/_Rows
// +1 是因为把格子左上角转换为格子左下角(想拿到这行的底边的 UV 坐标,而不是顶边)
// 1- 因为UV坐标采样时从左下角进行采样的
float2 frameUV = float2(frameIndex % _Columns / _Columns, 1 - (floor(frameIndex / _Columns) + 1)/_Rows);
//得到uv缩放比例 相当于从0~1大图 隐射到一个 0~1/n的一个小图中
float2 size = float2(1/_Columns, 1/_Rows);
//计算最终的uv采样坐标信息
//*size 相当于把0~1范围 缩放到了 0~1/8范围
//+frameUV 相当于把起始的采样位置 移动到了 对应帧小格子的起始位置
float2 uv = i.uv * size + frameUV;
//最终采样颜色
return tex2D(_MainTex, uv);
}
ENDCG
}
}
}
效果图

总结
Shader实现序列帧动画的关键点是
UV坐标原点为左下角,而序列帧图集“原点”为左上角
我们需要注意采样开始位置的转换
【纹理动画】滚动的背景
知识补充
内置函数 frac(参数)
该函数的内部计算规则为:
frac(x) = x - floor(x)
一般用于保留数值的小数部分,但是负数时要注意
比如:
frac(2.5) = 2.5 - 2 = 0.5
frac(3.75) = 3.75 - 3 = 0.75
frac(-0.25) = -0.25 - (-1) = 0.75
frac(-3.75) = -3.75 - (-4) = 0.25
它的主要作用是可以帮助我们保证 uv坐标 范围在0~1之间
相当于
大于1的uv值重新从0开始向1方向取值
小于0的uv值重新从1开始向0方向取值
知识点一 分析利用纹理坐标制作滚动的背景的原理
注意点:
滚动的背景使用的美术资源图片,往往是首尾循环相连的
基本原理:
不停地利用时间变量对uv坐标进行偏移运算
超过1的部分从0开始采样
小于0的部分从1开始采样
知识点二 用Shader实现滚动的背景
1.新建Shader,删除无用代码
2.声明属性,属性映射
主纹理、U轴速度、V轴速度(两个速度的原因是因为图片可能竖直或水平滚动)
3.透明Shader
往往这种滚动背景图片都会有透明区域
渲染标签修改、关闭深度写入、进行透明混合
4.结构体
顶点和纹理坐标
5.顶点着色器
顶点坐标转换,纹理坐标直接赋值
6.片元着色器
利用时间和速度对uv坐标进行偏移计算
利用偏移后的uv坐标进行采样
实现
Shader "Unlit/ScrollingBackground"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//水平和竖直的滚动速度
_ScrollSpeedU("ScrollSpeedU", float) = 0.5
_ScrollSpeedV("ScrollSpeedV", float) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float _ScrollSpeedU;
float _ScrollSpeedV;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//利用时间 来计算UV的偏移 因为时间一直在变化 所以最终的uv坐标也不停地在变
float2 scrollUV = frac(i.uv + float2(_Time.y * _ScrollSpeedU, _Time.y * _ScrollSpeedV));
return tex2D(_MainTex, scrollUV);
}
ENDCG
}
}
}
效果图

总结
Shader实现滚动的背景的关键点
1.纹理图片必须按规范制作,“首尾相连”
2.利用内置时间变量对纹理坐标进行偏移计算
【顶点动画】流动的2D河流 基本原理
总的来说,就用顶点偏移和波动算法来模拟2D河流
【顶点动画】流动的2D河流 具体实现
知识回顾
实现2D河流效果的关键公式:
某轴位置偏移量 = sin(_Time.y 波动频率 + 顶点某轴坐标 波长的倒数) * 波动幅度
知识补充
渲染标签
"DisableBatching" = "True"
主要作用:
是否对SubShader关闭批处理
我们在制作顶点动画时,有时需要关闭该Shader的批处理
因为我们在制作顶点动画时,有时需要使用模型空间下的数据
而批处理会合并所有相关的模型,这些模型各自的模型空间会丢失,导致我们无法正确使用模型空间下相关数据
在实现流程的2D河流效果时,我们就需要让顶点在模型空间下进行偏移
因此我们需要使用该标签,为该Shader关闭批处理
知识点一 导入资源 观察资源
1.导出测试用资源
2.观察资源模型空间轴向
该模型的模型空间坐标并不符合Unity轴向标准
它的上下是x轴 左右是z轴 前后是y轴

知识点二 流动的2D河流效果 具体实现
1.新建Shader,删除无用代码
2.声明属性、映射属性
主纹理(_MainTex)
叠加的颜色(_Color)
波动幅度(_WaveAmplitude)
波动频率(_WaveFrequency)
波长的倒数(_InvWaveLength)
3.透明Shader相关
渲染标签相关
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}
深度写入、透明混合相关
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
4.结构体相关
顶点和uv
5.顶点着色器
利用理论中讲解的公式,计算对应轴向偏移位置
注意,在模型空间中偏移
6.片元着色器
直接进行颜色采样,颜色叠加
实现
Shader "Unlit/2DWater"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
//波动幅度
_WaveAmplitude("WaveAmplitude", Float) = 1
//波动频率
_WaveFrequency("WaveFrequency", Float) = 1
//波长的倒数
_InvWaveLength("InvWaveLength", Float) = 1
//纹理变化速度
_Speed("Speed", Float) = 1
}
SubShader
{
//透明Shader相关渲染标签 + 关闭批处理标签
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
float _Speed;
v2f vert (appdata_base v)
{
v2f o;
//模型空间下的偏移位置
float4 offset;
//让它在模型空间的x轴方向进行偏移(请注意,这里的方向得看模型空间自身方向的)
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0,0,0);
o.vertex = UnityObjectToClipPos(v.vertex + offset);
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv += float2(0, _Time.y *_Speed );
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 color = tex2D(_MainTex,i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
}
}
效果图

【顶点动画】广告牌效果 基本原理
数学直觉上就是确认一个方向朝向摄像机的新模型空间Z轴,以此构建新的模型空间基坐标,然后自然的反作用回后续的顶点运算,目标模型就自然朝向了摄像机
一些启发:改模型空间的基坐标会作用到世界坐标和后续的坐标变化,而模型空间本身的运算固定而且少,所以完全可以很多方案上优先考虑基于模型空间的运算。
【顶点动画】广告牌效果 具体实现
知识回顾
广告牌效果实现关键点
1.新坐标系
原点确定(一般0,0,0)
坐标轴计算(x,y,z)
2.顶点计算
偏移位置 = 顶点坐标 C Center
新顶点位置 = Center + X轴 偏移位置.x + Y轴 偏移位置.y + Z轴 * 偏移位置.z
3.全向广告牌和垂直广告牌区别
计算normal轴时,y为0则为垂直广告牌
知识点 广告牌效果 具体实现思路
1.新建Shader,删除无用代码
2.声明属性,属性映射
主纹理、颜色叠加、垂直广告牌程度(0为垂直广告牌,1为全向广告牌)
3.透明Shader相关
注意:关闭批处理,并让其两面渲染
4.结构体相关
顶点和纹理坐标
5.顶点着色器
5-1:确定新坐标中心点
5-2:计算Z轴(normal),将摄像机坐标转到模型空间
5-3:用垂直广告牌程度改变Z轴y值后,单位化
5-4:声明Y轴(old up)
5-5:利用Z轴(normal)和Y轴(old up)叉乘计算出X轴(right)
5-6:利用Z轴(normal)和X轴(right)叉乘计算出Y轴(up)
5-7:得到顶点相对于新坐标系中心点的偏移位置
5-8:利用新中心点和3轴计算出顶点新位置
5-9:新顶点转到裁剪空间
5-10:UV缩放偏移
6:片元着色器
直接采样 叠加颜色即可
实现
Shader "Unlit/Lesson95_Billboarding"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
//用于控制垂直广告牌和全向广告牌的变化
_VerticalBillboarding("VerticalBillboarding", Range(0,1)) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _VerticalBillboarding;
v2f vert (appdata_base v)
{
v2f o;
//新坐标系的中心点(默认我们还是使用的模型空间原定,这里的center可以实现在非轴心点进行程序化旋转,下文会提到)
float3 center = float3(0,0,0);
//计算Z轴(normal)
float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
//得到Z轴对应的向量
float3 normalDir = cameraInObjectPos - center;
//相当于把y往下压,如果_VerticalBillboarding是0 就代表把我们Z轴压到了xz平面 如果是1 那么就是正常的视角方向
normalDir.y *= _VerticalBillboarding;
//单位化Z轴
normalDir = normalize(normalDir);
//模型空间下的Y轴正方向 作为它的 old up
//为了避免z轴和010重合 ,因为重合后再计算叉乘 可能会得到0向量
float3 upDir = normalDir.y > 0.999 ? float3(0,0,1) : float3(0,1,0);
//利用叉乘计算X轴(right)
float3 rightDir = normalize(cross(upDir, normalDir));
//去计算我们的Y轴 也就是newup
upDir = normalize(cross(normalDir, rightDir));
//得到顶点相对于新坐标系中心点的偏移位置
float3 centerOffset = v.vertex.xyz - center;
//利用3个轴向进行最终的顶点位置的计算
float3 newVertexPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir * centerOffset.z;
//把新顶点转换到裁剪空间
o.vertex = UnityObjectToClipPos(float4(newVertexPos, 1));
//uv坐标偏移缩放
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 color = tex2D(_MainTex, i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
}
}
关于center的用法
center 的主要作用:指定顶点变换的旋转中心,不一定非要用模型原点。(不过本shader简单处理了)
使用方法思路:
先确定你想绑定的点(例如怪物头顶、特效发射点)的世界空间位置。
将这个世界空间坐标传给 shader。
在 shader 内使用 unity_WorldToObject 将其转换为模型空间的 center。
顶点围绕这个 center 进行偏移、旋转或其他变换。
典型效果:
billboard 广告牌绕指定点旋转而不漂移
顶点动画或特效围绕怪物身体部位旋转
程序化旋转或局部运动,完全无需修改模型本身的 pivot
总结:shader 里的 center 就是“可自定义的旋转枢轴”,世界空间点 → 模型空间 center → 顶点变换,灵活控制旋转中心,便于实现各种动画效果。
效果图

仔细注意y轴不朝向和完全朝向的区别
【顶点动画的注意事项】批处理
知识回顾
我们在之前的顶点动画相关课程中一再强调
我们需要在渲染标签中添加
"DisableBatching"="True"
来让该Shader渲染的对象不进行批处理
目的是让基于模型空间的计算能够正确进行
不会影响最终的渲染结果
知识点一 为什么批处理会影响顶点动画
Unity中默认有静态批处理和动态批处理
批处理的主要作用是
合并多个对象,将他们作为一个DrawCall进行处理
之所以批处理会对顶点动画带来影响
是因为
不同的对象会拥有不同的变换矩阵(位置、旋转、缩放)
而批处理后
他们的变换矩阵会进行统一处理
举例:
物体A:位于世界空间位置 (0, 0, 0),无旋转。
物体B:位于世界空间位置 (5, 0, 0),无旋转。
他们是两个独立的对象,拥有不同的变换矩阵
不进行批处理时:
每个对象的变换矩阵会单独传递给Shader,顶点的模型空间位置会根据各自的变换进行正确计算
进行批处理时:
启用批处理后,Unity会将对象A和对象B合并为一个Draw Call,并使用一个统一的变换矩阵
比如在静态批处理中,Unity会将对象A和对象B的顶点合并为一个网格,并使用统一的变换进行渲染
批处理后顶点位置是混合的,Shader中无法区分不同对象的模型空间位置(也就是模型空间的顶点变化出问题)
可能带来的问题有:
顶点动画失效:
假设你希望顶点在模型空间的x方向上进行sin波动动画。
如果对象A和对象B的模型空间位置被混合,波动动画会变得不可预测
变换混淆:
对象A和对象B有不同的变换矩阵。
如果批处理后使用统一的变换矩阵,Shader无法区分每个顶点属于哪个对象,导致所有顶点的动画效果混淆。
总结:
批处理会让对象失去独立性
相当于将多个对象之间独立的模型空间坐标系合并为一个坐标系
从而影响顶点的相对位置和变换矩阵等信息
导致顶点动画结果异常
因此我们通过渲染标签来关闭批处理
总结:也就是有改到模型空间的关闭批处理
知识点二 关闭批处理带来的问题
关闭批处理带来的最直接问题就是导致
DrawCall的提升
DrawCall的提升可能会带来性能问题
如果DrawCall的增加并没有带来性能问题
那我们可以通过关闭批处理来解决顶点动画问题
如果带来了性能问题,并且必须优化带有顶点动画的Shader,我们应该如何解决呢
知识点三 如何解决问题
开启批处理
1.顶点颜色
利用顶点颜色来存储每个顶点的位置信息或相对位置信息
我们在C#代码中获取模型网格顶点数据,将数据存储到网格的颜色属性中
在Shader中通过颜色属性获取顶点信息
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;
}在Shader中直接在appdata_full结构体中点出颜色成员既可以利用它获取到顶点信息
2.uv通道
和上面的顶点颜色方案类似,只是把相关信息存储到uv通道中而已,但是一般在存储两个值时使用
等等
总结
如果改动到模型空间顶点的 Shader,我们需要关闭批处理。
因为在批处理(尤其是 Built-in 管线的动态批处理)中,多个物体会被合并为一次绘制调用,Unity 会在 CPU 端预先将每个物体的顶点坐标转换到 世界空间。
此时,Shader 接收到的 v.vertex 已经是世界空间坐标,而不再是模型空间坐标。
为了保持统一,Unity 会将 unity_ObjectToWorld 与 unity_WorldToObject 两个矩阵直接设为 单位矩阵。
因此,任何依赖模型空间变换的计算(如顶点动画、基于模型空间的法线或切线扰动等)和基于上面两个转换矩阵的信息获取都会出错。
若顶点动画 Shader 因关闭批处理带来了性能问题,
我们也可以去掉渲染标签 "DisableBatching" = "True",重新打开批处理,
并通过以下方式避免顶点动画渲染问题:
顶点颜色
将模型空间的局部信息(如顶点原始位置或偏移数据)编码进color通道,Shader 中直接读取颜色属性用于动画计算。UV 通道
使用多余的 UV 通道(如uv2、uv3)存储每顶点或每对象的局部信息,在 Shader 中利用这些数据实现与模型空间类似的动画逻辑。
【顶点动画的注意事项】阴影
知识回顾 如何让物体投射阴影
对应知识点 Lesson66_不透明物体阴影_让物体投射阴影
物体向其它物体投射阴影的关键点是:
1. 需要实现 LightMode(灯光模式) 为 ShadowCaster(阴影投射) 的 Pass(渲染通道)
这样该物体才能参与到光源的阴影映射纹理计算中
2. 一个编译指令,一个内置文件,三个关键宏
编译指令:
#pragma multi_compile_shadowcaster
该编译指令时告诉Unity编译器生成多个着色器变体
用于支持不同类型的阴影(SM,SSSM等等)
可以确保着色器能够在所有可能的阴影投射模式下正确渲染
内置文件:
#include "UnityCG.cginc"
其中包含了关键的阴影计算相关的宏
三个关键宏:
2-1.V2F_SHADOW_CASTER
顶点到片元着色器阴影投射结构体数据宏
这个宏定义了一些标准的成员变量
这些变量用于在阴影投射路径中传递顶点数据到片元着色器
我们主要在结构体中使用
2-2.TRANSFER_SHADOW_CASTER_NORMALOFFSET
转移阴影投射器法线偏移宏
用于在顶点着色器中计算和传递阴影投射所需的变量
主要做了
2-2-1.将对象空间的顶点位置转换为裁剪空间的位置
2-2-2.考虑法线偏移,以减轻阴影失真问题,尤其是在处理自阴影时
2-2-3.传递顶点的投影空间位置,用于后续的阴影计算
我们主要在顶点着色器中使用
2-3.SHADOW_CASTER_FRAGMENT
阴影投射片元宏
将深度值写入到阴影映射纹理中
我们主要在片元着色器中使用
3.利用这些内容在Shader中实现代码
由于投射阴影相关的代码较为通用
因此建议大家不用自己去实现相关Shader代码
直接通过FallBack调用Unity中默认Shader中的相关代码即可
知识点回顾 透明度混合物体投射阴影
对应知识点 Lesson70_透明物体阴影_透明度混合物体阴影实现
由于透明度混合需要关闭深度写入
而阴影相关的处理需要用到深度值参与计算
因此Unity中从性能方面考虑(要计算半透明物体的的阴影表现效果是相对复杂的)
所有的内置半透明Shader都不会产生阴影效果(比如 Transparent/VertexLit)
因此
2-1.透明混合Shader想要 投射阴影时
不管你在FallBack中写入哪种自带的半透明混合Shader
都不会有投射阴影的效果,因为深度不会写入
2-2.透明混合Shader想要 接受阴影时
Unity内置关于阴影接收计算的相关宏
不会计算处理 透明混合Shader
混合因子 设置为半透明效果(Blend SrcAlpha OneMinusSrcAlpha)的Shader
因为透明混合物体的深度值和遮挡关系无法直接用传统的深度缓冲和阴影贴图来处理
结论:
Unity中不会直接为透明度混合Shader处理阴影
强制投射:
在FallBack中设置一个非透明Shader,比如VertexLit、Diffuse等
用其中的灯光模式设置为阴影投射的渲染通道来参与阴影映射纹理的计算
把该物体当成一个实体物体处理
知识点一 顶点动画物体投射阴影
我们可以为有顶点动画的物体 使用 LightMode(灯光模式) 为 ShadowCaster(阴影投射) 的 Pass(渲染通道)
这样它便能投射阴影
但是如果我们直接使用内置的这种Pass(默认Shader中的,通过FallBack寻找到的)
投射的阴影会是不正确的,因为默认Pass当中并不会使用新的顶点位置来投射
而是按照模型原来的顶点位置来计算阴影的
举例:
1.新建一个Shader,复制 Lesson94_2DWater 流动的2D河流的Shader代码
2.为其加上一个默认的不透明的FallBackShader 比如VertexLit
3.在Mesh Renderer中开启双面投射阴影
这时我们使用该Shader投射出来的阴影是没有经过顶点动画变化的模型阴影
知识点二 让顶点动画物体投射正确的阴影
我们需要自定义一个LightMode(灯光模式) 为 ShadowCaster(阴影投射) 的 Pass(渲染通道)
在顶点着色器函数中进行顶点相关的计算
1.为知识点一种创建的Shader复制基础阴影投射渲染通道代码 Lesson64_ForwardLighting 中注释掉的Pass
2.在该Pass中加入 波形频率、波长的倒数、波形幅度 属性的映射
3.在该Pass中的顶点着色器函数中 加入顶点的偏移计算(直接复制前面的代码)
4.直接对模型空间中顶点进行偏移,不用进行裁剪坐标空间变换以及UV相关计算
实现
Shader "Unlit/Lesson97"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
//波动幅度
_WaveAmplitude("WaveAmplitude", Float) = 1
//波动频率
_WaveFrequency("WaveFrequency", Float) = 1
//波长的倒数
_InvWaveLength("InvWaveLength", Float) = 1
//纹理变化速度
_Speed("Speed", Float) = 1
}
SubShader
{
//透明Shader相关渲染标签 + 关闭批处理标签
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
float _Speed;
v2f vert (appdata_base v)
{
v2f o;
//模型空间下的偏移位置
float4 offset;
//让它在模型空间的x轴方向进行偏移
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0,0,0);
o.vertex = UnityObjectToClipPos(v.vertex + offset);
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv += float2(0, _Time.y *_Speed );
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 color = tex2D(_MainTex,i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
//该注释主要用于进行阴影投影 主要是用来计算阴影映射纹理的
Pass{
Tags{"LightMode" = "ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 该编译指令时告诉Unity编译器生成多个着色器变体
// 用于支持不同类型的阴影(SM,SSSM等等)
// 可以确保着色器能够在所有可能的阴影投射模式下正确渲染
#pragma multi_compile_shadowcaster
// 其中包含了关键的阴影计算相关的宏
#include "UnityCG.cginc"
struct v2f{
//顶点到片元着色器阴影投射结构体数据宏
//这个宏定义了一些标准的成员变量
//这些变量用于在阴影投射路径中传递顶点数据到片元着色器
//我们主要在结构体中使用
V2F_SHADOW_CASTER;
};
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
v2f vert(appdata_base v)
{
v2f data;
//模型空间下的偏移位置
float4 offset;
//让它在模型空间的x轴方向进行偏移
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0,0,0);
//需要进行顶点偏移位置的修改
//直接在模型空间下顶点坐标进行计算即可
v.vertex = v.vertex + offset;
//o.vertex = UnityObjectToClipPos(v.vertex + offset);
//转移阴影投射器法线偏移宏
//用于在顶点着色器中计算和传递阴影投射所需的变量
//主要做了
//2-2-1.将对象空间的顶点位置转换为裁剪空间的位置
//2-2-2.考虑法线偏移,以减轻阴影失真问题,尤其是在处理自阴影时
//2-2-3.传递顶点的投影空间位置,用于后续的阴影计算
//我们主要在顶点着色器中使用
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data);
return data;
}
float4 frag(v2f i):SV_Target
{
//阴影投射片元宏
//将深度值写入到阴影映射纹理中
//我们主要在片元着色器中使用
SHADOW_CASTER_FRAGMENT(i);
}
ENDCG
}
}
Fallback "VertexLit"
}
效果图

总结
想要让带有顶点动画的对象产生正确的阴影
我们需要自定义 投射阴影的Pass(渲染通道)
在其中加入对顶点的变换计算即可
第10节: Shader基础知识——屏幕后期处理效果
网格、网格渲染器、蒙皮网格渲染器、材质、着色器之间的关系
知识点一 网格、网格渲染器、材质、着色器 它们是什么
网格(Mesh)
网格是一个3D对象的几何数据。
它由顶点、边和面组成。网格描述了对象的形状和结构,定义了3D模型的轮廓。
网格中包含了模型的关键数据
比如:
顶点、法线、切线、纹理坐标、顶点颜色、骨骼权重、骨骼索引、网格边界等等信息
我们在Shader中使用的模型的数据就来自于Mesh
Unity中不带骨骼动画的模型网格数据一般在MeshFilter(网格过滤器)组件中进行关联
而带骨骼动画的模型网格数据一般在Skinned Mesh Renderer(蒙皮网格渲染器)中进行关联
网格渲染器(Mesh Renderer)
网格渲染器是Unity中的一个组件,用于将网格绘制到屏幕上
它主要用来
1.引用一个网格对象来获取几何数据,Mesh Renderer组件会自动寻找同一GameObject上
Mesh Filter组件中的网格(Mesh)并将其渲染出来
2.引用一个或多个材质,用于定义对象的外观
一般不带骨骼动画的模型都使用网格渲染器来进行渲染
比如:
游戏中的建筑物,箱子,地面等等不需要骨骼动画的模型
蒙皮网格渲染器(Skinned Mesh Renderer)
蒙皮网格渲染器是一种特殊的网格渲染器,用于处理带有骨骼动画的网格。
它不仅处理网格的几何数据,还处理骨骼和权重,允许网格根据骨骼动画进行变形。
使用蒙皮网格渲染器的对象不需要再使用Mesh Filter组件
它可以直接关联对应的网格信息
一般带有动画的模型都使用蒙皮网格渲染器来进行渲染
比如:
游戏中的角色、怪物、机关等等
材质(Material)
材质我们也可以称为材质球
它定义了模型网格的外观
材质包含对一个着色器的引用,并通过一组属性(例如颜色、纹理等等信息)来配置着色器
一个模型可以有多个材质,每个材质应用于模型的不同部分
着色器(Shader)
是一种用于描述如何渲染图形和计算图形外观的程序
主要用于控制图形的颜色、光照、纹理和其他视觉效果
它是运行在GPU上的程序,用于计算每个像素的颜色
我们这套课中学习的知识都是和着色器有关的
知识点二 它们之间的关系
Mesh Renderer(网格渲染器)
└── Mesh(网格) ―― MeshFilter(网格过滤器组件进行关联)
└── Geometry Data(几何数据)
└── Material(材质)
└── Shader(着色器)
└── Properties(属性:颜色、纹理等 ―― 在Shader中决定哪些属性暴露在材质上)
Skinned Mesh Renderer(蒙皮网格渲染器)
└── Mesh(网格)
└── Geometry Data(几何数据)
└── Bones & Weights(骨骼与权重)
└── Material(材质)
└── Shader(着色器)
└── Properties(属性:颜色、纹理等 ―― 在Shader中决定哪些属性暴露在材质上)从关系中我们可以得出
由于Mesh Renderer和Skinned Mesh Renderer都是Unity中的组件
那如果我们想要获取、修改一个对象Mesh(网格)、Material(材质)、材质上属性、材质关联的Shader(着色器)等等信息
都可以利用这两个组件去获取
C#代码修改材质参数
知识点一 如何得到对象使用的材质
1.获取到对象的渲染器Renderer
Mesh Renderer和Skinned Mesh Renderer都继承Renderer
我们可以用里式替换原则父类获取、装载子类对象
2.通过渲染器获取到对应材质
我们可以利用渲染器中的material或者sharedMaterial来获取物体的材质
如果存在多个材质,可以使用renderer.materials或renderer.sharedMaterials来获取
material和sharedMaterial的区别
material:
material属性会返回对象的实例化材质, 相当于它会为对象创建一个该材质的独立副本
当你通过material属性修改材质时,这些更改只会影响这个特定对象,而不会影响使用相同材质的其他对象
使用material会增加内存消耗,因为每个对象都有自己独立的材质副本,但是可以单独修改单个对象
sharedMaterial:
sharedMaterial属性会返回对象的共享材质,相当于它返回的是所有使用这个材质的对象共享的同一个材质实例
当你通过sharedMaterial属性修改材质时,这些更改会影响所有使用这个材质的对象
使用sharedMaterial不会增加内存消耗,但是会批量修改所有使用该材质的对象
知识点二 如何修改材质属性
1.颜色
材质对象中有color成员用于颜色修改
2.纹理
材质对象中有mainTexture成员用于主纹理修改
3.通用修改方式
材质中有各种Set方法,用于修改属性
通过传入属性名,以及对应值进行赋值
注意:属性值以SubShader中声明的属性名为准,而不是面板上的显示
4.修改Shader
调用材质中shader属性进行修改
利用Shader.Find(Shader名)方法得到对应Shader
知识点三 材质中常用方法
除了刚才学习的修改属性的相关方法
材质中还有:
1.判断某类型指定名字属性是否存在
2.获取某个属性值
3.修改渲染队列
4.设置纹理缩放偏移
等等
操作代码
private Material material;
public Color color;
[Range(0,1)]
public float fresnelScale;
private void Start() {
//获取对象的渲染器
Renderer renderer = GetComponent<Renderer>();
if(renderer != null)
{
//sharedMaterial和material的区别
//sharedMaterial:一个是改一个都变
//material:一个是改一个不会影响其它使用相同材质球的对象
//得到主材质球
material = renderer.material;//renderer.sharedMaterial;
//得到所有的材质球
Material[] materials = renderer.sharedMaterials; //renderer.materials;
//修改颜色
material.color = color;
//修改主纹理
material.mainTexture = Resources.Load<Texture2D>("路径");
if(material.HasColor("_Color"))
{
material.SetColor("_Color", color);
print(material.GetColor("_Color"));
}
if(material.HasFloat("_FresnelScale"))
material.SetFloat("_FresnelScale", fresnelScale);
//修改渲染队列
material.renderQueue = 2000;
//修改材质球使用的shader
material.shader = Shader.Find("Unlit/Lesson80_Fresnel");
material.SetTextureOffset("_MainTex", new Vector2(0.5f, 0.5f));
material.SetTextureScale("_MainTex", new Vector2(0.5f, 0.5f));
}总结
Unity中想要通过C#代码修改Shader相关参数信息
我们一般都是通过材质去进行修改的
需要使用材质提供的各种相关方法进行修改
屏幕后处理基类
知识回顾
屏幕后期处理效果的基本实现原理
就是利用 OnRenderImage函数 和 Graphics.Blit函数
来获取当前屏幕画面并利用Shader对该纹理进行自定义处理
捕获画面的关键 ―― void OnRenderImage(RenderTexture source, RenderTexture destination)
实现效果的关键 ―― Graphics.Blit (Texture source, RenderTexture dest, Material mat, int pass= -1);
知识点补充
1.Shader.isSupported
如何判断Shader在目标平台和硬件上是否能正确运行
我们可以通过获取Shader对象中的isSupported属性判断
如果返回false,不支持
如果返回true,支持
2.[ExecuteInEditMode]特性
用于使脚本在编辑器模式下也能执行
3.[RequireComponent(typeof(组件名))]特性
指定某个脚本所依赖的组件,它确保当你将脚本附加到游戏对象时,
所需的组件也会自动添加到该游戏对象中
如果这些组件已经存在,它们不会被重复添加
因为后处理脚本一般添加到摄像机上,因此我们用于依赖摄像机
4.材质球中的 HideFlags 枚举
从材质球对象中可以点出 HideFlags 枚举
HideFlags.None: 对象是完全可见和可编辑的。这是默认值。
HideFlags.HideInHierarchy: 对象在层级视图中被隐藏,但仍然存在于场景中。
HideFlags.HideInInspector: 对象在检查器中被隐藏,但仍然存在于层级视图中。
HideFlags.DontSaveInEditor: 对象不会被保存到场景中。适用于编辑器模式,不会影响播放模式。
HideFlags.NotEditable: 对象在检查器中是只读的,不能被修改。
HideFlags.DontSaveInBuild: 对象不会被包含在构建中。
HideFlags.DontUnloadUnusedAsset: 对象在资源清理时不会被卸载,即使它没有被引用。
HideFlags.DontSave: 对象不会被保存到场景中,不会在构建中保存,也不会在编辑器中保存。
这是 DontSaveInEditor | DontSaveInBuild | DontUnloadUnusedAsset 的组合。
如果想要设置枚举满足多个条件 直接多个枚举 进行位或运算即可 |
知识点一 为什么要实现屏幕后处理基类
原因一:
为了实现屏幕后期处理效果
我们每次都需要做的事情一定是
1.实现一个继承子MonoBehaviour的自定义C#脚本
2.关联对应的材质球或者Shader
3.实现OnRenderImage函数
4.在OnRenderImage函数中使用Graphics.Blit函数
那么这些共同点我们完全可以抽象到一个基类中去完成
以后只需要在子类中实现各自的基本逻辑即可
原因二:
我们可以在基类中用代码动态创建材质球
不需要为每个后处理效果都手动创建材质球
只需要在Inspector窗口关联对应使用的Shader即可
原因三:
在进行屏幕后处理之前,我们往往需要检查一系列条件是否满足
比如:
当前平台是否支持当前使用的Unity Shader
我们可以在基类中进行判断,避免每次书写相同逻辑
注意:
在一些老版本中,你可能还会在基类中判断目标平台是否支持屏幕后处理和渲染纹理
一般通过Unity中的SystemInfo类判断
该类可以用于确定底层平台和硬件相关的功能是否被支持
官方说明:https://docs.unity.cn/cn/2022.3/ScriptReference/SystemInfo.html
但是随着时代发展,目前几乎所有的现代图形硬件都是支持屏幕后处理和渲染纹理了
因此我们无需再进行类似的判断的
只需要判断Shader是否被支持即可
知识点二 实现基类功能
主要目标
1.声明基类,让其依赖Camera,并且让其在编辑模式下可运行,保证我们可以随时看到效果
2.基类中声明 公共 Shader,用于在Inspector窗口关联
3.基类中声明 私有 Material,用于动态创建
4.基类中实现判断Shader是否可用,并且动态创建Material的方法
5.基类中实现OnRenderImage的虚方法,完成基本逻辑
实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffectBase : MonoBehaviour
{
//屏幕后处理效果会使用的Shader
public Shader shader;
//一个用于动态创建出来的材质球 就不用再工程中手动创建了
private Material _material;
protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
Graphics.Blit(source, destination, material);
else//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
Graphics.Blit(source, destination);
}
protected Material material
{
get
{
//如果shader 没有 或者有但是不支持当前平台
if (shader == null || !shader.isSupported)
return null;
else
{
//避免每次调用属性都去new材质球
//如果之前new过了,并且shader也没有变化
//那就不用new了 直接返回使用即可
if (_material != null && _material.shader == shader)
return _material;
//除非材质球是空的 或者shader变化了 才会走下面的逻辑
//用支持的shader动态创建一个材质球 用于渲染
_material = new Material(shader);
//不希望材质球被保存下来 因此我们家一个标识
_material.hideFlags = HideFlags.DontSave;
return _material;
}
}
}
}