基于Mask算子的边缘检测描边效果

基于Mask算子的边缘检测描边效果

写在前面

本篇内容实现了在URP下获取深度、法线实现描边的后处理描边之前做的工作,包括讨论描边方案,以及写shader之前的自定义renderFeature和Volume组件的过程。

由于是想复刻《SCHiM》游戏里的画面风格,所以本篇文章的需求很明确,会夹杂一些自己的分析思考,并不是严格意义上的分享某一种描边技术的文章,更多的是个人的记录。

由于URP各个版本更新换代太快了,贴一下项目环境,给后面看到这篇文章的小伙伴提个醒,我的项目环境:

URP12.1.7

Unity2021.3.8f1

1 明确描边需求 1.1 分析

之前学习《入门精要》的时候就实现过基于Sobel算子的边缘检测描边效果:【Unity Shader】屏幕后处理2.0:实现Sobel边缘检测,这是一种基于颜色信息进行描边的方法,再来回顾一下效果:

简单总结一下这个实现的效果:

在实现任何效果之前,我们需要明确需求,再提出合理的渲染方案,才是一个正确的思路。

这里再明确一下需求,由于我是有针对性地复刻游戏画面,我希望:

进行场景分析的时候也总结过:

所以上述需求,单纯的Sobel算子边缘检测无法满足需求。

1.2 提出实现方案

场景中阴影描边自己来,通过shadow值step就行unity单片描边,不赘述。

主要是场景中的那些装饰性的框框怎么实现。想了很久,最后定了一种可行的方案——基于Mask图进行Sobel算子边缘检测描边,然后场景中的物体描边采用深度+法线纹理后处理描边法解决。

2基于Mask图的描边

原理大概是:场景中色彩不是很复杂游戏动态,是单色Shading,按理来说纹理是不需要的。这里我们就不传递sRGB的颜色纹理,选择传递储存Mask信息的单通道纹理。

纹理需要在建模阶段,给场景中对应的物件进行特别的绘制,例如地面的斑马线、花坛的小砖块等等,纹理类似这样:

由于我还没开始准备场景中的模型贴图等资产,只能先随便简单画几个框框游戏素材,看看铺在地面上的效果。

接下来我们进行正常的Sobel算子边缘检测,完全跟之前的实现过程一样,最后也是获得一个edge参数:

接下来

中间还需要把阴影考虑进去,再得到最后的值:

最后的效果(观察地板上的描边):

这样,场景中装饰性的平面上的描边效果,就实现了,并且还不是后处理,而是包含在了基本着色的Pass里。

接下来就是基于深度和法线的描边了,这里就开始了后处理描边的实现。我希望给他写成一个可以在Volume面板看到的一个后处理效果,所以可能步骤相对繁琐,需要脚本和shader之间的参数传递。先来回顾一下Volume组件:

3 URP下的后处理

URP下后处理都塞在了一个叫做Global Volume的组件中,我们右键可以创建出来:

挂到场景中后unity单片描边,可以在Volume下Add Override添加一些URP内置的后处理效果:

这些内置的后处理效果,Volume控制脚本都放在了这儿:

打开个Bloom后处理面板跟脚本对着看看:

会发现仅仅是可视化了面板,这个cs脚本再跟相应的RenderFeature想匹配,我们就可以实现Volume组件里控制后处理效果了!

4 自定义Volume

