解释Passthrough API的YUV到RGB转换

解读

Passthrough API 是 Meta Quest、Pico 等国产 XR 头显在 Unity XR SDK 中开放的“透视”接口,用于把摄像头采集的 YUV420SP(NV12/NV21) 裸流实时铺到背景或材质上。面试官问“YUV 到 RGB 转换”,表面看是颜色空间公式,实则考察三点:

  1. 是否理解 GPU 采样效率(为什么不在 CPU 做);
  2. 是否熟悉 Unity 渲染管线 里如何把外部纹理塞进材质;
  3. 是否能在 Android 层 处理不同厂商的 YUV 排布差异(NV12 vs NV21)。
    答不出“在 Shader 里用 3 个纹理采样器一次完成”或“用 GL 外部纹理减少一次 memcpy”,会被直接判定“只写过 Demo”。

知识点

  1. YUV420SP 内存排布:Y 平面宽×高,UV 平面宽×高/2 交错存放;NV12 是 UV 交错,NV21 是 VU 交错。
  2. Unity 纹理格式限制:Android 下 GraphicsFormat.None 不能直接映射 YUV,需要 ExternalOES 纹理 + GL 外部采样器,或拆成 3 张 R8 纹理。
  3. Shader 实时转换公式(BT.601 全范围):
    R = Y + 1.402 * (V - 0.5)
    G = Y - 0.344136 * (U - 0.5) - 0.714136 * (V - 0.5)
    B = Y + 1.772 * (U - 0.5)
    
  4. 性能关键点:在 片元着色器 里做转换,避免回读 CPU;使用 Unity 2021.2+ 的 XRDisplaySubsystem.renderPass 可以把透视纹理直接绑定到相机背景,省一次 Blit。
  5. 国产设备适配坑:Pico 4 默认 NV21,Quest 2 默认 NV12,需在 Android 的 onFrameAvailable 回调里动态切换 UV 顺序,否则人脸会泛绿。

答案

“Passthrough API 返回的是 YUV420SP NV12/NV21 裸流,CPU 侧不做转换,而是把 Y、U、V 三个平面拆成 R8 纹理 上传到 GPU;在 Shader 里按 BT.601 全范围公式一次性算出 RGB。具体步骤:

  1. 通过 XRDisplaySubsystem.GetRenderPass 拿到外部纹理 ID,用 GL.ExternalTexture 创建 Unity 纹理对象;
  2. 在 Android 插件里根据厂商标记判断 NV12 还是 NV21,决定 UV 交错顺序;
  3. 片元着色器采样三张纹理:sampler2D _YTex_UTex_VTex,用 2 个 mad 指令 完成矩阵乘法,输出 float3 RGB;
  4. 为了省带宽,把 Y 纹理设为 Linear、UV 纹理设为 Half 尺寸,并开启 Vulkan 的 sampler_ycbcr_conversion(若驱动支持),可把 3 次采样合并成 1 次;
  5. 最后把转换结果直接写入相机背景,不经过 CPU ReadPixels,在 Quest 2 上 72 fps 单眼 2880×1700 仅消耗 0.8 ms GPU 时间。”

拓展思考

如果面试官追问“为什么不用 ComputeShader 做 YUV2RGB”,可以答:

  • Quest 2 的 Adreno 650 在 144 Hz 模式下 Compute 队列会被 XR 运行时抢占,反而不如片元着色器稳;
  • Vulkan 的 ycbcr sampler 已经固化转换矩阵,用 ComputeShader 再写一次等于重复劳动;
  • 国产头显 Pico 4E 提供了 HardwareBuffer 零拷贝通路,可以把 HAL3 的 YUV_420_888 AHardwareBuffer 直接映射为 Unity 外部纹理,连拆分纹理都省了,此时 ComputeShader 毫无意义。
    再补一句:“如果以后上 OpenXR 1.1 的 XR_FB_passthrough_sample_8bit 扩展,驱动会直接给 RGBA,Shader 里连转换都省了,我们要做的只是 fallback 到 CPU 解码 的兼容分支。”