由于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的虚方法,完成基本逻辑
补充
当同一摄像机上有多个脚本实现 OnRenderImage() 时,Unity 会自动把上一个脚本的 destination 输出作为下一个脚本的 source 传入。(可以借此实现叠加)
实现
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;
}
}
}
}
【亮度、饱和度、对比度】基本原理
知识点一 颜色亮度的 基本原理
想要改变图像颜色的亮度
只需要对图像的每个像素进行加法(减法-加负数)或乘法(除法-乘小数)运算即可实现
增加亮度就是 增加像素的RGB值
减小亮度则是 减少像素的RGB值
也就是说我们只需要在Shader当中加入一个控制亮度的float类型的变量
然后用颜色的RGB乘以该变量或者加上该变量即可
一般我们会采用乘法的形式
即:
最终颜色 = 原始颜色 * 亮度变量
亮度变量 >1 时,图像变亮
亮度变量 <1 时,图像变暗
亮度变量 =1 时,图像不变
知识点二 颜色饱和度的 基本原理
想要改变图像颜色的饱和度
只需要对图像的每个像素的颜色值 相对于灰度颜色进行插值来实现
基本原理为以下三步:
1.计算灰度值(亮度)
利用图像颜色RGB计算一个平均值,得到一个灰度值
但是由于人眼对不同颜色的敏感度不同,所以在计算平均值时不会直接使用算数平均(R+G+B)/3
在图形学中我们一般使用加权平均法来计算灰度值
所谓加权平均法就是通过对不同数据分配不同权重,计算出更符合实际情况的平均值
常用的权重基于Rec. 709标准(高清电视和许多数字图像格式中常用的标准)
R 红色通道的权重:0.2126
G 绿色通道的权重:0.7152
B 蓝色通道的权重:0.0722
这些权重反映了人眼对绿色最敏感,对蓝色最不敏感
加权平均法公式:
灰度值(亮度)L = 0.2126*R + 0.7152*G + 0.0722*B
2.生成灰度颜色
利用第一步中计算出来的灰度值,生成一个灰度颜色
灰度颜色 = (L, L, L)
3.插值计算
使用插值函数lerp,在灰度颜色和原始颜色之间进行插值运算
插值系数就是用于控制饱和度的float类型的变量
公式如下:
最终颜色 = lerp( 灰度颜色, 原始颜色, 饱和度变量 )
lerp计算原理:最终颜色 = 灰度颜色 + (原始颜色 − 灰度颜色)*饱和度变量
饱和度变量 = 0时,结果为灰度颜色
饱和度变量 = 1时,保持原始颜色不变
饱和度变量 = 0~1之间时,灰度颜色和原始颜色的混合
饱和度变量 > 1时,颜色的RGB值超出原始范围,从而使颜色看起来更饱和
知识点三 颜色对比度的 基本原理
想要改变图像颜色的对比度
只需要对图像的每个像素的颜色值 相对于中性灰色进行插值来实现
基本原理为以下两步:
1.声明中性灰色变量,即RGB都为0.5的颜色变量
中性灰颜色 = (0.5,0.5,0.5)
2.在中性灰色和原始颜色之间进行插值运算
最终颜色 = lerp( 中性灰色, 原始颜色, 对比度变量 )
对比度变量 = 0时,此时对比度降到最低,变为中性灰色
对比度变量 = 1时,保持原始颜色不变
对比度变量 = 0~1之间时,降低对比度效果,图像的亮度差异减少,使图像颜色看起来更平淡
对比度变量 > 1时,颜色的RGB值超出原始范围,从而使颜色亮部更亮,暗部更暗,从而增加对比度
总结
1.亮度计算规则
对图像的每个像素颜色进行乘法运算
最终颜色 = 原始颜色 * 亮度变量
2.饱和度计算规则
相对于灰度颜色进行插值
第一步:灰度值(亮度)L = 0.2126*R + 0.7152*G + 0.0722*B
第二步:灰度颜色 = (L, L, L)
第三步:最终颜色 = lerp( 灰度颜色, 原始颜色, 饱和度变量 )
3.对比度计算规则
相对于中性灰色进行插值
第一步:中性灰颜色 = (0.5,0.5,0.5)
第二步:最终颜色 = lerp( 中性灰色, 原始颜色, 对比度变量 )
注意:
亮度、饱和度和对比度的计算规则是在视觉科学、色彩理论和图像处理技术的基础上逐步发展起来的
这些规则不是由某个人发明的,而是通过多年的研究和实践得出的
这些计算方法被广泛应用于各种图像处理工具和计算机图形学中,用于实现图像的调整和优化
【亮度、饱和度、对比度】具体实现
知识回顾
1.亮度计算规则
对图像的每个像素颜色进行乘法运算
最终颜色 = 原始颜色 * 亮度变量
2.饱和度计算规则
相对于灰度颜色进行插值
第一步:灰度值(亮度)L = 0.2126*R + 0.7152*G + 0.0722*B
第二步:灰度颜色 = (L, L, L)
第三步:最终颜色 = lerp( 灰度颜色, 原始颜色, 饱和度变量 )
3.对比度计算规则
相对于中性灰色进行插值
第一步:中性灰颜色 = (0.5,0.5,0.5)
第二步:最终颜色 = lerp( 中性灰色, 原始颜色, 对比度变量 )
知识点一 实现亮度、饱和度、对比度屏幕后期处理效果对应 Shader
1.新建一个Shader,名为BrightnessSaturationContrast(亮度饱和度对比度)
删除其中无用代码
2.声明变量,并进行属性映射
主纹理 _MainTex 2D
亮度 _Brightness Float
饱和度 _Saturation Float
对比度 _Contrast Float
3.设置深度测试、剔除、深度写入(后处理通用)
ZTest Always 开启深度测试
Cull Off 关闭剔除
Zwrite Off 关闭深度写入
这样的设置是屏幕后处理的标配
因为屏幕后处理效果相当于在场景上绘制了一个与屏幕同宽高的四边形面片
这样做的目的是避免它"挡住"后面的渲染物体
比如我们在OnRenderImage前加入[ImageEffectOpaque]特性时
透明物体会晚于该该屏幕后处理效果渲染,如果不关闭深度写入会影响后面的透明相关Pass
4.结构体相关
顶点、纹理坐标
5.顶点着色器
顶点转裁剪空间,uv缩放偏移
6.片元着色器
对主纹理进行采样
分别利用公式计算 亮度、饱和度、对比度
返回处理后的颜色
知识点二 实现亮度、饱和度、对比度屏幕后期处理效果对应 C#代码
1.创建C#脚本,名为BrightnessSaturationContrast(亮度饱和度对比度)
2.继承屏幕后处理基类PostEffectBase
3.声明亮度饱和度对比度变量,用于控制效果变化
4.重写OnRenderImage方法,在其中设置材质球对应属性
实现
重写PostEffectBase,增加UpdateProperty方法方便子类重写:
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)
{
//在进行渲染之前 先更新属性 在子类中重写即可
UpdateProperty();
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
Graphics.Blit(source, destination, material);
else//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
Graphics.Blit(source, destination);
}
/// <summary>
/// 更新材质球属性
/// </summary>
protected virtual void UpdateProperty()
{
}
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;
}
}
}
}
Shader实现:
Shader "Unlit/BrightnessSaturationContrast"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//亮度变量
_Brightness("Brightness", Float) = 1
//饱和度变量
_Saturation("Saturation", Float) = 1
//对比度变量
_Contrast("Contrast", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
Cull Off
ZWrite 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;
half _Brightness;
half _Saturation;
half _Contrast;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//从捕获的主纹理中采样
fixed4 renderTexColor = tex2D(_MainTex, i.uv);
//亮度计算
fixed3 finalColor = renderTexColor.rgb * _Brightness;
//饱和度计算
fixed L = 0.2126*finalColor.r + 0.7152*finalColor.g + 0.722*finalColor.b;
fixed3 LColor = fixed3(L,L,L);
finalColor = lerp(LColor, finalColor, _Saturation);
//对比度计算
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor.rgb, 1);
}
ENDCG
}
}
Fallback off
}
C#后处理子类脚本实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson103_BrightnessSaturationContrast : PostEffectBase
{
[Range(0,5)]
public float Brightness = 1;
[Range(0, 5)]
public float Saturation = 1;
[Range(0, 5)]
public float Contrast = 1;
/// <summary>
/// 更新相关属性
/// </summary>
protected override void UpdateProperty()
{
if (material != null)
{
material.SetFloat("_Brightness", Brightness);
material.SetFloat("_Saturation", Saturation);
material.SetFloat("_Contrast", Contrast);
}
}
}
效果图

