1/3
2/3
3/3
[笔记]URP-RenderFeature实践
艺术菌毯
2022年04月09日 17:06
收录于文集
共9篇
  • 前言

        为了更加熟练的掌握这个功能,我们来做一个描边的效果吧。

        方便其他人,也方便自己查询。

  • 描边效果

        看了下这几篇文章,大致理解了描边行为的实现思路

        https://zhuanlan.zhihu.com/p/396965255

        https://zhuanlan.zhihu.com/p/64138757

        网页链接​

        我们先来捋一遍大致的实现思路和需要前置了解的知识:

        首先,我们需要做的就是用RenderFeature在渲染流程的某个时机插入一次渲染命令,即ScriptableRenderContext.DrawRenderers;

        之前可能是不透明-天空盒-透明物体-后处理的绘制顺序。

        现在则可能是不透明-描个边-天空盒-透明物体-后处理这样的绘制顺序。

        当然,这个渲染命令需要需要一定的约束条件,规定在什么时机渲染,渲染什么内容,那么约束条件则是FilteringSettings,FilteringSettings中参数的意义建议去 Unity手册查看一下:https://docs.unity3d.com/2020.2/Documentation/ScriptReference/Rendering.FilteringSettings.html

        总而言之,我们通过上面的命令可以得知,我们在这个渲染通道中需要渲染哪些物体。

代码块
JavaScript
自动换行
复制代码
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref m_FilteringSettings);
复制成功

        当然,我们还需要知道,用哪个Shader来渲染,用Shader的第几个Pass来渲染,因为,一个ScriptablePass只能调用一个Pass进行渲染。

代码块
JavaScript
自动换行
复制代码
            var drawingSettings = CreateDrawingSettings(m_ShaderTagIdList, ref renderingData, sortingCriteria);
            drawingSettings.overrideMaterial = overrideMaterial;
            drawingSettings.overrideMaterialPassIndex = overrideMaterialPassIndex;
复制成功

        上文中的sortingCriteria代表的是物体的渲染顺序,比如按照深度值从前到后,还是从后到前。

代码块
JavaScript
自动换行
复制代码
SortingCriteria sortingCriteria = (m_renderQueueType == RenderQueueType.Transparent)? SortingCriteria.CommonTransparent: renderingData.cameraData.defaultOpaqueSortFlags;
复制成功

        此外,我们的Shader实现描边效果需要两个Pass,第一个Pass是模板测试,改变物体的模板值,第二个Pass则是用模板值过滤,然后让顶点沿着法线扩张并渲染纯色,就得到了描边效果。

        直接看代码吧。

        Shader源码如下:

代码块
C++
自动换行
复制代码
Shader "URP/URPOutlineStencil"
{
	Properties{
		_MainTex("MainTex",2D) = "white"{}
		_OutlineWidth("Outline Width", Range(0, 1.0)) = 1.0
		_OutlineColor("Outline Color", Color) = (1,1,1,1)
	}
	SubShader
	{
		Tags{
			"RenderPipeLine"="UniversalRenderPipeline"
		}

		HLSLINCLUDE

		#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

		CBUFFER_START(UnityPerMaterial)
			float4 _MainTex_ST;
			float _OutlineWidth;
			float4 _OutlineColor;
		CBUFFER_END

		TEXTURE2D(_MainTex);
		SAMPLER(sampler_MainTex);

		struct a2v{
			float4 positionOS: POSITION;
			float3 normalOS: NORMAL;
		};
		struct v2f{
			float4 positionCS: SV_POSITION;
		};
		ENDHLSL

		Pass
		{
			ZWrite Off
			ZTest Always
			ColorMask 0
			Stencil
			{
				Ref 1
				Pass Replace
			}
		}
		Pass
		{
			ZWrite Off
			ZTest Always
			Stencil
			{
				Ref 1
				Comp NotEqual
			}
			HLSLPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			v2f vert(a2v v)
			{
				v2f o;
				v.positionOS.xyz += v.normalOS * _OutlineWidth;
				o.positionCS = TransformObjectToHClip(v.positionOS);
				return o;
			}

			real4 frag(v2f i):SV_TARGET
			{
				return _OutlineColor;
			}
			ENDHLSL			
		}
	}
} 
复制成功

        这个比较简单,核心就是模板测试。

        模板测试相关内容请看这里:模板测试​

        接下来我们来写RenderPass,要点太多,直接看代码备注吧。

