当前位置:   article > 正文

【Unity URP】探讨描边方案 自定义后处理Volume_unity volume

unity volume

写在前面

本篇内容实现了在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算子边缘检测描边,然后场景中的物体描边采用深度+法线纹理后处理描边法解决。

基于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的脚本:

  1. using System;
  2. using UnityEngine;
  3. using UnityEngine.Rendering;
  4. using UnityEngine.Rendering.Universal;
  5. namespace UnityEngine.Rendering.Universal
  6. {
  7. [Serializable,VolumeComponentMenu("My-post-processing/Outline")]
  8. public class OutlineVolume : VolumeComponent, IPostProcessComponent
  9. {
  10. [Tooltip("边缘颜色")]
  11. public ColorParameter OutlineColor = new ColorParameter(Color.white);
  12. [Tooltip("边缘检测大小")]
  13. public ClampedFloatParameter Scale = new ClampedFloatParameter(1f, 0f, 10f);
  14. [Tooltip("深度")]
  15. public ClampedFloatParameter DepthThreshold = new ClampedFloatParameter(0.2f, 0f, 10f);
  16. [Tooltip("法线深度")]
  17. public ClampedFloatParameter NormalThreshold = new ClampedFloatParameter(0.4f, 0f, 1f);
  18. public ClampedFloatParameter DepthNormalThreshold = new ClampedFloatParameter(0.5f, 0f, 1f);
  19. public ClampedFloatParameter DepthNormalThresholdScale = new ClampedFloatParameter(7f, 0f, 10f);
  20. public bool IsActive() => Scale.value > 0;
  21. public bool IsTileCompatible() => false;
  22. }
  23. }

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

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

5 自定义RenderFeature

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

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

  1. using UnityEngine;
  2. using UnityEngine.Rendering;
  3. using UnityEngine.Rendering.Universal;
  4. public class OutlineRenderFeature : ScriptableRendererFeature
  5. {
  6. [System.Serializable]
  7. // 定义3个共有变量
  8. public class Settings
  9. {
  10. //public Shader shader; // 设置后处理shader
  11. public Material material; //后处理Material
  12. public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; // 定义事件位置,放在了官方的后处理之前
  13. }
  14. // 初始化一个刚刚定义的Settings类
  15. public Settings settings = new Settings();
  16. // 初始化Pass
  17. OutlinePass outlinePass;
  18. // 给pass传递变量,并加入渲染管线中
  19. public override void Create()
  20. {
  21. this.name = "OutlinePass"; // 外部显示的名字
  22. this.
  23. outlinePass = new OutlinePass(settings.renderPassEvent, settings.material);
  24. }
  25. public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
  26. {
  27. renderer.EnqueuePass(outlinePass);
  28. }
  29. }
  30. public class OutlinePass : ScriptableRenderPass
  31. {
  32. static readonly string renderTag = "Post Effects"; // 定义渲染Tag
  33. Material tmaterial;
  34. OutlineVolume outlineVolume; // 传递到volume,OutlineVolume是Volume那个类定义的类名
  35. public OutlinePass(RenderPassEvent evt, Material tmaterial)
  36. {
  37. renderPassEvent = evt; // 设置渲染事件位置
  38. //var shader = tshader; // 输入shader信息
  39. var material = tmaterial;
  40. if (material == null)
  41. {
  42. Debug.LogError("没有指定Material");
  43. return;
  44. }
  45. }
  46. // 后处理逻辑和渲染核心函数,相当于build-in 的OnRenderImage()
  47. public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
  48. {
  49. // 判断是否开启后处理
  50. if (!renderingData.cameraData.postProcessEnabled)
  51. {
  52. return;
  53. }
  54. // 渲染设置
  55. var stack = VolumeManager.instance.stack; // 传入volume
  56. outlineVolume = stack.GetComponent<OutlineVolume>(); // 拿到我们的volume
  57. if (outlineVolume == null)
  58. {
  59. Debug.LogError("Volume组件获取失败");
  60. return;
  61. }
  62. var cmd = CommandBufferPool.Get(renderTag); // 设置渲染标签
  63. Render(cmd, ref renderingData); // 设置渲染函数
  64. context.ExecuteCommandBuffer(cmd); // 执行函数
  65. CommandBufferPool.Release(cmd); // 释放
  66. }
  67. void Render(CommandBuffer cmd, ref RenderingData renderingData)
  68. {
  69. RenderTargetIdentifier source = renderingData.cameraData.renderer.cameraColorTarget; // 定义RT
  70. RenderTextureDescriptor inRTDesc = renderingData.cameraData.cameraTargetDescriptor;
  71. inRTDesc.depthBufferBits = 0; // 清除深度
  72. var camera = renderingData.cameraData.camera; // 传入摄像机
  73. Matrix4x4 clipToView = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true).inverse;
  74. tmaterial.SetColor("_Color", outlineVolume.OutlineColor.value); // 获取value 组件的颜色
  75. tmaterial.SetMatrix("_ClipToView", clipToView); // 反向输出到Shader
  76. tmaterial.SetFloat("_Scale", outlineVolume.Scale.value);
  77. tmaterial.SetFloat("_DepthThreshold", outlineVolume.DepthThreshold.value);
  78. tmaterial.SetFloat("_NormalThreshold", outlineVolume.NormalThreshold.value);
  79. tmaterial.SetFloat("_DepthNormalThreshold", outlineVolume.DepthNormalThreshold.value);
  80. tmaterial.SetFloat("_DepthNormalThresholdScale", outlineVolume.DepthNormalThresholdScale.value);
  81. int destination = Shader.PropertyToID("Temp1");
  82. // 获取一张临时RT
  83. cmd.GetTemporaryRT(destination, inRTDesc.width, inRTDesc.height, 0, FilterMode.Bilinear, RenderTextureFormat.DefaultHDR); //申请一个临时图像,并设置相机rt的参数进去
  84. cmd.Blit(source, destination); // 设置后处理
  85. cmd.Blit(destination, source, tmaterial, 0);
  86. }
  87. }

体现在面板上就是:

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

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

然后我就写了个模板:

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

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

参考

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

Unity Outline Shader Tutorial - Roystan

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/101724?site
推荐阅读
相关标签
  

闽ICP备14008679号