【边缘检测效果】基本原理
需要注意的知识是纹素获取一个点周围的像素,以及卷积计算
【边缘检测效果】具体实现
知识回顾
1.灰度值 L = 0.2126*R + 0.7152*G + 0.0722*B
2.边缘检测效果的基本原理
得到 当前像素以及其 上下左右、左上左下、右上右下共9个像素的灰度值
用这9个灰度值和 Sobel算子 进行卷积计算得到梯度值 G = abs(Gx) + abs(Gy)
最终颜色 = lerp(原始颜色,描边颜色,梯度值)
3.如何得到当前像素周围8个像素位置
利用 float4 纹理名_TexelSize 纹素 信息得到当前像素周围8个像素位置
准备工作
1.导入图片资源 设置为Sprite
2.新建场景
3.在场景中使用导入资源新建Sprite对象 将其填充满Game窗口 用于测试
知识点一 实现边缘检测屏幕后期处理效果对应 Shader
1.新建Shader,取名边缘检测EdgeDetection,删除无用代码
2.声明属性,进行属性映射
主纹理 _MainTex
边缘描边用的颜色 _EdgeColor
注意属性映射时 使用内置纹素变量 MainTexTexelSize
3.屏幕后处理效果标配
ZTest Always
Cull Off
ZWrite Off
4.结构体相关
顶点
uv数组,用于存储9个像素点的uv坐标
5.顶点着色器
顶点坐标转换
用uv数组装载9个像素uv坐标
6.片元着色器
利用卷积获取梯度值(可以声明一个Sobel算子计算函数和一个灰度值计算函数)
利用梯度值在原始颜色和边缘颜色之间进行插值得到最终颜色
7.FallBack Off
知识点二 实现边缘检测屏幕后期处理效果对应 C#代码
1.创建C#脚本,名为EdgeDetection(边缘检测)
2.继承屏幕后处理基类PostEffectBase
3.声明边缘颜色变量,用于控制效果变化
4.重写UpdateProperty方法,设置材质球属性
实现
Shader实现(本shader写法卷积没有用归一化,可能有描边失真隐患)
Shader "Unlit/Lesson104_EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//边缘线的颜色
_EdgeColor("EdgeColor", Color) = (0,0,0,0)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
//用于存储9个像素uv坐标的变量
half2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
//Unity内置纹素变量
half4 _MainTex_TexelSize;
fixed4 _EdgeColor;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//当前顶点的纹理坐标
half2 uv = v.texcoord;
//去对9个像素的uv坐标进行计算
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
//计算颜色的灰度值
fixed calcLuminance(fixed4 color)
{
return 0.2126*color.r + 0.7152*color.g + 0.0722*color.b;
}
//Sobel算子相关的卷积计算
half Sobel(v2f o)
{
//Sobel算子对应的两个卷积核
half Gx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half Gy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
half L;//灰度值
half edgeX = 0;//水平方向梯度值
half edgeY = 0;//数值方向梯度值
for (int i = 0; i < 9; i++)
{
//采样颜色后 计算灰度值 并记录下来
L = calcLuminance(tex2D(_MainTex, o.uv[i]));
edgeX += L * Gx[i];
edgeY += L * Gy[i];
}
//最终的一个该像素的梯度值
//half G = abs(edgeX) + abs(edgeY);
return abs(edgeX) + abs(edgeY);
}
fixed4 frag (v2f i) : SV_Target
{
//利用索贝尔算子计算梯度值
half edge = Sobel(i);
//利用计算出来的梯度值在原始颜色 和边缘线颜色之间进行插值
fixed4 color = lerp(tex2D(_MainTex, i.uv[4]), _EdgeColor, edge);
return color;
}
ENDCG
}
}
Fallback Off
}
C#后处理子类重写:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson104_EdgeDetection : PostEffectBase
{
public Color EdgeColor;
protected override void UpdateProperty()
{
if(material != null)
{
material.SetColor("_EdgeColor", EdgeColor);
}
}
}
效果图

