——内容源自唐老狮的shader课程
目录
1.3.蒙皮网格渲染器(Skinned Mesh Renderer)
8.2.5.其计算方式的优化(实际上这样并不符合高斯模糊的理论)
1.基本知识
1.1.网格(Mesh)
网格是一个3d对象的几何数据,它由顶点,边和面构成,其描述了对象的形状和结构,定义了3d模型的轮廓。
网格中包含了模型的关键数据。如:顶点,法线,切线,纹理坐标,顶点颜色,骨骼权重,骨骼索引,网格边界等。我们在Shader中使用的模型的数据就来自于Mesh。
unity中不带骨骼动画的模型网格数据一般在MeshFilter中(网格过滤器)组件中进行关联,而带骨骼动画的模型网格数据一般在Skinned Mesh Renderer(蒙皮网格渲染器)中进行关联。
1.2.网格渲染器(Mesh Renderer)
是unity中的一个组件,用于将网格绘制到屏幕上。
其主要是用来:
1.引用一个网格对象来获取几何数据,Mesh Renderer组件会自动寻找同一GameObject上Mesh Filter(网格过滤器)组件中的网格并将其渲染出来
2.引用一个或多个材质,用于定义对象的外观
一般不带骨骼动画的模型都使用网格渲染器来进行渲染
1.3.蒙皮网格渲染器(Skinned Mesh Renderer)
是一种特殊的网格渲染器,用于处理带有骨骼动画的网格,它不仅处理网格的几何数据,还处理骨骼和权重,允许网格根据骨骼动画进行变形。使用蒙皮网格渲染器的对象不需要再使用Mesh Filter组件,它可以直接关联对应的网格信息。
一般带有动画的模型都是用蒙皮网格渲染器来进行渲染
1.4.材质(Material)
定义了模型网格的外观,材质包含对一个着色器的引用,并通过一组数据(如颜色,纹理等)来配置着色器。
一个模型可以有多个材质,每个材质应用于模型的不同部分。
1.5.着色器(Shader)
是一种用于描述如何渲染图形和计算图形外观的程序,主要用于控制图形的颜色,光照,纹理和其他视觉效果。它是运行在GPU上的程序,用于计算每个像素的颜色。
1.6.它们之间的关系
Mesh Renderer(网格渲染器)
㇗ Mesh(网格)—— Mesh Filter(网格过滤器组件进行关联)
㇗ Geometry Data(几何数据)
㇗ Material(材质)
㇗ Shader(着色器)
㇗Properties(属性,在Shader中决定哪些属性暴露在材质上)
Skinned Mesh Renderer(蒙皮网格渲染器)
㇗ Mesh(网格)
㇗ Geometry Data(几何数据)
㇗ Bones & Weights(骨骼和权重)
㇗ Material(材质)
㇗ Shader(着色器)
㇗Properties(属性,在Shader中决定哪些属性暴露在材质上)
由上可知:如果我们需要获取,修改一个对象上的Mesh等属性,都可以利用这俩组件去获取
2.通过c#代码修改材质参数
2.1.得到对象使用的材质
1.获取到对象的渲染器:因为Mesh Renderer和Skinned Mesh Renderer都是继承自Renderer的组件,因此我们可以用父类来获取子类
2.通过渲染器得到对应材质:利用渲染器中的material或是sharedMaterial属性来获取物体的材质,如果有多个材质,可分别它们后面加s获取。
Renderer renderer = GetComponent<Renderer>();
if (renderer != null)
{
Material material_1 = renderer.material;
Material[] materials_1 = renderer.materials;
Material material_2 = renderer.sharedMaterial;
Material[] materials_2 = renderer.sharedMaterials;
}
material和sharedMaterial之间的区别:
material:会返回对象的实例化材质,相当于他会为对象创建一个材质的独立副本。修改它不会影响别的使用同样材质的物体。
sharedMaterial:返回的是共享材质,修改后作用于所有使用相同材质的对象。
2.2.如何修改材质属性
1.通用修改方式:材质对象中的各种Set方法,通过传入属性名与对应值进行修改
//第一参数为变量名,而非在材质面板上显示的那个
material_1.SetColor("_Color", color);
material_1.SetFloat("_FresnelScale", fresnelScale);
...
2.主纹理修改:可以通过赋值Resources.Load<Texture2D>("路径")的方式来修改
material_1.mainTexture = Resources.Load<Texture2D>("路径");
3.Shader修改:对其shader属性赋值Shader.Find("Shader名字(或者说位置)")来修改
material_1.shader = Shader.Find("Models_4/Water_2D");
2.3.材质中常用方法
1.判断某类型指定名字的属性是否存在,返回bool值
material_1.HasColor("_OneColor")
2.获取某个属性值
material_1.GetColor("_OneColor")
3.修改渲染队列
material_1.renderQueue = 2000
4.设置纹理缩放偏移
material_1.SetTextureOffset("_MainTexture", new Vector2(0f, 0f)); //偏移
material_1.SetTextureScale("_MainTexture", new Vector2(1f, 1f)); //缩放
3.什么是屏幕后期处理效果
简称为屏幕后处理,它是一种在渲染管线的最后阶段应用的视觉效果。允许你在场景渲染完成后对最终图像进行各种调整和效果处理。常见的效果有:景深,模糊,色彩处理等
简而言之:就是对渲染完成后的画面进行二次处理
4.实现屏幕后处理的关键
4.1.获取渲染完成后的画面信息
OnRenderImage函数:它可以捕获画面(这里不会使用GrabPass和RenderTexture),是个在继承了MonoBehaviour的脚本中能够自动调用的函数,类似生命周期函数。他会在图像的渲染操作完成后调用。
固定写法是:
void OnRenderImage(RenderTexture source, RenderTexture destination)
第一参数:源渲染纹理,渲染完成后的画面会存在其中
第二参数:目标渲染纹理,将经过处理的图像写入其中用于最终的显示
需要注意的是:该函数得到的源纹理默认是在所有的不透明和透明的Pass执行完毕后调用的,基于该源纹理进行修改会对游戏场景中的所有游戏物体产生影响,如果你想要在不透明的Pass执行完毕就调用该函数,只需要在函数前加上特性[ImageEffectOpaque],这样就不会对透明物体产生影响。
[ImageEffectOpaque]
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
}
4.2.如何添加自定义效果
Graphics.Blit():函数的用处是将一个图像从一个纹理复制到另一个纹理,同时在这个过程中用着色器对图像进行处理,其有多个重载,直说俩常用的:
1.Graphics.Blit(Texture source, RenderTexture dest):将源纹理直接复制到目标纹理中
2.Graphics.Blit(Texture source, RenderTexture dest, Material mat, int pass = -1):将源纹理传递给mat材质中名为_MainTex的纹理属性进行处理,pass参数默认为-1,表示会依次使用所有的Pass,否则就只会使用给定索引的Pass
[ImageEffectOpaque]
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//1
Graphics.Blit(source, destination);
//2
//即便mat中没有主纹理,也会将mat的结果传递给des
Graphics.Blit(source, destination, mat);
}
5.屏幕后处理基类
5.1.补充知识
1.shader.isSupported:用于判断Shader是否可以在目标平台上是否正常运行
2.[ExecuteInEditMode]特性:可以使脚本在编辑器模式下也能运行
3.[RequireComponent(typeof(组件名))]:指定某个脚本依赖的组件,在添加该脚本时,如果没有依赖的组件则会自动添加。已存在不会重复。
因为后处理脚本一般挂在摄像机上,所以我们需要依赖摄像机
4.材质球中的 HideFlags 枚举:直接从材质球中点出即可
HideFlags.None:对象是完全可见和可编辑的,这是默认值
HideFlags.HideInHierarchy:对象在层级视图中被隐藏,但仍然存在于场景中
HideFlags.HideInInspector:对象在检查器中被隐藏,但仍然存在层级视图中。
HideFlags.DontSaveInEditor:对象不会被保存到场景中。仅用于编辑器模式,不会影响播放模式
HideFlags.NotEditable:对象在检查器中是只读的,不能被修改
HideFlags.DontSaveInBuild:对象不会被保存在构建中
HideFlags.DontUnloadUnusedAsset:对象在资源清理时不会被卸载,即便他没有被引用
HideFlags.DontSave:对象不会被保存到场景中,不会在构建中保存,也不会在编辑器中保存
如果想要满足多个条件,进行位或运算即可( | )
5.2.为什么要实现屏幕后处理基类
1.材质球的创建与关联,实现OnRednerImage函数,在OnRenderImage函数等使每个后处理都要做的,所以要将其放到一个基类里,只需在子类中完成各自的逻辑即可
2.在基类中用代码动态创建材质球,不需要为每个后处理效果都手动创建。只需要在Inpector窗口关联对应的Shader即可
3.在进行屏幕后处理之前,我们一般都需要检查一系列条件是否满足。而在一些老版本中,还需要在基类中判断目标平台是否支持屏幕后处理和渲染纹理,一般通过Unity中的SystemInfo类判断。
5.3.实现
1.声明基类,让其依赖Camera,并且让其在编辑模式下可运行,保证我们可以随时看到效果
2.基类中声明 公共Shader,用于在Inpector窗口关联
3.基类中声明 私有Material,用于动态创建
4.基类中实现判断Shader是否可用,并且动态创建Material的方法
5.基类中实现OnRenderImage的虚方法,完成基本逻辑
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class Lesson69_Basic : MonoBehaviour
{
public Shader shader;
//一个动态创建的材质球,不用在工程中手动创建
private Material mat;
protected Material Mat
{
get
{
//如果shader为空或者shader不支持则不创建材质
if (shader == null || !shader.isSupported)
{
mat = null;
}
else
{
//避免每次调用属性都new一个,除非材质为空了或者shader变化了才会重新new
if (mat != null && mat.shader == this.shader)
{
return mat;
}
mat = new Material(shader);
//不希望材质球被保存下来
mat.hideFlags = HideFlags.DontSave;
}
return mat;
}
}
protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//材质不为空则进行处理,否则直接显示原画面
if (Mat != null)
{
//调用以更新属性
UpdateProperty();
Graphics.Blit(source, destination, Mat);
}
else
{
Graphics.Blit(source, destination);
}
}
protected virtual void UpdateProperty() {}
}
6.后处理效果——亮度,饱和度,对比度
6.1.颜色亮度的基本原理
想要改变图像颜色的亮度,只需要对图像的每个像素进行加法(减法-加负数)或乘法(除法-乘倒数)运算即可实现,增加亮度就是 增加像素的RGB值,减少亮度就是 减少像素的RGB值。
我们只需要在Shader中加入一个亮度变量,并用该变量乘以(一般用乘法)颜色的RGB值即可。即 最终颜色 = 原神i颜色 * 亮度变量
6.2.颜色饱和度的基本原理
若想改变图像的饱和度,需要对图像每个像素的颜色值相对于灰度颜色进行插值计算
1.计算灰度值(亮度):利用图像RGB计算一个平均值(即灰度值),但由于人眼对不同颜色的敏感度不同,所以使用的是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(灰度颜色, 原始颜色, 饱和度变量)
饱和度变量大于1时,颜色的RGB值超出原始范围,从而使颜色看起来更饱和
6.3.颜色对比度的基本原理
想要改变图像颜色的对比度,只需要对图像的每个像素的颜色值,相对于中性灰色进行插值来实现。
1.声明中性灰色变量,即RGB都为0.5的颜色变量
2.在中性灰色和原始颜色之间进行插值运算,即:
最终颜色 = lerp(中性灰色,原始颜色,对比度变量)
对比度变量 > 1时:颜色的RGB值超出原始范围,从而使颜色亮部更亮,暗部更暗即增加对比度;
对比度变量在 0-1 之间时,降低对比度效果,图像的亮度差异减少,使图像颜色看起来更平淡;
6.4.实现
设置深度测试(ZTest Always),剔除(Cull Off),深度写入(ZWrite Off),此为屏幕后处理的标配,因为屏幕后处理效果相当于在场景上绘制了一个与屏幕同宽高的四边形面片,这样是为了避免它挡住后面的渲染物体。本质和[ImageEffectOpauqe]相同
//Shader部分
Shader "Models_5/BrightnessSaturationContrast"
{
Properties
{
_MainTex("MainTex", 2D) = ""{}
//亮度
_Brightness("Brightness", Float) = 1
//饱和度
_Saturation("Saturation", Float) = 1
//对比度
_Contrast("Contrast", Float) = 1
}
SubShader
{
Tags {}
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float _Brightness;
float _Saturation;
float _Contrast;
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 * _MainTex_ST.xy + _MainTex_ST.zw;
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
float3 color = tex2D(_MainTex, f.uv);
float3 colorB = color.rgb * _Brightness;
float grayScale = 0.2126 * colorB.r + 0.7152 * colorB.g + 0.0722 * colorB.b;
float3 grayScaleColor = float3(grayScale, grayScale, grayScale);
float3 colorBS = lerp(grayScaleColor, colorB, _Saturation);
float3 halfGrayScaleColor = float3(0.5, 0.5, 0.5);
float3 colorBSC = lerp(halfGrayScaleColor, colorBS, _Contrast);
return fixed4(colorBSC, 1.0);
}
ENDCG
}
}
//使用失败了就不管
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson70_BrightnessSaturationContrast : Lesson69_Basic
{
[Range(-1, 3)]
public float brightness;
[Range(-1, 3)]
public float saturation;
[Range(-1, 3)]
public float contrast;
protected override void UpdateProperty()
{
Mat.SetFloat("_Brightness", brightness);
Mat.SetFloat("_Saturation", saturation);
Mat.SetFloat("_Contrast", contrast);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.OnRenderImage(source, destination);
}
}

