赞
踩
写在前面
本篇内容实现了在URP下获取深度、法线实现描边的后处理描边之前做的工作,包括讨论描边方案,以及写shader之前的自定义renderFeature和Volume组件的过程。
由于是想复刻《SCHiM》游戏里的画面风格,所以本篇文章的需求很明确,会夹杂一些自己的分析思考,并不是严格意义上的分享某一种描边技术的文章,更多的是个人的记录。
由于URP各个版本更新换代太快了,贴一下项目环境,给后面看到这篇文章的小伙伴提个醒,我的项目环境:
URP12.1.7
Unity2021.3.8f1
之前学习《入门精要》的时候就实现过基于Sobel算子的边缘检测描边效果:【Unity Shader】屏幕后处理2.0:实现Sobel边缘检测,这是一种基于颜色信息进行描边的方法,再来回顾一下效果:
简单总结一下这个实现的效果:
在实现任何效果之前,我们需要明确需求,再提出合理的渲染方案,才是一个正确的思路。
这里再明确一下需求,由于我是有针对性地复刻游戏画面,我希望:
进行场景分析的时候也总结过:
所以上述需求,单纯的Sobel算子边缘检测无法满足需求。
场景中阴影描边自己来,通过shadow值step就行,不赘述。
主要是场景中的那些装饰性的框框怎么实现。想了很久,最后定了一种可行的方案——基于Mask图进行Sobel算子边缘检测描边,然后场景中的物体描边采用深度+法线纹理后处理描边法解决。
原理大概是:场景中色彩不是很复杂,是单色Shading,按理来说纹理是不需要的。这里我们就不传递sRGB的颜色纹理,选择传递储存Mask信息的单通道纹理。
纹理需要在建模阶段,给场景中对应的物件进行特别的绘制,例如地面的斑马线、花坛的小砖块等等,纹理类似这样:
由于我还没开始准备场景中的模型贴图等资产,只能先随便简单画几个框框,看看铺在地面上的效果。
接下来我们进行正常的Sobel算子边缘检测,完全跟之前的实现过程一样,最后也是获得一个edge参数:
接下来
中间还需要把阴影考虑进去,再得到最后的值:
最后的效果(观察地板上的描边):
这样,场景中装饰性的平面上的描边效果,就实现了,并且还不是后处理,而是包含在了基本着色的Pass里。
接下来就是基于深度和法线的描边了,这里就开始了后处理描边的实现。我希望给他写成一个可以在Volume面板看到的一个后处理效果,所以可能步骤相对繁琐,需要脚本和shader之间的参数传递。先来回顾一下Volume组件:
URP下后处理都塞在了一个叫做Global Volume的组件中,我们右键可以创建出来:
挂到场景中后,可以在Volume下Add Override添加一些URP内置的后处理效果:
这些内置的后处理效果,Volume控制脚本都放在了这儿:
打开个Bloom后处理面板跟脚本对着看看:
会发现仅仅是可视化了面板,这个cs脚本再跟相应的RenderFeature想匹配,我们就可以实现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了。
刚接触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了!明天继续!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。