源码 / Ch25/LayerOutlineFeature.cs
Ch25/LayerOutlineFeature.cs
// 案例 25.5 / 25.6:只给某个 Layer 的物体画遮罩,再根据遮罩画描边。 // 两个 Pass:MaskPass 用 DrawRenderers 把选中层画进一张 R8 遮罩;CompositePass 读遮罩、膨胀、叠到屏幕上。 using System.Collections.Generic; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace Cookbook.Ch25 { public class LayerOutlineFeature : ScriptableRendererFeature { public enum Mode { Outline, DebugMask } [System.Serializable] public class Settings { public LayerMask layerMask = 1 << 8; [Tooltip("勾选:被其他物体挡住的部分也描(透视描边);不勾:只描看得见的部分")] public bool seeThrough = true; public Color outlineColor = new Color(1f, 0.55f, 0.1f, 1f); public Color fillColor = new Color(1f, 0.55f, 0.1f, 0f); [Range(1, 16)] public float width = 4f; public Mode mode = Mode.Outline; public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingTransparents; } public Settings settings = new Settings(); public Shader maskShader; public Shader outlineShader; Material m_MaskMaterial, m_OutlineMaterial; MaskPass m_MaskPass; CompositePass m_CompositePass; public override void Create() { Dispose(true); if (maskShader == null) maskShader = Shader.Find("Cookbook/Ch25/LayerMaskSolid"); if (outlineShader == null) outlineShader = Shader.Find("Cookbook/Ch25/LayerOutline"); m_MaskMaterial = CoreUtils.CreateEngineMaterial(maskShader); m_OutlineMaterial = CoreUtils.CreateEngineMaterial(outlineShader); m_MaskPass = new MaskPass(m_MaskMaterial); m_CompositePass = new CompositePass(m_OutlineMaterial); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { var camType = renderingData.cameraData.cameraType; if (camType == CameraType.Preview || camType == CameraType.Reflection) return; // 两个 Pass 同一个事件:入队顺序就是执行顺序(URP 用稳定排序) m_MaskPass.renderPassEvent = settings.passEvent; m_CompositePass.renderPassEvent = settings.passEvent; m_MaskPass.Setup(settings); m_CompositePass.Setup(settings, m_MaskPass); renderer.EnqueuePass(m_MaskPass); renderer.EnqueuePass(m_CompositePass); } public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { m_MaskPass.cameraDepth = renderer.cameraDepthTargetHandle; m_CompositePass.SetTarget(renderer.cameraColorTargetHandle, renderer.cameraDepthTargetHandle); } protected override void Dispose(bool disposing) { m_MaskPass?.Dispose(); CoreUtils.Destroy(m_MaskMaterial); CoreUtils.Destroy(m_OutlineMaterial); } class MaskPass : ScriptableRenderPass { // 只画原材质里有这些 LightMode 的物体(URP 前向渲染用到的三个标签) static readonly List<ShaderTagId> s_ShaderTags = new List<ShaderTagId> { new ShaderTagId("UniversalForward"), new ShaderTagId("UniversalForwardOnly"), new ShaderTagId("SRPDefaultUnlit"), }; readonly Material m_Material; FilteringSettings m_Filtering; RenderStateBlock m_StateBlock; public RTHandle mask; // 遮罩 RT(自己分配) public RTHandle cameraDepth; // 借用相机深度做遮挡测试 public MaskPass(Material material) { m_Material = material; profilingSampler = new ProfilingSampler("Ch25 Layer Mask"); m_Filtering = new FilteringSettings(RenderQueueRange.all); } public void Setup(Settings s) { m_Filtering.layerMask = s.layerMask; // 用 RenderStateBlock 覆盖材质里的深度状态:透视模式 ZTest Always,否则 LEqual m_StateBlock = new RenderStateBlock(RenderStateMask.Depth) { depthState = new DepthState(false, s.seeThrough ? CompareFunction.Always : CompareFunction.LessEqual) }; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { // 遮罩与相机同尺寸、同 MSAA(要和相机深度一起绑定,两者采样数必须一致),单通道 8 位即可 var desc = renderingData.cameraData.cameraTargetDescriptor; desc.graphicsFormat = GraphicsFormat.R8_UNorm; desc.depthBufferBits = 0; RenderingUtils.ReAllocateIfNeeded(ref mask, desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_Ch25LayerMask"); // 告诉 URP:这个 Pass 画到 mask 上,深度用相机深度;开始前只清颜色(深度绝不能清) ConfigureTarget(mask, cameraDepth); ConfigureClear(ClearFlag.Color, Color.clear); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, profilingSampler)) { // 先把命令缓冲里已有的命令提交,再调用 context 级别的 DrawRenderers context.ExecuteCommandBuffer(cmd); cmd.Clear(); var drawing = CreateDrawingSettings(s_ShaderTags, ref renderingData, SortingCriteria.CommonOpaque); drawing.overrideMaterial = m_Material; // 所有物体都换成纯白材质 drawing.overrideMaterialPassIndex = 0; context.DrawRenderers(renderingData.cullResults, ref drawing, ref m_Filtering, ref m_StateBlock); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public void Dispose() { mask?.Release(); mask = null; } } class CompositePass : ScriptableRenderPass { static readonly int s_OutlineColor = Shader.PropertyToID("_OutlineColor"); static readonly int s_FillColor = Shader.PropertyToID("_FillColor"); static readonly int s_OutlineWidth = Shader.PropertyToID("_OutlineWidth"); readonly Material m_Material; MaskPass m_MaskPass; RTHandle m_CameraColor, m_CameraDepth; int m_ShaderPass; public CompositePass(Material material) { m_Material = material; profilingSampler = new ProfilingSampler("Ch25 Outline Composite"); } public void Setup(Settings s, MaskPass maskPass) { m_MaskPass = maskPass; m_ShaderPass = s.mode == Mode.Outline ? 0 : 1; m_Material.SetColor(s_OutlineColor, s.outlineColor); m_Material.SetColor(s_FillColor, s.fillColor); m_Material.SetFloat(s_OutlineWidth, s.width); } public void SetTarget(RTHandle color, RTHandle depth) { m_CameraColor = color; m_CameraDepth = depth; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { // 画回相机颜色。显式 ConfigureTarget,URP 会在 MaskPass 之后重新绑定相机目标 ConfigureTarget(m_CameraColor, m_CameraDepth); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, profilingSampler)) { // 不切换目标,直接在当前目标(相机颜色)上画一个全屏三角形,靠混合叠加上去 Blitter.BlitTexture(cmd, m_MaskPass.mask, new Vector4(1, 1, 0, 0), m_Material, m_ShaderPass); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } } }