SteamVR(HTC Vive) Unity插件深度分析(二)

发表于2017-04-07
评论1 3.6k浏览

2.   openvr_api.cs

位于Plugins/openvr_api.cs。这个文件基本上是对C++ SDK中的openvr.hC#翻译(注:从Unity Asset Store里面下载的应该是最新稳定版,但在githubmaster分支上才是最新的,githubUnity插件位于unity_package目录下),而且这个翻译是自动完成的,在openvr_api.cs文件的最开头有注释说这个文件是自动生成的。

 

首先也有对openvr_api.dll中导出函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OpenVRInterop
{
   [DllImportAttribute("openvr_api", EntryPoint = "VR_InitInternal")]
   internal static extern uint InitInternal(ref EVRInitError peError, EVRApplicationType eApplicationType);
   [DllImportAttribute("openvr_api", EntryPoint = "VR_ShutdownInternal")]
   internal static extern void ShutdownInternal();
   [DllImportAttribute("openvr_api", EntryPoint = "VR_IsHmdPresent")]
   internal static extern bool IsHmdPresent();
   [DllImportAttribute("openvr_api", EntryPoint = "VR_IsRuntimeInstalled")]
   internal static extern bool IsRuntimeInstalled();
   [DllImportAttribute("openvr_api", EntryPoint = "VR_GetStringForHmdError")]
   internal static extern IntPtr GetStringForHmdError(EVRInitError error);
   [DllImportAttribute("openvr_api", EntryPoint = "VR_GetGenericInterface")]
   internal static extern IntPtr GetGenericInterface([In, MarshalAs(UnmanagedType.LPStr)] string pchInterfaceVersion, ref EVRInitError peError);
   [DllImportAttribute("openvr_api", EntryPoint = "VR_IsInterfaceVersionValid")]
   internal static extern bool IsInterfaceVersionValid([In, MarshalAs(UnmanagedType.LPStr)] string pchInterfaceVersion);
   [DllImportAttribute("openvr_api", EntryPoint = "VR_GetInitToken")]
   internal static extern uint GetInitToken();
}


 

我们再来看一下对VR接口类的定义,与C++的定义是类似的,不过,显然C#有一种机制将C++的类定义转换成C#的类定义。以IVRSystem为例,C++的定义为:

1
2
3
4
5
6
7
8
9
class IVRSystem
{
public:
 
    /** Suggested size for the intermediate render target that the distortion pulls from. */
    virtual void GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) = 0;
    ...
 
}


对应的C#定义为:

1
2
3
4
5
6
7
8
9
10
11
[StructLayout(LayoutKind.Sequential)]
public struct IVRSystem
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    internal delegate void _GetRecommendedRenderTargetSize(ref uint pnWidth, ref uint pnHeight);
 
    [MarshalAs(UnmanagedType.FunctionPtr)]
    internal _GetRecommendedRenderTargetSize GetRecommendedRenderTargetSize;
 
    ...
}


 

这里有一些语法点,我们先不管,我们先来探究一下,因为openvr_api.dll中导出的VR_GetGenericInterface返回的是C++的类指针,是不能直接转化为上面的C#的结构(或者类)对象的。它是如何转换的呢?我们再来看一下openvr_api.cs中的一个转换类CVRSystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CVRSystem
{
    IVRSystem FnTable;
 
    internal CVRSystem(IntPtr pInterface)
    {
          FnTable = (IVRSystem)Marshal.PtrToStructure(pInterface, typeof(IVRSystem));
    }
 
    public void GetRecommendedRenderTargetSize(ref uint pnWidth,ref uint pnHeight)
    {
          pnWidth = 0;
          pnHeight = 0;
          FnTable.GetRecommendedRenderTargetSize(ref pnWidth,ref pnHeight);
    }
 
    ...
}

 

我们再来看一下对CVRSystem的使用,在Init函数中间接调用的:

1
2
3
4
5
var eError = EVRInitError.None;
var pInterface = OpenVRInterop.GetGenericInterface(FnTable_Prefix+IVRSystem_Version, ref eError);
 
