Unity3D URP 仿蜘蛛侠风格化Bloom&AO

Unity3D URP 仿蜘蛛侠风格化Bloom&AO

本篇文章介绍在URP中如何进行风格化后处理,使用Renderer Feather 和自定义 Render Pass 实现。这种做法比起使用PostProcessing具有很大的自由度,能够自由控制渲染时机,减少束缚。

本教程使用Unity2022.3.5f1 版本。较低版本Shader Graph 没有Full Screen Sample Buffer。

以下两张图是蜘蛛侠动画剧照,高光Bloom部分很多是点阵的方式表现,一些AO使用混合平行斜线来表现。

类似的卡通渲染方案也被游戏 HiFi Rush 所使用。

Bloom

Bloom效果流程:

Composite Shader Bloom Texture Screen

先制作Bloom效果,将Bloom渲染到一张Texture上,再将这个Texture通过我们的风格化Shader最后渲染到屏幕上

原始Bloom基本参照Unity的做法,在Packages/com.unity.render-pipelines.core/Runtime 文件夹中可找到相关代码

制作控制面板VolumeComponent.CS

  1. 直接复制Unity Bloom需要的控制参数。
  2. 添加我们风格化点阵需要的控制参数。
csharp 复制代码
[VolumeComponentMenuForRenderPipeline("CustomBloomEffect", typeof(UniversalRenderPipeline))]
public class CustomBloomEffectComponent : VolumeComponent, IPostProcessComponent
{
    //bloom settings copy from Unity default Bloom
    [Header("Bloom Settings")]
    public FloatParameter threshold = new FloatParameter(0.9f,true);
    public FloatParameter intensity = new FloatParameter(1,true);
    public ClampedFloatParameter scatter = new ClampedFloatParameter(0.7f,0,1,true);
    public IntParameter clamp = new IntParameter(65472,true);
    public ClampedIntParameter maxIterations = new ClampedIntParameter(6,0,10);
    public NoInterpColorParameter tint = new NoInterpColorParameter(Color.white);
	
	//Custom Bloom Dots
    [Header("Dots")] 
    public IntParameter dotsDensity = new IntParameter(10,true);
    public ClampedFloatParameter dotsCutoff = new ClampedFloatParameter(0.4f,0,1, true);
    public Vector2Parameter scrollDirection = new Vector2Parameter(new Vector2());
    
    [Header("AOLines")]
    public ClampedFloatParameter linesWidth = new ClampedFloatParameter(0.001f,0.001f,0.01f, true);
    public ClampedFloatParameter linesIntensity = new ClampedFloatParameter(0.05f,0,0.05f, true);
    public ColorParameter linesColor = new ColorParameter(Color.black, true, true, true);
    public FloatParameter linesAngle = new FloatParameter(30f, true);
    
    public bool IsActive()
    {
        return true;
    }

    public bool IsTileCompatible()
    {
        return false;
    }
}
  1. 将这个脚本挂载到场景中,我们就得到了一个和Unity原生很相识的一个控制面板,并且有新增的Dots控制功能:

Custom Renderer Feather

参照Unity自带的Renderer Feather 我们可仿写一个我们自己的Renderer Feather

Unity自带Renderer Feather 目录:

Custom Renderer Pass

先创建一个简单的自定义Pass,这是渲染Pass,在FrameDebugger中这些根节点都是一个Pass,如图:

最简代码如下:

csharp 复制代码
[System.Serializable]
public class CustomPostProcessPass : ScriptableRenderPass{
	public override void Execute(ScriptableRenderContext context,ref RenderingData renderingData)
	}
	
	}
}

然后我们再创建一个Custom Renderer Feather

代码:

csharp 复制代码
[System.Serializable]
public class CustomPostProcessRendererFeature : ScriptableRendererFeature{
	private CustomPostProcessPass m_customPass;
	public override void AddRenderPasses(ScriptableRenderer renderer,ref RenderingData renderingData){
		renderer.EnqueuePass(m_customPass);
	}
	public override void Create(
		m_customPass = new CustomPostProcessPass()
	}
}

有了这两个后,我们就能在Renderer Data 面板中添加这个新Feather了

