SteamVR(HTC Vive) Unity插件深度分析(三)
3. SteamVR Unity插件的核心原理
这里先总结一下SteamVR的核心原理,先有个总体概念,有需要了再看后面的细节。这里主要讲两点:一是渲染流程是怎么样的?也就是我们左右眼看到图像是怎么生成的,还包括为什么在PC上还能看到一个同步的图像(这个称为伴随窗口,Companion Window),这个图像又是怎么生成的?二是VR的另一个很重要的概念是跟踪,那么在SteamVR插件中,头显、手柄的位置是如何跟踪和更新的?
3.1. 渲染细节
渲染的流程控制位于SteamVR_Render.RenderLoop里面,它是个协程,里面渲染流程如下图所示:
总的来说就是,如果有overlay(overlay可以认为是一种显示在最前面的2D界面),则先更新overlay。不过overlay与场景图像的渲染及显示是完全分开的,overlay的显示也是在SteamVR的运行时(甚至可能是硬件里)做的。然后就是分别渲染左右眼的图像,然后分别提交到插件(openvr_api.dll),插件可能会再进行相应的处理,当然也可能直接提交给运行时(因为看不到插件的实现,所以没法确认)
3.1.1. Overlay的实现
最终的实现当然是通过IVROverlay这个接口,可以直接使用这个接口通过将纹理句柄设置进去来更新overlay(在SteamVR_LoadLevel中就是这样做的)。也可以使用SteamVR_Overlay这个类来使用或管理overlay,做法就是共用同一个纹理,所有绘制到这个纹理上的内容都会以overlay的形式显示出来。
首先在SteamVR插件的Textures目录下有一个overlay.renderTexture,然后在SteamVR_Render的Inspector中需要指定这个纹理:
然后在所有希望绘制到overlay上的地方,都可以绘制到这个纹理上,比如在[Status]预制体中的用于显示统计信息的_Stats对象上,将这个overlay纹理设置成了相机的Render Texture:
最后在SteamVR_Overlay里面将这个overlay纹理通过IVROverlay设置进去可以了
3.1.2. 左右眼的渲染
在VR中左右眼看到的图像只是有细微的差别,所以针对左右眼的渲染流程是一样的,只有参数的细微差别,最重要的参数差别就是两只眼睛的位置和角度是有差别的,所以在渲染前,最重要的就是调整好渲染相机的位置和角度,这个动作是在SteamVR_Render.RenderEye中做的:
c.transform.localPosition= vr.eyes[i].pos;
c.transform.localRotation= vr.eyes[i].rot;
vr.eyes是SteamVR类中的一个左右眼姿态的数组,姿态更新通知的OnNewPoses里面根据头显的位置更新。
相机的渲染都是手动控制的,在SteamVR_Render.RenderEye中遍历所有的相机并调用其Render函数,在渲染前会将相机的targetTexture设为SteamVR_Camera中的_sceneTexture,也就是说,最终左右眼所能看到的所有内容都会画到这个_sceneTexture上面。
最终渲染得到的图像需要提交给SteamVR运行时(或者硬件),这个过程是在SteamVR_Camera.OnRenderImage中,OnRenderImage是对渲染的图像进行最后处理的地方,在SteamVR_Camera.OnRenderImage当中,使用了SteamVR_Blit这个shader来对图像做最后的处理,更重要的是在这里将左右眼的图像提交给了SteamVR运行时(或者硬件):
SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_SubmitL);
SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_SubmitR);
【重要发现】上面讲到针对两只眼的参数有细微差别,代码中也有体现,在SteamVR类中有一个eyes数组保存了两只眼睛的姿态信息(位置及角度),但实际通过log打印出来的时候会发现,其实这里面的信息都是0(对于位置信息为(0,0,0),对于旋转信息为(0,0,01)),也就是两只眼睛的参数是一致的,没有偏移!可以做个试验,屏蔽掉对右眼的渲染(屏蔽SteamVR_Render里面对RenderEye(vr,EVREye.Eye_Right)的调用,同时在渲染左眼的时候将左眼图像同时提交给右眼,会发现一点问题都没有)。同时如果先看过了C++版SDK的openvr.h的分析,也有印象IVRSystem有一个函数叫ComputeDistortion,是用来计算变形的。因为VR眼镜的透镜会造成看到的图像变形,为了纠正这个变形,需要对要显示的图像进行反变形,这样看到的图像才会没有变形。那在SteamVR的Unity插件里并看不到有对图像进行变形操作,我们可以很理所当然地想到,这个操作是在系统的某个地方(比如可能在openvr_api.dll这个插件中,可能在SteamVR运行时中,也有可能直接在硬件中)自动进行的。而左右眼的偏移我们也可以推测也是在系统的某个地方自动做的。那么,为什么OpenvR要留这样一个接口呢?因为OpenVR的目标是作为一个通用的VR接口,支持各种VR设备,比如Oculus(事实上也是支持的),那么不一定所有的VR平台都能够自动进行左右眼的偏移及变形的校正,那么在这些平台上,这些参数将会起到作用。
【Oculus实测】经实测Oculus的左右眼也没有区别,看来也是在OpenVR插件里面支持了
【注】在SteamVR的控制台菜单下面有一个“显示器映射(Headset Mirror)”菜单项,打开后可以同步看到头显中看到的左右眼的原始图像。
3.1.3. 伴随窗口
伴随窗口,Companion Window,是指PC上显示的一个与眼镜中看到的场景保持同步的窗口,就与使用Unity开发普通的PC游戏一样在PC上显示的窗口。本节所要分析的就是这个窗口的图像是怎么生成的。
也许你会认为,伴随窗口的图像是不用生成的,Unity的Camera能自动生成对应的图像。确实,使用Unity开发游戏,我们不需要关注图像的生成,Unity的Camera机制会自动生成图像。但在SteamVR插件中,由于要生成左右眼的图像(是进行离屏渲染的——即直接生成到纹理当中的,不是直接显示到屏幕上的),所有的添加了SteamVR_Camera脚本的原始Unity相机都会被禁用,所以是不能自动渲染出图像的。伴随窗口的图像是在SteamVR_GameView中绘制中,SteamVR_GameView会加到CameraRig当中的head上面,同时head上面还会把原始相机移动上面,也就是说head上面也有一个Unity Camera,不要以为这个Camera可以自动生成图像,看看它的配置:
由于Culling Mask设成了Nothing,这个相机缺省情况下什么都看不到。
伴随窗口的绘制是在SteamVR_GameView.OnPostRender里面做的,前面已经提到了SteamVR_Camera里面有一个_sceneTexture的纹理,在渲染左右眼的时候离屏渲染到的正是这个纹理,也就是这个纹理有我们从眼镜中看到的场景,因此在OnPostRender里面把这个纹理绘制出来就可以在伴随窗口中显示了。注意由于眼睛渲染的顺序问题,是先渲染左眼再渲染右眼,但左右眼是共享_sceneTexture的,因此伴随窗口中将只能看到右眼所能看到的场景,通过设置SteamVR_Render里面的Left Mask和Right Mask,比如将Left Mask设为Everything,将Right Mask设为Nothing,在伴随窗口中将看不到场景中的任何物体了。另外,如果设置了drawOverlay选项,也就是说在伴随窗口中显示overlay的界面,因为前面讲了overlay是绘制在overlay纹理中的,这里同样把overlay纹理绘制到伴随窗口就可以了。
3.2. 位置跟踪
硬件设备产生跟踪数据(比如Vive的Lighthouse技术),在SteamVR_Render.RenderLoop里面调用ICompositor.GetLastPoses不停获取,然后通过SteamVR_Utils.Event.Send发出“new_poses”通知事件,SteamVR_TrackedObject会挂到所有的跟踪设备Unity对象上,比如头显所在的Camera head,手柄所在的Controller,它会监听“new_poses”事件,从而可以把设备的位置更新到跟踪设备的Unity对象上。
这里,SteamVR插件并没有从C++直接调用C#接口方式的回调,而是采用在SteamVR_Render.RenderLoop里面主动调用ICompositor.GetLastPoses获取的方式。这种方式是合理的,因为RenderLoop里面每帧循环一次,每帧更新位置就可以了,采用底层通知的方式,可能会更及时,但没有意义。SteamVR_Utils里面有一个简单的通知系统,通过SteamVR_Utils.Event.Listen监听事件,通过SteamVR_Utils.Event.Send发出事件广播,实际上是通过C#委托链来简单实现的