【边缘检测效果】加入纯色背景功能
知识点一 什么是纯色背景功能
我们在边缘描边时,有时只想保留描边的边缘线
不想要显示原图的背景颜色
比如把整个背景变为白色、黑色、等等自定义颜色
而抛弃掉原本图片的颜色信息
效果就像是一张描边图片
知识点二 加入纯色背景功能
在上节课的Shader代码中进行修改
1.新属性声明 属性映射
添加 背景颜色程度变量 _BackgroundExtent 0表示保留图片原始颜色,1表示完全抛弃图片原始颜色,0~1之间可以自己控制保留程度
添加自定义背景颜色 _BackgroundColor,定义用于替换图片原始颜色的颜色
2.修改片元着色器
利用插值运算,记录纯色背景中像素描边颜色
利用插值运算,在 原始图片描边 和 纯色图片描边 之间用程度变量进行控制
在上节课的C#代码中进行修改
添加背景颜色程度变量
添加自定义背景光颜色
在UpdateProperty函数中添加属性设置
实现
shader实现
Shader "Unlit/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//边缘线的颜色
_EdgeColor("EdgeColor", Color) = (0,0,0,0)
//背景颜色程度 1为纯色 0为原始颜色
_BackgroundExtent("BackgroundExtent", Range(0,1)) = 0
//背景颜色
_BackgroundColor("BackgroundColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
//用于存储9个像素uv坐标的变量
half2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
//Unity内置纹素变量
half4 _MainTex_TexelSize;
fixed4 _EdgeColor;
fixed _BackgroundExtent;
fixed4 _BackgroundColor;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//当前顶点的纹理坐标
half2 uv = v.texcoord;
//去对9个像素的uv坐标进行计算
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
//计算颜色的灰度值
fixed calcLuminance(fixed4 color)
{
return 0.2126*color.r + 0.7152*color.g + 0.0722*color.b;
}
//Sobel算子相关的卷积计算
half Sobel(v2f o)
{
//Sobel算子对应的两个卷积核
half Gx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half Gy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
half L;//灰度值
half edgeX = 0;//水平方向梯度值
half edgeY = 0;//数值方向梯度值
for (int i = 0; i < 9; i++)
{
//采样颜色后 计算灰度值 并记录下来
L = calcLuminance(tex2D(_MainTex, o.uv[i]));
edgeX += L * Gx[i];
edgeY += L * Gy[i];
}
//最终的一个该像素的梯度值
//half G = abs(edgeX) + abs(edgeY);
return abs(edgeX) + abs(edgeY);
}
fixed4 frag (v2f i) : SV_Target
{
//利用索贝尔算子计算梯度值
half edge = Sobel(i);
//利用计算出来的梯度值在原始颜色 和边缘线颜色之间进行插值
fixed4 withEdgeColor = lerp(tex2D(_MainTex, i.uv[4]), _EdgeColor, edge);
//纯色上描边
fixed4 onlyEdgeColor = lerp(_BackgroundColor, _EdgeColor, edge);
//通过程度变量 去控制 是纯色描边 还是 原始颜色描边 在两者之间 进行过渡
return lerp(withEdgeColor, onlyEdgeColor, _BackgroundExtent);
}
ENDCG
}
}
Fallback Off
}
C#后处理实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EdgeDetection : PostEffectBase
{
public Color EdgeColor;
public Color BackgroundColor;
[Range(0,1)]
public float BackgroundExtent;
protected override void UpdateProperty()
{
if(material != null)
{
material.SetColor("_EdgeColor", EdgeColor);
material.SetColor("_BackgroundColor", BackgroundColor);
material.SetFloat("_BackgroundExtent", BackgroundExtent);
}
}
}
效果图