代码块
C++
自动换行
复制代码
using System.Collections.Generic;
using UnityEngine;

namespace UnityEngine.Rendering.Universal
{
	public class OutlineRenderPass : ScriptableRenderPass
	{
        //标签名,用于续帧调试器中显示缓冲区名称
        const string CommandBufferTag = "Outline Render Pass";
        // 用于后处理的材质
        public Material m_Material;
        // 材质中第几个pass来渲染
        public int m_PassID;
        // 颜色渲染标识符
        RenderTargetIdentifier m_ColorAttachment;
        // 过滤设置
        private RenderQueueRange m_renderQueueRange; //渲染队列的范围
        private FilteringSettings m_FilteringSettings; //渲染过滤设置
        private RenderStateBlock m_RenderStateBlock; //渲染状态的设置,比如深度测试,开启混合等等
        private SortingCriteria m_SortingCriterial; //渲染物体的渲染顺序,从前往后,还是从后往前
        // 轮廓线颜色
        private Color m_OutlineColor;
        // 轮廓线宽度
        private float m_OutlineWidth;
        // 多Pass渲染需要不同的ShaderTagId,应该是这样吧?懒得验证了。
        private List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
        };
        // 设置渲染参数
        public void Setup(Material Material, int passID,
            RenderPassEvent renderPassEvent, FilterSettings filterSettings)
        {
            //this.m_ColorAttachment = _ColorAttachment;
            this.m_Material = Material;
            this.m_PassID = passID;
            this.m_OutlineColor = filterSettings.outlineColor;
            this.m_OutlineWidth = filterSettings.outlineWidth;
            // 渲染时机
            this.renderPassEvent = renderPassEvent;
            // 过滤范围,处于哪些渲染队列的可以被渲染
            this.m_renderQueueRange = 
                filterSettings.renderQueue == RenderQueue.Geometry? 
                RenderQueueRange.opaque: RenderQueueRange.transparent;
            // render layer,这里基本上不靠这个过滤,靠物体的object layer过滤
            uint renderingLayerMask = (uint)1 << filterSettings.renderingLayerMask - 1;
            // 整合一下,得到一个全新的参数
            this.m_FilteringSettings = new FilteringSettings(m_renderQueueRange, filterSettings.layerMask, renderingLayerMask);
            // 渲染状态的设置,比如是否写入深度呀,是否开启混合呀,等等
            this.m_RenderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
            // 排序,不多哔哔
            this.m_SortingCriterial = 
                filterSettings.renderQueue == RenderQueue.Geometry ?
                SortingCriteria.CommonOpaque : SortingCriteria.CommonTransparent;
        }
        // 重新设置渲染状态
        public void SetDepthState(bool writeEnable, CompareFunction function = CompareFunction.Less)
        {
            m_RenderStateBlock.mask |= RenderStateMask.Depth;
            m_RenderStateBlock.depthState = new DepthState(writeEnable, function);
        }
        /// <summary>
        /// URP会自动调用该执行方法
        /// </summary>
        /// <param name="context"></param>
        /// <param name="renderingData"></param>
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            m_Material.SetColor("_OutlineColor", m_OutlineColor);
            m_Material.SetFloat("_OutlineWidth", m_OutlineWidth);
            var cmd = CommandBufferPool.Get(CommandBufferTag);
            var drawingSettings = CreateDrawingSettings
                (m_ShaderTagIdList, ref renderingData, m_SortingCriterial);
            drawingSettings.overrideMaterial = m_Material;
            drawingSettings.overrideMaterialPassIndex = m_PassID;
            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref m_FilteringSettings);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }
    }
}
复制成功

        接下来是Render Feature,写的不是很严谨,可以优化归类的地方有很多,但是功能可以正常使用,嘿嘿。

代码块
C++
自动换行
复制代码
using System;
using System.Collections.Generic;
using UnityEngine;