7.后处理效果——边缘检测
7.1.是什么
是一种用于突出图像中的边缘,使物体的轮廓更加明显的图像处理技术。其主要目的是找到图像中亮度变化显著的区域,这些区域通常对应于物体的边界。
通常来讲:边缘检测相当于利用Shader代码自动给屏幕图像进行描边处理
7.2.基本原理
7.2.1.概述
计算每个像素的灰度值(亮度),用灰度值结合卷积核进行卷积处理,得到该像素的梯度值。梯度值越大越靠近边界,越趋近于描边颜色。反之则越趋近于原始颜色。
7.2.2.卷积核
也被称为边缘检测因子,最常用的是Sobel算子(3x3的矩阵):
Gx:-1,-2,-1 Gy:-1,0,1
0, 0, 0 -2,0,2
1, 2, 1 -1,0,1
每个算子包含了两个方向的卷积核,它们他们分别用来检测水平(x)和竖直(y)方向上的边缘信息故需要计算两个方向 Gx 和 Gy。
而像素整体的梯度值G为:
G = abs(Gx) + abs(Gy)
G = sqrt(Gx ^ 2 + Gy ^ 2)(效果更好,但其计算量大)
最后,我们只需要定义一个描边颜色,然后在原始颜色和描边颜色之间利用梯度值进行插值即可。梯度值越大越表明接近边缘,越接近描边颜色。
7.2.3.卷积
是一种数学计算方式,他就像是一个放大镜(卷积核)在图片上移动,放大镜(卷积核)的作用是帮助我们看到图片上的细微变化,用它扫描整张图片,颜色突然变化的地方往往就是物体的边缘。
对于一个像素,以其为中心往外延伸,构成一个与卷积核大小对应的矩阵,然后用这个矩阵跟卷积核进行对应相乘相加。所得结果即为该像素的梯度值。计算出两个方向的梯度值后进行运算即可获得G,即像素整体的梯度值。
7.2.4.如何得到周围8个像素的位置
unity提供了用于 访问纹理对应的每个纹素(像素)的大小的 变量
float4 _纹理名_TexelSize
四个分量分别为(假设纹理宽高1920 * 1080):
x:1 / 纹理宽度 = 1 / 1920
y:1 / 纹理高度 = 1 / 1080
z:纹理宽度 = 1920
z:纹理高度 = 1080
然后我们可以使用它对uv坐标进行偏移,如:
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
...
7.3.实现
//shader部分
Shader "Models_5/Stroke"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_EdgeColor("EdgeColor", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float4 _EdgeColor;
float _BackgroundExtent;
float4 _BackgroundColor;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv[9] : TEXCOORD0;
};
float CalculateLuminance(float3 _color)
{
float luminanceValue = _color.r * 0.2126 + _color.g * 0.7152 + _color.b * 0.0722;
return luminanceValue;
}
v2f vert(appdata_full v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
//将周围八个像素及自身传递给片元着色器
float2 uv = v.texcoord.xy;
data.uv[0] = uv + _MainTex_TexelSize.xy * float2(-1, 1);
data.uv[1] = uv + _MainTex_TexelSize.xy * float2( 0, 1);
data.uv[2] = uv + _MainTex_TexelSize.xy * float2( 1, 1);
data.uv[3] = uv + _MainTex_TexelSize.xy * float2(-1, 0);
data.uv[4] = uv + _MainTex_TexelSize.xy * float2( 0, 0);
data.uv[5] = uv + _MainTex_TexelSize.xy * float2( 1, 0);
data.uv[6] = uv + _MainTex_TexelSize.xy * float2(-1, -1);
data.uv[7] = uv + _MainTex_TexelSize.xy * float2( 0, -1);
data.uv[8] = uv + _MainTex_TexelSize.xy * float2( 1, -1);
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
float4 originColor = tex2D(_MainTex, f.uv[4]);
//进行梯度值的运算
float sobelGx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
float sobelGy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
float luminanceGx = 0;
float luminanceGy = 0;
for (int i = 0; i < 9; i++)
{
float tempLuminance = CalculateLuminance(tex2D(_MainTex, f.uv[i]));
luminanceGx += sobelGx[i] * tempLuminance;
luminanceGy += sobelGy[i] * tempLuminance;
}
float G = abs(luminanceGx) + abs(luminanceGy);
//切记,摄像机看到的和屏幕上的已经不一样了,摄像机看到是未经处理的画面
//在这个地方,插值的第一个参数更像是一个画板,画什么和它是没有关系的
//因为在前面就已经通过计算得到在对应位置应该画什么了
fixed4 withEdgeColor = lerp(originColor, _EdgeColor, G);
return withEdgeColor;
}
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson71_TODO : Lesson69_Basic
{
public Color edgeColor;
public Color backgroundColor;
[Range(0, 1)]
public float backgroundExtent;
protected override void UpdateProperty()
{
Mat.SetColor("_EdgeColor", edgeColor);
Mat.SetColor("_BackgroundColor", backgroundColor);
Mat.SetFloat("_BackgroundExtent", backgroundExtent);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.OnRenderImage(source, destination);
}
}

