ARCore 中文文档 下载 App

遮挡渲染让虚拟物体真正融入环境

什么是遮挡

没有遮挡的 AR 应用里,虚拟物体永远"盖在"所有真实物体上面——你放的虚拟恐龙会穿过桌子腿、穿出墙壁、盖在人的头上。这会让 AR 体验看起来非常"假"。

遮挡(Occlusion)的目标是:让虚拟物体出现在真实物体的正确位置关系上——桌子上的虚拟杯子被手挡住、虚拟龙的尾巴被墙遮住、人站在虚拟电视前面时电视就应该被人挡住。

遮挡渲染的基本原理

Depth API 每一帧都提供一张深度图(每个像素 = 该点到相机的距离)。虚拟物体渲染时也会生成自己的深度值(每个像素 = 虚拟物体到相机的距离)。两者对比:

深度关系渲染结果
虚拟物体距离 ≤ 真实物体距离虚拟物体在前面 → 正常渲染
虚拟物体距离 > 真实物体距离虚拟物体在后面 → 不渲染该像素(被真实物体遮挡)

这本质上是传统 3D 渲染中的 Z-Buffer(深度缓冲)测试——不同之处在于 AR 场景的背景是摄像头画面,不是 3D 场景,所以背景的深度值需要从 Depth API 读进来写入 Z-Buffer。

实现流程概览

每一帧的渲染流程:

1. 更新 Session → 获取最新 RGB 画面 + 深度图
2. 把 RGB 画面全屏渲染到屏幕
3. 把深度图的每个像素写入深度缓冲 Z-Buffer
4. 像普通 3D 渲染一样渲染虚拟物体
   → GPU 自动用 Z-Buffer 做遮挡测试
5. 最终画面 = RGB 背景 + 带遮挡的虚拟物体

第 3 步是关键:把 Depth API 的数据传给 GPU 深度缓冲。

OpenGL ES 中的实现思路

在 Android 原生开发中,使用 OpenGL ES:

// 1. Depth API 已经把深度数据渲染到了一个 FBO(Frame Buffer Object)
//    这个 FBO 的 color attachment 是深度纹理,depth attachment 是一个可写的深度缓冲

// 2. 在渲染虚拟物体之前,把深度缓冲切换成 Depth API 提供的深度缓冲
//    让后续的渲染调用用真实世界的深度做遮挡测试
glBindFramebuffer(GL_FRAMEBUFFER, depthApiFboId)
glDrawBuffers(GL_NONE)  // 不写颜色,只写深度

// 3. 渲染虚拟物体
glBindFramebuffer(GL_FRAMEBUFFER, defaultFboId)  // 切回屏幕 FBO
glDrawBuffers(GL_COLOR_ATTACHMENT0)

// 禁用 glDepthMask,防止虚拟物体覆盖真实深度
glDepthMask(GL_FALSE)
// 启用深度测试,用真实深度做遮挡判断
glEnable(GL_DEPTH_TEST)

// 渲染虚拟物体...
// GPU 自动对比虚拟物体的深度值和 Z-Buffer 中的真实世界深度值
// 在后方的像素会被丢弃

glDepthMask(GL_TRUE)

Unity AR Foundation 中的遮挡

Unity 把遮挡的麻烦封装好了——用一个组件就能启用:

方式一:XR Occlusion Manager(自动)

AR Foundation 4.0+ 内置了遮挡支持:

  1. 确保 AR Camera 上有 AROcclusionManager 组件
  2. 把 Occlusion Prefab 设为 Human Stencil 或 Environment
  3. Unity 会自动处理好所有深度缓冲的绑定和测试
// 运行时控制遮挡模式
var occlusionManager = GetComponent<AROcclusionManager>();

// 环境遮挡(用 Depth API 的真实世界深度)
occlusionManager.environmentOcclusionMode = EnvironmentOcclusionMode.Automatic;

// 人体遮挡(检测到的人脸/身体)
occlusionManager.humanOcclusionMode = HumanOcclusionMode.Automatic;

方式二:手动用深度纹理做自定义遮挡着色器

如果你需要精细控制(比如只让某些虚拟物体受遮挡),可以写自定义着色器:

// 片段着色器片段
Texture2D _OcclusionDepthTexture;
float4x4 _OcclusionDepthTransform;  // 深度纹理坐标变换矩阵

float PseudoRandom(float2 uv) {
    return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}

fixed4 frag(v2f i) : SV_Target
{
    // 把屏幕 UV 变换到深度纹理坐标
    float4 depthUV = mul(_OcclusionDepthTransform, float4(i.screenUV, 0, 1));
    float realDepth = _OcclusionDepthTexture.Sample(sampler, depthUV.xy).r;
    float virtualDepth = i.position.z;

    // 如果虚拟物体在真实物体后面,丢弃
    if (virtualDepth > realDepth)
        discard;

    return col;
}

遮挡的视觉质量问题

遮挡渲染并不是完美的,实际开发中会遇到以下问题:

边缘锯齿(Aliasing)

深度图分辨率较低时,遮挡边缘会有阶梯状锯齿。解决方案:

深度图质量差

光滑表面(白色墙壁、玻璃)的深度值噪声大,会导致:

解决方案:

ToF vs 被动深度的遮挡质量

深度来源遮挡质量原因
ToF 硬件好深度值噪声小、分辨率稳定
被动深度中~差纹理丰富时还行,光滑表面会失败
双摄中等精度和 ToF 相当但需要双摄硬件

只用平面检测做"伪遮挡"

如果设备不支持 Depth API,可以用平面检测做一种近似遮挡:

效果不如 Depth API 好,但比完全没有遮挡强。适合低端设备的降级方案。