【高斯模糊效果】基本原理
依然使用卷积
需要注意:使用二次卷积降低运算消耗,以及归一化保真
【高斯模糊效果】基础实现
知识回顾
利用两个一维高斯滤波核来计算高斯模糊时
对于5x5的滤波核,它其中的元素,不管是水平还是竖直方向,都是
(0.0545, 0.2442, 0.4026, 0.2442, 0.0545)
而其中主要的数值就三个
0.4026, 0.2442, 0.0545
我们将利用该高斯核来进行卷积计算
知识补充
在实现高斯模糊效果时,
我们将在Shader中利用两个Pass来分别计算水平卷积和竖直卷积
而两个Pass会存在相同的代码
我们将使用一个新的预处理指令
CGINCLUDE
....
中间包裹CG代码
....
ENDCG
它写在SubShader语句块中,Pass外
它的作用是用于封装共享代码
可以在其中定义常量、函数、结构体、宏等等内容
这些封装起来的代码可以在同一个Shader文件中的多个Pass中使用
也可以在其他Shader文件中引用
使用它可以避免我们重复编写一些相同的代码
从而提高代码复用性和可维护性
知识点一 实现高斯模糊基础效果的制作思路
在Shader中写两个Pass
一个Pass用来计算 水平方向卷积
一个Pass用来计算 竖直方向卷积
两个Pass的区别:
顶点着色器中计算的uv偏移位置不同,一个水平偏移,一个竖直偏移
两个Pass的共同点:
1.使用的内置文件相同
2.使用的属性相同
3.片元着色器的计算规则可以相同
我们可以用uv数组存储5个像素的UV坐标偏移
数组中存储的像素UV偏移分别为
index 0 1 2 3 4
x或y偏移 0 1 -1 2 -2
其中
第0个元素 对应的高斯核元素为 0.4026
第1,2个元素 对应的高斯核元素为 0.2442
第3,4个元素 对应的高斯核元素为 0.0545
那么不管竖直还是水平可以统一一套计算规则进行计算
知识点二 实现 高斯模糊基础屏幕后期处理效果 对应 Shader
1.新建Shader,取名为高斯模糊GaussianBlur,删除无用代码
2.声明变量
主纹理 _MainTex
3.利用补充知识 CGINCLUDE ... ENDCG
实现两个Pass共享的代码
2-1.内置文件引用
2-2.属性映射,注意映射纹素,需要用于uv偏移计算
2-3.结构体
顶点和uv数组(用于存储5个像素的uv偏移)
2-4.两个顶点着色器函数
一个水平偏移采样
一个竖直偏移采样
2-5.片元着色器
共同的卷积计算方式,对位相乘后相加
4.屏幕后处理效果标配
ZTest Always
Cull Off
ZWrite Off
5.实现两个Pass
主要用编译指令指明顶点和片元着色器调用的函数即可
知识点三 实现 高斯模糊基础屏幕后期处理效果 对应 C#
补充知识
由于我们需要用两个Pass对图像进行处理
相当于先让捕获的图像进行水平卷积计算得到一个结果
再用这个结果进行竖直卷积计算得到最终结果
因此我们需要利用Graphics.Blit进行两次Pass代码的执行
所以我们需要一个中间纹理缓存区,用于记录中间的处理结果
我们需要用到
RenderTexture.GetTemporary方法
它的作用是获取一个临时的RenderTexture对象,我们可以利用它来存储中间结果
我们使用它传入3个参数的重载
RenderTexture.GetTemporary(纹理宽、纹理高、深度缓冲-一般填0即可)
需要注意的是
使用该方法返回的 RenderTexture对象
需要配合使用 RenderTexture.ReleaseTemporary(对象) 方法来释放该对象缓存
1.创建C#脚本,名为高斯模糊GaussianBlur
2.继承屏幕后处理基类PostEffectBase
3.重写OnRenderImage函数
4.在其中利用Graphics.Blit、RenderTexture.GetTemporary、 RenderTexture.ReleaseTemporary
函数对纹理进行两次Pass处理
实现
Shader实现(注意为了节约计算量的序号循环操作):
Shader "Unlit/GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
//用于包裹共用代码 在之后的多个Pass当中都可以使用的代码
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
//纹素 x=1/宽 y=1/高
half4 _MainTex_TexelSize;
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下坐标
float4 vertex : SV_POSITION;
};
//片元着色器函数
//两个Pass可以使用同一个 我们把里面的逻辑写的通用即可
fixed4 fragBlur(v2f i):SV_Target
{
//卷积运算
//卷积核 其中的三个数 因为只有这三个数 没有必要声明为5个单位的卷积核
float weight[3] = {0.4026, 0.2442, 0.0545};
//先计算当前像素点
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
//去计算左右偏移1个单位的 和 左右偏移两个单位的 对位相乘 累加
for (int it = 1; it < 3; it++)
{
//要和右元素相乘
sum += tex2D(_MainTex, i.uv[it*2 - 1]).rgb * weight[it];
//和左元素相乘
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1);
}
ENDCG
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
//水平方向的 顶点着色器函数
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//5个像素的uv偏移
half2 uv = v.texcoord;
//去进行5个像素 水平位置的偏移获取
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x*1, 0);
o.uv[2] = uv - half2(_MainTex_TexelSize.x*1, 0);
o.uv[3] = uv + half2(_MainTex_TexelSize.x*2, 0);
o.uv[4] = uv - half2(_MainTex_TexelSize.x*2, 0);
return o;
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
//竖直方向的 顶点着色器函数
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//5个像素的uv偏移
half2 uv = v.texcoord;
//去进行5个像素 水平位置的偏移获取
o.uv[0] = uv;
o.uv[1] = uv + half2(0, _MainTex_TexelSize.x*1);
o.uv[2] = uv - half2(0, _MainTex_TexelSize.x*1);
o.uv[3] = uv + half2(0, _MainTex_TexelSize.x*2);
o.uv[4] = uv - half2(0, _MainTex_TexelSize.x*2);
return o;
}
ENDCG
}
}
Fallback Off
}
C#后处理实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson106_GaussianBlur : PostEffectBase
{
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);
if (material != null)
{
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
//因为我们需要用两个Pass 处理图像两次
//进行第一次 水平卷积计算
Graphics.Blit(source, buffer, material, 0); //Color1
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, destination, material, 1);//在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}
效果图

