做游戏时,“角色被墙挡住但还想让玩家看到轮廓”“任务目标藏在障碍物后面需要高亮提示”这类需求非常常见。实现方式也有好几种:模型边缘光、顶点膨胀描边、屏幕后处理描边。今天这篇文章聚焦屏幕空间着色器路线,用 Unity 后处理把“物体轮廓”和“幽灵遮挡效果”一起做出来。看完你可以直接把它接到自己的项目里,按 Layer 控制哪些物体需要高亮,哪些物体被遮挡后显示半透明幽灵轮廓。
这个方案不依赖模型顶点数,不要求额外建模,只需要在主相机上挂一个后处理脚本,再配两个 Shader 就能工作。对于需要批量标记多个目标、团队副本指引、战术指示器、解谜高亮这类场景,它比单独给每个物体写材质要省事得多。
本文会包含这样几块内容:
- 屏幕空间物体轮廓的检测原理
- 幽灵遮挡效果为什么需要二次深度渲染
- 目标物体深度写入 Shader 的实现
- 后处理边缘检测与遮挡叠加 Shader 的实现
- C# 控制脚本与 Layer 配置方式
- 参数调优、URP 迁移和性能注意事项
先说明,本文示例以 Unity Built-in Render Pipeline 为主。URP 项目也可以迁移,最后会单独讲迁移思路。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 技术路线 | Unity 后处理 + 屏幕空间边缘检测 + 二次深度目标渲染 |
| 适用管线 | Built-in Render Pipeline;URP 需要改成 Scriptable Renderer Feature |
| 核心功能 | 可见物体边缘描边、被遮挡物体透视幽灵轮廓 |
| 目标控制 | Layer/Tag/指定 Renderer 列表均可 |
| 显存压力 | 较低,只增加 1 张半分辨率或全分辨率临时 RT |
| 对模型要求 | 无额外要求,不需要顶点色或第二套 UV |
| 启动方式 | 场景挂脚本 + 材质引用,运行时自动生效 |
| 批量扩展 | 支持任意数量目标物体,统一由 Layer 控制 |
| 适合场景 | 任务指引、队友透视、道具高亮、解谜提示、技能目标标记 |
从项目上看,最推荐的做法是“边缘检测 + 目标掩码分离”。也就是说,屏幕上的轮廓线通过深度/法线差异计算出来,而哪些像素属于“需要透视的目标”,则由一张额外的目标深度图来判断。
2. 物体轮廓与幽灵遮挡效果的原理拆解
2.1 屏幕空间轮廓从哪里来
屏幕空间描边和后处理描边最常用的方案有几种:
- 拉普拉斯边缘检测
- Sobel 边缘检测
- 深度差 + 法线差判断
单纯对颜色做边缘检测,会把贴图纹理和光照变化也识别成边缘,视觉效果很脏。更稳定的是结合深度纹理和法线纹理来判断:相邻像素如果深度差距大,或者法线方向差距大,就认为这里存在几何边缘。
Unity 里开启相机的深度法线模式,可以在 Shader 中通过_CameraDepthNormalsTexture拿到一张同时包含视角空间深度和视角空间法线的纹理。然后使用类似 Roberts 或 Sobel 的卷积核,在屏幕空间采样 4 到 8 个邻域点,通过比较深度差和法线差,就能得到屏幕上的轮廓线。
这是比较经典的后处理描边思路,计算量远小于在模型空间重新渲染一遍轮廓层,也适合做全屏角色描边。
2.2 幽灵遮挡效果的难点在哪
如果只是“给物体描边”,那用深度法线边缘检测就可以收工。难点在于“幽灵遮挡效果”:
当目标物体被墙体挡住时,屏幕上的像素来自墙壁。后处理不知道这个像素原本应该属于哪个物体,因此边缘检测只能画出墙壁的边缘,画不出墙后面目标物体的轮廓。
要让遮挡状态下的轮廓也显示,必须额外渲染一张“目标物体的屏幕空间信息图”。这张图上只有目标物体,其他物体不参与渲染。然后在后处理阶段比对场景深度和目标深度,如果当前像素的场景深度比目标深度更靠近相机,就认为目标物体在该像素位置被遮挡了。
此时把这些边界像素显示成半透明、发光、或者带 ghost 效果的颜色,就是“幽灵遮挡效果”。
换句话说,完整流程分三步:
- 主相机正常渲染场景。
- 单独渲染目标物体到一张低分辨率深度图,图中只存储目标物体到相机的距离。
- 后处理阶段采样场景深度、场景法线、目标深度,经过边缘检测和遮挡判断后输出描边与幽灵效果。
3. 环境准备与工程配置
建议使用 Unity 2021.3 LTS 或 2022.3 LTS 测试,以下配置都按 Built-in Render Pipeline 准备。
3.1 基础工程要求
在 Unity 中创建 3D 工程后,确认几下基础设置:
- 渲染管线为 Built-in,Project Settings 的 Graphics Settings 中没设置 URP Asset。
- 主相机开启
Allow HDR、MSAA按项目承受力选择,如果只要观察轮廓可以先关闭。 - 项目使用线性空间即可。
3.2 相机深度纹理
由于我们要在 Shader 中读取主相机的深度和法线,需要让相机生成深度法线纹理。在 C# 脚本里可以用如下方式强制开启:
Camera.main.depthTextureMode = DepthTextureMode.DepthNormals;如果你在 Unity 编辑器中查看相机的Depth Texture选项,它会显示Depth Normals。这一步务必检查,否则屏幕后处理材质拿到_CameraDepthNormalsTexture后,会是黑色或者全白,轮廓出不来。
3.3 场景结构建议
建议建一层空物体OutlineSystem,然后按下列结构管理场景:
场景根节点 ├── Directional Light ├── Main Camera (挂 OutlineEffect.cs) ├── Environment (普通场景物体,默认层) └── Targets ├── Target_Cube (层设为 GhostTarget) ├── Target_Sphere (层设为 GhostTarget) └── Wall (默认层,遮挡目标)Targets 下所有需要透视和描边的物体,统一放到GhostTargetLayer。后面代码会把该 Layer 的所有物体写入目标深度图。
4. 编写目标物体深度渲染 Shader
思路是使用 Camera.RenderWithShader 或额外摄像机的cullingMask只渲染目标层,但更轻量的做法是用CommandBuffer或者额外的Camera渲染一遍目标物体。
为了减少额外相机维护成本,示例采用最简单直观的方案:创建一个独立相机,它的cullingMask只包含GhostTarget层,并输出到一张 RenderTexture。为了让普通物体的 Shader 不干扰深度写入,我们直接覆盖渲染 Shader,输出“目标深度编码颜色”。
4.1 深度编码思路
我们需要在 Shader 中输出目标物体某个像素到相机的距离,或者直接输出“当前像素是否属于目标物体”的 Mask。
如果只做 Mask,幽灵效果只能是纯色轮廓,但遇到遮挡物离得很近时,仍然要区分目标物体的远近。输出“深度值”更通用。
写一张 RGBA 纹理,A 通道保存目标物体标记,B 通道保存非线性或线性深度。后处理综合判断时,需要知道“这个像素处有没有目标”,然后比较场景深度和目标深度。
4.2 GhostDepthShader.shader
新建一个 Shader 文件,命名为GhostDepthShader.shader。它的作用是:正常按顶点位置输出,但颜色输出为目标深度编码。
Shader "Hidden/GhostDepthShader" { SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { float depth = distance(_WorldSpaceCameraPos.xyz, i.worldPos); depth = saturate(depth / 50.0); return fixed4(depth, depth, depth, 1.0); } ENDCG } } }这个 Shader 并不是真正渲染到主相机屏幕,而是被独立相机用RenderWithShader执行。它把“目标物体到相机的距离,除以 50 米后压缩到 0 到 1”写入颜色。50 是最大显示距离,你可以按场景调大。
也可以用观察空间深度代替距离,只需要在 vert 里编码COMPUTE_EYEDEPTH(o.pos.z)。但用距离值更直观,因为后面判断遮挡时直接比“谁离相机更近”即可。
4.3 创建独立深度相机
不直接改主相机,新建一个子物体GhostDepthCamera,挂 Camera 组件。设置如下:
Clear Flags: Solid Color Background: Black Culling Mask: GhostTarget Target Texture: 后续创建 RenderTexture 后拖入然后在 C# 脚本中,每个 LateUpdate 或 OnPreCull 阶段设置该相机的RenderWithShader,并手动调用一次渲染。
5. 编写屏幕空间后处理 Shader
后处理 Shader 分为两个 Pass:
- Pass 0:利用场景深度 + 法线纹理检测边缘
- Pass 1:采样目标深度纹理,判断遮挡,混合幽灵颜色
也可以直接合成一个 Pass,先算边缘,再用目标深度图做二次判断。下面给出完整实现。
5.1 ScreenSpaceOutline.shader
Shader "Hidden/ScreenSpaceOutline" { Properties { _OutlineColor ("Outline Color", Color) = (1,1,1,1) _GhostColor ("Ghost Color", Color) = (0.2,0.8,1,0.5) _EdgeThreshold ("Edge Threshold", Range(0.1, 2)) = 0.5 } SubShader { ZTest Always ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; sampler2D _CameraDepthNormalsTexture; sampler2D _GhostDepthTex; float4 _MainTex_TexelSize; fixed4 _OutlineColor; fixed4 _GhostColor; float _EdgeThreshold; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } float3 DecodeViewNormal(float2 uv) { float4 enc = tex2D(_CameraDepthNormalsTexture, uv); return DecodeViewNormalStereo(enc); } float DecodeViewDepth(float2 uv) { float4 enc = tex2D(_CameraDepthNormalsTexture, uv); float depth; DecodeDepthNormal(enc, depth, float4(0,0,0,0)); return depth; } bool IsEdge(float2 uv) { float2 texel = _MainTex_TexelSize.xy; float2 offsets[4] = { float2(-texel.x, 0), float2(texel.x, 0), float2(0, -texel.y), float2(0, texel.y) }; float depthCenter = DecodeViewDepth(uv); float3 normalCenter = DecodeViewNormal(uv); float depthSum = 0; float3 normalSum = 0; for (int i = 0; i < 4; i++) { float2 sampleUV = uv + offsets[i]; float d = DecodeViewDepth(sampleUV); float3 n = DecodeViewNormal(sampleUV); depthSum += abs(d - depthCenter); normalSum += abs(n - normalCenter); } float depthDiff = depthSum / 4.0; float normalDiff = (normalSum.x + normalSum.y + normalSum.z) / 3.0; return (depthDiff + normalDiff) > _EdgeThreshold; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); float ghostMask = tex2D(_GhostDepthTex, i.uv).a; float targetDepth = tex2D(_GhostDepthTex, i.uv).b; float sceneDepth = DecodeViewDepth(i.uv); bool isEdge = IsEdge(i.uv); float occlusionFactor = (sceneDepth < targetDepth - 0.05) ? 1 : 0; float finalEdge = isEdge ? 1 : 0; if (ghostMask > 0.5 && finalEdge > 0.5) { if (occlusionFactor > 0.5) { fixed4 ghost = _GhostColor; ghost.a = 1.0; return lerp(col, ghost, 0.85); } else { return lerp(col, _OutlineColor, 0.8); } } return col; } ENDCG } } }这段代码的关键逻辑在 frag 中:
- 读取目标深度纹理
_GhostDepthTex。 - 用
_CameraDepthNormalsTexture解码场景深度和法线。 - 通过 IsEdge 判断当前像素是否处于屏幕空间边缘。
- 如果是目标物体区域,再比较场景深度和目标深度。
- 场景深度小于目标深度,说明目标被挡住,输出幽灵颜色;否则输出普通描边色。
5.2 这段代码的边界判断方式
上述代码每帧会对全屏每个像素做一遍深度法线采样和边缘判断。实际工作中可以直接用固定的 Roberts 算子或 Sobel 算子代替四个方向比较,也可以增加采样点让边缘更连续。
写 Demo 时四个方向已经够用。如果要更稳定,建议把 IsEdge 改成 Sobel 卷积核计算,并从深度与法线两个通道分别提取梯度,然后合并。法线差异用向量距离计算,深度差异直接用线性深度差。
6. C# 控制脚本实现
6.1 OutlineEffect.cs
在主相机上挂一个OutlineEffect.cs,把材质、目标深度渲染相机关联起来。
using UnityEngine; [RequireComponent(typeof(Camera))] public class OutlineEffect : MonoBehaviour { public Material outlineMaterial; public Camera ghostDepthCamera; [Range(0.1f, 2f)] public float edgeThreshold = 0.5f; public Color outlineColor = Color.white; public Color ghostColor = new Color(0.2f, 0.8f, 1f, 0.6f); private RenderTexture ghostDepthRT; private void OnEnable() { var cam = GetComponent<Camera>(); cam.depthTextureMode |= DepthTextureMode.DepthNormals; } private void OnDisable() { if (ghostDepthCamera != null) { ghostDepthCamera.targetTexture = null; } if (ghostDepthRT != null) { ghostDepthRT.Release(); ghostDepthRT = null; } } private void LateUpdate() { if (ghostDepthCamera == null) { return; } if (ghostDepthRT == null) { int w = Screen.width / 2; int h = Screen.height / 2; ghostDepthRT = new RenderTexture(w, h, 16, RenderTextureFormat.ARGB32); ghostDepthRT.name = "GhostDepthRT"; } ghostDepthCamera.targetTexture = ghostDepthRT; ghostDepthCamera.enabled = false; ghostDepthCamera.RenderWithShader(Shader.Find("Hidden/GhostDepthShader"), null); UpdateMaterialProperties(); } private void UpdateMaterialProperties() { if (outlineMaterial == null) { return; } outlineMaterial.SetTexture("_GhostDepthTex", ghostDepthRT); outlineMaterial.SetColor("_OutlineColor", outlineColor); outlineMaterial.SetColor("_GhostColor", ghostColor); outlineMaterial.SetFloat("_EdgeThreshold", edgeThreshold); } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (outlineMaterial == null) { Graphics.Blit(source, destination); return; } Graphics.Blit(source, destination, outlineMaterial); } }6.2 脚本说明
OnEnable 里让主相机生成深度法线纹理。
LateUpdate 里做了三件事:
- 创建半分辨率 RenderTexture
- 让一个不参与主相机渲染的独立相机,用覆盖 Shader 只渲染目标物体层
- 把材质参数同步到后处理材质
OnRenderImage 最终把后处理材质应用到整屏。
6.3 目标层配置
目标相机GhostDepthCamera的 Culling Mask 只勾选GhostTarget。
也就是说,控制器里的目标角色、道具、机关都放到这一层。后处理只对这些物体输出轮廓和遮挡效果。
7. 功能测试与效果验证
7.1 验证可见物体轮廓
摆放一个不带遮挡的 Cube 在相机正前方,将 Cube 的 Layer 设置为 GhostTarget。
运行场景后:
- 从相机看到的 Cube 边缘会出现
_OutlineColor描边。 - 描边稳定,不闪烁。
- 旋转相机,Cube 边缘轮廓连续。
如果描边没有出现,优先检查:
- 主相机是否挂上了 OutlineEffect 脚本
- 主相机 DepthTextureMode 是否是 DepthNormals
- 后处理材质是否引用了 ScreenSpaceOutline shader
- 目标相机 Culling Mask 是否正确
7.2 验证幽灵遮挡效果
在目标 Cube 前面放一面墙,层设置为 Default。运行后在主相机视角中,Cube 被墙完全挡住的区域,边界部分应显示半透明的 GhostColor,形成类似 X 光透视效果。
由于边缘检测只保留边界,没有边界的位置不会整片填充。这样做的好处是避免整面墙被大面积半透明颜色盖住,画面更像“轮廓透视”。
如果希望整颗目标物体都显示半透明而不是只有边缘,可以把后处理里的finalEdge判断去掉,直接对 ghostMask 区域做混合。这样就是“穿墙透视全体高亮”,适合某些显示需求,但视觉上会比较抢眼。
7.3 目标深度纹理精确性检查
为了调试目标深度图是否正常,可以在 LateUpdate 里实时把 ghostDepthRT 显示到屏幕:
if (showDebugRT) { Graphics.Blit(ghostDepthRT, (RenderTexture)null); }不过这样会覆盖主相机输出。更方便的方法是在 Game 视图旁边开一个 RawImage UI,把 ghostDepthRT 动态赋给它的 texture:
public UnityEngine.UI.RawImage debugImage; private void LateUpdate() { if (debugImage != null && ghostDepthRT != null) { debugImage.texture = ghostDepthRT; } }如果 RawImage 里只有目标物体的白色轮廓,说明深度写入成功。如果整屏黑色,目标物体没有进入目标深度图。
7.4 参数调优观察
| 参数 | 作用 | 推荐起始值 |
|---|---|---|
| edgeThreshold | 控制边缘敏感度 | 0.3 到 0.8 |
| ghostColor 的 alpha | 控制幽灵效果混合强度 | 0.5 到 0.7 |
| outlineColor | 描边颜色 | 按团队配色决定 |
| GhostDepthRT 分辨率 | 半分辨率即可,过高会增加带宽 | Screen / 2 |
| 最大显示距离 | GhostDepthShader 中 50.0 | 按场景视距调整 |
edgeThreshold太小会导致贴图明暗变化也被误识别为轮廓,太大则会导致轮廓断裂,需要根据具体场景不断测试。
8. 接口 API 化与批量目标控制
如果后面要做系统集成,建议把这套效果整理成对外接口。例如 RTS 中玩家选中多个单位,需要同时为它们生成轮廓;或者副本里给多个 BOSS 目标做透视提示。
8.1 开放接口
OutlineEffect.cs可以加挂公有的开启/关闭接口:
public void SetTargetLayer(LayerMask targetLayer) { if (ghostDepthCamera != null) { ghostDepthCamera.cullingMask = targetLayer; } } public void SetEnabled(bool enable) { outlineMaterial.enabled = enable; }要动态变更目标物体,也可以不用 Layer,而是把目标 Renderer 列表传入深度相机:
public void AddTarget(Renderer targetRenderer) { int layer = LayerMask.NameToLayer("GhostTarget"); targetRenderer.gameObject.layer = layer; } public void RemoveTarget(Renderer targetRenderer) { int defaultLayer = LayerMask.NameToLayer("Default"); targetRenderer.gameObject.layer = defaultLayer; }这种方式适合运行时切换选中目标。缺点是动态改 Layer,需要维护好原先的 Layer 值,移除目标时还原。
8.2 排除个别物体
如果不想整层统一,更稳妥的方案是额外使用一份目标物体列表,在独立深度相机渲染时临时切换每个物体的 Layer 并还原。代码大概是:
Using (new LayerScope(targetRenderer, ghostLayerId)) { ghostDepthCamera.RenderWithShader(...); }LayerScope 写一个临时改 Layer 并在 Dispose 时还原的辅助类即可。不过这个做法依赖于渲染同步时机,后续可以进一步整理成 CommandBuffer 方案,避免频繁改 Layer。
8.3 与帧循环集成
因为后处理采用 OnRenderImage,目标深度渲染发生在 LateUpdate,顺序大概是:主相机渲染场景 -> LateUpdate 生成目标深度图 -> OnRenderImage 叠加轮廓。这个顺序在多数情况下没有问题。
如果追求更严格的帧内同步,可以使用 Camera.onPreRender 或 CommandBuffer。在 Built-in 管线中,最简单稳当的还是 LateUpdate + OnRenderImage。如果单位拾取或目标切换频繁,只需要在 LateUpdate 之前更新 Layer 即可。
9. 资源占用与性能观察
9.1 主要开销
这套效果的开销来自三部分:
- 主相机生成深度法线纹理
- 独立相机再次渲染目标物体
- 全屏后处理对每个像素的深度、法线、颜色采样
第一部分不可避免,只要做屏幕空间深度/法线效果就需要。第二部分只渲染 GhostTarget 层,如果目标数量少、模型简单,开销很低。第三部分才是最大的性能影响因素。
所以适合做“目标数量少但需要长时间显示”的效果。如果场景中有几百个角色都需要透视,那就应该改成 CommandBuffer 绘制深度的方案,而不是单独相机逐帧渲染。
9.2 如何降低开销
推荐这几个方法:
- 把 GhostDepthRT 设为半分辨率,Ghost 轮廓会稍微有锯齿,但性能可以多出不少余量。
- 后处理 Shader 只保留一个 Pass,减少中间 RT 读写。
- 用
_MainTex_TexelSize控制采样步长,避免在每帧做多层模糊。 - 如果不需要满屏检测,可以把边缘检测的像素范围限制到包围盒区域,但工程复杂度会上升。
9.3 Frame Debugger 观察方法
打开 Window -> Analysis -> Frame Debugger,点击 Enable,然后逐条查看 Draw Call:
- GhostDepthCamera 会生成一批目标物体的绘制。
- 主相机最后会多一个 Post Process Pass。
- 如果发现 GhostTarget 层物体绘制两次,检查是否给 GhostDepthCamera 也开启了自动渲染而没有被禁用。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 轮廓完全没有显示 | 主相机未开启 DepthNormals | 检查 OutlineEffect 中 depthTextureMode | 在 OnEnable 设置DepthTextureMode.DepthNormals |
| 后处理材质全黑 | 目标深度 RT 未传入材质 | 在 LateUpdate 后检查_GhostDepthTex是否有值 | 在 OnRenderImage 前重新设置材质纹理 |
| 轮廓噪点很多 | edgeThreshold 太小 | 调大阈值 | 将几个采样方向的深度差改成线性深度做差阈 |
| 轮廓断裂 | 目标模型法线变化平缓,深度差不够 | 增大法线差分权重 | 改用多方向边缘检测或增加一次膨胀 |
| 目标被挡住时没有 ghost 效果 | 场景深度和目标深度比较条件错误 | 打印场景深度和目标深度 | 检查遮挡判断的符号 |
| 目标物体在墙后大面积变亮 | ghostMask 全区域参与混合 | 在 frag 中加 finalEdge 限制 | 只对边缘像素做混合 |
| URP 下没有效果 | URP 不支持 OnRenderImage | 查看 URP 资产配置 | 改用 Renderer Feature + Blit |
| 独立深度相机渲染了场景其他物体 | Culling Mask 配置不对 | 检查目标相机 | 只勾选 GhostTarget 层 |
| 移动端性能不足 | 全屏多采样次数太多 | 降低目标 RT 分辨率 | 将 IsEdge 改成采 8 点并降半率 |
对于移动端,深度法线全屏采样压力明显,建议打包真机测试。若压力过高,可以优先缩小 GhostDepthRT 或改成隔帧更新。
11. URP 迁移与扩展方向
11.1 OnRenderImage 的限制
URP 移除了 OnRenderImage,不能再像 Built-in 一样直接挂脚本完成全屏后处理。迁移时需要把后处理写到 ScriptableRendererFeature 或 RenderPass 中。
基本思路:
- 创建
ScreenSpaceOutlineRenderFeature - 在
Execute中获取 camera depth texture - 使用 CommandBuffer 执行一次
Blit,把源 RT 传到临时 RT,再用材质 Blit 到目标 RT - 目标深度 RT 可以在自定义 Pass 中通过一个自定义相机渲染
11.2 Renderer Feature 核心结构
URP 项目里,新建一个OutlineRenderFeature.cs:
public class OutlineRenderFeature : ScriptableRendererFeature { public Material outlineMaterial; public LayerMask targetLayer; public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(new OutlineRenderPass(outlineMaterial, targetLayer)); } }真正的逻辑在ScriptableRenderPass.Execute中。
核心点有两个:
- 从
renderingData.cameraData.camera获取目标深度 RT。 - 使用
OutlineRenderPass在当前相机渲染完成后,对cameraColorTarget执行 Blit。
不同 URP 版本的 API 有差异,例如ScriptableRenderer.cameraColorTarget在部分版本已标记为 obsolete,需要按版本换成RenderTargetIdentifier。
11.3 进一步扩展方向
- 描边渐变消除:让越靠近屏幕边缘的轮廓透明度越低。
- 多层目标优先级:把目标分成队友、敌人、道具三层,分别输出不同颜色。
- 与 UI 指示器组合:当目标物完全不在屏幕内时,只显示屏幕边缘箭头。
- 改成 CommandBuffer 方案:去掉独立相机,用
CommandBuffer.DrawRenderer绘制目标深度,避免反复开关相机。
12. 效果验收检测清单
部署完成后,建议按下面清单快速验证:
- [ ] 主相机能看到目标物体的正常轮廓描边
- [ ] 目标物体完全被墙遮挡时,边缘出现半透明 Ghost 色
- [ ] 无目标物体区域保持原画面,不闪轮廓
- [ ] 多个目标物体同时存在时都能正常显示
- [ ] 动态修改目标 Layer 能实时生效
- [ ] 关闭 OutlineEffect 后渲染恢复正常
- [ ] GhostDepthRT 分辨率调整后性能符合预期
- [ ] 移动端目标帧率下无严重发热或崩溃
关于资源的合规使用同样需要注意,这套描边和透视效果本身不涉及版权文件,但项目中的角色模型、贴图、动作素材,需要确保均在授权范围内使用。如果做多人对战或协同功能,涉及玩家对象、队伍可见性时,一定要处理好信息边界,避免把这种透视能力扩展到不合规的作弊场景。功能本身是技术手段,能用在任务指引,也能被滥用,工程落地时需要在产品规则层面对齐边界。
如果后续要继续优化,最值得投入的方向不是继续堆后处理采样点,而是把整套流程迁移到 ScriptableRenderPipeline 的 RenderPass 体系里。这样画面质量、目标独立渲染、以及不同平台的多相机管理都会更加可控。先用当前方案验证效果,等项目需求稳定后,再做管线迁移和性能压测,是比较务实的推进顺序。