if (pInterface != IntPtr.Zero && eError == EVRInitError.None)
    m_pVRSystem = new CVRSystem(pInterface);


显然,OpenVRInterop.GetGenericInterface就是上面定义的dll中的导出函数VR_GetGenericInterface,而它的返回值是相应C++接口类的指针。可以看到把它作为参数传给了CVRSystem构造函数并创建了它。CVRSystem构造函数中最重要的是下面这句:

FnTable =(IVRSystem)Marshal.PtrToStructure(pInterface, typeof(IVRSystem));

虽然不知道Marshal.PtrToStructure实现细节,但从名字上看意思是将一个C++的指针(实际上就是C++类对象内存起始地址)转换成C#结构体。因为C++对象的内存结构实际上就是成员变量+函数指针,而由于接口类定义的全是函数,因此整个C++对象就是一个函数指针表,这也是为什么成员变量名字定义为FnTable的原因。

 

从上面的转换关系可知,最终对外提供接口的是CVRxxx类,它封装了所有的IVRxxx调用,IVRxxx可以将相关的C#调用转换成对C++对象函数指针(即内存地址)的调用。

 

C++的版本中,对外并没有暴露VR_InitInternalVR_ShutdownInternal这两个初始化及释放函数,而是提供了vr::VR_Initvr::VR_Shutdown两个封装函数。在C#中,有着完全相同的实现,它提供了OpenVR.InitOpenVR.Shutdown两个静态函数封装,实现逻辑与C++版本是一样的。这里也把代码贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 返回值为IVRSystem接口的封装类CVRSystem对象,并通过peError引用参数返回错误代码
public static CVRSystem Init(ref EVRInitError peError, EVRApplicationType eApplicationType = EVRApplicationType.VRApplication_Scene)
{
    // 先调用内部的InitInternal来做实际的初始化。OpenVR.InitInternal是对 openvr_api.dll中的导出函数VR_InitInternal的封装
    VRToken = InitInternal(ref peError, eApplicationType);
 
    //OpenVRInternal_ModuleContext中保存了所有的接口类的对象,这里先清空
    OpenVRInternal_ModuleContext.Clear();
 
    if (peError != EVRInitError.None)
       return null;
 
    // 这个函数会返回IVRSystem对象,这里先判断这个接口是否有效
    bool bInterfaceValid = IsInterfaceVersionValid(IVRSystem_Version);
    if (!bInterfaceValid)
    {
       // 如果无效,则释放VR系统
       ShutdownInternal();
       peError = EVRInitError.Init_InterfaceNotFound;
       return null;
    }
 
    // 返回OpenVR.System。这是个存取属性,会通过VR_GetGenericInterface获取  IVRSystem对象
    return OpenVR.System;
}

 

OpenVR.Shutdown就是简单地调用了VR_ShutdownInternal来实现的。

 

openvr_api.cs总结一下就是:总体上它是对C++版的SDK openvr.h的完全翻版,实现上,它有对openvr_api.dll导出函数的直接封装,封装类为OpenVRInterop,然后有对VR_GetGenericInterface获取到的C++接口类对象指针的直接封装(IVRxxx系列,比如IVRSystem)和间接封装(CVRxxx系列,比如CVRSystem)。CVRxxx系列是对外提供的接口,内部通过IVRxxx将托管调用转换为C++非托管调用。需要注意的是,还有一个类OpenVR,它是对OpenVRInterop的一个封装,里面进行了一些简单的数据类型的转换,比如将字符串类型(本质上也是内存指针)转换成C#string。同C++的实现中有一个COpenVRContext类封装了所有的VR接口对象的指针一样,C#中在OpenVR中也有一个子类COpenVRContext,它保存的就是相应的CVRxxx对象了。C++中有一个保存token的静态变量,并且通过VRToken函数来存取,当时就说了用函数来存取没有意义,还不如直接存取变量。这里C#里也有一个VRToken,不过这里就直接是静态变量了。同样,也有一个OpenVRInternal_ModuleContext来创建并返回COpenVRContext对象,在C++中它是一个存取函数,在C#里,同样变成了静态变量。另外一点要注意的是在通过GetGenericInterface获取相应的接口指针时,在C++的实现中是直接传入了“IVRSystem_001”这样的版本号,而在C#里面在版本号前面加了前缀“FnTable:”,即传入的是“FnTable:IVRSystem_001”这样的,估计是在openvr_api.dll中做了一些区分处理。

 