Bloom Shader

这个为了方便直接复制Unity自带的Bloom。地址:Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Bloom.shader

Composite Shader

  1. 使用Shader Graph 制作用于风格化Bloom后的Texture。创建一个FullscreenShaderGraph(Unity 2022以上)
  2. 创建SampleTexture2D 节点,并且修改名称,注意Reference名称,我们需要通过这个名称向shader传入bloom texture
  3. 使用Voronoi Node输 设置AngleOffset为0, 使用Screen Position 作为UV 得到一组排列整齐的圆点格子,创建Density属性,用于控制格子密度(大小)
  4. 再通过一个Comparision Node 这样得到1,0分明的圆点,并创建Cutoff属性进行圆点占据格子比例大小控制
  5. 使用URP Sample Buffer(这个就是当前屏幕渲染图像Screen Texture) 和点阵相加。
  6. 完整的shader graph:

完善Custom Feather

这个主要参考Unity URP的Bloom PostProcession写法。

CustomPostProcessRenderFeature 完整代码:

csharp 复制代码
public class CustomPostProcessRenderFeature : ScriptableRendererFeature
{
    [SerializeField]
    private Shader m_bloomShader;
    [SerializeField]
    private Shader m_compositeShader;
    
    private Material m_bloomMaterial;
    private Material m_compositeMaterial;
    
    private CustomPostProcessPass m_customPass;
    
    public override void Create()
    {
        m_bloomMaterial = CoreUtils.CreateEngineMaterial(m_bloomShader);
        m_compositeMaterial = CoreUtils.CreateEngineMaterial(m_compositeShader);
        m_customPass = new CustomPostProcessPass(m_bloomMaterial, m_compositeMaterial);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        
        renderer.EnqueuePass(m_customPass);
    }
    
    public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
    {
        if (renderingData.cameraData.cameraType == CameraType.Game)
        {
            m_customPass.ConfigureInput(ScriptableRenderPassInput.Depth);
            m_customPass.ConfigureInput(ScriptableRenderPassInput.Color);
            m_customPass.SetTarget(renderer.cameraColorTargetHandle, renderer.cameraDepthTargetHandle);
        }
    }
    
    protected override void Dispose(bool disposing)
    {
        CoreUtils.Destroy(m_bloomMaterial);
        CoreUtils.Destroy(m_compositeMaterial);
    }
}

CustomPostProcessingPass 完整代码:

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.Universal.Internal;

public class CustomPostProcessPass : ScriptableRenderPass
{
    private Material m_bloomMaterial;
    private Material m_compositeMaterial;

    //RTHandles 是一种特殊RenderTexture,它可以在运行时动态调整大小,而不是在编辑器中预先分配固定大小的RenderTexture。
    private RTHandle m_CameraColorTarget;
    private RTHandle m_CameraDepthTarget;
    
    const int k_MaxPyramidSize = 16;
    private int[] _BloomMipUp;
    private int[] _BloomMipDown;
    private RTHandle[] m_BloomMipUp;
    private RTHandle[] m_BloomMipDown;
    private GraphicsFormat hdrFormat;
    private CustomBloomEffectComponent m_BloomEffect;
    private RenderTextureDescriptor m_Descriptor;
    private static readonly int ScreenSpaceOcclusionTexture = Shader.PropertyToID("_ScreenSpaceOcclusionTexture");


    public void SetTarget(RTHandle cameraColorTarget, RTHandle cameraDepthTarget)
    {
        this.m_CameraColorTarget = cameraColorTarget;
        this.m_CameraDepthTarget = cameraDepthTarget;
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        m_Descriptor = renderingData.cameraData.cameraTargetDescriptor;
    }

