URP屏幕后处理
由于Unity对URP的屏幕后处理需集成要到2019.4版本后才稳定,所以升级Unity版本到2019.4.9
Post Processing
在创建我们自己屏幕后处理系统前,我们需要先学会使用UnityURP管线中集成的屏后处理系统。
低于Unity2019.4版本的URP是没有Volume组件的
-
我们可以将Volume组件添加到GameObject,或者在Hierarchy面板中中右键单击并在Volume标题下选择某些内容。
- Global Volume代表后处理效果应用所有摄像机
- Box Volume会在场景中创建一个盒子区域(该区域在Scene中用BoxCollider组件显示轮廓),只有摄像机进入该区域才会应用后处理效果
- Sphere Volume是一个球形区域,效果同上
- Convex Mesh Volume允许我们使用自定义的网格区域
-
在Volume组件上有如下参数
属性 描述 Mode Global:使Volume无边界并影响场景中的每一个摄像机。 Local:为Volume指定边界,Volume只影响在边界内部的射线机。 Blend Distance URP从Volume Collider开始融合的最远距离。0表示URP立即应用Volume的Override;该属性只有在非Global Volume下才出现 Weight Volume在场景中的影响值 Priority 当Volume在场景中有多个Volume时,URP通过此值决定使用哪一个Volume。URP优先使用priority更高的 Profile Profile Asset存储URP处理Volume的数据 -
点击Profile旁的New按钮,新建一个后处理配置文件,然后我们就可以通过Add Override添加各种URP内置后处理效果了。
有关URP后处理效果清单见https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@8.0/manual/EffectList.html
-
当我们为配置文件添加色差效果Chromatic Aberration,并开启效果后会发现,在Scene视图中已经出现了效果,但在Game视图中却没有任何变化。
接下来我们需要将场景中的摄像机开启屏幕后处理,选中场景中摄像机,在Inspector面板中找到Rendering栏下的Post Processing开关,勾选上。
现在Game中已经出现效果
这里列出URP中可用的后处理效果
- 高光(Bloom)高光效果从图片中的光亮部分的边缘长生羽毛形状的光。并且产生遮挡。Bloom中有一个Lens Dirt特征,这个可以用于产生全屏光斑。
- 通道混合器(Channel Mixer):通道混合器提供每个颜色通道的信息,通过调整不同通道的影响可以实现不同的屏幕效果。
- 色差(Chromatic Aberration):仿真实现了真实摄像机无法将所有颜色融合到一个点。
- 颜色调整(Color Adjustments) 调整整体的色调、亮度和对比度
- 颜色曲线(Color Curves):通过曲线的方式调整色相、饱和度或者亮度。
- 景深(Depth Of Field): 景深组件模拟相机镜头的聚焦特性。现实生活中,相机只能对特定距离清晰对焦。距离摄像机较远和较近的物体无法聚焦。根据模糊效果引入了Bokeh(散景),Bokeh指当图像失焦时出现在明亮区域的视觉伪像。
- 胶片颗粒(Film Grain):Film Grain是加工摄影胶片的随机光学纹理,因为存在卤化银产生的金属银或染料云的小颗粒,其已经接收到了足够的光子。
- 镜头变形(Lens Distortion):扭曲图片模拟真实的镜头
- 提升伽马增益(Lift Gamma Gain):调整图像的lift、Gamma、Gain
- 运动模糊(Motion Blur):模拟真实摄像机在拍摄运动物体时,运动速度大于曝光时间的时候出现的模糊效果。
- 帕尼尼投影(Panini Projection):一种圆柱形投影,保持垂直方向上的垂直,在渲染大视角的时候提供更好的效果。视角120度无后处理。
- 阴影 中间色调 高光(Shadows Midtones Hightlights):使用圆环工具进行三个熟知的调整
- 分割色调(Split Toning):根据亮度值对不同的区域进行着色,帮助我们实现更加独特的视觉效果。可以通过此效果实现阴影和高光部分不同的色调。
- 色调映射(Tonemapping):在屏幕上重新映射HDR的过程
- 玩具相机(Vignette):暗角效果和去饱和,现实中造成这种结果的原因通常是堆叠滤镜,辅助镜片和不当的遮光罩。
- 白平衡(White Balance):应用白平衡组件,消除不真实的偏色,从而使白色在图像中呈现白色。我们也可以用来呈现冷色或者暖色的渲染效果。
扩展Volume
既然我们已经学会了如何使用Volume组件,那么我们接下来所需做的就是如何去扩展该组件,增加我们的屏幕后处理效果。
解读Volume运作
首先我们可以在URP包目录下的com.unity.render-pipelines.universal@7.5.1\Runtime\Overrides
文件夹中找到,包含了所有为Volume配置文件添加的效果的属性脚本。为方便做比较,我们打开目录中的Bloom脚本,可以发现脚本中只有属性,且这些属性名对应了Volume组件中的效果属性。
然后我们可以找到一个文件com.unity.render-pipelines.universal@7.5.1\Runtime\Passes\PostProcessPass.cs
,这个PostProcessPass就是URP内置后处理的核心,它继承了ScriptableRenderPass,方便在URP管线下扩展额外的Pass通道。
所有继承ScriptableRenderPass组件的子类需要实现一个方法:
public abstract void Execute(ScriptableRenderContext context, ref RenderingData renderingData);
其实该方法的作用和我们在Built-in管线下写后处理框架时用到的oid OnRenderImage(RenderTexture src, RenderTexture dest)
方法功能是一样的,在每帧渲染,渲染管线都会调用该方法。然后我们会在该方法里通过Shader的Pass进行后处理。
其中的ScriptableRenderContext 参数我们在前面SRP尝试章节里已经见识过,它是定义自定义渲染管道使用的状态和绘制命令,使用该参数我们可以进行额外的Pass绘制。RenderingData也好理解,就是当前调用该方法的绘制信息,其中包含调用该次绘制的相机信息、光源信息、阴影信息等;注意,该参数带有ref关键字,说明我们是可以直接对这些绘制信息进行修改,并影响后续管线的成像的。
可编程渲染功能
从前面URP源代码可以看出,其实URP的后处理方式和Built-in管线下的后处理方式思路相同,都是通过C#代码去调用Shader进行后处理。但有一点不同,在Built-in管线下,后处理脚本我们是继承Mono的,直接将脚本挂在对象上就可运作(mono自动调用OnRenderImage方法);但是URP下继承的是ScriptableRenderPass,该类是一个对象,我们由如何让他运作呢。
在我们URP管线的Inspector面板下,RendererList中有我们当前真正使用的渲染器列表,
跳转到使用渲染器的Inspector面板后我们可以看到Post ProcessData,Post ProcessData正是URP用于引用各种后处理Shader的对象。
而下方的Rederer Features则是我们的重点,它允许我们添加额外的渲染功能,也就是我们可以写一个功能脚本,在脚本中创建我们自定义的ScriptableRenderPass对象,然后添加到该列表中,后续URP管线在渲染过程中会自动调用该功能,也就是间接调用我们自定义的后处理对象,以此实现URP管线下的自定义后处理。
想要编写额外的渲染功能,我们需要将脚本继承ScriptableRendererFeature,并且实现AddRenderPasses跟Create函数。Create方法会在该功能初始化时调用,我们可以在方法中创建我们的后处理对象。AddRenderPasses将会在相机被设置渲染器时调用。
调整屏幕亮度、饱和度、对比度
既然运作方式解析了,那么进入实操环节。
创建Shader
创建Shader-Brightness Saturation And Contrast,该Shader比较简单,没有什么新的语法。
在不知道该Shader是否正确的情况下,可以创建一个Plane拖入到摄像机视野内(这很重要),再创建该Shader的材质,将材质赋给Palne。最后再给材质添加纹理,调整不同的参数,在Game中看效果是否正确。
Shader "URP/Brightness Saturation And Contrast"
{
Properties
{
// 基础纹理
_MainTex ("Base (RGB)", 2D) = "white" { }
// 亮度
_Brightness ("Brightness", Float) = 1
// 饱和度
_Saturation ("Saturation", Float) = 1
// 对比度
_Contrast ("Contrast", Float) = 1
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half _Brightness;
half _Saturation;
half _Contrast;
CBUFFER_END
ENDHLSL
Pass
{
// 开启深度测试 关闭剔除 关闭深度写入
ZTest Always Cull Off ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 声明纹理
TEXTURE2D(_MainTex);
// 声明采样器
SAMPLER(sampler_MainTex);
struct a2v
{
float4 vertex: POSITION;
float4 texcoord: TEXCOORD0;
};
struct v2f
{
float4 pos: SV_POSITION;
half2 uv: TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
half4 frag(v2f i): SV_Target
{
// 纹理采样
half4 renderTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
// 调整亮度 = 原颜色 * 亮度值
half3 finalColor = renderTex.rgb * _Brightness;
// 调整饱和度
// 亮度值(饱和度为0的颜色) = 每个颜色分量 * 特定系数
half luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
half3 luminanceColor = half3(luminance, luminance, luminance);
// 插值亮度值和原图
finalColor = lerp(luminanceColor, finalColor, _Saturation);
// 调整对比度
// 对比度为0的颜色
half3 avgColor = half3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return half4(finalColor, renderTex.a);
}
ENDHLSL
}
}
Fallback Off
}
创建属性参数脚本
编写类BrightnessSaturationContrast继承VolumeComponent组件和IPostProcessComponent接口,用以继承Volume框架。
-
在方法中我们根据材质所需的三个参数亮度、饱和度、对比度创建三个属性。
public ClampedFloatParameter brightness = new ClampedFloatParameter(0f, 0, 3); public ClampedFloatParameter saturation = new ClampedFloatParameter(0f, 0, 3); public ClampedFloatParameter contrast = new ClampedFloatParameter(0f, 0, 3);
创建可编程渲染Pass脚本
创建C#脚本AdditionPostProcessPass,继承自ScriptableRenderPass,并实现Execute方法。
-
创建所需属性
//标签名,用于续帧调试器中显示缓冲区名称 const string CommandBufferTag = "AdditionalPostProcessing Pass"; // 用于后处理的材质 public Material m_Material; // 属性参数组件 BrightnessSaturationContrast m_BrightnessSaturationContrast; // 颜色渲染标识符 RenderTargetIdentifier m_ColorAttachment; // 临时的渲染目标 RenderTargetHandle m_TemporaryColorTexture01;
-
创一个入口函数,用于后续渲染管线功能脚本写入参数
// 设置渲染参数 public void Setup(RenderTargetIdentifier _ColorAttachment, Material Material) { this.m_ColorAttachment = _ColorAttachment; m_Material = Material; }
-
修改Execute方法,在方法中我们通过
VolumeManager.instance.stack
单例获取Volume框架中所有的堆栈,在从堆栈中获取我们上一部创建的属性参数组件,由于Execute是每帧调用,所有该组件也是实时更新的。然后我们使用标签名获取一个命令缓冲区,将该命令缓冲区与Execute的参数RenderingData渲染信息一起交给Render方法进行后处理。
在后处理完成后我们调用
context.ExecuteCommandBuffer(cmd);
方法执行该命令缓冲区,最后释放内存。public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // 从Volume框架中获取所有堆栈 var stack = VolumeManager.instance.stack; // 从堆栈中查找对应的属性参数组件 m_BrightnessSaturationContrast = stack.GetComponent<BrightnessSaturationContrast>(); // 从命令缓冲区池中获取一个带标签的命令缓冲区,该标签名可以在后续帧调试器中见到 var cmd = CommandBufferPool.Get(CommandBufferTag); // 调用渲染函数 Render(cmd, ref renderingData); // 执行命令缓冲区 context.ExecuteCommandBuffer(cmd); // 释放命令缓存 CommandBufferPool.Release(cmd); // 释放临时RT cmd.ReleaseTemporaryRT(m_TemporaryColorTexture01.id); }
-
接下来我们编写渲染方法Render,在Render方法中我们获取属性参数组件中的参数,赋值给材质。
然后通过RenderingData对象中的相机信息创建一个临时缓冲区,然后将颜色渲染器中的颜色通过Shader进行计算后保存到临时缓冲区中。
最后在从临时缓冲区中读取出来返还到主纹理中。
// 渲染 void Render(CommandBuffer cmd, ref RenderingData renderingData) { // VolumeComponent是否开启,且非Scene视图摄像机 if (m_BrightnessSaturationContrast.IsActive() && !renderingData.cameraData.isSceneViewCamera) { // 写入参数 m_Material.SetFloat("_Brightness", m_BrightnessSaturationContrast.brightness.value); m_Material.SetFloat("_Saturation", m_BrightnessSaturationContrast.saturation.value); m_Material.SetFloat("_Contrast", m_BrightnessSaturationContrast.contrast.value); // 获取目标相机的描述信息 RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; // 设置深度缓冲区 opaqueDesc.depthBufferBits = 0; // 通过目标相机的渲染信息创建临时缓冲区 cmd.GetTemporaryRT(m_TemporaryColorTexture01.id, opaqueDesc); // 通过材质,将计算结果存入临时缓冲区 cmd.Blit(m_ColorAttachment, m_TemporaryColorTexture01.Identifier(), m_Material); // 再从临时缓冲区存入主纹理 cmd.Blit(m_TemporaryColorTexture01.Identifier(), m_ColorAttachment); }
创建可编程渲染功能脚本
创建类AdditionPostProcessRendererFeature,继承ScriptableRendererFeature抽象类,并实现AddRenderPasses跟Create方法
-
设置三个参数,公开属性Shader、私有属性AdditionPostProcessPass对象和材质
// 用于后处理的Shader public Shader shader; // 后处理Pass AdditionPostProcessPass postPass; // 根据Shader生成的材质 Material _Material=null;
修改Create方法,在方法中创建AdditionPostProcessPass对象,并修改该类的渲染时机,在透明物体渲染后。
-
修改AddRenderPasses方法,在方法中通过Shader创建材质,然后从ScriptableRenderer参数中获取主纹理;接下来将纹理和材质传入AdditionPostProcessPass对象中,最后将该对象添加到渲染管线队列中。
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // 检测Shader是否存在 if (shader == null) return; // 创建材质 if (_Material==null) _Material = CoreUtils.CreateEngineMaterial(shader); // 获取当前渲染相机的目标颜色,也就是主纹理 var cameraColorTarget = renderer.cameraColorTarget; // 设置调用后处理Pass postPass.Setup(cameraColorTarget, _Material); // 添加该Pass到渲染管线中 renderer.EnqueuePass(postPass); }
配置管线
-
配置Volume组件,将BrightnessSaturationContrast属性参数组件添加到效果列表中
-
配置管线,在URP管线的配置文件中添加我们编写的渲染功能AdditionPostProcessRendererFeature
-
赋值Shader
-
修改属性参数组件的参数,以降低饱和度
完整代码
Shader——Brightness Saturation And Contrast
Shader "URP/Brightness Saturation And Contrast"
{
Properties
{
// 基础纹理
_MainTex ("Base (RGB)", 2D) = "white" { }
// 亮度
_Brightness ("Brightness", Float) = 1
// 饱和度
_Saturation ("Saturation", Float) = 1
// 对比度
_Contrast ("Contrast", Float) = 1
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half _Brightness;
half _Saturation;
half _Contrast;
CBUFFER_END
ENDHLSL
Pass
{
// 开启深度测试 关闭剔除 关闭深度写入
ZTest Always Cull Off ZWrite Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 声明纹理
TEXTURE2D(_MainTex);
// 声明采样器
SAMPLER(sampler_MainTex);
struct a2v
{
float4 vertex: POSITION;
float4 texcoord: TEXCOORD0;
};
struct v2f
{
float4 pos: SV_POSITION;
half2 uv: TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
half4 frag(v2f i): SV_Target
{
// 纹理采样
half4 renderTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
// 调整亮度 = 原颜色 * 亮度值
half3 finalColor = renderTex.rgb * _Brightness;
// 调整饱和度
// 亮度值(饱和度为0的颜色) = 每个颜色分量 * 特定系数
half luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
half3 luminanceColor = half3(luminance, luminance, luminance);
// 插值亮度值和原图
finalColor = lerp(luminanceColor, finalColor, _Saturation);
// 调整对比度
// 对比度为0的颜色
half3 avgColor = half3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return half4(finalColor, renderTex.a);
}
ENDHLSL
}
}
Fallback Off
}
BrightnessSaturationContrast
using System;
// 通用渲染管线程序集
namespace UnityEngine.Rendering.Universal
{
// 实例化类 添加到Volume组件菜单中
[Serializable, VolumeComponentMenu("Addition-Post-processing/BrightnessSaturationContrast")]
// 继承VolumeComponent组件和IPostProcessComponent接口,用以继承Volume框架
public class BrightnessSaturationContrast : VolumeComponent, IPostProcessComponent
{
// 在框架下的属性与Unity常规属性不一样,例如 Int 由 ClampedIntParameter 取代。
public ClampedFloatParameter brightness = new ClampedFloatParameter(0f, 0, 3);
public ClampedFloatParameter saturation = new ClampedFloatParameter(0f, 0, 3);
public ClampedFloatParameter contrast = new ClampedFloatParameter(0f, 0, 3);
// 实现接口
public bool IsActive()
{
return active;
}
public bool IsTileCompatible()
{
return false;
}
}
}
AdditionPostProcessPass
namespace UnityEngine.Rendering.Universal
{
/// <summary>
/// 附加的后处理Pass
/// </summary>
public class AdditionPostProcessPass : ScriptableRenderPass
{
//标签名,用于续帧调试器中显示缓冲区名称
const string CommandBufferTag = "AdditionalPostProcessing Pass";
// 用于后处理的材质
public Material m_Material;
// 属性参数组件
BrightnessSaturationContrast m_BrightnessSaturationContrast;
// 颜色渲染标识符
RenderTargetIdentifier m_ColorAttachment;
// 临时的渲染目标
RenderTargetHandle m_TemporaryColorTexture01;
public AdditionPostProcessPass()
{
m_TemporaryColorTexture01.Init("_TemporaryColorTexture1");
}
// 设置渲染参数
public void Setup(RenderTargetIdentifier _ColorAttachment, Material Material)
{
this.m_ColorAttachment = _ColorAttachment;
m_Material = Material;
}
/// <summary>
/// URP会自动调用该执行方法
/// </summary>
/// <param name="context"></param>
/// <param name="renderingData"></param>
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// 从Volume框架中获取所有堆栈
var stack = VolumeManager.instance.stack;
// 从堆栈中查找对应的属性参数组件
m_BrightnessSaturationContrast = stack.GetComponent<BrightnessSaturationContrast>();
// 从命令缓冲区池中获取一个带标签的渲染命令,该标签名可以在后续帧调试器中见到
var cmd = CommandBufferPool.Get(CommandBufferTag);
// 调用渲染函数
Render(cmd, ref renderingData);
// 执行命令缓冲区
context.ExecuteCommandBuffer(cmd);
// 释放命令缓存
CommandBufferPool.Release(cmd);
// 释放临时RT
cmd.ReleaseTemporaryRT(m_TemporaryColorTexture01.id);
}
// 渲染
void Render(CommandBuffer cmd, ref RenderingData renderingData)
{
// VolumeComponent是否开启,且非Scene视图摄像机
if (m_BrightnessSaturationContrast.IsActive() && !renderingData.cameraData.isSceneViewCamera)
{
// 写入参数
m_Material.SetFloat("_Brightness", m_BrightnessSaturationContrast.brightness.value);
m_Material.SetFloat("_Saturation", m_BrightnessSaturationContrast.saturation.value);
m_Material.SetFloat("_Contrast", m_BrightnessSaturationContrast.contrast.value);
// 获取目标相机的描述信息
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
// 设置深度缓冲区
opaqueDesc.depthBufferBits = 0;
// 通过目标相机的渲染信息创建临时缓冲区
cmd.GetTemporaryRT(m_TemporaryColorTexture01.id, opaqueDesc);
// 通过材质,将计算结果存入临时缓冲区
cmd.Blit(m_ColorAttachment, m_TemporaryColorTexture01.Identifier(), m_Material);
// 再从临时缓冲区存入主纹理
cmd.Blit(m_TemporaryColorTexture01.Identifier(), m_ColorAttachment);
}
}
}
}
AdditionPostProcessRendererFeature
namespace UnityEngine.Rendering.Universal
{
/// <summary>
/// 可编程渲染功能
/// 必须要继承ScriptableRendererFeature抽象类,
/// 并且实现AddRenderPasses跟Create函数
/// </summary>
public class AdditionPostProcessRendererFeature : ScriptableRendererFeature
{
// 用于后处理的Shader
public Shader shader;
// 后处理Pass
AdditionPostProcessPass postPass;
// 根据Shader生成的材质
Material _Material=null;
//在这里,您可以在渲染器中注入一个或多个渲染通道。
//每个摄像机设置一次渲染器时,将调用此方法。
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
// 检测Shader是否存在
if (shader == null)
return;
// 创建材质
if (_Material==null)
_Material = CoreUtils.CreateEngineMaterial(shader);
// 获取当前渲染相机的目标颜色,也就是主纹理
var cameraColorTarget = renderer.cameraColorTarget;
// 设置调用后处理Pass
postPass.Setup(cameraColorTarget, _Material);
// 添加该Pass到渲染管线中
renderer.EnqueuePass(postPass);
}
// 对象初始化时会调用该函数
public override void Create()
{
postPass = new AdditionPostProcessPass();
// 渲染时机 = 透明物体渲染后
postPass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}
}
}