俯视角视锥可见性系统

原理详解 · 交互演示 · 新手友好

一、我们要解决什么问题?

想象一个俯视角射击游戏——摄像机从正上方向下看,玩家角色在场景中移动和射击。我们希望:

演示:拖动鼠标旋转视野
玩家 扇形视野 墙壁 敌人(可见 / 被遮挡)
挑战在于——扇形判断要精确到每一个像素;墙壁遮挡需要知道"玩家到目标之间有无墙壁";每帧计算必须在 16 ms 内完成。

二、常见方案 & 为什么选 GPU RT

❌ 方案 A:CPU 射线检测

从玩家发出数百条射线(Raycast),检测是否碰到墙壁。

缺点:射线有间隙,无法做像素级裁剪;射线数越多 CPU 越慢。

❌ 方案 B:Mesh 裁剪

生成扇形 Mesh + Stencil 裁剪可见区域。

缺点:墙壁遮挡让 Mesh 形状不规则,每帧重建昂贵。

✅ 我们的方案:GPU Render Texture

  1. 用 Shader 在一张小纹理上逐像素"画"出可见区域 → Vision Mask
  2. 渲染敌人时,Shader 查看这张纹理,不可见像素 → clip() 丢弃
  3. 墙壁遮挡借助 极坐标深度图(类似 Shadow Map)在 GPU 端高效完成

优势:像素级精度、GPU 并行性能极佳、墙壁数量增加时性能劣化小。

三、核心思路:把"能不能看见"画成一张图

想象你站在建筑顶层俯瞰场景,手里有张方格纸——每个格子对应地面的一小块。在可见区域涂白、不可见涂黑,这就是 Vision Mask RT

演示:Vision Mask 概念(拖动鼠标移动玩家)
白色 = 可见 黑色 = 不可见 玩家

不考虑墙壁时的算法

1

UV → 世界坐标

根据 RT 的 UV 和覆盖范围,线性插值得到该像素对应的世界坐标 (x, z)。

2

距离检测

距离 > 视野半径?→ 涂黑(不可见)。

3

角度检测

方向不在扇形范围内?→ 涂黑。

4

墙壁遮挡检测

"从玩家到该点的连线上有墙壁挡着?"→ 涂黑。
这一步用极坐标深度图高效解决——下一章详细讲解。

四、Render Texture 基础

普通纹理(Texture)Render Texture(RT)
来源图片文件 (.png / .jpg)GPU 实时渲染生成
内容固定不变每帧重绘
用途模型贴图、UI镜面反射、小地图、迷雾

本系统用到两张 RT:

RT 名称尺寸记录内容类比
_WallDepthTex720 × 1每个角度方向上最近墙壁距离一条环形"测距雷达"
_VisionMaskTex512 × 512场景中每个点能否被看见俯视图上的可见性标记

五、极坐标深度图 — 墙壁遮挡的秘密武器

5.1 从一个生活例子说起

你站在房间正中央,拿着激光测距仪原地转一圈(360°),每 0.5° 测量到最近墙壁的距离。把结果排成一行像素 → 这就是极坐标深度图

演示:极坐标深度图的生成过程
玩家 墙壁 扫描射线 下方色条 = 深度图(亮 = 远,暗 = 近)

5.2 为什么叫"极坐标"?

因为纹理的 U 轴 = 角度,像素值 = 距离,正是极坐标系的两个维度。

5.3 和阴影映射(Shadow Mapping)的关系

Shadow Mapping(阴影)我们的方案(视野遮挡)
光源 出发玩家 出发
记录各方向最近 物体 距离记录各方向最近 墙壁 距离
距离 > 记录 → 在阴影中距离 > 记录 → 被墙壁遮挡

5.4 为什么是一维纹理?

俯视角的遮挡全在 XZ 平面。从一点描述各方向最近障碍只需角度 → 距离映射,一维足矣。

5.5 深度测试自动取最近墙壁

GPU 硬件 ZTest Less + ZWrite On → 同一角度方向有多面墙时,自动保留距离最小(最近)的,无需额外代码。

演示:多面墙 & 深度测试

同一角度方向上先写入远墙 0.7,再写入近墙 0.3。ZTest Less 只保留 0.3。
点击 "写入远墙 / 写入近墙" 按钮观察深度缓冲的变化。

5.6 0°/360° 分界线问题

当一面墙横跨 350° ~ 10°,端点 U = 0.97 与 U = 0.03,GPU 直接光栅化会反方向遍历整条纹理——产生错误。

解决方案:双 Pass 渲染
  • Pass 1:正常极坐标映射 [0°, 360°)
  • Pass 2:偏移 180° 后映射,仅保留原始角度靠近分界线(U < 0.1 或 U > 0.9)的像素,其余丢弃

5.7 Shadow Bias

比较距离时加小偏移 + 0.005,防止浮点精度导致的"自遮挡"(紧贴墙壁前方的像素被误判为在墙后)。

六、Vision Mask:三重检测合一

6.1 全屏 Shader 逐像素判定

对 512×512 RT 的每个像素依次做三重检测。只要任意一项不通过就涂黑(不可见)。