我们可以仿照这自定义一个Outline Volume组件,当然,这个Outline组件具体需要什么参数,只有写完shader之后才能明确知道,文章其实也是写完pass之后再回来补充的,所以直接给出Volume的脚本:

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace UnityEngine.Rendering.Universal
{
    [Serializable,VolumeComponentMenu("My-post-processing/Outline")]
    public class OutlineVolume : VolumeComponent, IPostProcessComponent
{
    [Tooltip("边缘颜色")]
    public ColorParameter  OutlineColor = new ColorParameter(Color.white);
    [Tooltip("边缘检测大小")]
    public ClampedFloatParameter Scale = new ClampedFloatParameter(1f, 0f, 10f);
    [Tooltip("深度")]
    public ClampedFloatParameter DepthThreshold = new ClampedFloatParameter(0.2f, 0f, 10f);
    [Tooltip("法线深度")]
    public ClampedFloatParameter NormalThreshold = new ClampedFloatParameter(0.4f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThreshold = new ClampedFloatParameter(0.5f, 0f, 1f);
    public ClampedFloatParameter DepthNormalThresholdScale = new ClampedFloatParameter(7f, 0f, 10f);
    public bool IsActive() => Scale.value > 0;
    public bool IsTileCompatible() => false;
}
}

这样就能在自定义路径下添加组件了。

当然这仅仅是写参数,还需要自定义一个实现方法。我们用RenderFeature来实现,完全把URP内置的实现路径和我们自定义的后处理过程剥离开,下一步就是自定义RenderFeature了。

5 自定义RenderFeature

刚接触URP的时候,一直不想去用RenderFeature,,觉得很麻烦,这次静下心来扒了一下整个过程,感觉还是足以理解的!

学习,我主要参考unityURP管线学习+后处理这篇文章最后的Volume相关的内容,最后的定义过程,参考了URP | 后处理-描边和Unity Outline Shader Tutorial,学习并实现了RenderFeature和Volume面板,完成的话接下来就能安心写主要的shader内容了:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class OutlineRenderFeature : ScriptableRendererFeature
{
    [System.Serializable]
    // 定义3个共有变量
    public class Settings
    {
        //public Shader shader; // 设置后处理shader
        public Material material; //后处理Material
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; // 定义事件位置,放在了官方的后处理之前
    }
    // 初始化一个刚刚定义的Settings类
    public Settings settings = new Settings(); 
    // 初始化Pass
    OutlinePass outlinePass;
    // 给pass传递变量,并加入渲染管线中
    public override void Create()
    {
        this.name = "OutlinePass"; // 外部显示的名字
        this.
        outlinePass = new OutlinePass(settings.renderPassEvent, settings.material);
    }
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(outlinePass);
    }
    
}
public class OutlinePass : ScriptableRenderPass
{
    static readonly string renderTag = "Post Effects"; // 定义渲染Tag
    Material tmaterial;
    OutlineVolume outlineVolume;  // 传递到volume,OutlineVolume是Volume那个类定义的类名
    public OutlinePass(RenderPassEvent evt, Material tmaterial)
    {
        renderPassEvent = evt; // 设置渲染事件位置
        //var shader = tshader;  // 输入shader信息
        var material = tmaterial;
        if (material == null)
        {
            Debug.LogError("没有指定Material");
            return;
        }
    }
    // 后处理逻辑和渲染核心函数,相当于build-in 的OnRenderImage()
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 判断是否开启后处理
        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }
        // 渲染设置
        var stack = VolumeManager.instance.stack;             // 传入volume
        outlineVolume = stack.GetComponent();  // 拿到我们的volume
        if (outlineVolume == null)
        {
            Debug.LogError("Volume组件获取失败");
            return;
        }
        var cmd = CommandBufferPool.Get(renderTag);     // 设置渲染标签
        Render(cmd, ref renderingData);                 // 设置渲染函数
        context.ExecuteCommandBuffer(cmd);              // 执行函数
        CommandBufferPool.Release(cmd);                 // 释放
	}
	void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget;                 // 定义RT
        RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
        inRTDesc.depthBufferBits = 0;                                                                          // 清除深度
        var camera = renderingData.cameraData.camera;                         // 传入摄像机
        Matrix4x4 clipToView = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true).inverse;
        tmaterial.SetColor("_Color", outlineVolume.OutlineColor.value);   // 获取value 组件的颜色
        tmaterial.SetMatrix("_ClipToView", clipToView);   // 反向输出到Shader
        tmaterial.SetFloat("_Scale", outlineVolume.Scale.value);
        tmaterial.SetFloat("_DepthThreshold", outlineVolume.DepthThreshold.value);
        tmaterial.SetFloat("_NormalThreshold", outlineVolume.NormalThreshold.value);
        tmaterial.SetFloat("_DepthNormalThreshold", outlineVolume.DepthNormalThreshold.value);
        tmaterial.SetFloat("_DepthNormalThresholdScale", outlineVolume.DepthNormalThresholdScale.value);
        int destination = Shader.PropertyToID("Temp1");
        // 获取一张临时RT
        cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
        cmd.Blit(source, destination);                            // 设置后处理
        cmd.Blit(destination, source, tmaterial, 0);
    }
}

体现在面板上就是:

关于展示到面板部分的内容,需要给定义的结构体前加上[System.Serializable]。

我发现,如果只是创建一个RenderFeature脚本,跟URP下创建shader一样,函数啥的都缺胳膊少腿的,为什么不像创建URP Shader模板那样,也创建一个带有Pass的RenderFeature脚本模板呢!

然后我就写了个模板:

用的话Asset->Rendering->MyRenderFeature,就能创建自定义的模板啦!

那么下一步,就是写shader了!明天继续!

参考

如何扩展Unity URP的后处理Volume组件 ()

Unity Outline Shader Tutorial - Roystan

文章来源:https://blog.csdn.net/qq_41835314/article/details/130114153