7.4.加入纯色背景功能
7.4.1.是什么
在边缘描边时,只保留描边的边缘线。而把整个背景变为白色等自定义颜色(抛弃图片原本的颜色信息)
7.4.2.实现
用一个背景颜色程度变量来控制背景的程度。然后利用插值运算在纯色背景中进行描边。最后再次利用插值运算在原始图片描边和纯色背景描边之间通过程度变量进行控制。
//主要是Shader中的这两句
fixed4 onlyEdgeColor = lerp(_BackgroundColor, _EdgeColor, G);
fixed4 finalColor = lerp(withEdgeColor, onlyEdgeColor, _BackgroundExtent);

8.后处理效果——高斯模糊
8.1.是什么
是一种用于 平滑图像并减少图像噪声的 图像处理技术,其目的是使图像的边缘和细节变得模糊和平滑。
说白了就是用Shader代码自动对屏幕图像进行模糊处理
8.2.基本原理
8.2.1.概述
利用 高斯函数 计算出 高斯滤波核 中每个元素并进行归一化处理后,再和目标像素通过 卷积 计算后得到最终的效果。需要注意的是,这里的卷积是高斯滤波核与对应像素的颜色直接相乘,而非其灰度值。
8.2.2.高斯滤波核
也称高斯核,其为一个 N x N 的卷积核,其大小可以自定义,但一般会是一个奇数x奇数。滤波核越大,模糊效果越明显。综合来讲,我们一般使用5x5。而高斯滤波核中各元素的具体值,我们通过高斯函数来确定。
8.2.3.高斯函数
我们会使用二维高斯函数来计算高斯模糊效果中的卷积核。
σ为标准方差,一般为1;
x,y是相对于高斯核中心的整数距离;
通过高斯函数计算得到的高斯滤波核不能直接使用,还需要对其进行归一化处理。具体操作是:让卷积核中的 各元素值 / 所有元素总和,令其所有元素的和为1。这样做是为了避免在卷积计算中引入额外的亮度变化或偏差。比如当卷积核元素都为1时,则卷积将会图像的亮度放大九倍。
虽然计算相对比较复杂,但是高斯滤波核中的数值是定死的规则,可以直接写死参与计算。
8.2.4.计算公式优化
首先我们知道:直接基于基本原理计算的话,对于一张长w宽h的图像,就要进行 5 * 5 * w * h 次纹理采样。显而易见得计算效率低。因此,为了降低次数,我们将二维高斯函数表示为两个一维高斯函数的乘积,从而大幅减少计算量
Gx和Gy可以分别代表沿 x轴 和 y轴 的一维高斯函数。只需要将 每个像素分别与 Gx 进行水平卷积计算,与Gy进行垂直卷积计算,然后将最终的结果相乘即可得到与原来一样的结果
这个是原理
通过这两个一维高斯函数得到的卷积核内容,一个水平一个垂直,但内容一样,都是(0.0545, 0.2442, 0.4026, 0.2442, 0.0545)。
然后对一个像素,只采样它的纵横5个(以自己为中心,两边各2个),然后与卷积核相乘后相加,最后的两个结果(水平垂直各一个)再相乘即得该像素模糊后的结果
这样计算的时间复杂度为O(n)
8.2.5.其计算方式的优化(实际上这样并不符合高斯模糊的理论)
8.2.5.1.控制缩放纹理大小
缩放源纹理,源纹理尺寸小了,计算就会更少,也能更模糊。
我们可以在OnRenderImage函数中,获取渲染纹理缓存时,用源纹理尺寸除以 downSample(自定义得一个缩放变量,在方法中直接除),这样在调用Graphics.Blit进行图像复制操作时,就相当于将源纹理缩小了。
在进行复制处理值之前,我们可以将渲染纹理缓存对象的 缩放过滤模式 设置为双线性过滤,几种缩放过滤模式分别为:
Point:点过滤,不进行插值,每个像素都直接从最近的纹理像素中获取颜色
Bilinear:双线性过滤,他在纹理采样时使用响铃四个纹理像素的加权平均值进行插值,以生成更平滑的图像
Trilinear:三线性过滤
8.2.5.2.控制模糊代码执行次数
多进行几次,一次比一次模糊
8.2.5.3.控制纹理采样间隔距离
uv采样时,不以一个单位来当作计算间隔,具体几个可以自定义(也能增加模糊程度)。
比如对某一个像素进行采样,使用 -4,-2,0,2,4来采样,而非-2 ,1,0,1,2。
8.3.补充知识
1.CGINCLUDE:一个预处理指令,它被写在Subshader语句块中,Pass外。其用处是封装共享代码,这些封装起来的代码可以在同一个shader文件的多个pass中使用,也可以在其他shader文件中使用。
同一个SubShader中不需要特别指明,每一个Pass中都有CGINCLUDE中的代码。
2.RendertTexture.GetTemporary方法:其作用是获取一个临时的RenderTexture对象,我们可以利用它来存储中间结果。我们使用它三个参数的重载
RenderTexture.GetTemporary(纹理宽,纹理高,深度缓冲-一般填0即可)
需要注意的是:该方法获取的RenderTexture对象,使用完之后需要用RnederTexture.ReleaseTemporary(对象) 方法来释放该缓存对象
8.4.基本实现
要实现两个Pass,区别在于它们计算的uv偏移不同,一个计算上下,一个计算左右。而且它们共同点很多,如使用的属性相同,使用的内置文件相同,片元的计算规则可以相同等。
而为了让不论是水平还是竖直都可以用同一套规则计算,我们需要对uv数组存储的方式进行一点处理,这时,数组中存储的像素uv偏移分别为:
index 0 1 2 3 4
x或y偏移 0 1 -1 2 -2
//Shader代码
Shader "Models_5/GaussianBlurWithChange"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
//这个直接在x和y那里一乘就行了,frag里的计算不用管
_BlurSpread("BlurSpread", Int) = 1
}
SubShader
{
Tags { }
ZTest Always
Cull Off
ZWrite Off
//不用特别使用,写了已经是使用了
CGINCLUDE
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
sampler2D _MainTex;
//x = 1 / w; y = 1 / h
float4 _MainTex_TexelSize;
int _BlurSpread;
v2f vertBlurHorizontal(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
float2 uv = v.texcoord.xy;
data.uv[0] = uv;
data.uv[1] = uv + float2(_MainTex_TexelSize.x * 1 * _BlurSpread, 0);
data.uv[2] = uv + float2(_MainTex_TexelSize.x * -1 * _BlurSpread, 0);
data.uv[3] = uv + float2(_MainTex_TexelSize.x * 2 * _BlurSpread, 0);
data.uv[4] = uv + float2(_MainTex_TexelSize.x * -2 * _BlurSpread, 0);
return data;
}
v2f vertBlurVertical(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
float2 uv = v.texcoord.xy;
data.uv[0] = uv;
data.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1 * _BlurSpread);
data.uv[2] = uv + float2(0, _MainTex_TexelSize.y * -1 * _BlurSpread);
data.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2 * _BlurSpread);
data.uv[4] = uv + float2(0, _MainTex_TexelSize.y * -2 * _BlurSpread);
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
//此乃卷积核,由于只有三个数,所以没必要全写出来
float weight[3] = {0.4026, 0.2442, 0.0545};
//一个通用的计算方式
float3 sum = tex2D(_MainTex, f.uv[0]).rgb * weight[0];
for (int i = 1; i < 3; i++)
{
//右
sum += tex2D(_MainTex, f.uv[i * 2 - 1]).rgb * weight[i];
//左
sum += tex2D(_MainTex, f.uv[i * 2]).rgb * weight[i];
}
return fixed4(sum, 1);
}
ENDCG
Pass
{
Name "HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment frag
ENDCG
}
Pass
{
Name "VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment frag
ENDCG
}
}
Fallback off
}
//c#代码
using UnityEngine;
public class Lesson73_GaussianBlurWithChange : Lesson69_Basic
{
[Range(1, 10)]
//这单词叫迭代
public int iteration = 1;
[Range(1, 10)]
public int blurSpread = 1;
[Range(1, 10)]
public int downSample = 1;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (Mat != null)
{
Mat.SetInt("_BlurSpread", blurSpread);
RenderTexture[] tempBuffer = { RenderTexture.GetTemporary(source.width / downSample, source.height / downSample, 0),
RenderTexture.GetTemporary(source.width / downSample, source.height / downSample, 0),};
tempBuffer[0].filterMode = FilterMode.Bilinear;
tempBuffer[1].filterMode = FilterMode.Bilinear;
Graphics.Blit(source, tempBuffer[1]);
for (int i = 0; i < iteration; i++)
{
///另一种写法
//Mat.SetInt("_BlurSpread", 1 + i * blurSpread);
Graphics.Blit(tempBuffer[1], tempBuffer[0], Mat, 0);
Graphics.Blit(tempBuffer[0], tempBuffer[1], Mat, 1);
}
Graphics.Blit(tempBuffer[1], destination);
for (int i = 0; i < 2; i++)
{
RenderTexture.ReleaseTemporary(tempBuffer[i]);
}
}
else
{
Graphics.Blit(source, destination);
}
}
}