整理成表格为:

作用

OpenVRInterop

openvr_api.dll中的导出函数的直接封装

IVRxxx(比如IVRSystem

C#的方法调用转换成对openvr_api.dll中导出的C++对象指针的调用

CVRxxx(比如CVRSystem

对外提供OpenVR核心功能的接口,Unity插件中对OpenVR核心功能折访问都是通过该系列类来访问的

OpenVR

OpenVRInterop进一步封装,并提供简单的数据类型转换。同时相当于是C++版本的vr名字空间,提供一些全局静态数据及方法,包括一些常量定义、Init/Shutdown方法,所有CVRxxx接口对象

COpenVRContext

这个是OpenVR的内部类,主要提供对CVRxxx对象的保存及获取。在每次获取CVRxxx对象前都会去检查是否失效(token是否有效),如果已经失效,会重新获取

 

画成类图如下:

 

另外,由于Unity插件中的openvr_api.dll还导出了一些Unity前缀的显然是用于Unity的函数,这些没有在openvr_api.cs中导入,但会在其它cs文件中导入,见后面的分析。

 

2.1.  部分C#语法细节

对一些不太常见的C#语法做一些分析,看它是如何与C++的(非托管)dll及指针打交道的。

2.1.1.    StructLayout

IVRxxx类的定义都采用的是struct定义,前面都加了StructLayout声明,比如IVRSystem的定义格式为:

1
2
3
4
5
[StructLayout(LayoutKind.Sequential)]
public struct IVRSystem
{
    ...
}

 

首先类定义前的中括号[]是干嘛的?这个类似于java中的annotation。在C#里面,这个叫属性Attribute,可以用来描述程序集、类、方法、属性、事件、字段、参数、返回值等。这些描述信息可以在运行时取到,也可以在运行前检查。属性有些是系统预定义的,开发者也可以自定义,都需要从Attribute派生。

 

再来看StructLayout,它是CLRCommon Language Runtime)控制类或结构体的字段在托管内存中的物理布局的。这里的StructLayout(LayoutKind.Sequential)表示接下来的类或结构体定义中数据字段是按顺序存储的(这也是默认的情况)。这里之所以要使用Sequential类型的StructLayout,是因为后面会有Marshal.PtrToStructure将从非托管DLL中取到的C++的接口指针(连续内存,连续的函数指针表)转换成这里的C#结构体,要让函数一一对应,就得使用顺序存储

2.1.2.    UnmanagedFunctionPointer

这个看起来是对非托管函数指针的属性进行约束的,在IVRxxx结构体定义中,大量使用的是这种:

1
2
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void _GetRecommendedRenderTargetSize(ref uint pnWidth, ref uint pnHeight);


从名字上看,显然是指定调用约定的(所谓调用约定是指函数参数的入栈顺序及堆栈清理的规则等)。在做Windows开发时,经常可以看到cdeclstdcall两种调用约定。C/C++缺省是cdecl调用约定,但WindowsAPI都是stdcall约定,通常导出的APIdll中的导出函数)都会用stdcall约定。上面的例子中,就是指定了_GetRecommendedRenderTargetSize这个委托定义的函数调用规则为stdcall调用规则。

2.1.3.    MarshalAs

这个属性用于指定托管代码与非托管代码之间数据传递的规则。准确地说应该就是非托管的数据(变量)如何赋值(转换)给非托管代码变量。Marshal这个词在COM的时候就经常见到,COM或者说任何RPC都会有这样的一个过程,那就是把数据(包括对象)序列化,然后再反序列化,这个过程就是marshalunmarshal。比如:

