在unity默认bulit-in管线中,后处理效果一般可以在在相机上挂一个脚本加上OnRenderImage函数,并使用Graphics.Blit用某个材质对最后相机展现的画面进行处理。
在URP中OnRenderImage不生效了,并且有了一个专门做后处理的Volume。但由于相关代码都写在了一个叫PostProcessPass的脚本中,除非修改源码,否则无法仅通过扩展一个VolumeComponent来实现一个自定义后处理。好在URP提供了一个RendererFeature的功能,我们可以通过这个自行添加一个pass管理我们的自定义Volume。事实上URP也是这么做的。
1.创建自定义VolumeComponent
要注意的是这里要写在面板上显示参数,不能定义int,float这些基础类型,unity做了一层封装FloatParameter,IntParameter之类。要定义限制范围的参数要使用ClampedFloatParameter等而不是FloatRangeParameter,一开始被坑了一下。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
[System.Serializable, VolumeComponentMenu("XPostProcess/ColorAdjustMenstsBasedPosAndDepth")]
public class ColorAdjustMenstsBasedPosAndDepth : VolumeComponent, IPostProcessComponent
{
public BoolParameter enableEffect = new BoolParameter(true);
public FloatParameter radius = new FloatParameter(1000000);
public Vector3Parameter center = new Vector3Parameter(Vector3.zero);
public ClampedFloatParameter intensity = new ClampedFloatParameter(0.5f, 0, 1);
public ClampedFloatParameter backgraoundIntensity = new ClampedFloatParameter(0.5f, 0, 1);
public ColorParameter backgroundColor = new ColorParameter(Color.black);
public Vector3Parameter colorScale = new Vector3Parameter(Vector3.one);
public bool IsActive() => enableEffect == true;
public bool IsTileCompatible() => false;
}
VolumetiComponent定义完后就可以在Volume的面板上看到并添加了。
2.创建自定义RenderFeature
using UnityEngine.Rendering.Universal;
public class XPostProcessRenderFeature : ScriptableRendererFeature
{
XPostProcessRenderPass m_ScriptablePass;
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
var src = renderer.cameraColorTarget;
var dest = RenderTargetHandle.CameraTarget;
m_ScriptablePass.Setup(src, dest, renderingData.cameraData.camera);
renderer.EnqueuePass(m_ScriptablePass);
}
public override void Create()
{
m_ScriptablePass = new XPostProcessRenderPass();
}
}
3.创建自定义RenderPass
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class XPostProcessRenderPass : ScriptableRenderPass
{
RenderTargetHandle m_temporaryColorTexture;
RenderTargetIdentifier source;
RenderTargetHandle destination;
Camera m_camera;
private ColorAdjustMenstsBasedPosAndDepth m_CustomVolume;
private readonly string CUSTOM_PASS_TAG = "Custom Pass";
private readonly MaterialLibrary m_MaterialLibrary = new MaterialLibrary();
public XPostProcessRenderPass()
{
this.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
m_temporaryColorTexture.Init("temporaryColorTexture");
}
public void Setup(RenderTargetIdentifier src, RenderTargetHandle dest, Camera camera)
{
this.source = src;
this.destination = dest;
this.m_camera = camera;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (!renderingData.cameraData.postProcessEnabled) return;
ConfigCamera();
VolumeStack stack = VolumeManager.instance.stack;
m_CustomVolume = stack.GetComponent<ColorAdjustMenstsBasedPosAndDepth>();
// 这里可以继续添加别的后处理效果
CommandBuffer cmd = CommandBufferPool.Get(CUSTOM_PASS_TAG);
Render(cmd, ref renderingData);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
private void Render(CommandBuffer cmd, ref RenderingData renderingData)
{
var frustumCorners = ConfigCamera();
if (m_CustomVolume.IsActive() && m_CustomVolume.active)
{
var colorAdjustmentsBasedPosAndDepth = m_MaterialLibrary.colorAdjustmentsBasedPosAndDepth;
colorAdjustmentsBasedPosAndDepth.SetFloat(ShaderConstants.Intensity, m_CustomVolume.intensity.value);
colorAdjustmentsBasedPosAndDepth.SetFloat(ShaderConstants.BackGroundIntensity, m_CustomVolume.backgraoundIntensity.value);
colorAdjustmentsBasedPosAndDepth.SetVector(ShaderConstants.Center, m_CustomVolume.center.value);
colorAdjustmentsBasedPosAndDepth.SetVector(ShaderConstants.ColorScale, m_CustomVolume.colorScale.value);
colorAdjustmentsBasedPosAndDepth.SetFloat(ShaderConstants.Radius, m_CustomVolume.radius.value);
colorAdjustmentsBasedPosAndDepth.SetColor(ShaderConstants.BarkGroundColor, m_CustomVolume.backgroundColor.value);
colorAdjustmentsBasedPosAndDepth.SetMatrix("_FrustumCornersRay", frustumCorners);
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
opaqueDesc.depthBufferBits = 0;
//不能读写同一个颜色target,创建一个临时的render Target去blit
if (destination == RenderTargetHandle.CameraTarget)
{
cmd.GetTemporaryRT(m_temporaryColorTexture.id, opaqueDesc, FilterMode.Bilinear);
Blit(cmd, source, m_temporaryColorTexture.Identifier(), colorAdjustmentsBasedPosAndDepth);
Blit(cmd, m_temporaryColorTexture.Identifier(), source);
}
else
{
Blit(cmd, source, destination.Identifier(), colorAdjustmentsBasedPosAndDepth);
}
}
}
public override void FrameCleanup(CommandBuffer cmd)
{
if (destination == RenderTargetHandle.CameraTarget)
cmd.ReleaseTemporaryRT(m_temporaryColorTexture.id);
}
private Matrix4x4 ConfigCamera()
{
Matrix4x4 frustumCorners = Matrix4x4.identity;
float fov = m_camera.fieldOfView;
float near = m_camera.nearClipPlane;
float aspect = m_camera.aspect;
Transform transform = m_camera.transform;
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = transform.right * halfHeight * aspect;
Vector3 toTop = transform.up * halfHeight;
Vector3 topLeft = transform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;
Vector3 topRight = transform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;
Vector3 bottomLeft = transform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = transform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
return frustumCorners;
}
}
class MaterialLibrary
{
public readonly Material colorAdjustmentsBasedPosAndDepth;
public MaterialLibrary()
{
colorAdjustmentsBasedPosAndDepth = Load(Shader.Find("XPostProcess/ColorAdjustmentsBasedPosAndDepth"));
}
Material Load(Shader shader)
{
if (shader == null)
{
Debug.LogErrorFormat($"Missing shader. {GetType().DeclaringType.Name} render pass will not execute. Check for missing reference in the renderer resources.");
return null;
}
else if (!shader.isSupported)
{
return null;
}
return CoreUtils.CreateEngineMaterial(shader);
}
internal void Cleanup()
{
CoreUtils.Destroy(colorAdjustmentsBasedPosAndDepth);
}
}
static class ShaderConstants
{
public static readonly int Intensity = Shader.PropertyToID("_Intensity");
public static readonly int BackGroundIntensity = Shader.PropertyToID("_BackGroundIntensity");
public static readonly int Center = Shader.PropertyToID("_Center");
public static readonly int Radius = Shader.PropertyToID("_Radius");
public static readonly int BarkGroundColor = Shader.PropertyToID("_BarkGroundColor");
public static readonly int ColorScale = Shader.PropertyToID("_ColorScale");
}
4.添加RenderFeature
至此就可以给RenderData添加一个自定义feature了,添加完后自定义的Volume效果就可以使用了。如果有超过一个后处理效果需要添加,可以新增自定义Volume并修改RenderPass。
5.小结
目前还有一个问题没有找到原因,就是自定义的Voume添加之后再移除,效果依然在,并没有从VolumeManager.instance.stack中去掉。不知道这是个bug还是我哪儿写的有问题。不过不怎么影响,给Volume加个enable参数就行了,就随他去了。也有办法解决,VolumeComponent就是个存储后处理数据的方式,并不实际处理后处理效果,完全可以自己写个管理器管理自定义Volume的添加删除,就是还得自己再写一下编辑器扩展。