没有参数可调,初始版本模糊度可能没那么明显。
下一节的完整版演示会明显的多
【高斯模糊效果】完整实现
知识回顾 高斯模糊效果的计算方式优化
想要图片变模糊,那么需要扩大高斯滤波核的大小,越大越模糊
如果通过扩大高斯滤波核的大小来达到更模糊的目的,付出的代价就是会更加消耗性能
因此在Shader中我们不会提供控制高斯滤波核大小的参数,我们的滤波核始终会使用5x5大小的
因此,我们就只能使用其他方式来控制模糊程度了,我们一般会使用以下三种方式:
1.控制缩放纹理大小
2.控制模糊代码执行次数
3.控制纹理采样间隔距离
知识点一 添加控制纹理大小参数
在高斯模糊效果的C#代码中加入一个控制缩放的参数
它主要是用来降低采样质量的,因此取名叫 downSample(降低采样)
如何使用该参数:
在OnRenderImage函数中
我们使用RenderT.GetTemporary获取渲染纹理缓存区时
用源纹理尺寸除以 downSample
这样在调用Graphics.Blit进行图像复制处理时
相当于就将源纹理缩小了,同时在缩小的过程过程中还会用材质球进行效果处理
注意:
在进行复制处理之前,我们可以设置渲染纹理缓存对象的 缩放过滤模式
buffer.filterMode = FilterMode.Bilinear;
FilterMode.Point:点过滤。不进行插值。每个像素都直接从最近的纹理像素获取颜色
FilterMode.Bilinear:双线性过滤。它在纹理采样时使用相邻四个纹理像素的加权平均值进行插值,以生成更平滑的图像
FilterMode.Trilinear:三线性过滤。它在双线性过滤的基础上增加了在不同 MIP 贴图级别之间的插值。
知识点二 添加控制模糊代码执行次数参数
在高斯模糊效果的C#代码中加入一个控制模糊代码执行次数的参数
它主要是用来多次执行材质球中的两个Pass,因此取名叫 iteration(迭代)
如何使用该参数:
在OnRenderImage函数中
我们使用一个for循环,来对原图像进行多次高斯模糊效果处理
注意:
要保证每次使用完 RenderT.GetTemporary 分配的缓存区
都要使用 RenderTexture.ReleaseTemporary 函数将其释放
知识点三 添加控制纹理采样间隔距离
在高斯模糊效果的Shader代码中加入一个控制纹理采样间隔距离的属性
它主要是用来控制间隔多少单位偏移uv坐标,因此取名叫 _BlurSpread(模糊半径)
如何使用该参数:
在顶点着色器进行uv坐标偏移时,乘以该属性,可以通过它控制偏移的多少
注意:
理论上来说像素是1个单位1个单位偏移的
_BlurSpread应该为整数变化
但是为了更精细的控制模糊程度,我们可以让其为小数
小数变化可以更细微的调整模糊程序
实现
shader实现(主要是多了一个参数控制采样间隔距离):
Shader "Unlit/GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSpread("BlurSpread", Float) = 1
}
SubShader
{
//用于包裹共用代码 在之后的多个Pass当中都可以使用的代码
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
//纹素 x=1/宽 y=1/高
half4 _MainTex_TexelSize;
//纹理偏移间隔单位
float _BlurSpread;
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下坐标
float4 vertex : SV_POSITION;
};
//片元着色器函数
//两个Pass可以使用同一个 我们把里面的逻辑写的通用即可
fixed4 fragBlur(v2f i):SV_Target
{
//卷积运算
//卷积核 其中的三个数 因为只有这三个数 没有必要声明为5个单位的卷积核
float weight[3] = {0.4026, 0.2442, 0.0545};
//先计算当前像素点
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
//去计算左右偏移1个单位的 和 左右偏移两个单位的 对位相乘 累加
for (int it = 1; it < 3; it++)
{
//要和右元素相乘
sum += tex2D(_MainTex, i.uv[it*2 - 1]).rgb * weight[it];
//和左元素相乘
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1);
}
ENDCG
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
//水平方向的 顶点着色器函数
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//5个像素的uv偏移
half2 uv = v.texcoord;
//去进行5个像素 水平位置的偏移获取
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x*1, 0)*_BlurSpread;
o.uv[2] = uv - half2(_MainTex_TexelSize.x*1, 0)*_BlurSpread;
o.uv[3] = uv + half2(_MainTex_TexelSize.x*2, 0)*_BlurSpread;
o.uv[4] = uv - half2(_MainTex_TexelSize.x*2, 0)*_BlurSpread;
return o;
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
//竖直方向的 顶点着色器函数
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//5个像素的uv偏移
half2 uv = v.texcoord;
//去进行5个像素 水平位置的偏移获取
o.uv[0] = uv;
o.uv[1] = uv + half2(0, _MainTex_TexelSize.x*1)*_BlurSpread;
o.uv[2] = uv - half2(0, _MainTex_TexelSize.x*1)*_BlurSpread;
o.uv[3] = uv + half2(0, _MainTex_TexelSize.x*2)*_BlurSpread;
o.uv[4] = uv - half2(0, _MainTex_TexelSize.x*2)*_BlurSpread;
return o;
}
ENDCG
}
}
Fallback Off
}
C#后处理实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GaussianBlur : PostEffectBase
{
[Range(1,8)]
public int downSample = 1;
[Range(1,16)]
public int iterations = 1;
[Range(0,3)]
public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);
if (material != null)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放写入到缓存纹理中
Graphics.Blit(source, buffer);
//在使用材质球之前设置 模糊半径属性
//material.SetFloat("_BlurSpread", blurSpread);
//多次去执行 高斯模糊逻辑
for (int i = 0; i < iterations; i++)
{
//如果想要模糊半径影响模糊想过更强烈 更平滑
//一般可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//又声明一个新的缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//因为我们需要用两个Pass 处理图像两次
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); //Color1
//这时 关键内容都在buffer1中 buffer没用了 释放掉
RenderTexture.ReleaseTemporary(buffer);
buffer = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1);//在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//buffer和buffer1指向的都是这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的模糊结果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}
补充
经测试和验证,C#实现里面的两个纹理缓存是可以直接交替也实现,上文写法的好处是可以让同一帧数下显存占用偏低,但是cpu占用高了(因为每次释放旧纹理再申请新的,用了立刻释放,然后才赋值下一张图)
众所周知,显存比 CPU 性能“珍贵”或敏感
但是,现代设备的发展下,对显存的占用要求可能没有这么高
如果追求代码简洁和cpu性能,可以考虑用下面这一版本
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GaussianBlur : PostEffectBase {
[Range(1, 8)] public int downsample = 1;
[Range(1, 32)] public int iterations = 1;//迭代次数
[Range(0,3)]
public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination) {
//base.OnRenderImage(source, destination);
if (material != null) {
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(source.width / downsample, source.height / downsample, 0);
buffer.filterMode = FilterMode.Bilinear;//双线性缩放,这样有插值,不会离散的太离谱
//缩放写入纹理缓存
Graphics.Blit(source, buffer);
for (int i = 0; i < iterations; i++) {
//如果想要模糊半径影响模糊想过更强烈 更平滑
//一般可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//新缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(source.width / downsample, source.height / downsample, 0);
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0);//Color1
//进行第二次 垂直卷积计算
Graphics.Blit(buffer1, buffer, material, 1);//在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
RenderTexture.ReleaseTemporary(buffer1);
}
Graphics.Blit(buffer, destination);
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
}
else
Graphics.Blit(source, destination);
}
}效果图