9.后处理效果——Bloom
9.1.是什么
是一种使 画面中亮度较高的区域产生一种光晕或发光效果的 图像处理技术。其主要目的是模拟现实世界中强光光源在相机镜头或人眼中造成的散射和反射现象。使画面中较亮的区域扩散到周围的区域,造成一种朦胧的效果
9.2.基本原理
我们会使用到四个Pass:
一个用来提取亮度区域并将其存储到新纹理中
两个用来模糊处理提取出来的纹理,即高斯模糊
一个用来将模糊后的纹理与原图像进行合成
9.2.1.提取
在shader中声明一个亮度阈值变量,亮度低于该值的区域不会被提取,然后用当前的灰度值和阈值进行计算,用灰度值减去阈值,并将其限制在0-1之间。其目的是保留高亮区域的颜色信息,同时衰减低亮区域的颜色。
然后用GetTemporary函数和Blit函数就可将这些颜色信息存储到新纹理中
9.2.2.模糊
直接用高斯模糊的Pass即可;需要使用UsePass(使用时需要将名字全大写),使用它的前提是需要为对应Pass设置一个名字,名字使用Name进行设置:
//Name是个字符串
Pass { Name MyPass }
9.2.3.合成
将模糊后的纹理与源纹理进行合成。所以声明一个纹理属性 _Bloom,在模糊完成之后就可以通过c#代码将对应纹理赋值给_Bloom。
在合成中,将主纹理和模糊纹理使用加法进行颜色叠加即可。因为加法会增加亮度,使得原本高亮的部分变得更加显眼,从而达到Bloom效果
9.3.补充知识
使用RenderTexture写入到Shader的纹理变量时,unity可能会对其进行y轴翻转,我们可以利用Unity提供的预处理宏进行判断,如果该宏被定义,说明当前平台的纹理坐标系的y轴原点在顶部。还可以在该宏中用纹素进行判断,如果纹素的y小于0,为负数,表示需要对y轴进行调整。
一般只在使用RenderTexture时才会考虑这个问题。
//shader中判断y轴翻转的代码
#if UNITY_UV_STARTS_AT_TOP
{
if (_MainTex_TexelSize.y < 0)
{
//如果y为负数,则对其y轴进行调整
data.uv.y = 1 - data.uv.y;
}
}
9.4.实现
//Shader部分
Shader "Models_5/Bloom"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_BrThreshold("BrThreshold", Range(0, 1)) = 1
_Bloom("Bloom", 2D) = "" {}
_BlurSpread("BlurSpread", Int) = 1
}
SubShader
{
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _BrThreshold;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return data;
}
float GetLuminance(fixed4 color)
{
return color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722;
}
fixed4 frag (v2f f) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, f.uv);
float brightness = clamp(GetLuminance(texColor) - _BrThreshold, 0.0, 1.0);
return texColor * brightness;
}
ENDCG
}
UsePass "Models_5/GaussianBlurWithChange/HORIZONTAL"
UsePass "Models_5/GaussianBlurWithChange/VERTICAL"
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
sampler2D _Bloom;
float4 _Bloom_ST;
v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
data.uv.zw = v.texcoord.xy * _Bloom_ST.xy + _Bloom_ST.zw;
#if UNITY_UV_STARTS_AT_TOP
{
if (_MainTex_TexelSize.y < 0)
{
//如果y为负数,则对其y轴进行调整
data.uv.w = 1 - data.uv.w;
}
}
return data;
}
fixed4 frag(v2f f) : SV_TARGET
{
fixed4 texColor = tex2D(_MainTex, f.uv.xy);
fixed4 bloomColor = tex2D(_Bloom, f.uv.zw);
fixed3 finalColor = texColor.rgb + bloomColor.rgb;
return fixed4(finalColor, 1.0);
}
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson74_Bloom : Lesson69_Basic
{
[Range(0, 1)]
public float brThreshold;
[Range(1, 10)]
//这单词叫迭代
public int iteration = 1;
[Range(1, 10)]
public int blurSpread = 1;
[Range(1, 10)]
public int downSample = 1;
protected override void UpdateProperty()
{
Mat.SetFloat("_BrThreshold", brThreshold);
Mat.SetFloat("_BlurSpread", blurSpread);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (Mat != null)
{
UpdateProperty();
int rtW = source.width / downSample;
int rtH = source.height / downSample;
RenderTexture[] buffer = { RenderTexture.GetTemporary(rtW, rtH, 0),
RenderTexture.GetTemporary(rtW, rtH, 0),
RenderTexture.GetTemporary(rtW, rtH, 0),};
//提取
Graphics.Blit(source, buffer[0], Mat, 0);
Graphics.Blit(buffer[0], buffer[1]);
//模糊
for (int i = 0; i < iteration; i++)
{
Graphics.Blit(buffer[1], buffer[2], Mat, 1);
Graphics.Blit(buffer[2], buffer[1], Mat, 2);
}
//合并
Mat.SetTexture("_Bloom", buffer[1]);
Graphics.Blit(source, destination, Mat, 3);
for (int i = 0; i < buffer.Length; i++)
{
RenderTexture.ReleaseTemporary(buffer[i]);
}
}
else
{
Graphics.Blit(source, destination);
}
}
}