1
2
[MarshalAs(UnmanagedType.FunctionPtr)]
internal _GetRecommendedRenderTargetSize GetRecommendedRenderTargetSize;


这里定义的Marshal规则为接下来的变量定义对应于非托管(C++)数据类型FunctionPtr,即函数指针。前面说过了在CVRxxx类中使用Marshal.PtrToStructure将获得的C++对象(函数指针表)转换成了IVRxxx结构体,应该和这里为每个函数指针(函数委托)的MarshalAs定义有关。

 

这里也整体说明一下IVRxxx(以IVRSystem为例)结构体中的一个变量的定义:

1
2
3
4
5
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void _GetRecommendedRenderTargetSize(ref uint pnWidth, ref uint pnHeight);
 
[MarshalAs(UnmanagedType.FunctionPtr)]
internal _GetRecommendedRenderTargetSize GetRecommendedRenderTargetSize;


这里就是定义了一个函数委托变量GetRecommendedRenderTargetSize,然后它可以与非委托的函数指针类型互相转化。这里使用了internal关键字,表示不能被外部(程序集)调用。调用的地方位于CVRSystem.GetRecommendedRenderTargetSize

1
2
3
4
5
6
public voidGetRecommendedRenderTargetSize(ref uint pnWidth,ref uint pnHeight)
{
    pnWidth= 0;
    pnHeight= 0;
    FnTable.GetRecommendedRenderTargetSize(refpnWidth,ref pnHeight);
}


其中FnTable为将C++接口指针Marshal.PtrToStructure后的IVRSystem对象:

FnTable =(IVRSystem)Marshal.PtrToStructure(pInterface, typeof(IVRSystem));

2.2.  IVRxxx

再单独说一下IVRxxx结构体定义,它的作用是定义与C++IVRxxx接口类虚函数表一一对应的方法委托变量。最重要的是通过[UnmanagedFunctionPointer(CallingConvention.StdCall)]定义了调用约定,通过[MarshalAs(UnmanagedType.FunctionPtr)]定义了Marshal规则。典型的示例:

1
2
3
4
5
6
7
8
9
10
11
[StructLayout(LayoutKind.Sequential)]
public structIVRSystem
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    internaldelegate void _GetRecommendedRenderTargetSize(ref uint pnWidth, ref uintpnHeight);
 
    [MarshalAs(UnmanagedType.FunctionPtr)]
    internal_GetRecommendedRenderTargetSize GetRecommendedRenderTargetSize;
     
    ...
}

 

2.3.  CVRxxx

CVRxxx的作用是将从openvr_api.dll中获取到的C++接口对象指针通过Marshal.PtrToStructure转换为C#结构体IVRxxx。同时对外提供封装接口,封装了所有IVRxxx提供的功能,将所有调用转换到对IVRxxx中委托的调用,同时,需要的话做相关数据类型的转换,因为部分C++数据类型与C#数据类型不能简单的一对一转换(主要是字符串类型)。典型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public classCVRSystem
{
   IVRSystemFnTable;
   internalCVRSystem(IntPtr pInterface)
   {
          FnTable= (IVRSystem)Marshal.PtrToStructure(pInterface,typeof(IVRSystem));
   }
 
   publicvoid GetRecommendedRenderTargetSize(ref uint pnWidth,ref uint pnHeight)
   {
          pnWidth= 0;
          pnHeight= 0;
          FnTable.GetRecommendedRenderTargetSize(refpnWidth,ref pnHeight);
   }
 
   ...
 
}


2.3.1.    字符串类型的转换

C++接口IVRSystem::GetPropErrorNameFromEnum为例,它在C++中的定义为:

virtual constchar *GetPropErrorNameFromEnum( ETrackedPropertyError error ) = 0;

因为返回的是char*类型,对应的C#中的定义变成了:

internal delegateIntPtr _GetPropErrorNameFromEnum(ETrackedPropertyError error);