namespace UnityEngine.Rendering.Universal
{
	[Serializable] // 参数面板设置
	public class FilterSettings
	{
		// 轮廓线颜色
		public Color outlineColor = Color.blue;
		// 轮廓线宽度
		[Range(0, 0.1f)] public float outlineWidth = 0.04f;
		// 过滤用的渲染队列
		public RenderQueue renderQueue = RenderQueue.Geometry;
		// 过滤用的物体层
		public LayerMask layerMask = -1;
		// 过滤用的渲染层
		[Range(1, 32)] public int renderingLayerMask;
	}
	public class OutlineRenderFeature : ScriptableRendererFeature
	{
		// 渲染时机
		public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingSkybox;
		// 参数面板设置实例化
		public FilterSettings filterSettings;
		// 渲染用shader
		public Shader shader;
		// 材质
		private Material m_Material;
		// 使用第几个pass的参数列表
		public int[] passes;
		[Space(10)]
		// 是否重新写入渲染状态
		public bool overrideDepthState = false;
		// 深度比较参数
		public CompareFunction depthCompareFunction = CompareFunction.LessEqual;
		// 是否开启深度写入
		public bool enableWrite = true;
		// 我们一共要渲染几个Pass
		List<ScriptableRenderPass> m_ScriptablePasses = new List<ScriptableRenderPass>();
		
		// 将Pass添加到渲染列表
		public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
		{
			if (passes==null || shader==null)
			{
				return;
			}
			foreach (OutlineRenderPass item in m_ScriptablePasses)
			{
				var cameraColorTarget = renderer.cameraColorTarget;
				renderer.EnqueuePass(item);
			}
		}

		// 创建Passes
		public override void Create()
		{
			if (shader == null || passes == null)
				return;
			// 创建材质
			if (m_Material == null)
				m_Material = CoreUtils.CreateEngineMaterial(shader);

			m_ScriptablePasses.Clear();
			for (int i = 0; i < passes.Length; i++)
			{
				// Pass参数的初始化
				var outlineRenderPass = new OutlineRenderPass();
				outlineRenderPass.Setup(m_Material, passes[i], passEvent, filterSettings);
				if (overrideDepthState)
				{
					outlineRenderPass.SetDepthState(enableWrite, depthCompareFunction);
				}
				m_ScriptablePasses.Add(outlineRenderPass);
			}
		}
	}
}
复制成功

        如果不出意外,调整好参数以后,我们就可以得到下面的结果了。

        在物体后面也不会被挡住

        但是很明显由硬边造成了割裂的感觉,所以再优化一下shader

代码块
C++
自动换行
复制代码
Shader "URP/URPOutlineStencil"
{
	Properties{
		_MainTex("MainTex",2D) = "white"{}
		_OutlineWidth("Outline Width", Range(0, 1.0)) = 1.0
		_OutlineColor("Outline Color", Color) = (1,1,1,1)
	}
	SubShader
	{
		Tags{
			"RenderPipeLine"="UniversalRenderPipeline"
		}

		HLSLINCLUDE

		#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

		CBUFFER_START(UnityPerMaterial)
			float4 _MainTex_ST;
			float _OutlineWidth;
			float4 _OutlineColor;
		CBUFFER_END

		TEXTURE2D(_MainTex);
		SAMPLER(sampler_MainTex);

		struct a2v{
			float4 positionOS: POSITION;
			float3 normalOS: NORMAL;
		};
		struct v2f{
			float4 positionCS: SV_POSITION;
		};
		ENDHLSL

		Pass
		{
			ZWrite Off
			ZTest Always
			ColorMask 0
			Stencil
			{
				Ref 1
				Pass Replace
			}
		}
		Pass
		{
			ZWrite Off
			ZTest Always
			Cull Off
			Stencil
			{
				Ref 1
				Comp NotEqual
			}
			HLSLPROGRAM

			#pragma vertex vert
			#pragma fragment frag

			v2f vert(a2v v)
			{
				v2f o;
				float3 normalVS = mul((float3x3)UNITY_MATRIX_IT_MV, v.normalOS);
				normalVS.z = -0.5;
				float4 positionVS = mul(UNITY_MATRIX_MV,v.positionOS);
				
				positionVS.xyz += normalVS * _OutlineWidth;
				o.positionCS = mul(UNITY_MATRIX_P,positionVS);
				return o;
			}

			real4 frag(v2f i):SV_TARGET
			{
				return _OutlineColor;
			}
			ENDHLSL			
		}
	}
} 
复制成功

        最后得到下面的效果,其实还是有瑕疵,如果想得到光滑组的法线,请看这里吧:https://zhuanlan.zhihu.com/p/130921684