Unity中实现后处理

Unity中实现后处理

一月 29, 2024

Unity中的自定义渲染通道:PixelEffect实现

在Unity中,我们可以通过自定义渲染通道来实现各种特效。今天,我们将介绍一个名为PixelEffect的自定义渲染通道,它可以实现像素化的效果。

首先,我们定义了一个名为PixelEffect的类,该类继承自ScriptableRendererFeature。在这个类中,我们定义了一个名为PixelPass的内部类,该类继承自ScriptableRenderPass。

在PixelPass类中,我们定义了一些用于渲染的变量,包括源纹理、目标纹理、临时颜色纹理、性能分析标签、当前使用的Shader和材质,以及一个表示屏幕每行将被均分为多少个像素块的参数。

我们还定义了一个名为material的属性,该属性用于获取当前使用的材质。如果当前没有使用的材质(即CurMaterialnull),则创建一个新的材质,并将其赋值给CurMaterial

然后,我们定义了一个名为PixelPass的构造函数,该构造函数接受一个字符串参数profilerTag,并将其赋值给成员变量m_ProfilerTag。同时,我们初始化了临时颜色纹理句柄m_TemporaryColorTexture

我们还定义了一个名为Setup的方法,该方法接受两个参数:一个是渲染的源纹理source,另一个是渲染的目标纹理destination。这两个参数分别赋值给成员变量sourcedestination

在PixelEffect类中,我们找到了我们需要的Shader,并创建了一个新的PixelPass实例。我们设置了渲染通道的渲染事件为RenderPassEvent.AfterRenderingTransparents,这意味着这个渲染通道将在渲染透明物体之后执行。

AddRenderPasses方法中,我们获取了渲染器的颜色目标作为源纹理,将摄像机目标作为目标纹理。然后,我们检查了Shader是否存在,如果不存在,我们将打印一个错误信息并返回。

接下来,我们将PixelNumPerRowAutoCalulateRatioRatio这三个参数绑定到了我们的PixelPass实例上。然后,我们调用了Setup方法来设置渲染的源和目标。最后,我们将这个渲染通道添加到了渲染器中。

我们以像素化后处理为例子

像素的代码来自于毛星云的[PixelEffect](https://github.com/QianMo/Awesome-Unity-Shader/tree/master/Volume 11 屏幕像素化特效Shader/PixelEffect)

贴上原模原样的shader代码PixelEffect.shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "浅墨Shader编程/Volume11/PixelEffect"
{
//------------------------------------【属性值】------------------------------------
Properties
{
//主纹理
_MainTex("Texture", 2D) = "white" {}
//封装的变量值
_Params("PixelNumPerRow (X) Ratio (Y)", Vector) = (80, 1, 1, 1.5)
}

//------------------------------------【唯一的子着色器】------------------------------------
SubShader
{
//关闭剔除操作
Cull Off
//关闭深度写入模式
ZWrite Off
//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTest Off)
ZTest Always

//--------------------------------唯一的通道-------------------------------
Pass
{
//===========开启CG着色器语言编写模块===========
CGPROGRAM

//编译指令:告知编译器顶点和片段着色函数的名称
#pragma vertex vert
#pragma fragment frag

//包含头文件
#include "UnityCG.cginc"

//顶点着色器输入结构
struct vertexInput {
float4 vertex : POSITION;//顶点位置
float2 uv : TEXCOORD0;//一级纹理坐标
};

//顶点着色器输出结构
struct vertexOutput {
float4 vertex : SV_POSITION;//像素位置
float2 uv : TEXCOORD0;//一级纹理坐标
};

//--------------------------------【顶点着色函数】-----------------------------
// 输入:顶点输入结构体
// 输出:顶点输出结构体
//---------------------------------------------------------------------------------
//顶点着色函数
vertexOutput vert(vertexInput v) {
//【1】实例化一个输入结构体
vertexOutput o;
//【2】填充此输出结构
//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
o.vertex = UnityObjectToClipPos(v.vertex);
//输入的UV纹理坐标为顶点输出的坐标
o.uv = v.uv;

//【3】返回此输出结构对象
return o;
}

//变量的声明
sampler2D _MainTex;
half4 _Params;

//进行像素化操作的自定义函数PixelateOperation
half4 PixelateOperation(sampler2D tex, half2 uv, half scale, half ratio) {
//【1】计算每个像素块的尺寸
half PixelSize = 1.0 / scale;
//【2】取整计算每个像素块的坐标值,ceil函数,对输入参数向上取整
half coordX = PixelSize * ceil(uv.x / PixelSize);
half coordY = (ratio * PixelSize) * ceil(uv.y / PixelSize / ratio);
//【3】组合坐标值
half2 coord = half2(coordX,coordY);
//【4】返回坐标值
return half4(tex2D(tex, coord).xyzw);
}

//--------------------------------【片段着色函数】-----------------------------
// 输入:顶点输出结构体
// 输出:float4型的像素颜色值
//---------------------------------------------------------------------------------
fixed4 frag(vertexOutput Input) : COLOR
{
//使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值
return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y);
}

//===========结束CG着色器语言编写模块===========
ENDCG
}
}
}