即返回的是int指针类型(C/C++指针类型在C#中都使用IntPtr表示),在CVRSystem::GetPropErrorNameFromEnum中就要进行转换:

1
2
3
4
5
public stringGetPropErrorNameFromEnum(ETrackedPropertyError error)
{
    IntPtrresult = FnTable.GetPropErrorNameFromEnum(error);
    return Marshal.PtrToStringAnsi(result);
}


调用Marshal.PtrToStringAnsiIntPtr转换成了C# string

2.4.  枚举

C#中的枚举和C++中的枚举是一样的,比如:

1
2
3
4
5
6
7
8
public enumETrackingResult
{
    Uninitialized= 1,
    Calibrating_InProgress= 100,
    Calibrating_OutOfRange= 101,
    Running_OK= 200,
    Running_OutOfRange= 201,
}


2.5.  结构体

C++中的结构体几乎和类是等价的,C#中也是如此。因此C#中的结构体的定义和前面C#中对类的定义是一样的,为了能和C++中的结构体能互相转换,需要使用StructLayout属性,比如:

1
2
3
4
5
6
7
[StructLayout(LayoutKind.Sequential)]public struct VREvent_t
{
    publicuint eventType;
    publicuint trackedDeviceIndex;
    publicfloat eventAgeSeconds;
    publicVREvent_Data_t data;
}

 

上面这种使用了Sequential属性,即字段(在内存中)是按顺序排列的,另外还有一种结构需要特殊表示,那就是C++中的union。因为C#中没有union,因此只能用struct代替,因为联合的含义就是各字段是共享内存的,因此在C#的定义中使用[StructLayout(LayoutKind.Explicit)]+FieldOffset来显示指定每个字段的位置,要达到union的效果,就必须所有字段都使用FieldOffset(0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[StructLayout(LayoutKind.Explicit)]public struct VREvent_Data_t
{
    [FieldOffset(0)]public VREvent_Reserved_t reserved;
    [FieldOffset(0)]public VREvent_Controller_t controller;
    [FieldOffset(0)]public VREvent_Mouse_t mouse;
    [FieldOffset(0)]public VREvent_Scroll_t scroll;
    [FieldOffset(0)]public VREvent_Process_t process;
    [FieldOffset(0)]public VREvent_Notification_t notification;
    [FieldOffset(0)]public VREvent_Overlay_t overlay;
    [FieldOffset(0)]public VREvent_Status_t status;
    [FieldOffset(0)]public VREvent_Ipd_t ipd;
    [FieldOffset(0)]public VREvent_Chaperone_t chaperone;
    [FieldOffset(0)]public VREvent_PerformanceTest_t performanceTest;
    [FieldOffset(0)]public VREvent_TouchPadMove_t touchPadMove;
    [FieldOffset(0)]public VREvent_SeatedZeroPoseReset_t seatedZeroPoseReset;
    [FieldOffset(0)]public VREvent_Screenshot_t screenshot;
    [FieldOffset(0)]public VREvent_Keyboard_t keyboard;
}


2.6.  多维数组

由于C#中的数组与C/C++中的数组无论从本质和使用上都不太一致,C#中的数组是引用类型,需要动态new出来,因此原来C++中使用数组在C#中会直接使用N个数组元素来表示,比如,C++中的数组:

1
2
3
4
structHmdVector3_t
{
    floatv[3];
};


C#中变成:

1
2
3
4
5
6
[StructLayout(LayoutKind.Sequential)]public struct HmdVector3_t
{
    publicfloat v0; //float[3]
    publicfloat v1;
    publicfloat v2;
}


2.7.  指针类型

C++中的指针类型在C#中都用IntPtr表示,比如C++中的结构体定义:

1
2
3
4
5
struct RenderModel_TextureMap_t
{
    uint16_tunWidth, unHeight;
    constuint8_t *rubTextureMapData;
};


C#中变成:

1
2
3
4
5
6
7
[StructLayout(LayoutKind.Sequential)]
public struct RenderModel_TextureMap_t
{
    publicchar unWidth;
    publicchar unHeight;
    publicIntPtr rubTextureMapData;
}


2.8.  OpenVR

前面已经提到过,OpenVR类是对OpenVRInterOp类的封装,对OpenVRInterOp中直接导入的dll中的函数进行了封装,同时进行了必要的数据类型转换,比如:

1
2
3
4
public staticstring GetStringForHmdError(EVRInitError error)
{
    returnMarshal.PtrToStringAnsi(OpenVRInterop.GetStringForHmdError(error));
}


 

然后就是将C++中在vr名字空间中的所有全局定义(变量、函数)都移到了这个OpenVR类里(大概是因为C#中并没有纯粹的全局变量与函数的概念)。另外C#里同样有一个COpenVRContext类,它也是放到OpenVR里面作子类的。OpenVR里面还有N个静态变量,并且提供了get及(或)set属性来访问。这在C++里面都是通过一个函数来实现的。比如:

static uintVRToken { get; set; }

VRToken用来记录当前接口指针的tokentoken变化表示接口指针失效,需要重新获取。在C++中它的定义为:

1
2
3
4
5
inline uint32_t&VRToken()
{
    staticuint32_t token;
    returntoken;
}

 

又比如在C++中提到了一个很奇葩的实现的OpenVRInternal_ModuleContext,实际上就是COpenVRContext类的全局对象,实现起来却很奇葩。在这里的C#里就能看到是完全正常的实现了:

1
2
3
4
5
6
7
8
9
10
11
12
private staticCOpenVRContext _OpenVRInternal_ModuleContext = null;
staticCOpenVRContext OpenVRInternal_ModuleContext
{
    get
    {
        if(_OpenVRInternal_ModuleContext == null)
 
             _OpenVRInternal_ModuleContext= new COpenVRContext();
 
        return_OpenVRInternal_ModuleContext;
    }
}


然后就是对所有接口对象的封装了:

1
2
3
4
5
6
7
8
9
10
public static CVRSystem System { get { returnOpenVRInternal_ModuleContext.VRSystem(); } }
public static CVRChaperone Chaperone { get { returnOpenVRInternal_ModuleContext.VRChaperone(); } }
public static CVRChaperoneSetup ChaperoneSetup { get { returnOpenVRInternal_ModuleContext.VRChaperoneSetup(); } }
public static CVRCompositor Compositor { get { returnOpenVRInternal_ModuleContext.VRCompositor(); } }
public static CVROverlay Overlay { get { returnOpenVRInternal_ModuleContext.VROverlay(); } }
public static CVRRenderModels RenderModels { get { returnOpenVRInternal_ModuleContext.VRRenderModels(); } }
public static CVRApplications Applications { get { returnOpenVRInternal_ModuleContext.VRApplications(); } }
public static CVRSettings Settings { get { return OpenVRInternal_ModuleContext.VRSettings();} }
public static CVRExtendedDisplay ExtendedDisplay { get { returnOpenVRInternal_ModuleContext.VRExtendedDisplay(); } }
public static CVRScreenshots Screenshots { get { returnOpenVRInternal_ModuleContext.VRScreenshots(); } }


这里可以看到外部要使用OpenVR的接口的话,应该使用OpenVR类提供的接口,整个封装层次关系为(以IVRSystem为例):OpenVR->COpenVRContext->OpenVRInterop->CVRsystem->IVRSystem

 

最后,OpenVR里面提供了InitShutdown两个公开静态方法,这两个方法与C++中的VR_InitVR_Shutdown的实现完全是一样的。


2.8.1.    COpenVRContext

C++中的COpenVRContext一样,它保存了所有的VR接口类的对象,所有的这些接口类对象一起就称为Context。里面只有一个简单的逻辑就是每次使用某个接口前先通过VRToken判断接口是否有效,如果接口无效(接口为空或者token变了都表示接口无效),则调用OpenVRInterop.GetGenericInterface重新获取。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引