1. 为什么你的游戏角色总被墙挡住?聊聊遮挡显示
不知道你有没有遇到过这种情况:辛辛苦苦做好的游戏角色,一跑到墙后面或者大树后面,直接就“消失”了。玩家操控的角色被完全遮挡,瞬间就失去了方向感,体验大打折扣。这在第三人称游戏、MOBA游戏或者一些需要清晰视野的AR应用中,简直是个灾难。
传统的3D渲染遵循“谁离相机近谁先画”的深度测试规则,这本身没错,保证了视觉正确性。但游戏体验有时候需要一点“作弊”。我们得让玩家知道,“哦,我的角色还在那儿,只是被挡住了”。这就是“遮挡显示”(Occlusion Highlight/Outline)技术要解决的问题。它不是去改变渲染的物理正确性,而是在那个被遮挡的物体上,叠加一层特殊的、穿透遮挡物的视觉效果,比如一个发光的轮廓、一圈半透明的描边,或者像X光一样的透视效果。
在Unity的URP(通用渲染管线)里实现这个功能,听起来很高大上,涉及Shader、模板测试、RenderFeature这些词。但别怕,我干了这么多年技术美术,可以很负责任地告诉你,它的核心思想非常直白:让同一个物体画两次。第一次,让它正常渲染,同时偷偷做个“标记”;第二次,专门去渲染那些被标记了“我被挡住了”的部分,画上我们想要的特殊效果。整个流程就像给被挡住的角色贴了个“请注意”的荧光贴纸。
这篇文章,我就带你从零开始,手把手实现一遍。我会掰开揉碎了讲,从Shader里怎么写那个“标记”,到URP里怎么配置那个负责“第二次画画”的RenderFeature。保证你跟着做下来,不仅能搞定功能,还能真正明白背后的“所以然”。咱们不玩虚的,直接上干货。
2. 核心原理拆解:两次渲染与模板缓冲区
在深入代码之前,我们必须把原理吃透。很多教程只给代码,原理一笔带过,结果你换了个需求就懵了。咱们这里得搞清楚。
2.1 深度测试与模板测试:GPU的两位门卫
你可以把GPU渲染想象成一场严格的入场检查。每个像素点(屏幕上的一个点)就是一个小座位。
- 深度测试(ZTest) 是第一位门卫。他手里有个本子,记录着当前座位上已经坐下的那位客人离摄像机的距离(深度值)。新来的客人(像素)想坐下,必须报上自己的距离。如果新客人比本子上记的距离更近(值更小),说明他应该在前面,门卫就让他进去坐下,并更新本子上的记录。如果更远,对不起,你被挡住了,不能进。这就是为什么远处的物体会被近处的挡住。
- 模板测试(Stencil Test) 是第二位门卫。他也有个本子,但这个本子记录的不是距离,而是一个编号(模板值)。这个本子一开始全是0。门卫可以按照规则,给进场的客人盖个章(写入模板值),或者根据客人是否满足某些条件(比如深度测试过没过)来决定盖不盖章。
我们的遮挡显示,就要巧妙地利用这两位“门卫”的协作。
2.2 我们的“作弊”流程
-
第一次渲染(正常渲染 + 秘密盖章):我们的角色模型正常渲染。对于每个像素:
- 深度测试门卫照常工作。如果这个像素没被挡住(深度测试通过),它就正常上色,显示在屏幕上。
- 关键在这里:我们给模板测试门卫下了一个特殊的指令:“不管这个像素的深度测试过没过,你都给它盖一个‘2’的章(写入模板值2)。” 注意,这个“盖章”动作是独立于深度测试结果的。也就是说,即使这个像素因为被墙挡住而没有通过深度测试(最终颜色没画到屏幕上),它的模板值也被写成了2。
-
第二次渲染(只画被挡住的轮廓):我们创建一个新的、只画轮廓或半透效果的Pass(可以理解为一支特殊的画笔)。这支画笔的规则是:
- 深度测试 Always:无视深度,永远通过。因为我们要画的轮廓需要穿透前面的墙显示出来。
- 模板测试 Equal 2:模板测试门卫检查,只有座位上盖的章正好是2的像素,才允许这支笔画上去。
- 那么,哪些像素的模板值是2呢?根据第一步的规则,就是那些被遮挡的像素(因为通过的像素正常显示了,被挡的像素没显示但被标记为2)。于是,这支特殊的画笔就精准地只在了被墙挡住的那部分角色模型上,画出了我们想要的发光轮廓。
这样一来,玩家看到的画面就是:角色正常显示,当角色走到墙后时,他原本应该被遮住的部分,会浮现出一层我们自定义的高亮效果,清晰指示位置。整个过程中,墙的渲染完全不受影响,深度关系在视觉上依然是正确的,我们只是额外叠加了一层信息。
3. 实战第一步:编写被遮挡物体的Shader
理论通了,咱们开始动手。首先处理需要被高亮显示的物体,比如你的游戏主角。我们需要修改它的Shader,加入那个关键的“盖章”逻辑。
3.1 创建与基础设置
在Unity中新建一个Unlit Shader(URP下通常选择“Universal Render Pipeline/Unlit Shader”作为模板),命名为“OccludedObjectShader”。我们先搭建一个最基础的、只显示颜色的Shader框架。
Shader "Custom/OccludedObjectShader"
{
Properties
{
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
_BaseMap ("Base Map", 2D) = "white" {}
}
SubShader
{
Tags
{
"RenderType"="Opaque"
"RenderPipeline"="UniversalPipeline"
"Queue"="Geometry"
}
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes { ... };
struct Varyings { ... };
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
float4 _BaseMap_ST;
half4 _BaseColor;


887

被折叠的 条评论
为什么被折叠?