再其源代码上做了urp的适配修改PixelEffect.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using static UnityEditor.ShaderData;

public class PixelEffect : ScriptableRendererFeature {
class PixelPass : ScriptableRenderPass {
private RenderTargetIdentifier source { get; set; }
private RenderTargetHandle destination { get; set; }
RenderTargetHandle m_TemporaryColorTexture;
string m_ProfilerTag;
//-----------------------------变量声明部分---------------------------
#region Variables

//着色器和材质实例
public Shader CurShader;
private Material CurMaterial;

//三个可调节的自定义参数
[Range(1f, 1024f), Tooltip("屏幕每行将被均分为多少个像素块")]
public float PixelNumPerRow = 580.0f;

[Tooltip("自动计算正方形像素所需的长宽比与否")]
public bool AutoCalulateRatio = true;

[Range(0f, 24f), Tooltip("此参数用于自定义长宽比")]
public float Ratio = 1.0f;

#endregion

//-------------------------材质的get&set----------------------------
#region MaterialGetAndSet
Material material {
get {
if (CurMaterial == null) {
CurMaterial = new Material(CurShader);
CurMaterial.hideFlags = HideFlags.HideAndDontSave;
}
return CurMaterial;
}
}
#endregion

public PixelPass(Shader shader) {
//出现在render feature中的名字
m_ProfilerTag = "Pixel Pass";
//初始化一个临时的颜色纹理句柄m_TemporaryColorTexture。Init方法用于设置纹理句柄的名称,这里的名称是_TemporaryColorTexture。
m_TemporaryColorTexture.Init("_TemporaryColorTexture");
//找到对应的Shader中的变量
this.CurShader = shader;
}
//重写的方法,用于设置渲染目标
public void Setup(RenderTargetIdentifier source, RenderTargetHandle destination) {
this.source = source;
this.destination = destination;
}
//重写的方法,用于执行渲染操作
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {

// 创建一个命令缓冲区并将其分配给变量 'cmd'。
CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
// 从上下文中获取相机的颜色目标,并将其复制到临时渲染纹理中。
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
// 临时渲染纹理的颜色格式应与相机的颜色目标相同,因为我们将使用Blit命令来复制颜色。
opaqueDesc.depthBufferBits = 0;

// 不能读写相同的颜色目标,创建一个临时渲染目标进行传输。
cmd.GetTemporaryRT(m_TemporaryColorTexture.id, opaqueDesc, FilterMode.Point);

if (CurShader != null) {
float pixelNumPerRow = PixelNumPerRow;
// 给Shader中的外部变量赋值
material.SetVector("_Params", new Vector2(pixelNumPerRow,
AutoCalulateRatio ? ((float)Camera.main.pixelWidth / (float)Camera.main.pixelHeight) : Ratio));
// 将数据复制到临时纹理
cmd.Blit(source, m_TemporaryColorTexture.Identifier(),material,0);
// 将数据从临时纹理复制到相机的颜色目标中。
cmd.Blit(m_TemporaryColorTexture.Identifier(), source);
}
else {
// 将数据复制到临时纹理
cmd.Blit(source, m_TemporaryColorTexture.Identifier());
cmd.Blit(m_TemporaryColorTexture.Identifier(), source);
}
// 将命令缓冲区添加到渲染管线中。
context.ExecuteCommandBuffer(cmd);
// 释放命令缓冲区以进行重用。
CommandBufferPool.Release(cmd);
}
}



PixelPass m_ScriptablePass;
public Shader CurShader;
//三个可调节的自定义参数
[Range(1f, 1024f), Tooltip("屏幕每行将被均分为多少个像素块")]
public float PixelNumPerRow = 580.0f;

[Tooltip("自动计算正方形像素所需的长宽比与否")]
public bool AutoCalulateRatio = true;

[Range(0f, 24f), Tooltip("此参数用于自定义长宽比")]
public float Ratio = 1.0f;
//重写的方法,用于创建渲染通道
public override void Create() {
//执行一次,如果开了两个窗口就会执行两次
CurShader = Shader.Find("浅墨Shader编程/Volume11/PixelEffect");
m_ScriptablePass = new PixelPass(CurShader);
//设置渲染通道的渲染事件
m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}
//重写的方法,用于添加渲染通道
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
//每一帧执行
var src = renderer.cameraColorTarget;
var dest = RenderTargetHandle.CameraTarget;

if (CurShader == null) {
Debug.LogError("未分配材质。必须为自定义像素化通道分配一个材质。");
return;
}
//绑定参数
m_ScriptablePass.PixelNumPerRow = PixelNumPerRow;
m_ScriptablePass.AutoCalulateRatio = AutoCalulateRatio;
m_ScriptablePass.Ratio = Ratio;
//设置渲染目标
m_ScriptablePass.Setup(src, dest);
//添加渲染通道
renderer.EnqueuePass(m_ScriptablePass);

}

}

注意

记得在相应的管线下添加Renderer Feature及打开摄像头的后处理