SteamVR媒体播放器分析(一):UI 渲染与用户交互

最近对 SteamVR 媒体播放器比较感兴趣,简单记录下其技术细节和实现方式(顺带看看它和之前本人从业做过的播放器有何不同)。

这是第一篇笔记,主要描述了 SteamVR 媒体播放器的 UI 渲染和交互事件响应的实现。
本篇只讨论 UI 的渲染,至于视频 3D 渲染及视频 2D 窗口镜像、文件列表等,后续文章再写。

UI 显示与渲染

这是 SteamVR 媒体播放器的 Desktop 2D 窗口图(左)与 VR 内 3D 视图(右)。

虽然看上去有两套交互方式与显示维度都不同的 UI(即一套 2D 窗口 UI 和另一套 VR 3D 空间 UI),但实际程序中只有一套 2D UI。

当然,这套 2D UI 就是用 UGUI 拼的。

如何同时在 2D 窗口 与 3D 空间中显示一份 UI?

反编译可发现,媒体播放器在 Unity Player 的逻辑帧 + 渲染帧执行结束时,会通过简单的 Blit 将绘制的内容拷贝到 3D 空间面片的 RT 上:

// 在 WaitForEndOfFrame 之后调用
// = 在逻辑帧 + 渲染帧完成的情况下执行这些代码来拷贝渲染结果
this.m_lastFrameGuiTextureCommandBuffer = new CommandBuffer();
this.m_lastFrameGuiTextureCommandBuffer.name = "m_lastFrameGuiTextureCommandBuffer";
this.m_lastFrameGuiTextureCommandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, this.m_lastFrameGuiTexture);
Graphics.ExecuteCommandBuffer(this.m_lastFrameGuiTextureCommandBuffer);

CommandBuffer 的 Blit 与 Graphic 的 Blit 可以理解为相同的作用,都是用来拷贝内容。

用 OpenGL 类比的话,Unity 的 Blit 就相当于下面三个步骤:

glBindFramebuffer(GL_READ_FRAMEBUFFER, CurrentActiveFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, lastFrameGuiTextureFBO);
glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, dstWidth, dstHeight,
                  GL_COLOR_BUFFER_BIT, GL_NEAREST);
// glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
// glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

那有人就要问了,为啥拷贝的当前 Target 没有左右眼摄像机的图像呢?

这个问题很好解答:

VR 头盔上屏,大多是在左右眼相机渲染时直接渲到 XR SwapChain 图像上。

虽然一些 VR SDK 会默认让 2D 窗口显示一些内容(如旧版 Steam VR SDK 默认为 LeftEye 或 RightEye 的镜像,某些三方 SDK 默认配置为黑屏,以及新版 Unity 的 XRSettings.gameViewRenderMode API),但这些内容即使有,也被全屏且不透明的 UI 盖住了。

因此,简单的一个 CommandBuffer 就将 2D UI 渲染结果拷贝到了 3D 面片中,且没什么损耗(仅多了一张 RT,一次 GPU 端拷贝,可忽略不计)。

但这也有一个小缺点:VR 中的 3D UI 面片刷新会慢 1 帧。原因很简单:我们在渲染帧结束后才更新的 3D UI 面片 RT。

不过,由于 VR 的渲染帧率都在 72 帧或 90 帧以上,小缺点无伤大雅。

UI 的用户交互

和其他 VR 播放器相比,SteamVR 媒体播放器最大的交互优点是:即可以 VR 手柄操作,也可以鼠标操作,且鼠标操作时会在 VR 中显示鼠标箭头位置。

鼠标操作与事件分发(2D 窗口 UI 交互)

直接走 Unity 默认的那套 EventSystem + InputModule + GraphicRaycaster 事件分发,无需多言。

VR手柄操作(伪 · VR 3D UI 交互)

三方 VR SDK 常见的 3D UI 适配方式是写一套 3D 空间下的 CustomXRInputModule + CustomXRGraphicRaycaster。

但是,SteamVR 媒体播放器的 3D UI 并不是真实的 UI Canvas,而是一个毫无意义的贴图面片,因此 CustomXRInputModule + CustomXRGraphicRaycaster 在此处帮助不大。

Valve 的解决方法很简单,将手柄 Pose 和面片做一次 Raycast,将交点坐标转化为 Screen 坐标,然后调用 Windows user32.dll 中的接口,强制设置 Windows 鼠标位置,并根据手柄按键信息分发鼠标事件:

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetCursorPos(int X, int Y);

[DllImport("user32.dll")]
private static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo);

这一通操作相当于模拟鼠标移动和点击行为,而 UI 事件触发自然也就走上面的鼠标事件分发流程。

不得不说,这个方法虽然简单粗暴,却很完美的解决了问题。

鼠标位置在 VR 展现

VR 面片上会显示鼠标的位置,这个也很简单。

Windows 的 user32.dll 中有个接口可以拿到鼠标位置:

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetCursorPos(out MouseOperations.MousePoint lpMousePoint);

将鼠标位置做两次转换,先将鼠标从 Windows 屏幕坐标转化为 UICanvas 坐标系下的坐标,再从 UICanvas 坐标系转换到 3D 面片坐标系下坐标。

坐标已经有了,之后就是创建个鼠标 Texture 面片,限制一下坐标边界条件,每帧更新它的位置即可。

一个衍生的现象:
当鼠标移出媒体播放器窗口 或者 媒体播放器窗口被其他应用覆盖时,VR 中仍可见到鼠标,但此时鼠标点击等行为无反应。
这是因为鼠标位置是通过 user32.dll 获取并映射计算的,永远都有值;而 Windows 只会向聚焦的窗口发送鼠标事件,因此应用收不到鼠标操作事件。

梓喵出没博客(azimiao.com)版权所有,转载请注明链接:https://www.azimiao.com/9864.html
欢迎加入梓喵出没博客交流群:313732000

发表评论

*

*