注意:
1.downsample毕竟是直接压缩纹理,会导致整体模糊,如果想尽量保留原图细节,需要主要调节 iterations 和 blurSpread。
2.从效果图可以看出,我是在 iterations 增大后才调节 blurSpread,因为在低迭代次数下,单独增加 blurSpread 的效果非常微弱。GaussianBlur 的模糊效果主要依赖 iterations 与 blurSpread 的配合。值得注意的是,单独把 blurSpread 拉大时,高斯卷积 5×5 对视觉的模糊贡献有限:单次卷积只是对局部像素加权平均,突变被平滑但幅度很小,不会产生明显质变(可对比描边卷积的突变)。显著模糊需要依靠迭代——每次卷积都在上一次模糊的基础上累积,才能形成明显柔化效果。因此,低迭代次数下单纯增加 blurSpread 意义不大,而增加 iterations 才是让模糊质变的关键。
总结
我们并没有通过改变高斯滤波核的大小来控制最终的模糊程度
而是通过以上三种方式
虽然这样并不符合高斯模糊的理论
但是这样更加的高效简单,灵活性也更强,效果也是可以接受的
这样更加印证了
图像学中,只要最终的效果是好的,那么不必严格遵循数学和物理规则
我们应该更多的从效果优先、性能优先、开发效率优先的方向去解决问题
【Bloom效果】基本原理
总体由3个pass逐步实现:
高亮提取
从原图中提取亮度超过一定阈值的区域,得到高亮纹理。
高斯模糊
对高亮纹理进行模糊处理,通常是水平+垂直两次 Pass 或多次迭代。
合成
将模糊后的高亮纹理与原图进行加法叠加(或更复杂的融合),得到最终带光晕效果的图像。
【Bloom效果】具体实现
知识回顾 Bloom效果基本原理
利用4个Pass进行3个处理步骤
提取(1个Pass) :提取原图像中的亮度区域存储到一张新纹理中
模糊(2个Pass) :将提取出来的纹理进行模糊处理(一般采用高斯模糊)
合成(1个Pass) :将模糊处理后的亮度纹理和源纹理进行颜色叠
知识点 Bloom效果具体实现
准备工作
新建Shader,取名 Bloom ,删除无用代码
第一步 提取
主要目的:提取原图像中的亮度区域存储到一张新纹理中
Shader代码
1.声明属性
主纹理 _MainTex
亮度区域纹理 _Bloom
亮度阈值 _LuminanceThreshold
2.在CGINCLUDE...ENDCG中实现共享CG代码
2-1:属性映射
2-2:结构体(顶点,uv)
2-3: 灰度值(亮度值)计算函数
3.屏幕后处理标配
ZTest Always
Cull Off
ZWrite Off
4.提取Pass 实现
顶点着色器
顶点转换、UV赋值
片元着色器
颜色采样、亮度贡献值计算、颜色*亮度贡献值
C#代码
1.创建C#脚本,名为Bloom
2.继承屏幕后处理基类PostEffectBase
3.声明亮度阈值成员变量
3.重写OnRenderImage函数
5.设置材质球的亮度阈值
4.在其中利用
Graphics.Blit、RenderTexture.GetTemporary、 RenderTexture.ReleaseTemporary
函数对纹理进行Pass处理
第二步 模糊
主要目的:将提取出来的纹理进行模糊处理(一般采用高斯模糊)
Shader代码
1.添加模糊半径属性 _BlurSize,进行属性映射(注意:需要用到纹素)
2.修改之前高斯模糊Shader,为其中的两个Pass命名
3.在Bloom Shader中,利用UsePass 复用 高斯模糊Shader中两个Pass
C#代码
1.复制高斯模糊中的3个属性
2.复制高斯模糊中C#代码中处理高斯模糊的逻辑
3.将模糊处理后的纹理,存储_Bloom纹理属性
第三步 合成
主要目的:将模糊处理后的亮度纹理和源纹理进行颜色叠
Shader代码
合并Pass实现
1.结构体
顶点坐标,4维的uv(xy存主纹理,wz存亮度提取纹理)
2.顶点着色器
顶点坐标转换,纹理坐标赋值(亮度纹理需要判断平台情况)
3.片元着色器
两个纹理颜色采样后相加
4.FallBack Off
C#代码
对源纹理进行合并处理
实现
Shader实现:
Shader "Unlit/Lesson108_Bloom"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//用于存储亮度纹理模糊后的结果
_Bloom("Bloom", 2D) = ""{}
//亮度阈值 控制亮度纹理 亮度区域的
_LuminanceThreshold("LuminanceThreshold", Float) = 0.5
//模糊半径
_BlurSize("BlurSize", Float) = 1
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
//计算颜色的亮度值(灰度值)
fixed luminance(fixed4 color)
{
return 0.2125*color.r + 0.7154*color.g + 0.0721*color.b;
}
ENDCG
ZTest Always
Cull Off
ZWrite Off
//提取的Pass
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
v2f vert(appdata_base v){
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i):SV_Target{
//采样源纹理颜色
fixed4 color = tex2D(_MainTex, i.uv);
//得到亮度贡献值
fixed value = clamp(luminance(color) - _LuminanceThreshold, 0, 1);
//返回颜色*亮度贡献值
return color * value;
}
ENDCG
}
//复用高斯模糊的2个Pass(这里需要回到高斯模糊的pass那里命名pass)
UsePass "Unlit/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
UsePass "Unlit/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
//用于合成的Pass
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
struct v2fBloom
{
float4 pos:SV_POSITION;
//xy主要用于对主纹理进行采样
//zw主要用于对亮度模糊后的纹理采样
half4 uv:TEXCOORD0;
};
v2fBloom vertBloom(appdata_base v)
{
v2fBloom o;
o.pos = UnityObjectToClipPos(v.vertex);
//亮度纹理和主纹理 要采样相同的地方进行颜色叠加
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//用宏去判断uv坐标是否被翻转
#if UNITY_UV_STARTS_AT_TOP
//如果纹素的y小于0 为负数 表示需要对Y轴进行调整
if(_MainTex_TexelSize.y < 0)
o.uv.w = 1 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i):SV_TARGET
{
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
}
}
Fallback Off
}
C#后处理实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson108_Bloom : PostEffectBase
{
//亮度阈值变量
[Range(0, 4)]
public float luminanceThreshold = 0.5f;
[Range(1, 8)]
public int downSample = 1;
[Range(1, 16)]
public int iterations = 1;
[Range(0, 3)]
public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if(material != null)
{
//设置亮度阈值变量
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//渲染纹理缓冲区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//第一步 提取处理 用我们的提取Pass去得到对应的亮度信息 存入到缓冲区纹理中
Graphics.Blit(source, buffer, material, 0);
//第二步 模糊处理
//多次去执行 高斯模糊逻辑
for (int i = 0; i < iterations; i++)
{
//如果想要模糊半径影响模糊想过更强烈 更平滑
//一般可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//又声明一个新的缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//因为我们需要用两个Pass 处理图像两次
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 1); //Color1
//这时 关键内容都在buffer1中 buffer没用了 释放掉
RenderTexture.ReleaseTemporary(buffer);
buffer = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 2);//在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//buffer和buffer1指向的都是这一次高斯模糊处理的结果
buffer = buffer1;
}
//把提取出来的内容进行高斯模糊后 存储Shader当中的一个纹理变量
//用于之后进行合成
material.SetTexture("_Bloom", buffer);
//测试 看到提取效果
//Graphics.Blit(buffer, destination);
//合成步骤
Graphics.Blit(source, destination, material, 3);
RenderTexture.ReleaseTemporary(buffer);
}
else
{
Graphics.Blit(source, destination);
}
}
}
效果图

