解释Passthrough API的YUV到RGB转换
解读
Passthrough API 是 Meta Quest、Pico 等国产 XR 头显在 Unity XR SDK 中开放的“透视”接口,用于把摄像头采集的 YUV420SP(NV12/NV21) 裸流实时铺到背景或材质上。面试官问“YUV 到 RGB 转换”,表面看是颜色空间公式,实则考察三点:
- 是否理解 GPU 采样效率(为什么不在 CPU 做);
- 是否熟悉 Unity 渲染管线 里如何把外部纹理塞进材质;
- 是否能在 Android 层 处理不同厂商的 YUV 排布差异(NV12 vs NV21)。
答不出“在 Shader 里用 3 个纹理采样器一次完成”或“用 GL 外部纹理减少一次 memcpy”,会被直接判定“只写过 Demo”。
知识点
- YUV420SP 内存排布:Y 平面宽×高,UV 平面宽×高/2 交错存放;NV12 是 UV 交错,NV21 是 VU 交错。
- Unity 纹理格式限制:Android 下
GraphicsFormat.None不能直接映射 YUV,需要 ExternalOES 纹理 + GL 外部采样器,或拆成 3 张 R8 纹理。 - 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) - 性能关键点:在 片元着色器 里做转换,避免回读 CPU;使用 Unity 2021.2+ 的 XRDisplaySubsystem.renderPass 可以把透视纹理直接绑定到相机背景,省一次 Blit。
- 国产设备适配坑:Pico 4 默认 NV21,Quest 2 默认 NV12,需在 Android 的 onFrameAvailable 回调里动态切换 UV 顺序,否则人脸会泛绿。
答案
“Passthrough API 返回的是 YUV420SP NV12/NV21 裸流,CPU 侧不做转换,而是把 Y、U、V 三个平面拆成 R8 纹理 上传到 GPU;在 Shader 里按 BT.601 全范围公式一次性算出 RGB。具体步骤:
- 通过
XRDisplaySubsystem.GetRenderPass拿到外部纹理 ID,用 GL.ExternalTexture 创建 Unity 纹理对象; - 在 Android 插件里根据厂商标记判断 NV12 还是 NV21,决定 UV 交错顺序;
- 片元着色器采样三张纹理:
sampler2D _YTex、_UTex、_VTex,用 2 个 mad 指令 完成矩阵乘法,输出 float3 RGB; - 为了省带宽,把 Y 纹理设为 Linear、UV 纹理设为 Half 尺寸,并开启 Vulkan 的 sampler_ycbcr_conversion(若驱动支持),可把 3 次采样合并成 1 次;
- 最后把转换结果直接写入相机背景,不经过 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 解码 的兼容分支。”