写在前面
本篇内容实现了在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就行,不赘述。
主要是场景中的那些装饰性的框框怎么实现。想了很久,最后定了一种可行的方案——基于Mask图进行Sobel算子边缘检测描边,然后场景中的物体描边采用深度+法线纹理后处理描边法解决。
2 基于Mask图的描边
原理大概是:场景中色彩不是很复杂,是单色Shading,按理来说纹理是不需要的。这里我们就不传递sRGB的颜色纹理,选择传递储存Mask信息的单通道纹理。
纹理需要在建模阶段,给场景中对应的物件进行特别的绘制,例如地面的斑马线、花坛的小砖块等等,纹理类似这样:
由于我还没开始准备场景中的模型贴图等资产,只能先随便简单画几个框框,看看铺在地面上的效果。
接下来我们进行正常的Sobel算子边缘检测,完全跟之前的实现过程一样,最后也是获得一个edge参数:
接下来
中间还需要把阴影考虑进去,再得到最后的值:
最后的效果(观察地板上的描边):
这样,场景中装饰性的平面上的描边效果,就实现了,并且还不是后处理,而是包含在了基本着色的Pass里。
接下来就是基于深度和法线的描边了,这里就开始了后处理描边的实现。我希望给他写成一个可以在Volume面板看到的一个后处理效果,所以可能步骤相对繁琐,需要脚本和shader之间的参数传递。先来回顾一下Volume组件:
3 URP下的后处理
URP下后处理都塞在了一个叫做Global Volume的组件中,我们右键可以创建出来:
挂到场景中后,可以在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<OutlineVolume>(); // 拿到我们的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组件 (zhihu.com)
Unity Outline Shader Tutorial - Roystan