    public CustomPostProcessPass(Material bloomMaterial, Material compositeMaterial)
    {
        this.m_bloomMaterial = bloomMaterial;
        this.m_compositeMaterial = compositeMaterial;
        
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;

        _BloomMipUp = new int[k_MaxPyramidSize];
        _BloomMipDown = new int[k_MaxPyramidSize];
        m_BloomMipUp = new RTHandle[k_MaxPyramidSize];
        m_BloomMipDown = new RTHandle[k_MaxPyramidSize];

        for (int i = 0; i < k_MaxPyramidSize; i++)
        {
            _BloomMipUp[i] = Shader.PropertyToID("_BloomMipUp" + i);
            _BloomMipDown[i] = Shader.PropertyToID("_BloomMipDown" + i);
            
            m_BloomMipUp[i] = RTHandles.Alloc(_BloomMipUp[i], name: "_BloomMipUp" + i);
            m_BloomMipDown[i] = RTHandles.Alloc(_BloomMipDown[i], name: "_BloomMipDown" + i);
        }

        const FormatUsage usage = FormatUsage.Linear | FormatUsage.Render;
        if (SystemInfo.IsFormatSupported(GraphicsFormat.B10G11R11_UFloatPack32, usage)) //判断是否支持HDR格式
        {
            hdrFormat = GraphicsFormat.B10G11R11_UFloatPack32;
        }
        else
        {
            hdrFormat = QualitySettings.activeColorSpace == ColorSpace.Linear ? GraphicsFormat.R8G8B8_SRGB : GraphicsFormat.R8G8B8_UNorm;
        }
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        VolumeStack stack = VolumeManager.instance.stack;
        m_BloomEffect = stack.GetComponent<CustomBloomEffectComponent>();
        
        CommandBuffer cmd = CommandBufferPool.Get();

        //使用ProfilingScope 才能在FrameDebugger中看到
        using (new ProfilingScope(cmd, new ProfilingSampler("Custom Post Process Effect")))
        {
            Texture ssaoTex = Shader.GetGlobalTexture(ScreenSpaceOcclusionTexture);
            m_compositeMaterial.SetTexture("_SSAOTexture", ssaoTex);
            //Shader.SetGlobalTexture("_SSAOTexture", ssaoTex);
            
            SetupBloom(cmd, m_CameraColorTarget);
            
            m_compositeMaterial.SetFloat("_Cutoff", m_BloomEffect.dotsCutoff.value);
            m_compositeMaterial.SetFloat("_Density", m_BloomEffect.dotsDensity.value);
            m_compositeMaterial.SetVector("_Direction", m_BloomEffect.scrollDirection.value);
            m_compositeMaterial.SetFloat("_LineWidth", m_BloomEffect.linesWidth.value);
            m_compositeMaterial.SetFloat("_LineIntensity", m_BloomEffect.linesIntensity.value);
            m_compositeMaterial.SetColor("_LineColor", m_BloomEffect.linesColor.value);
            m_compositeMaterial.SetFloat("_LineAngle", m_BloomEffect.linesAngle.value);
            
            Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_compositeMaterial, 0);
        }
        
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();
        CommandBufferPool.Release(cmd);
    }

    private void SetupBloom(CommandBuffer cmd, RTHandle source)
    {
        // 初始大小减半 降采样
        int downres = 1;
        int tw = m_Descriptor.width >> downres;
        int th = m_Descriptor.height >> downres;
        
        //Determine the iteration count based on the size of the pyramid
        int maxSize = Mathf.Max(tw, th);
        int iterations = Mathf.FloorToInt(Mathf.Log(maxSize,2f) - 1);
        int mipCount = Mathf.Clamp(iterations,1, m_BloomEffect.maxIterations.value);
        
        // Pre-filtering parameters
        float clamp = m_BloomEffect.clamp.value;
        float threshold = Mathf.GammaToLinearSpace(m_BloomEffect.threshold.value);
        float thresholdKnee = threshold * 0.5f;// Hardcoded soft knee
        
        // Material setup
        float scatter = Mathf.Lerp(0.05f,0.95f,m_BloomEffect.scatter.value);
        var bloomMaterial = m_bloomMaterial;
        bloomMaterial.SetVector("_Params",new Vector4(scatter,clamp,threshold,thresholdKnee));
        
        //Prefilter
        var desc = GetCompatibleDescriptor(tw,th,hdrFormat);
        for (int i = 0; i < mipCount; i++)
        {
            RenderingUtils.ReAllocateIfNeeded(ref m_BloomMipUp[i],desc,FilterMode.Bilinear,TextureWrapMode.Clamp,name: m_BloomMipUp[i].name);
            RenderingUtils.ReAllocateIfNeeded(ref m_BloomMipDown[i], desc, FilterMode.Bilinear, TextureWrapMode.Clamp,
                name: m_BloomMipDown[i].name);
            desc.width = Mathf.Max(1, desc.width >> 1);
            desc.height = Mathf.Max(i, desc.height >> i);
        }
        
        Blitter.BlitCameraTexture(cmd, source, m_BloomMipDown[0], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 0);
        
        //Downsample
        var lastdown = m_BloomMipDown[0];
        for (int i = 0; i < mipCount - 1; i++)
        {
            //第一个Pass是 2x 降采样 + 9tap高斯模糊
            //第二个Pass是 使用5tap过滤的9tap高斯模糊 + 双线性过滤 
            Blitter.BlitCameraTexture(cmd, lastdown, m_BloomMipUp[i], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 1);
            Blitter.BlitCameraTexture(cmd, m_BloomMipUp[i], m_BloomMipDown[i], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 2);

            lastdown = m_BloomMipDown[i];
        }

        // Upsample
        for (int i = mipCount - 2; i >= 0; i--)
        {
            
            var lowMip = (i == mipCount - 2)? m_BloomMipDown[i + 1] : m_BloomMipUp[i + 1];
            var highMip = m_BloomMipDown[i];
            var dst = m_BloomMipUp[i];
            
            cmd.SetGlobalTexture("_SourceTexLowMip", lowMip);

            Blitter.BlitCameraTexture(cmd, highMip, dst, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 3);
        }
        
        m_compositeMaterial.SetTexture("_Bloom_Texture", m_BloomMipUp[0]);
        m_compositeMaterial.SetFloat("_BloomIntensity", m_BloomEffect.intensity.value);
        cmd.SetGlobalFloat("_BloomIntensity", m_BloomEffect.intensity.value);
    }

    private RenderTextureDescriptor GetCompatibleDescriptor()
    {
        return GetCompatibleDescriptor(m_Descriptor.width, m_Descriptor.height, m_Descriptor.graphicsFormat);
    }

    private RenderTextureDescriptor GetCompatibleDescriptor(int tw, int th, GraphicsFormat graphicsFormat, DepthBits depthBufferBits = DepthBits.None)
    {
        return GetCompatibleDescriptor(m_Descriptor, tw, th, graphicsFormat, depthBufferBits);
    }
    
    internal static RenderTextureDescriptor GetCompatibleDescriptor(RenderTextureDescriptor desc, int tw, int th, GraphicsFormat format, DepthBits depthBufferBits = DepthBits.None)
    {
        desc.depthBufferBits = (int)depthBufferBits;
        desc.width = tw;
        desc.height = th;
        desc.graphicsFormat = format;
        desc.msaaSamples = 1;
        return desc;
    }
    
}

在Renderer Data上吧Bloom Shader 和Composite Shader 拖拽进去

成功运行就能看到结果了:

风格化AO

要点: 添加Screen Space Ambient Occlusion 然后通过_ScreenSpaceOcclusionTexture 获取AO贴图,再把AO贴图作为Mask,在Mask内画斜线。

csharp 复制代码
private static readonly int ScreenSpaceOcclusionTexture = Shader.PropertyToID("_ScreenSpaceOcclusionTexture");

可以在Composite Shader一起处理

一种效果如下:

总结

在实际游戏中,这些效果处理要更加复杂一些,这里给大家开个头,发挥想象把效果做得更出彩吧~

相关推荐
小春熙子1 小时前
Unity图形学之Shader结构
unity·游戏引擎·技术美术
Sitarrrr4 小时前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧4 小时前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
逐·風12 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i13 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
Leoysq1 天前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
_oP_i1 天前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质
Padid2 天前
Unity SRP学习笔记(二)
笔记·学习·unity·游戏引擎·图形渲染·着色器
Tp_jh2 天前
推荐一款非常好用的C/C++在线编译器
linux·c语言·c++·ide·单片机·unity·云原生
dangoxiba2 天前
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule
游戏·unity·c#·游戏引擎·playmaker