【运动模糊效果】基本原理
【运动模糊效果】具体实现
知识回顾 运动模糊基本原理
保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中
通过RenderTexture来进行保存,用2个Pass来进行混合叠加
一个Pass混合RGB通道,由两张图片根据模糊程度决定最终混合效果
Blend SrcAlpha OneMinusSrcAlpha((源颜色 SrcAlpha) + (目标颜色 (1 - SrcAlpha)))
ColorMask RGB (只改变颜色缓冲区中的RGB通道)
一个Pass混合A通道,由当前屏幕图像的A通道来决定
Blend One Zero(最终颜色 = (源颜色 1) + (目标颜色 0))
ColorMask A(只改变颜色缓冲区中的A通道)
知识点一 实现 运动模糊屏幕后期处理效果 对应 Shader
1.新建Shader 名为运动模糊(MotionBlur) 删除其中无用代码
2.属性声明
主纹理 _MainTex
模糊程度 _BlurAmount
3.共享CG代码 CGINCLUDE...ENDCG
内置文件UnityCG.cginc引用
属性映射
结构体(顶点和UV)
顶点着色器(裁剪空间转换 uv坐标赋值)
4.屏幕后处理效果标配
ZTest Always
Cull Off
ZWrite Off
5.第一个Pass(用于混合RGB通道)
混合因子 和 颜色蒙版设置
Blend SrcAlpha OneMinusSrcAlpha((源颜色 SrcAlpha) + (目标颜色 (1 - SrcAlpha)))
ColorMask RGB (只改变颜色缓冲区中的RGB通道)
片元着色器
对主纹理采样后利用模糊程度作为A通道与颜色缓冲区颜色进行混合
6.第二个Pass(用户混合A通道)
混合因子 和 颜色蒙版设置
Blend One Zero(最终颜色 = (源颜色 1) + (目标颜色 0))
ColorMask A(只改变颜色缓冲区中的A通道)
片元着色器
对主纹理采样
7.FallBack Off
知识点二 实现 运动模糊屏幕后期处理效果 对应 C#
1.创建C#脚本,名为运动模糊MotionBlur
2.继承屏幕后处理基类PostEffectBase
3.声明成员属性
公共的模糊程度
私有的堆积纹理 accumulation Texture(用于存储上一次渲染结果)
4.重写OnRenderImage函数
4-1.若堆积纹理为空或宽高变化 则初始化渲染纹理
设置其hideFlags为HideFlags.HideAndDontSave(让其不保存)
4-2.设置模糊程度属性
4-3.将源纹理利用材质写入到堆积纹理中(相当于记录本次渲染结果)
4-4.将堆积纹理写入目标纹理中
5.组件失活是销毁堆积纹理
原理解释
Unity 中常见的累积帧动态模糊实现主要依赖一个**累积纹理(accumulationTex)**来存储上一帧的渲染结果,通过每帧将当前帧的颜色与累积纹理混合,实现运动模糊效果。
核心逻辑
RGB混合(Pass1)
当前帧颜色乘以
_BlurAmount,累积纹理颜色乘以1 - _BlurAmount,然后叠加。通过迭代累积,每次卷积都是在上一次模糊基础上进行,形成拖尾模糊。
这是运动模糊效果的核心。
Alpha更新(Pass 2)
第二个 Pass 单独写入累积纹理的 Alpha 通道,不是为了显示,因为后处理输出压根不会拿你纹理的alpha,而是为了记录当前帧的混合权重。
下一帧混合时使用这个 Alpha 作为
SrcAlpha,保证累积权重正确。屏幕输出默认只看 RGB,不会导致画面透明。
设计意义
Alpha 在这里本质上是一种 内部缓存,存储累积比例,用于下一帧计算。
屏幕后处理输出只显示 RGB,Alpha 完全被忽略。
这种做法保证了动态模糊效果在多帧累积时连续、自然,而不需要额外传递或计算权重。
总结
动态模糊效果的关键在于累积帧和混合比例。
第二个 Pass 的 Alpha 更新只是为累积逻辑服务,并非显示用途。
这样可以在保证效果连续的同时,不影响屏幕显示,也不浪费额外显存。
实现
shader实现:
Shader "Unlit/Lesson109_MotionBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//模糊程度变量
_BlurAmount("BlurAmount", Float) = 0.5
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
ENDCG
//屏幕后处理效果标配
ZTest Always
Cull Off
ZWrite Off
//第一个Pass 用于混合RGB通道
Pass
{
//((源颜色 * _BlurAmount) + (目标颜色 * (1 - _BlurAmount)))
Blend SrcAlpha OneMinusSrcAlpha
//(只改变颜色缓冲区中的RGB通道)
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
fixed4 fragRGB (v2f i) : SV_Target
{
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
ENDCG
}
Pass
{
//(最终颜色 = (源颜色 * 1) + (目标颜色 * 0))
Blend One Zero
//(只改变颜色缓冲区中的A通道)
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
fixed4 fragA (v2f i) : SV_Target
{
return fixed4(tex2D(_MainTex, i.uv));
}
ENDCG
}
}
Fallback Off
}
C#后处理实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson109_MotionBlur : PostEffectBase
{
[Range(0,0.9f)]
public float blurAmount = 0.5f;
//堆积纹理 用于存储之前渲染的结果的 渲染纹理
private RenderTexture accumulationTex;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//初始化堆积纹理 如果为空 或者 屏幕宽高变化了 都需要重新初始化
if( accumulationTex == null ||
accumulationTex.width != source.width ||
accumulationTex.height != source.height)
{
DestroyImmediate(accumulationTex);
//初始化
accumulationTex = new RenderTexture(source.width, source.height, 0);
accumulationTex.hideFlags = HideFlags.HideAndDontSave;
//保证第一次 累积纹理中也是有内容 因为之后 它的颜色 会作为颜色缓冲区中的颜色
Graphics.Blit(source, accumulationTex);
}
//1 - 模糊程度的目的 是因为 希望大到的效果是 模糊程度值越大 越模糊
//因为Shader中的混合因子的计算方式决定的 因此 我们需要1 - 它
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
//利用我们的材质 进行混合处理
//第二个参数 有内容时 它会作为颜色缓冲区的颜色来进行处理
//没有直接写入目标中的目的 也是可以通过accumulationTex记录当前渲染结果
//那么在下一次时 它就相当于是上一次的结果了
Graphics.Blit(source, accumulationTex, material);
Graphics.Blit(accumulationTex, destination);
}
else
Graphics.Blit(source, destination);
}
/// <summary>
/// 如果脚本失活 那么把累积纹理删除掉
/// </summary>
private void OnDisable()
{
DestroyImmediate(accumulationTex);
}
}
注意:关于天空盒可能留下影子无法消除的问题
"这是因为当主相机正在以**HDR(高动态范围)**模式渲染,而后处理效果中用于累积图像RenderTexture没有被正确地设置为HDR格式。
1. 天空盒是光源:在HDR管线中,天空盒不仅仅是一个颜色,它是一个亮度极高的光源。一个明亮的蓝色天空,在缓冲区里的实际颜色值可能不(0.5, 0.7, 1.0),而(5.0, 7.0, 10.0),远远超过了标准颜1.0的上限。
2. 默认RT的陷阱:当你使new RenderTexture(width, height, 0)创建RT时,它的格式可能无法正确处理这些超过1.0的HDR值。
3. 视觉上的“卡死”:你的混合算法在数学上可能仍在将残影的亮度20.0向天空10.0混合。但因为最终显示在LDR屏幕上时,存入输出纹理的时候,所有大1.0的值都会被截断显示1.0(纯白),所以你看上去就好像颜色值1.01.0,没有任何变化。"
天空盒贴图显示亮蓝色,线性空间数值可能是 (5.0, 7.0, 10.0),而不是普通 LDR [0,1] 范围。
非 HDR 贴图
如果天空盒贴图本身是普通 8-bit 贴图,颜色值才严格限制在 [0,1]。
修复代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MotionBlur : PostEffectBase {
[Range(0, 0.9f)] public float blurAmount = 0.5f;
//堆积纹理 用于存储之前渲染的结果的 渲染纹理
private RenderTexture accumulationTex;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination) {
if (material!=null) {
if (accumulationTex == null || accumulationTex.width!= source.width || accumulationTex.height!= source.height) {
DestroyImmediate(accumulationTex);
//设置DefaultHDR参数可以解决天空盒拖影问题
accumulationTex=new RenderTexture(source.width,source.height,0,RenderTextureFormat.DefaultHDR);
accumulationTex.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(source, accumulationTex);
}
//因为Shader中的混合因子的计算方式决定的 因此 我们需要1 - 它
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
Graphics.Blit(source, accumulationTex, material);
Graphics.Blit(accumulationTex, destination);
}
else {
Graphics.Blit(source, destination);
}
}
}效果图