演示:三重检测——拖动鼠标旋转玩家朝向
通过三重检测 = 可见 任一不通过 = 不可见 墙后遮挡区(深度图)

6.2 角度检测的数学原理

两个单位向量的点积 = 夹角余弦:

dot(forward, dir) = cos(θ)

cos 在 [0°, 180°] 递减,所以 cos(θ) ≥ cos(halfAngle) 等价于 θ ≤ halfAngle

💡 只需一次 dot() 运算,不需要昂贵的 acos()。C# 侧预计算 cos(halfAngle) 传入 Shader。

6.3 墙壁遮挡检测

// 1. 当前像素方向角 → 深度图 U
normalizedAngle = atan2(delta.x, delta.y) / (2π) + 0.5

// 2. 查深度图:该角度最近墙壁距离
wallDist = texture(depthTex, vec2(normalizedAngle, 0.5)).r

// 3. 比较
if (pixelDist / viewRange > wallDist + bias)
    → 被遮挡,涂黑

七、敌人 Shader 像素级裁剪

7.1 clip() 函数

clip(value);
// value < 0 → 丢弃这个像素(discard)
// value ≥ 0 → 正常渲染

7.2 像素级裁剪演示

演示:clip() 像素级裁剪——拖动视野边界
敌人像素(被 clip 丢弃) 敌人像素(通过 clip,可见)

7.3 在 Shader 中使用

// 只需两行
#include "Assets/FogOfWar/Shaders/VisionClip.hlsl"

// 在 Fragment 函数最顶部:
VisionHardClip(IN.positionWS);  // 不可见 → discard

八、URP 渲染管线集成

8.1 我们的 Feature 和 Pass

VisionMaskFeature (ScriptableRendererFeature)
  ├── WallDepthPass → BeforeRenderingOpaques
  │    └── 输出 _WallDepthTex (720×1)
  └── VisionMaskPass → BeforeRenderingOpaques
       └── 输出 _VisionMaskTex (512×512)

8.2 渲染时序

动画:URP 每帧渲染流程

8.3 关键概念

概念作用
overrideMaterial强制所有墙壁使用 WallDepth Shader(忽略原始材质)
SetGlobalTexture_VisionMaskTex 变成全局可访问,任何 Shader 自动绑定
Blitter.BlitTexture"画一个全屏矩形"渲染操作,用于生成 Vision Mask

九、完整数据流演示

下面的综合演示将所有步骤串联:在场景中拖动鼠标控制玩家朝向,实时观察深度图和 Vision Mask 的生成过程,以及敌人的可见性变化。

综合演示:完整数据流(拖动鼠标旋转玩家朝向)
上方 = 场景俯视图 中间 = 极坐标深度图 (720×1) 下方 = Vision Mask (小缩略图)

十、常见疑问 Q&A

Q1: 为什么不在敌人 Shader 里直接做判断,而要先生成 Vision Mask?

解耦:Vision Mask 是通用数据,敌人、地面、小地图都可直接采样,不需要在每个 Shader 中重复逻辑。

性能:Vision Mask 在 512×512 上只算一次,之后所有敌人只需一次简单纹理采样。

Q2: 512×512 的 Vision Mask 够精确吗?

覆盖 100×100 m 场景时,每像素 ≈ 0.2 m 精度。加上 Bilinear 过滤 + 可选模糊,视觉效果柔和自然。需要更高精度可提升到 1024×1024(仅多 ~0.3 ms)。

Q3: 深度图为什么用 R16F(半精度浮点)?

距离值为连续浮点数(如 0.533),R16F 约 3 位有效数字,归一化 [0,1] 距离足够。仅 2 字节/像素,带宽极小。

Q4: 玩家移动时怎么办?

两张 RT 每帧重新生成,使用最新的玩家位置和朝向。完全实时更新,无需额外处理。

Q5: 高斯模糊有什么用?

不加模糊 → 视野边缘硬切。加模糊后,0→1 有几个像素的平滑过渡,视觉更舒适。

Q6: 支持多个视野源吗?

每个视野源各自生成 _WallDepthTex,VisionMaskPass 中 Additive Blend 叠加。敌人 Shader 无需修改。

Q7: 性能开销大吗?

总计 ≈ 0.3~0.6 ms/帧(1080p 中端 GPU)。深度图 720×1 几乎免费;全屏 Blit 512² ~0.1 ms;可选模糊 ~0.1 ms。

Q8: 能让地面也变暗吗?

可以。在地面 Shader 中采样 _VisionMaskTexcolor.rgb *= lerp(0.15, 1.0, vis)。视野外区域变暗为 15% 亮度。

附录:关键术语表

术语英文含义
渲染纹理Render Texture (RT)GPU 实时绘制的纹理
极坐标Polar Coordinates角度 + 距离的坐标系
深度图Depth Map各方向最近表面距离
阴影映射Shadow Mapping用深度图判断阴影
深度测试Z-TestGPU 硬件只保留最近像素
裁剪Clip / DiscardShader 中丢弃像素
BlitBlit"画全屏矩形"渲染操作
点积Dot Product向量运算,单位向量时 = cos(夹角)
自遮挡Shadow Acne浮点精度导致表面误判为阴影
阴影偏移Shadow Bias小偏移防止自遮挡