10.后处理效果——运动模糊
10.1.是什么
一种 用于模拟真实世界中物体快速移动产生的模糊现象 的图形处理技术。
常用的方式有两种:
1.累计缓存:物体快速移动时存储多帧图像信息,取它们之间的加权平均值作为最后的运动模糊图像
优点:质量高,效果好;缺点:计算量大,存储开销大
2.速度缓存:物体快速移动时存储多帧运动速度信息,利用速度来决定模糊的方向和大小
优点:性能相较于累计缓存好;缺点:效果较差,可能产生重影和伪影
虽然但是,这俩都不用。我们会使用一种基于累计缓存的方式来实现动态模糊效果。与累计缓存不同的是,保存的是之前的渲染结果,并不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果,相当于是基于累计缓存的优化。
10.2.补充知识
在使用Graphics时候,如果目标纹理包含内容,会直接认为目标纹理中的颜色为颜色缓冲区中的颜色,也就是说会将目标纹理和源纹理进行混合。而且它会把混合的结果存储到目标纹理中。
10.3.基本原理
用一个RenderTexture记录上一次渲染的信息,然后每一次用新的屏幕图像信息和上一次的图像信息进行混合渲染,从而产生模糊效果(相当于一张图保留了之前n次的叠加渲染结果)。
我们可以使用Graphics的特性,让图像不断混合,主要思路为:
1.RGB通道由两张图片根据模糊程度决定最终效果
2.A通道根据当前屏幕图像的透明度决定
所以我们要先声明一个模糊程度变量,值越大模糊程度越强。然后使用两个Pass进行混合处理
第一个Pass:让当前屏幕图像 和 上一次的屏幕图像 进行 指定RGB通道 的颜色混合,目的是利用模糊参数控制两张图片和混合效果,值越大上一次屏幕内容保留的越多。
需要使用Blend SrcAlpha OneMinusSrcAlpha以及ColorMask RGB,后者是为了只改颜色缓冲区的RGB通道。而前者则帮助我们实现模糊程度的控制,因为我们会将模糊程度变量作为颜色的透明度传过去
第二个Pass:利用第一个pass处处理后得到的颜色再和 源纹理进行 A 通道的颜色混合,目的是保留源纹理透明度信息。
需要使用Blend One Zero和ColorMask A,后者是为了只改变颜色缓冲区的A通道,前者是为了只保留源颜色的A值
//Shader部分
Shader "Models_5/MotionBlur"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
//这玩意变成0的话,物体就会不动,原因是目前的物体完全由过去的颜色决定
_BlurExtent("BlurExtent", Range(0, 0.9)) = 0
}
SubShader
{
ZTest Always
Cull Off
ZWrite Off
CGINCLUDE
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _BlurExtent;
v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex);
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return data;
}
fixed4 fragRGB (v2f f) : SV_Target
{
return fixed4(tex2D(_MainTex, f.uv).rgb, _BlurExtent);
}
fixed4 fragA (v2f f) : SV_Target
{
return tex2D(_MainTex, f.uv);
}
ENDCG
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass
{
Blend One Zero
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
Fallback off
}
//c#部分
using UnityEngine;
public class Lesson75_MotionBlur : Lesson69_Basic
{
[Range(0f, 0.9f)]
public float blurExtent;
private RenderTexture accumulationTex;
protected override void UpdateProperty()
{
//1 - 的目的是让该值变大的时候,模糊程度也跟着变大
Mat.SetFloat("_BlurExtent", 1 - blurExtent);
}
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (Mat != null)
{
UpdateProperty();
int rtW = source.width, rtH = source.height;
//初始化堆积纹理,宽高检测的原因是:混合的时候,两个纹理的宽高一定要一致
if (accumulationTex == null || accumulationTex.width != rtW || accumulationTex.height != rtH)
{
accumulationTex = new RenderTexture(rtW, rtH, 0);
accumulationTex.hideFlags = HideFlags.HideAndDontSave;
//保证第一次的时候也会有颜色,该值会在后面一直作为颜色缓冲区的颜色
Graphics.Blit(source, accumulationTex);
}
//这里没有直接写入纹理还有一个效果是,accumulationTex还会直接存储这一次的结果,作为下一次的堆叠纹理继续使用
//当然,另一个效果是让这俩混合,混合后的会存到accumulatitonTex里头
Graphics.Blit(source, accumulationTex, Mat);
Graphics.Blit(accumulationTex, destination);
}
else
{
Graphics.Blit(source, destination);
}
}
/// <summary>
/// 脚本失活时,将累计纹理删掉
/// </summary>
private void OnDisable()
{
DestroyImmediate(accumulationTex);
}
}
一个比较明显的缺点是:会在运动过的地方留下“印子”