原理详解 · 交互演示 · 新手友好
想象一个俯视角射击游戏——摄像机从正上方向下看,玩家角色在场景中移动和射击。我们希望:
从玩家发出数百条射线(Raycast),检测是否碰到墙壁。
缺点:射线有间隙,无法做像素级裁剪;射线数越多 CPU 越慢。
生成扇形 Mesh + Stencil 裁剪可见区域。
缺点:墙壁遮挡让 Mesh 形状不规则,每帧重建昂贵。
clip() 丢弃优势:像素级精度、GPU 并行性能极佳、墙壁数量增加时性能劣化小。
想象你站在建筑顶层俯瞰场景,手里有张方格纸——每个格子对应地面的一小块。在可见区域涂白、不可见涂黑,这就是 Vision Mask RT。
根据 RT 的 UV 和覆盖范围,线性插值得到该像素对应的世界坐标 (x, z)。
距离 > 视野半径?→ 涂黑(不可见)。
方向不在扇形范围内?→ 涂黑。
"从玩家到该点的连线上有墙壁挡着?"→ 涂黑。
这一步用极坐标深度图高效解决——下一章详细讲解。
| 普通纹理(Texture) | Render Texture(RT) | |
|---|---|---|
| 来源 | 图片文件 (.png / .jpg) | GPU 实时渲染生成 |
| 内容 | 固定不变 | 每帧重绘 |
| 用途 | 模型贴图、UI | 镜面反射、小地图、迷雾 |
本系统用到两张 RT:
| RT 名称 | 尺寸 | 记录内容 | 类比 |
|---|---|---|---|
_WallDepthTex | 720 × 1 | 每个角度方向上最近墙壁距离 | 一条环形"测距雷达" |
_VisionMaskTex | 512 × 512 | 场景中每个点能否被看见 | 俯视图上的可见性标记 |
你站在房间正中央,拿着激光测距仪原地转一圈(360°),每 0.5° 测量到最近墙壁的距离。把结果排成一行像素 → 这就是极坐标深度图。
因为纹理的 U 轴 = 角度,像素值 = 距离,正是极坐标系的两个维度。
| Shadow Mapping(阴影) | 我们的方案(视野遮挡) |
|---|---|
| 从 光源 出发 | 从 玩家 出发 |
| 记录各方向最近 物体 距离 | 记录各方向最近 墙壁 距离 |
| 距离 > 记录 → 在阴影中 | 距离 > 记录 → 被墙壁遮挡 |
俯视角的遮挡全在 XZ 平面。从一点描述各方向最近障碍只需角度 → 距离映射,一维足矣。
GPU 硬件 ZTest Less + ZWrite On → 同一角度方向有多面墙时,自动保留距离最小(最近)的,无需额外代码。
同一角度方向上先写入远墙 0.7,再写入近墙 0.3。ZTest Less 只保留 0.3。
点击 "写入远墙 / 写入近墙" 按钮观察深度缓冲的变化。
当一面墙横跨 350° ~ 10°,端点 U = 0.97 与 U = 0.03,GPU 直接光栅化会反方向遍历整条纹理——产生错误。
比较距离时加小偏移 + 0.005,防止浮点精度导致的"自遮挡"(紧贴墙壁前方的像素被误判为在墙后)。
对 512×512 RT 的每个像素依次做三重检测。只要任意一项不通过就涂黑(不可见)。
两个单位向量的点积 = 夹角余弦:
dot(forward, dir) = cos(θ)
cos 在 [0°, 180°] 递减,所以 cos(θ) ≥ cos(halfAngle) 等价于 θ ≤ halfAngle。
💡 只需一次 dot() 运算,不需要昂贵的 acos()。C# 侧预计算 cos(halfAngle) 传入 Shader。
// 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)
→ 被遮挡,涂黑
clip(value);
// value < 0 → 丢弃这个像素(discard)
// value ≥ 0 → 正常渲染
// 只需两行
#include "Assets/FogOfWar/Shaders/VisionClip.hlsl"
// 在 Fragment 函数最顶部:
VisionHardClip(IN.positionWS); // 不可见 → discard
_WallDepthTex (720×1)_VisionMaskTex (512×512)
| 概念 | 作用 |
|---|---|
overrideMaterial | 强制所有墙壁使用 WallDepth Shader(忽略原始材质) |
SetGlobalTexture | 让 _VisionMaskTex 变成全局可访问,任何 Shader 自动绑定 |
Blitter.BlitTexture | "画一个全屏矩形"渲染操作,用于生成 Vision Mask |
下面的综合演示将所有步骤串联:在场景中拖动鼠标控制玩家朝向,实时观察深度图和 Vision Mask 的生成过程,以及敌人的可见性变化。
解耦:Vision Mask 是通用数据,敌人、地面、小地图都可直接采样,不需要在每个 Shader 中重复逻辑。
性能:Vision Mask 在 512×512 上只算一次,之后所有敌人只需一次简单纹理采样。
覆盖 100×100 m 场景时,每像素 ≈ 0.2 m 精度。加上 Bilinear 过滤 + 可选模糊,视觉效果柔和自然。需要更高精度可提升到 1024×1024(仅多 ~0.3 ms)。
距离值为连续浮点数(如 0.533),R16F 约 3 位有效数字,归一化 [0,1] 距离足够。仅 2 字节/像素,带宽极小。
两张 RT 每帧重新生成,使用最新的玩家位置和朝向。完全实时更新,无需额外处理。
不加模糊 → 视野边缘硬切。加模糊后,0→1 有几个像素的平滑过渡,视觉更舒适。
每个视野源各自生成 _WallDepthTex,VisionMaskPass 中 Additive Blend 叠加。敌人 Shader 无需修改。
总计 ≈ 0.3~0.6 ms/帧(1080p 中端 GPU)。深度图 720×1 几乎免费;全屏 Blit 512² ~0.1 ms;可选模糊 ~0.1 ms。
可以。在地面 Shader 中采样 _VisionMaskTex,color.rgb *= lerp(0.15, 1.0, vis)。视野外区域变暗为 15% 亮度。
| 术语 | 英文 | 含义 |
|---|---|---|
| 渲染纹理 | Render Texture (RT) | GPU 实时绘制的纹理 |
| 极坐标 | Polar Coordinates | 角度 + 距离的坐标系 |
| 深度图 | Depth Map | 各方向最近表面距离 |
| 阴影映射 | Shadow Mapping | 用深度图判断阴影 |
| 深度测试 | Z-Test | GPU 硬件只保留最近像素 |
| 裁剪 | Clip / Discard | Shader 中丢弃像素 |
| Blit | Blit | "画全屏矩形"渲染操作 |
| 点积 | Dot Product | 向量运算,单位向量时 = cos(夹角) |
| 自遮挡 | Shadow Acne | 浮点精度导致表面误判为阴影 |
| 阴影偏移 | Shadow Bias | 小偏移防止自遮挡 |