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

发表于2017-05-04
评论1 5.2k浏览

10.          Scripts

10.1.     SteamVR.cs

这个脚本对CVRSystem(头显相关)、CVRCompositor(合成器相关)、CVROverlay接口的访问进行了一些封装,这三个接口可以认为是OpenVR最核心的功能。逐行来看:

 

它从IDisposable派生,作用是调用者可以使用using语法糖,在跳出using语句块时自动调用Dispose方法,以释放相应的资源,类似于C++局部对象在出作用域时自动析构的功能

public class SteamVR : System.IDisposable
{
   
// Use this to check if SteamVR is currently active withoutattempting
    // to activate it in theprocess.

这个类对象是个单例,用静态变量_instance保存实例。这个active属性来判断对象是否 实例化
    public static bool active { get { return _instance != null; } }

   
// Set this to false to keep from auto-initializing when callingSteamVR.instance.

_enable用于控制是否在访问SteamVR.instance时自动实例化。True为自动实例化,false       不会自动实例化。实际上可以根据字面意思来理解,就是启用还是禁用本脚本。
    private static bool _enabled = true;

通过enable属性来访问私有变量_enable
    public static bool enabled
    {
      
get { return _enabled; }
      
set
       {
           _enabled =
value;
          
if (!_enabled)

            如果将_enable设为false,还会调用SafeDispose自动(安全)销毁实例
              SafeDispose();
       }
    }

静态单一实例
    private static SteamVR _instance;

通过公有属性访问私有_instance
    public static SteamVR instance
    {
      
get
       {
#if UNITY_EDITOR
          
if (!Application.isPlaying)

            如果是在Unity编辑器中调用,如果没有在播放,则禁止实例化。也就是                      在编辑模式下是不会启动头显的
              return null;
#endif
           if (!enabled)

            如果脚本禁用了,也不实例化
              return null;

          
if (_instance == null)
           {

            如果尚未实例化,则创建实例
              _instance= CreateInstance();

             
// If init failed, thenauto-disable so scripts don't continue trying to re-initialize things.
              if (_instance == null)

                如果实例化失败,则禁用脚本,以避免不断地重试
                  _enabled= false;
           }

          
return _instance;
       }
    }

表示Unity是否本身支持VR
    public static bool usingNativeSupport
    {
      
get
       {

        5.0以上版本不使用Unity本身的VR支持,其它版本则看Unity是否支持VR
#if !(UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)

       returnUnityEngine.VR.VRDevice.GetNativePtr() != System.IntPtr.Zero;
#else
           return false;
#endif
       }
    }

创建实例
    static SteamVR CreateInstance()
    {
      
try
       {
          
var error = EVRInitError.None;
          
if (!SteamVR.usingNativeSupport)
           {

            不使用Unity本身的VR支持,但如果当前版本不是5.0以上版本,则出                        错。提示检查Player Settings配置,以及OpenVR是否加到了UnityVR                             SDK里面(前面的SteamVR_Settings.cs里面有将OpenVR加到Unity                      支持列表里面的做法)。实际上就是OpenVR不支持5.0以下的,要靠Unity                          自身的支持
#if !(UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)
              Debug.Log("OpenVRinitialization failed.  Ensure 'Virtual Reality Supported' is checked inPlayer Settings, and OpenVR is added to the list of Virtual RealitySDKs.");
              returnnull;
#else

            接下来就是初始化OpenVR的过程了。之前分析openvr_api.cs有分析过                         了,就是调用OpenVR.Init
              OpenVR.Init(ref error);
             
if (error != EVRInitError.None)
              {
                  ReportError(error);
                  ShutdownSystems();
                 
return null;
              }
#endif
           }

          
// Verify commoninterfaces are valid.

        这里通过尝试获取IVRCompositorIVROverlay接口判断是否可用,并没有                 实际使用
           OpenVR.GetGenericInterface(OpenVR.IVRCompositor_Version, ref error);
          
if (error != EVRInitError.None)
           {
              ReportError(error);
              ShutdownSystems();
             
return null;
           }

          
OpenVR.GetGenericInterface(OpenVR.IVROverlay_Version, ref error);
          
if (error != EVRInitError.None)
           {
              ReportError(error);
              ShutdownSystems();
             
return null;
           }
       }
      
catch (System.Exception e)
       {
          
Debug.LogError(e);
          
return null;
       }

    最后创建实例
       return new SteamVR();
    }

通过打印log的方法报告错误,几个与初始化相关的错误是直接打印的,其它错误(此 时初始化已经成功),则可以通过OpenVR.GetStringForHmdError来获取了
    static void ReportError(EVRInitError error)
    {
      
switch (error)
       {
          
case EVRInitError.None:
             
break;
          
case EVRInitError.VendorSpecific_UnableToConnectToOculusRuntime:
             
Debug.Log("SteamVR InitializationFailed!  Make sure device is on, Oculus runtime is installed, andOVRService_*.exe is running.");
             
break;
          
case EVRInitError.Init_VRClientDLLNotFound:
            
Debug.Log("SteamVR drivers notfound!  They can be installed via Steam under Library > Tools. Visit http://steampowered.com to install Steam.");
             
break;
          
case EVRInitError.Driver_RuntimeOutOfDate:
             
Debug.Log("SteamVRInitialization Failed!  Make sure device's runtime is up to date.");
             
break;
          
default:
             
Debug.Log(OpenVR.GetStringForHmdError(error));
             
break;
       }
    }

   
// native interfaces

本类主要封装了三个接口:CVRSystemCVRCompositorCVROverlay。只允许外部    get,不允许外部set
    public CVRSystem hmd { get; private set; }
   
public CVRCompositor compositor { get; private set; }
   
public CVROverlay overlay { get; private set; }

   
// tracking status

与跟踪相关的几个状态:是否正在初始化、是否正在测量、是否走出游玩区边界(也包       括失去跟踪)
    static public bool initializing { get; private set; }
   
static public bool calibrating { get; private set; }
   
static public bool outOfRange { get; private set; }

保存所有的跟踪设备(总共16个)是否连接的状态
    static public bool[] connected = new bool[OpenVR.k_unMaxTrackedDeviceCount];

   
// render values

渲染相关的参数

场景宽度
    public float sceneWidth { get; private set; }

场景高度
    public float sceneHeight { get; private set; }

宽高比
    public float aspect { get; private set; }

视场角
    public float fieldOfView { get; private set; }

这个是(最大)半视场角(非角度,准确地说是最大半个视口值)
    public Vector2 tanHalfFov { get; private set; }

左右两只眼的纹理映射坐标,是根据上面的tanHalFov算出来的。通常情况下应该就是 (0,0)(1,1),但实际情况是左右两只眼的视场并不对称,一只眼睛的视场中心定义也不    一定在中间。下图是网上搜到的不同VR设备的视场分布图(来自:       https://www.reddit.com/r/Vive/comments/4ceskb/fov_comparison/):

可以看到HTC Vive的是很特别的,上图是右眼的,它的左边会缺一块(它的镜片就是  这样的)。具体的数值可以通过IVRSystem::GetProjectionRaw获取,在网上找到一组数 值也可以看出Vive的视场是不对称的(来自       https://steamcommunity.com/app/358720/discussions/0/535150948617380074/,垂直方向上 基本是对称的,左右不对称):

左眼:left-1.396024 right 1.246448

右眼:left-1.247468 right 1.398274

画个示意图就是:

另外,注意圆圈上标的数字就是视场角的大小,4个设备的视场角大小也不太一样,基 本上是从80-120度之间,通常认为ViveOculus的视场角都是110度(理论上最佳的      视场角为120度)。

至于为什么Vive的视场左边缺一块,看下面的图片,Vive的镜片本来就是这样的形状  (左边为oculus,右边为vive):

说明: 1469505413_89_w607_h263
    public VRTextureBounds_t[] textureBounds { get; private set; }

这个是眼睛相对于头部的偏移矩阵,因为要提供立体视差,因此眼睛要相对于头部有一       定的偏移。参看openvr.h分析中的GetEyeToHeadTransform。但实际打印出来,两只眼     睛的位置及旋转是一样的,都是0——原因是跟实际的VR设备的实现有关的。如果    VR设备自身会处理两眼的视差,那在脚本中就不用处理了。像Vive这里打印出来的就 0。像Cardboard之类的简易VR眼镜,那就得自己处理了
    public SteamVR_Utils.RigidTransform[] eyes { get; private set; }

图形API的类型,OpenGLDirectX
    public EGraphicsAPIConvention graphicsAPI;

   
// hmd properties

头显的一些属性,数据未作缓存,没次调用都会重新获取,这样不好

这里vive实际打印出来的是“lighthouse”(看样子确实是指跟踪技术的名称)
    public string hmd_TrackingSystemName { get { return GetStringProperty(ETrackedDeviceProperty.Prop_TrackingSystemName_String);} }

这里打印出来的是“Vive MV
    public string hmd_ModelNumber { get { return GetStringProperty(ETrackedDeviceProperty.Prop_ModelNumber_String);} }

这里打印出来的是“LHR-E50E32C8”,应该是基站硬件的序列号
    public string hmd_SerialNumber { get { return GetStringProperty(ETrackedDeviceProperty.Prop_SerialNumber_String);} }
   
public float hmd_SecondsFromVsyncToPhotons { get { return GetFloatProperty(ETrackedDeviceProperty.Prop_SecondsFromVsyncToPhotons_Float);} }
   
public float hmd_DisplayFrequency { get { return GetFloatProperty(ETrackedDeviceProperty.Prop_DisplayFrequency_Float);} }

这个是获取跟踪设备id字符串的方法
    public string GetTrackedDeviceString(uint deviceId)
    {
      
var error = ETrackedPropertyError.TrackedProp_Success;

    注意做法都是先用空缓冲去调用,以获取需要的空间大小,分配空间然后再次调用
       var capacity = hmd.GetStringTrackedDeviceProperty(deviceId, ETrackedDeviceProperty.Prop_AttachedDeviceId_String, null, 0, ref error);
      
if (capacity > 1)
       {
          
var result = new System.Text.StringBuilder((int)capacity);
           hmd.GetStringTrackedDeviceProperty(deviceId,
ETrackedDeviceProperty.Prop_AttachedDeviceId_String,result, capacity, ref error);
          
return result.ToString();
       }
      
return null;
    }

IVRSystem.GetStringTrackedDeviceProperty进行封装,获取字符串属性
    string GetStringProperty(ETrackedDeviceProperty prop)
    {
      
var error = ETrackedPropertyError.TrackedProp_Success;
     
var capactiy = hmd.GetStringTrackedDeviceProperty(OpenVR.k_unTrackedDeviceIndex_Hmd,prop, null, 0, ref error);
      
if (capactiy > 1)
       {
          
var result = new System.Text.StringBuilder((int)capactiy);
           hmd.GetStringTrackedDeviceProperty(
OpenVR.k_unTrackedDeviceIndex_Hmd,prop, result, capactiy, ref error);
          
return result.ToString();
       }
      
return (error != ETrackedPropertyError.TrackedProp_Success) ? error.ToString() : "";
    }

获取浮点类型的属性值,对IVRSystem.GetFloatTrackedDeviceProperty进行封装
    float GetFloatProperty(ETrackedDeviceProperty prop)
    {
      
var error = ETrackedPropertyError.TrackedProp_Success;
      
return hmd.GetFloatTrackedDeviceProperty(OpenVR.k_unTrackedDeviceIndex_Hmd, prop, ref error);
    }

region指示符可以对代码进行分块折叠
    #region Event callbacks

下面这些用于事件回调

正在初始化
    private void OnInitializing(params object[] args)
    {

    第一个参数表示是否正在初始化
       initializing= (bool)args[0];
    }

正在测量
    private void OnCalibrating(params object[] args)
    {

    第一个参数表示是否正在测量
       calibrating= (bool)args[0];
    }

用户走出了跟踪范围
    private void OnOutOfRange(params object[] args)
    {

    第一个参数表示走出还是进入跟踪范围
       outOfRange= (bool)args[0];
    }

是否有设备连接
    private void OnDeviceConnected(params object[] args)
    {

    第一个参数表示跟踪设备的索引
       var i = (int)args[0];

    第二个参数表示是连接还是断开连接
       connected[i]= (bool)args[1];
    }

这个表示用户有新的姿势,比如走动、头部移动、手柄移动等。这个回调会被不停地调      
    private void OnNewPoses(params object[] args)
    {

    第一个参数为TrackedDevicePose_t数组(所有跟踪设备的姿态)
       var poses = (TrackedDevicePose_t[])args[0];

      
// Update eye offsets to account forIPD changes.

    在这里会更新眼睛相对于头的偏移,原因是IPD(瞳间距)可能会变化。对于Vive           即使调了IPD,这里得到的也是全0Vive不关注这个参数,由插件或者运行时,        或者硬件处理

       eyes[0] = new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Left));
       eyes[
1] = new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Right));

    首先遍历设备的连接状态
       for (int i = 0; i < poses.Length; i++)
       {
          
var connected =poses[i].bDeviceIsConnected;
          
if (connected != SteamVR.connected[i])
           {

            连接状态发生变化,发出连接状态发生变化通知。所以监听者都会收到,                            包括上面的OnDeviceConnected
              SteamVR_Utils.Event.Send("device_connected", i, connected);
           }
       }

    头显是第0个跟踪设备,永远排第一个
       if (poses.Length > OpenVR.k_unTrackedDeviceIndex_Hmd)
       {

        这里是对头显的一些状态进行判断
           var result = poses[OpenVR.k_unTrackedDeviceIndex_Hmd].eTrackingResult;

          
var initializing = result == ETrackingResult.Uninitialized;
          
if (initializing != SteamVR.initializing)
           {

            头显的初始化状态发生变化,发出通知
              SteamVR_Utils.Event.Send("initializing", initializing);
           }

          
var calibrating =
              result==
ETrackingResult.Calibrating_InProgress||
              result==
ETrackingResult.Calibrating_OutOfRange;
          
if (calibrating != SteamVR.calibrating)
           {

            头显的测量状态发生变化,发出通知
              SteamVR_Utils.Event.Send("calibrating", calibrating);
           }

          
var outOfRange =
              result==
ETrackingResult.Running_OutOfRange ||
              result==
ETrackingResult.Calibrating_OutOfRange;
          
if (outOfRange != SteamVR.outOfRange)
           {

            头显的跟踪状态发生变化,发出通知
              SteamVR_Utils.Event.Send("out_of_range", outOfRange);
           }
       }
    }

   
#endregion

SteamVR构造方法,私有,只能内部调用,进行单例实例化时由内部调用
    private SteamVR()
    {

    通过OpenVRSystem属性获取到CVRSystem对象。前面调用了OpenVR.Init          就会获取CVRSystem等各种接口了。这里可以看到对CVRSystem对象的命名为         hmd,也就是IVRSystem接口其实就是与HMD(头显)相关的接口了,这是最复              杂的一个接口
       hmd= OpenVR.System;

    这里虽然是打log,但会触发对hmd属性的一些获取
       Debug.Log("Connected to " + hmd_TrackingSystemName+ ":" + hmd_SerialNumber);

    同样获取到CVRCompositorCVROverlay接口
       compositor= OpenVR.Compositor;
       overlay =
OpenVR.Overlay;

      
// Setup render values

    推荐的屏幕渲染大小
       uint w = 0, h = 0;
       hmd.GetRecommendedRenderTargetSize(
ref w, ref h);
       sceneWidth = (
float)w;
       sceneHeight = (
float)h;

    获取左右眼的原始投影参数,也就是左右眼的视场参数,如图(大图见上面):

说明: {5F3473CD-51B5-45F0-9F70-A9688D5EF007}
      
float l_left = 0.0f, l_right = 0.0f, l_top = 0.0f, l_bottom = 0.0f;
       hmd.GetProjectionRaw(
EVREye.Eye_Left, ref l_left, ref l_right, ref l_top, ref l_bottom);

      
float r_left = 0.0f, r_right = 0.0f, r_top = 0.0f, r_bottom = 0.0f;
       hmd.GetProjectionRaw(
EVREye.Eye_Right, ref r_left, ref r_right, ref r_top, ref r_bottom);

    这里获取的就是最大的半视场大小(如果从理论上正中间来说,就是视场一半的大              小)
       tanHalfFov= new Vector2(
          
Mathf.Max(-l_left, l_right,-r_left, r_right),
          
Mathf.Max(-l_top, l_bottom,-r_top, r_bottom));

    创建左右眼的纹理映射坐标
       textureBounds= new VRTextureBounds_t[2];

    是以最大的视场坐标来定义的,这样纹理最多出现拉伸,而不会出现压缩
       textureBounds[0].uMin = 0.5f + 0.5f * l_left / tanHalfFov.x;
       textureBounds[
0].uMax = 0.5f + 0.5f * l_right / tanHalfFov.x;
       textureBounds[
0].vMin = 0.5f - 0.5f * l_bottom / tanHalfFov.y;
       textureBounds[
0].vMax = 0.5f - 0.5f * l_top / tanHalfFov.y;

       textureBounds[
1].uMin = 0.5f + 0.5f * r_left / tanHalfFov.x;
       textureBounds[
1].uMax = 0.5f + 0.5f * r_right / tanHalfFov.x;
       textureBounds[
1].vMin = 0.5f - 0.5f * r_bottom / tanHalfFov.y;
       textureBounds[
1].vMax = 0.5f - 0.5f * r_top / tanHalfFov.y;

#if (UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)

    这个看起来是OpenVR特有的接口。因为对于5.0以上的版本,会强制使用OpenVR             而不是Unity自带的VR。而SteamVR.Unity类就是对OpenVR特有(额外)接口              的一个封装
       SteamVR.Unity.SetSubmitParams(textureBounds[0], textureBounds[1], EVRSubmitFlags.Submit_Default);
#endif
       // Grow the recommendedsize to account for the overlapping fov

    这里是根据左右眼的视场的微小差异调整渲染屏幕的宽高,会略微放大一点。纹理              坐标的差值最大为1(理论值),实际值总是会小于但接近于1
       sceneWidth= sceneWidth / Mathf.Max(textureBounds[0].uMax - textureBounds[0].uMin, textureBounds[1].uMax - textureBounds[1].uMin);
       sceneHeight = sceneHeight /
Mathf.Max(textureBounds[0].vMax - textureBounds[0].vMin, textureBounds[1].vMax - textureBounds[1].vMin);

       aspect = tanHalfFov.x / tanHalfFov.y;

    计算视场角。Atan是反正切函数,关于反正切函数已经搞不清楚是啥意思了,正          切的意义还是很简单的,反正切是正切的反函数,反函数的概念已经能让我喝一壶           了,啥时候把高数再看一遍。关于反正切,下面一张图也许能够解释(来源:https://zh.wikipedia.org/wiki/%E5%8F%8D%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0):

Arctan是一个角度(上图中的θ)。下面使用了tanHalFov.y,注意这里算的是垂直  方向的视场角,在图形学中,貌似都是用垂直方向的视场角来做定义的,比如    OpenGL中的gluPerspective定义。视场角的计算关系如下图:

计算出来的单位是弧度,乘以Mathf.Rad2Deg换算成角度。一弧度是指圆周上长度 为半径的一段弧对应的角度。因此2*PI=360度。所以弧度与角度之间有固定的换      算关系,1弧度约等于57.3度。

    关于视场角,参看:http://www.csdn.net/article/a/2015-06-08/15825101?_t_t_t=0.7028455995023251http://www.hdpfans.com/thread-661540-1-1.html
       fieldOfView= 2.0f * Mathf.Atan(tanHalfFov.y) * Mathf.Rad2Deg;

    初始化眼睛相对于头部的偏移,在前面的OnNewPoses中会实时更新
       eyes= new SteamVR_Utils.RigidTransform[] {
          
new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Left)),
          
new SteamVR_Utils.RigidTransform(hmd.GetEyeToHeadTransform(EVREye.Eye_Right)) };

    设置图形API类型,看Unity的选择
       if (SystemInfo.graphicsDeviceVersion.StartsWith("OpenGL"))
           graphicsAPI =
EGraphicsAPIConvention.API_OpenGL;
      
else
           graphicsAPI= EGraphicsAPIConvention.API_DirectX;

    添加几个事件的监听回调,包括初始化、测量、走出边界、跟踪设备连接、用户姿             
       SteamVR_Utils.Event.Listen("initializing", OnInitializing);
      
SteamVR_Utils.Event.Listen("calibrating", OnCalibrating);
      
SteamVR_Utils.Event.Listen("out_of_range", OnOutOfRange);
      
SteamVR_Utils.Event.Listen("device_connected", OnDeviceConnected);

    上面4个通知都是在本类中发出的,下面的“new_poses”是在                     SteamVR_UpdatePoses中发出的
       SteamVR_Utils.Event.Listen("new_poses", OnNewPoses);
    }

析构方法。对于C#对象来说,和java对象一样,对象的回收也是由系统自动回收的,  平时并是不会调用的
    ~SteamVR()
    {
       Dispose(
false);
    }

Dispose方法是在出调用using语句块时自动调用的
    public void Dispose()
    {
       Dispose(
true);

    这个是告诉系统,GC时不要调用指定对象的Finalize方法了
       System.GC.SuppressFinalize(this);
    }

释放资源,相当于析构
    private void Dispose(bool disposing)
    {

    移除监听回调
       SteamVR_Utils.Event.Remove("initializing", OnInitializing);
      
SteamVR_Utils.Event.Remove("calibrating", OnCalibrating);
      
SteamVR_Utils.Event.Remove("out_of_range", OnOutOfRange);
      
SteamVR_Utils.Event.Remove("device_connected", OnDeviceConnected);
      
SteamVR_Utils.Event.Remove("new_poses", OnNewPoses);
           关闭OpenVR。这样感觉这个类不应该使用Dispose机制啊,因为它是一个最重要          的全局类,不应该使用using语法的,而应该小心的手动创建和销毁,这样才能保         证这个对象在整个进程的生命周期中有效
       ShutdownSystems();
       _instance =
null;
    }

关闭OpenVR
    private static void ShutdownSystems()
    {
#if (UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)

    5.0以上版本使用OpenVR插件,需要调用Shutdown来关闭
       OpenVR.Shutdown();
#endif
    }

   
// Use this interface to avoid accidentally creating theinstance in the process of attempting to dispose of it.

看不到注释说的保护作用,只是判断了_instance不为空才调用Dispose
    public static void SafeDispose()
    {
      
if (_instance != null)
           _instance.Dispose();
    }

#if (UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)
   
// Unityhooks in openvr_api.

这个是在5.0以上版本的OpenVR实现中,有多出几个导出函数。在最开始的时候已经 贴过图了,就是会在openvr_api.dll中会比C++的版本多出几个以UnityHooks为前缀的      导出函数:

TODO 这个函数的意义暂时不明
    public class Unity
    {
      
public const int k_nRenderEventID_WaitGetPoses = 201510020;
      
public const int k_nRenderEventID_SubmitL = 201510021;
      
public const int k_nRenderEventID_SubmitR = 201510022;
      
public const int k_nRenderEventID_Flush = 201510023;
      
public const int k_nRenderEventID_PostPresentHandoff= 201510024;

       [
DllImport("openvr_api", EntryPoint = "UnityHooks_GetRenderEventFunc")]
      
public static extern System.IntPtr GetRenderEventFunc();

       [
DllImport("openvr_api", EntryPoint = "UnityHooks_SetSubmitParams")]
      
public static extern void SetSubmitParams(VRTextureBounds_t boundsL, VRTextureBounds_t boundsR, EVRSubmitFlags nSubmitFlags);

       [
DllImport("openvr_api", EntryPoint = "UnityHooks_SetColorSpace")]
      
public static extern void SetColorSpace(EColorSpace eColorSpace);

       [
DllImport("openvr_api", EntryPoint = "UnityHooks_EventWriteString")]
      
public static extern void EventWriteString([In, MarshalAs(UnmanagedType.LPWStr)] string sEvent);
    }
#endif
}

10.2.     SteamVR_Camera.cs

这个就是OpenVR作为Unity插件最重要的一个脚本了。把它加到场景中的相机上,点击脚本Inspector中的Expand按钮,就能自动为场景添加OpenVR的支持了

 

脚本要求所在物体上有Camera组件

[RequireComponent(typeof(Camera))]
public class SteamVR_Camera : MonoBehaviour
{

使用SerializeField属性来指定私有成员可以被序列化。缺省情况下,所有的public  的非静态成员都是自动序列化的。注:属性是不会序列化的,比如下面的headoffset  origin等。关于SerializedField参看:    http://docs.unity3d.com/ScriptReference/SerializeField.html    http://docs.unity3d.com/Manual/script-Serialization.html
    [SerializeField]

这里head表示头部(的transform组件),也就是HMD头显,会根据头显的位置实时    更新
   
private Transform _head;
    public Transform head { get { return _head; } }

head老的变量命名是offset
    public Transform offset { get { return _head; } } // legacy

origin表示head的父亲,是整个Camera的顶层对象
    public Transform origin { get { return _head.parent; } }

因为这个脚本是加到原始的相机上的,而这个原始相机就是眼睛eye,所以这里没有  eye的相关定义

    [SerializeField]

ears是耳朵组件,用于接听声音
    private Transform _ears;
   
public Transform ears { get { return _ears; } }

这个是获取头部正前方的射线
    public Ray GetRay()
    {
      
return new Ray(_head.position, _head.forward);
    }

这个用于控制是否打开底层的网格渲染功能。打开后,物体都是以网格渲染的(看到的   就是网格),据Unity文档,移动版的OpenGL ES不支持这个功能。这个和Unity 场景视图中的Shading Mode菜单设置wireframe效果类似,不过那个只能用于编辑    视图
    public bool wireframe = false;

    [SerializeField]

这个是同SteamVR_Camera脚本同级置于原始相机上面的脚本,用于图像的翻转
    private SteamVR_CameraFlip flip;

   
#region Materials

这个是使用了前面SteamVR_Blit shader的材质。TODO 这个材质的效果暂不清楚
    static public Material blitMaterial;

#if (UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)

5.0以上版本使用OpenVR插件。下面这段话翻译一下:

使用一个共享离屏缓冲区来渲染场景。这个缓冲区比后台缓冲区要大一些,因为要考虑    变形校正。缺省的分辨率为在视场中心采用1:1的像素(貌似看到有视场中心分辨率   高,周围分辨率低的说法),但这个显示质量是可以通过下面这个     sceneResolutionScale来调整的,可以调高也可以调低,关键在于性能的平衡
    // Using a single sharedoffscreen buffer to render the scene.  This needs to be larger
    // than the backbuffer toaccount for distortion correction.  The default resolution
    // gives us 1:1 sizedpixels in the center of view, but quality can be adjusted up or
    // down using thefollowing scale value to balance performance.

SteamVR_Menu脚本中调的就是这个参数。在[Status]预制体中的_Stats物体上就   有这个脚本。如果有把[Status]预制体拖到场景中,可以通过Esc菜单显示一个设置 界面,里面就可以调这个参数,可以看到场景的放大与缩小
    static public float sceneResolutionScale = 1.0f;

场景纹理。如上面的注释所说,最终头显里左右眼看到的图像是通过离屏渲染生成的,    是直接渲染到一张纹理图片上的。这里就是那张目标纹理
    static private RenderTexture _sceneTexture;
   
static public RenderTexture GetSceneTexture(bool hdr)
    {
      
var vr = SteamVR.instance;
      
if (vr == null)
          
return null;

    根据scale设定得到实际屏幕分辨率
       int w = (int)(vr.sceneWidth *sceneResolutionScale);
      
int h = (int)(vr.sceneHeight * sceneResolutionScale);

    所谓aa即为antiAliasing,即反走样。它是在Edit->Progject                  Settings->Quality里面设置的,有0(禁用)、248四种设置。可以看到这       里强制使用反走样,即使在设置中禁用了反走样,也会使用1倍像素采样来抗锯齿
       int aa = QualitySettings.antiAliasing == 0 ? 1 : QualitySettings.antiAliasing;

    这里定义了RenderTexture的格式。HDR是相机中的术语(high-dynamic range       高动态光照渲染),这里的hdrCamera对象中的一个参数。ARGBHalf            每个颜色通道采用16位浮点数。这里的half是针对                          RenderTextureFormat.ARGBFloat而言的,它采用的是每个通道32位浮点。     Unity文档说,不是所有的显卡都支持ARGBHalf
       var format = hdr ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB32;

      
if (_sceneTexture != null)
       {
          
if (_sceneTexture.width !=w || _sceneTexture.height != h || _sceneTexture.antiAliasing != aa ||_sceneTexture.format != format)
           {

            参数发生变化,重新创建RenderTexture
              Debug.Log(string.Format("Recreating scenetexture.. Old: {0}x{ 1} MSAA={ 2} [{ 3}] New: { 4}x{ 5} MSAA={ 6} [{ 7}]",
                  _sceneTexture.width,_sceneTexture.height, _sceneTexture.antiAliasing, _sceneTexture.format, w, h,aa, format));

            通常C#java对象创建了都是不管的,直接赋为null即可。这里Unity             提供了强制销毁的方法
              Object.Destroy(_sceneTexture);
              _sceneTexture=
null;
           }
       }

      
if (_sceneTexture == null)
       {
           _sceneTexture =
new RenderTexture(w, h, 0, format);
           _sceneTexture.antiAliasing= aa;

          
// OpenVR assumesfloating point render targets are linear unless otherwise specified.

        颜色空间缺省为Linear,除非指定了为Gamma。注意这里调用了                 SteamVR.Unity.SetColorSpace,这是直接调到了openvr_api.dll中的接        
           var colorSpace = (hdr&& QualitySettings.activeColorSpace == ColorSpace.Gamma) ? EColorSpace.Gamma : EColorSpace.Auto;
          
SteamVR.Unity.SetColorSpace(colorSpace);
       }

      
return _sceneTexture;
    }
#else

对于使用Unity自身的VR支持来说,分辨率缩放因子是直接与系统参数关联的
    staticpublic float sceneResolutionScale
    {
       get { returnUnityEngine.VR.VRSettings.renderScale; }
       set {UnityEngine.VR.VRSettings.renderScale = value; }
    }
#endif

    #endregion

    #region Enable / Disable

    void OnDisable()
    {

    脚本禁用时从SteamVR_Render中移除SteamVR_CameraSteamVR_Render    控制渲染的核心类,它里面保存了一个所有的SteamVR_Camera列表,用于控制      手动离屏渲染
       SteamVR_Render.Remove(this);
    }

   
void OnEnable()
    {
      
// Bail if no hmd is connected
       var vr = SteamVR.instance;
      
if (vr == null)
       {

        初始化失败
           if (head != null)
           {

            head上的两个SteamVR组件禁用。在没有HMD的情况下运行示例就            可以看到这个现象
              head.GetComponent<SteamVR_GameView>().enabled = false;
              head.GetComponent<
SteamVR_TrackedObject>().enabled = false;
           }

          
if (flip != null)

            禁用SteamVR_CameraFlip
              flip.enabled= false;

           enabled =
false;
          
return;
       }
#if !(UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)
       //Convert camera rig for native OpenVR integration.

    Unity内置VR支持的情况,这种情况可以不关注
       vart = transform;
       if (head != t)
       {

        调用Expand建立所谓的相机骨骼,即将eyeearheadorigin关系建        立起来
           Expand();

        这里应该与OpenVR的不太一样,这里将eye的父亲直接设成了origin,那        eyehead是同级的
           t.parent= origin;

        这里又将head的第一个子对象的父亲设成了eye
           while(head.childCount > 0)
              head.GetChild(0).parent= t;

           // Keep thehead around, but parent to the camera now since it moves with the hmd
           // butexisting content may still have references to this object.

        head的父亲也设为了当前对象(eye
           head.parent= t;
           head.localPosition= Vector3.zero;
           head.localRotation= Quaternion.identity;
           head.localScale =Vector3.one;
           head.gameObject.SetActive(false);

        将当前对象设成了head,相对于headeye进行了交换
           _head= t;
       }

       if (flip != null)
       {

        还会销毁SteamVR_CameraFlip
           DestroyImmediate(flip);
           flip = null;
       }
#else
       // Ensure rig is properlyset up

    第一步也是建立骨骼
       Expand();

      
if (blitMaterial == null)
       {

        创建Blit材质。什么是Blit材质?最好看一下SteamVR_Blit这个shader        Unity中有一个Graphics.Blit方法,Blit的意思差不多就是拷贝图像(全         部或者一部分)
           blitMaterial= new Material(Shader.Find("Custom/SteamVR_Blit"));
       }

      
// Set remaining hmd specificsettings

    将头显的参数设置到原始相机上(原始相机作为头显的代理)
       var camera =GetComponent<Camera>();
       camera.fieldOfView = vr.fieldOfView;
       camera.aspect = vr.aspect;

    禁用鼠标事件
       camera.eventMask= 0;           // disable mouse events

    强制使用透视投影
       camera.orthographic= false;    // force perspective

    禁用相机(在运行后可以看到Inspector中的Camera前面的勾勾被去掉了),由       SteamVR_Render手动渲染
       camera.enabled= false;         // manually rendered bySteamVR_Render

       if (camera.actualRenderingPath != RenderingPath.Forward && QualitySettings.antiAliasing > 1)
       {

        这里就是readme.txt中所描述的,MSAAMultiSampleAnti-Aliasing         即多重采样反走样)只支持前向渲染路径,而SteamVRMSAA支持是通过          UnityQuality设置的。
           Debug.LogWarning("MSAA only supportedin Forward rendering path. (disabling MSAA)");
          
QualitySettings.antiAliasing = 0;
       }

      
// Ensure game view camera hdrsetting matches

    Head中也有一个Camerahead上的Camera就是PC上的伴随窗口
       var headCam =head.GetComponent<Camera>();
      
if (headCam != null)
       {

        将两个相机的参数(HDR和渲染路径)设为一致
           headCam.hdr= camera.hdr;
           headCam.renderingPath= camera.renderingPath;
       }
#endif
       if (ears == null)
       {

        在新的插件中,earseyes是在同一级的,并且会在Expand的时候自动创          建并赋值的,所以不会到这里来。这里在子节点中进行查找,应该是老插件(或           者说Unity自身的VR支持)中的情况
           var e =transform.GetComponentInChildren<SteamVR_Ears>();
          
if (e != null)
              _ears= e.transform;
        }

      
if (ears != null)

        设置SteamVR_Ears中的vrcam为当前SteamVR_Camera对象
           ears.GetComponent<SteamVR_Ears>().vrcam = this;

    添加到SteamVR_Render
       SteamVR_Render.Add(this);
    }

   
#endregion

    #region Functionality toensure SteamVR_Camera component is always the last component on an object
    确保SteamVR_Camera组件总是在最后,之所以要放到最后是因为希望它对渲染后的 图像的处理(OnRenderImage)是放到最后的
    void Awake() { ForceLast(); }
    临时记录public变量及SerializeField变量的值,用于在移动后恢复(移动会先 销毁再创建)。这里是static的,所以对象销毁后还存在
    static Hashtable values;

   
public void ForceLast()
    {
      
if (values != null)
       {

       values不为空表示因为要移动之前已经销毁了,当前对象是新创建的,下面        为那些需要序列化的变量重新赋值
           // Restore values on newinstance
           foreach (DictionaryEntry entry in values)
           {
             
var f = entry.Key as FieldInfo;
              f.SetValue(
this, entry.Value);
           }
           values =
null;
       }
      
else
       {
          
// Make sure it's thelast component

        获取所有组件
           var components =GetComponents<Component>();

          
// But first make surethere aren't any other SteamVR_Cameras on this object.

        销毁当前对象上其它的SteamVR_Camera组件
           for (int i = 0; i < components.Length; i++)
           {
             
var c = components[i] as SteamVR_Camera;
             
if (c != null && c != this)
              {
                 
if (c.flip != null)

                    引用的SteamVR_CameraClip也记得销毁
                     DestroyImmediate(c.flip);
                  DestroyImmediate(c);
              }
           }

        没有从一个数组里面删掉一个元素的方法吗?要重新获取一次?
           components= GetComponents<Component>();

#if !(UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)

       Unity原生的OpenVR估计不带SteamVR_CameraFlip
           if(this != components[components.Length - 1])
           {
#else
           if (this !=components[components.Length - 1]|| flip == null)
           {

            如果不是最后一个,或者没有SteamVR_CamereFlip
              if (flip == null)

                如果不存在SteamVR_CameraFlip,则添加之。                             SteamVR_CameraFlip的作用是将图像上下翻转过来,所以是很重               要的
                  flip= gameObject.AddComponent<SteamVR_CameraFlip>();
#endif
              // Store off values to berestored on new instance
              values= new Hashtable();
             
var fields =GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
             
foreach (var f in fields)
                 
if (f.IsPublic ||f.IsDefined(typeof(SerializeField), true))

                    保存publicSerializeField变量的值,也就是所有可以序                     列化的值都会保存起来,留着后面恢复(只需要保存可以序列化                   的值就可以了,因为像关闭场景再恢复也只是会保存可以序列化                   的值)
                     values[f]= f.GetValue(this);

            删掉当前对象,重新加载
              var go = gameObject;
              DestroyImmediate(
this);
              go.AddComponent<
SteamVR_Camera>().ForceLast();
           }
       }
    }

   
#endregion

    #region Expand / Collapseobject hierarchy

#if
UNITY_EDITOR

用于编辑器,判断是否展开(即是否完成构造相机骨骼),判断条件是head存在,并 head是当前(eye)的父节点
    public bool isExpanded { get { return head != null && transform.parent== head; } }
#endif

用于Hierarchy视图中的名字后缀
    const string eyeSuffix = " (eye)";
   
const string earsSuffix = " (ears)";
   
const string headSuffix = " (head)";
   
const string originSuffix = " (origin)";

这个是添加SteamVR插件前原始相机的名字,也是作为所有新添加节点的名字基准
    public string baseName { get { return name.EndsWith(eyeSuffix) ?name.Substring(0, name.Length -eyeSuffix.Length) : name; } }

   
// Object hierarchy creation to make it easy to parent otherobjects appropriately,
    // otherwise this getscalled on demand at runtime. Remaining initialization is
    // performed at startup,once the hmd has been identified.

Expand是正式构建VR相册骨骼结构的方法,构造完后是这样的:

构造之前是这样的:


    public void Expand()
    {

    首先将origin设为当前相机的父节点(就是如果已经存在父节点了,就不会创建    一个origin节点了)
       var _origin =transform.parent;
      
if (_origin == null)
       {

        如果没有父节点,则创建一个空对象
           _origin= new GameObject(name + originSuffix).transform;

       transform参数设为当前相机的参数,这里没有必要设置local参数了,因        为进入这个分支表明当前相机没有parent,设置local参数并没有作用,应         该取全局坐标的
           _origin.localPosition= transform.localPosition;
           _origin.localRotation= transform.localRotation;
           _origin.localScale= transform.localScale;
       }

      
if (head == null)
       {

        第一次head为空,则创建一个空对象作为head,同时添加                     SteamVR_GameViewSteamVR_TrackedObject组件
           _head= new GameObject(name + headSuffix, typeof(SteamVR_GameView), typeof(SteamVR_TrackedObject)).transform;

        head的父结点设为origin
           head.parent= _origin;

        继承当前相机的位置参数
           head.position= transform.position;
           head.rotation =transform.rotation;
           head.localScale =
Vector3.one;
           head.tag = tag;

        上面并没有看到添加Camerahead当中,但因为SteamVR_GameView在定          义时有使用RequireComponent依赖于Camera,所以Camera会自动添加
           var camera =head.GetComponent<Camera>();
           camera.clearFlags=
CameraClearFlags.Nothing;
           camera.cullingMask=
0;
           camera.eventMask =
0;
           camera.orthographic=
true;
           camera.orthographicSize=
1;
           camera.nearClipPlane=
0;
           camera.farClipPlane=
1;
           camera.useOcclusionCulling=
false;
       }

      
if (transform.parent != head)
       {

        head设为eye的父节点,前面已经将origin设为父节点,然后又将head          设为origin的子节点,这里再将head设为eye的父节点,这样一来整个相         机的骨骼就建立起来了
           transform.parent= head;

        重置当前的相对坐标,因为实际坐标已经赋值给origin/head
           transform.localPosition= Vector3.zero;
           transform.localRotation=
Quaternion.identity;
           transform.localScale=
Vector3.one;

          
while (transform.childCount> 0)

            如果eye有子节点,将其移到head下面。总之,就是把原来的相机当作           眼睛,将其它的关联关系全部移交给head就对了。可以认为head就是             原来的相机,而原来的相机就变成了眼睛
              transform.GetChild(0).parent = head;

        删掉GUILayer组件并添加到head上面。GUILayer依赖于Camera,是2D        UI层。所以所有通过UGUI绘制的界面将只会出现在伴随窗口里
           var guiLayer =GetComponent<GUILayer>();
          
if (guiLayer != null)
           {
              DestroyImmediate(guiLayer);
              head.gameObject.AddComponent<
GUILayer>();
           }

          
var audioListener =GetComponent<AudioListener>();
          
if (audioListener != null)
           {

            如果相机上面有AudioListener(缺省相机上面都有),则销毁它,同时              创建一个同级的ears对象用于作为AudioListener的宿主
              DestroyImmediate(audioListener);
              _ears=
new GameObject(name + earsSuffix, typeof(SteamVR_Ears)).transform;
              ears.parent= _head;
              ears.localPosition=
Vector3.zero;
              ears.localRotation=
Quaternion.identity;
              ears.localScale=
Vector3.one;
           }
       }

    在原始相机的名字后面加上“(eye)”后缀
       if (!name.EndsWith(eyeSuffix))
           name += eyeSuffix;
    }

折叠,也就是还原原始的相机配置
    public void Collapse()
    {

    先将父节点置空
       transform.parent= null;

      
// Move children and components fromhead back to camera.

    将原先添加到head下面的子节点移回来
       while (head.childCount > 0)
           head.GetChild(
0).parent = transform;

    GUILayer移回来
       var guiLayer = head.GetComponent<GUILayer>();
      
if (guiLayer != null)
       {
           DestroyImmediate(guiLayer);
           gameObject.AddComponent<
GUILayer>();
       }

    AudioListener移回来,删除ears
       if (ears != null)
       {
          
while (ears.childCount > 0)
              ears.GetChild(
0).parent = transform;

           DestroyImmediate(ears.gameObject);
           _ears =
null;

           gameObject.AddComponent(
typeof(AudioListener));
       }

      
if (origin != null)
       {

        如果原来相机就有父节点(此时它的后缀将不会有“(origin)”),简单将原       始相机的父节点重置,如果新增了origin节点,则重置父节点后删除
           // If we created theorigin originally, destroy it now.
           if (origin.name.EndsWith(originSuffix))
           {
             
// Reparent any childrenso we don't accidentally delete them.
              var _origin = origin;
             
while (_origin.childCount > 0)
                  _origin.GetChild(
0).parent = _origin.parent;

              DestroyImmediate(_origin.gameObject);
           }
          
else
           {
              transform.parent= origin;
           }
       }

    删除head
       DestroyImmediate(head.gameObject);
       _head =
null;

    去掉“(eye)后缀”
       if (name.EndsWith(eyeSuffix))
           name =name.Substring(
0, name.Length -eyeSuffix.Length);
    }

   
#endregion

#if
(UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)

   
#region Render callbacks

onPreRender是在相机即将渲染场景之前调用
    void OnPreRender()
    {
      
if (flip)

       SteamVR_CameraFlip的启用条件是当前Camera是最后渲染的Camera              depth参数最大)并且使用DirectX。原因大概是DirectXOpenGLY        坐标定义是反的,然后只需要在最后渲染的相机中将图像翻转过来就可以了
           flip.enabled= (SteamVR_Render.Top() == this && SteamVR.instance.graphicsAPI == EGraphicsAPIConvention.API_DirectX);

      
var headCam = head.GetComponent<Camera>();
      
if (headCam != null)

       head上的Camera也只在top camera上才启用(因为最终是通过               SteamVR_GameView直接将最后的图像绘制/渲染的)。实际通常就只有一个          相机了,所以总是启用的
           headCam.enabled= (SteamVR_Render.Top() == this);

      
if (wireframe)

        设置线框模式
           GL.wireframe = true;
    }

   
void OnPostRender()
    {
      
if (wireframe)

        每次在OnPreRender时设置线框模式,在OnPostRender取消线框模式
           GL.wireframe = false;
    }

OnRenderImage是在所有渲染都完成后的回调,可以用于修改最后生成的图像
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {

    OpenVR相机骨骼中只有一个eye相机,但VR两只眼睛输出的图像是有略微差别     的,从下面也可以看到,对两只眼睛有不同的处理。这里会针对两只眼睛调用两次,       这个控制是在SteamVR_Render里面做的
       if (SteamVR_Render.Top() == this)
       {
          
int eventID;
          
if (SteamVR_Render.eye == EVREye.Eye_Left)
           {
             
// Get gpu started onwork early to avoid bubbles at the top of the frame.
这里的SteamVR_Utils.QueueEventOnRenderThread会通过Unitynative插件机制通知到openvr_api.dll当中(参看:http://docs.unity3d.com/ScriptReference/GL.IssuePluginEvent.htmlopenvr_api.dll相当于是Unitynative插件。所以它与github上纯c++openvr_api.dll还是有些不同的),所以细节并不清楚。姑且当作上面的注释这样,它是通知底层启用gpu?从这个方法的名    字看是将一个事件加到渲染线程的事件队列中。而这里的事件是flush  它只针对左眼做一次,看起来像是在提交左右眼的图像帧前要做一个flush操作的意思。

SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_Flush);
              eventID=
SteamVR.Unity.k_nRenderEventID_SubmitL;
           }
          
else
           {
              eventID=
SteamVR.Unity.k_nRenderEventID_SubmitR;
           }

          
// Queue up a call on therender thread to Submit our render target to the compositor.

            发出一个(左/右眼)提交事件。TODO 为什么是在实际渲染前发出提交事件        呢?根据GL.IssuePluginEvent的解释                                http://docs.unity3d.com/ScriptReference/GL.IssuePluginEvent.html          这里的QueueEventOnRenderThread最终调用了Unity中的                     GL.IssuePluginEvent),这里提交的事件实际上会在这个方法结束后才会发         送到native插件当中。所以它叫做Queue以及放到前面也就可以理解了。

            加强注解:这两个左右眼submit事件是将渲染好的图像提交给硬件或者运行           时的核心。如果缺省这两个事件的提交,SteamVR会提示“无响应”。这应该          SteamVR Unity插件对提交的封装(实现在openvr_api.dll里面)。而        如果直接使用OpenVRC++版的SDK,则需要手动提交。可以看到在示例        hellovr_opengl示例中,有直接调用ICompositor::Submit提交渲染       帧的操作
           SteamVR_Utils.QueueEventOnRenderThread(eventID);
       }

        下面的过程很简单,就是将已经渲染好图像再做一次加工

        先将目标纹理设为RenderTarget,这样接下来绘制的东西都会体现在目标纹理     上。这个跟前面截屏的做法不太一样,之前用的是ReadPixels方法(其实是差不     多的,ReadPixels是将图像读取Texture2D里面)
       Graphics.SetRenderTarget(dest);

        将原始图像作为纹理
       SteamVR_Camera.blitMaterial.mainTexture= src;

      
GL.PushMatrix();

        使用正交投影
       GL.LoadOrtho();

        这个是设置材质上shader的通道,可以有多个通道,0是第一个
       SteamVR_Camera.blitMaterial.SetPass(0);

        画一个四边形
       GL.Begin(GL.QUADS);

        将原始图像作为纹理贴到绘制的四边形上,四边形大小为1x1
       GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-11, 0);
      
GL.TexCoord2(1.0f, 0.0f); GL.Vertex3( 11, 0);
      
GL.TexCoord2(1.0f, 1.0f); GL.Vertex3( 1,-1, 0);
      
GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1,-1, 0);
      
GL.End();
      
GL.PopMatrix();

      
Graphics.SetRenderTarget(null);
    }

 

不过这段代码的意义何在呢?只是简单地将原图像拷贝了一份啊,难道最核心的时材质上的shader?确实是shader在发挥作用。将上面那段代码随便放到一个场景里面,然后通过修改指定不同的材质中的shader就可以看到不同的效果。可以将上面的材质参数作为一个public变量放到脚本里,然后新建一个材质,通过下图中的位置修改不同的shader查看效果:

上面代码中blitMaterial选的shaderSteamVR插件中的SteamVR_Blt.shader,从材质的预览看,感觉它就是对图像进行了拉伸(具体效果应该看shader代码,但现在看不懂):

最终运行的结果并看不出针对原图像做了什么修改。但换成Ulit/Texture还是能看到效果的:

原始图像是:

当然,代码是小改了一下才能看到这种效果:

    GL.PushMatrix();

    //GL.LoadOrtho();

    mat.SetPass(0);

 

    GL.Begin(GL.QUADS);

    GL.TexCoord2(0.0f,0.0f); GL.Vertex3(-1,  1, 3);

    GL.TexCoord2(1.0f,0.0f); GL.Vertex3( 1,  1, 3);

    GL.TexCoord2(1.0f,1.0f); GL.Vertex3( 1, -1, 3);

    GL.TexCoord2(0.0f,1.0f); GL.Vertex3(-1, -1, 3);

    GL.End();

    GL.PopMatrix();

首先是不用透视投影,因为正交如果画面大小与视见区大小一样就看不出来,然后将Z坐标往后移了点,因为相机位置在0点也看不到。

TODO 还是要弄清楚SteamVR_Blit.shader的原理,然后才能弄清楚onRenderImage针对两只眼睛做了什么。其实理论上来说,因为两只眼睛的图像变形(包括位移)只是很小一点,是不是看不出来?


   
#endregion

#endif
}

10.3.     SteamVR_CameraFlip.cs

这个脚本太简单了,做的最终工作其实和SteamVR_Camera是一样的,只不过使用的shaderSteamVR_BlitFlip这个shader来做最后的图像处理,而这个shader的作用可以明确效果是将图像在Y方向颠倒了一下。原因是DirectXOpenGLY方向的定义不同,这个脚本只在DirectX环境下起作用

 

首先这个脚本也是加载到(eye)相机上的,是和SteamVR_Camera一级的,如果没有加,SteamVR_Camera也会自动添加它。

public class SteamVR_CameraFlip : MonoBehaviour
{
   
static Material blitMaterial;

   
void OnEnable()
    {
      
if (blitMaterial == null)
           blitMaterial =
new Material(Shader.Find("Custom/SteamVR_BlitFlip"));
    }

   
void OnRenderImage(RenderTexture src, RenderTexture dest)
    {

       Graphics.Blit的作用和上面那段代码的作用是一样的,就是使用blitMaterial      中的shadersrc拷贝到dest当中
       Graphics.Blit(src, dest,blitMaterial);
    }
}

这个脚本的作用,从名字看是Flip,从脚本的最开始的注释看应该是翻转图像用的。(经过试验,确实是这样的,可以在一个Camera对象上加一个SteamVR_CameraFlip脚本,运行,就能看到图像翻转了)。从shader源码看,虽然还看不懂,但下面一句显然是在Y方向做了翻转:

o.tex.y = 1 - v.texcoord.y;

 

从上面SteamVR_Camera的代码可以看到,只有在Top(最后渲染)相机上的SteamVR_CameraFlip脚本才是启用的。然后由于SteamVR_Camera总是在最后,所以最终的图像是在top相机上翻转后再由SteamVR_Camera做最后的处理。

10.4.     SteamVR_CameraMask.cs

这个脚本用于隐藏那些在头显里看不到的像素。TODO 是因为左右眼观察的范围的差别吗?这个脚本只看到在SteamVR_Render脚本中有使用,它会在SteamVR_Render中自动创建并添加。它的作用显然可以提高性能。

 

依赖于MeshFilterMeshRenderer组件,意思是如果没有会自动添加。这两个就是用来控制显示的

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class SteamVR_CameraMask : MonoBehaviour
{
   
static Material material;

要隐藏的网格?
    static Mesh[] hiddenAreaMeshes = new Mesh[] { null, null };

   
MeshFilter meshFilter;

   
void Awake()
    {
       meshFilter = GetComponent<
MeshFilter>();

      
if (material == null)

        使用SteamVR_HiddenArea shader。通常MeshFilter中的Mesh是要显示          的网格。这里通过使用SteamVR_HiddenArea是不是就反过来了,将Mesh          中的网格过滤掉。可以随便创建一个物体,然后使用带这个shader的材质看         效果
           material= new Material(Shader.Find("Custom/SteamVR_HiddenArea"));

      
var mr = GetComponent<MeshRenderer>();
       mr.material = material;
       mr.shadowCastingMode =
ShadowCastingMode.Off;
       mr.receiveShadows =
false;
#if !(UNITY_5_3 || UNITY_5_2|| UNITY_5_1 || UNITY_5_0)
       mr.lightProbeUsage= LightProbeUsage.Off;
#else
       mr.useLightProbes= false;
#endif
       mr.reflectionProbeUsage= ReflectionProbeUsage.Off;
    }

SteamVR_Render调用
    public void Set(SteamVR vr, Valve.VR.EVREye eye)
    {
      
int i = (int)eye;
      
if (hiddenAreaMeshes[i] == null)

        调用IVRSystem.GetHiddenAreaMesh获取要隐藏的网格,然后设到            MeshFilter当中。嗯,原来UnityMeshFilter提供隐藏网格的功能,但        从没有见到用过。关于IVRSystem.GetHiddenAreaMesh可以参看openvr.h         分析。
           hiddenAreaMeshes[i]= SteamVR_Utils.CreateHiddenAreaMesh(vr.hmd.GetHiddenAreaMesh(eye),vr.textureBounds[i]);
       meshFilter.mesh = hiddenAreaMeshes[i];
    }

   
public void Clear()
    {
       meshFilter.mesh =
null;
    }
}

10.5.     SteamVR_Controller.cs

这个脚本的作用是对SteamVR控制器的输入的封装,前面在Extras目录下已经有另外一个对控制器的封装了:SteamVR_TrackedController.cs,这两个都可以用。这个脚本的最开始的注释还给出了这个类的用法:

var deviceIndex = SteamVR_Controller.GetDeviceIndex(SteamVR_Controller.DeviceRelation.Leftmost);

if (deviceIndex != -1 &&SteamVR_Controller.Input(deviceIndex).GetPressDown(SteamVR_Controller.ButtonMask.Trigger))

SteamVR_Controller.Input(deviceIndex).TriggerHapticPulse(1000);

 

可以看到它是一个纯C#

public class SteamVR_Controller
{

按键定义。SteamVR的控制器返回的按键状态是通过一个64位的整型返回的,每一位 代表某种按键的状态。参看openvr.h里相关的解析,它里面有一个ButtonMaskFromId       来做这件事情
    public class ButtonMask
    {
      
public const ulong System          =(1ul << (int)EVRButtonId.k_EButton_System); // reserved
       public const ulong ApplicationMenu = (1ul << (int)EVRButtonId.k_EButton_ApplicationMenu);
      
public const ulong Grip            =(1ul << (int)EVRButtonId.k_EButton_Grip);
      
public const ulong Axis0        =(1ul << (int)EVRButtonId.k_EButton_Axis0);
      
public const ulong Axis1        =(1ul << (int)EVRButtonId.k_EButton_Axis1);
      
public const ulong Axis2        =(1ul << (int)EVRButtonId.k_EButton_Axis2);
      
public const ulong Axis3        =(1ul << (int)EVRButtonId.k_EButton_Axis3);
      
public const ulong Axis4        =(1ul << (int)EVRButtonId.k_EButton_Axis4);
      
public const ulong Touchpad        =(1ul << (int)EVRButtonId.k_EButton_SteamVR_Touchpad);
      
public const ulong Trigger        =(1ul << (int)EVRButtonId.k_EButton_SteamVR_Trigger);
    }

对控制器设备的封装(实际上可以用于所有的跟踪设备)
    public class Device
    {

    根据设备索引创建Device对象
       public Device(uint i) { index = i; }

    设备索引
       public uint index { get; private set; }

    是否有效,这个是调用IVRSystem.GetControllerStateWithPose的返回值,       这个函数实时获取控制器状态(包括姿态,通俗讲应该就是位置),取不到就返回      false,就是无效,差不多就是断开连接了
       public bool valid { get; private set; }

    是否连接。可以看到先调用了Update,这个就是会调用                        IVRSystem.GetControllerStateWithPose
       public bool connected { get { Update(); return pose.bDeviceIsConnected;} }

    是否正在被跟踪。也是Update后再判断状态。如果返回了有效的姿态,那就是处    于跟踪状态
       public bool hasTracking { get { Update(); return pose.bPoseIsValid; } }
       是否出了跟踪范围。这里这些变量都是主动去获取的,而每次都会调用Update    如果调用频繁的话,会有性能问题。前面SteamVR类中有被动通知的做法
       public bool outOfRange { get { Update(); return pose.eTrackingResult == ETrackingResult.Running_OutOfRange ||pose.eTrackingResult == ETrackingResult.Calibrating_OutOfRange; } }

    是否正在测量
       public bool calibrating { get { Update(); return pose.eTrackingResult == ETrackingResult.Calibrating_InProgress|| pose.eTrackingResult == ETrackingResult.Calibrating_OutOfRange; } }

    是否有初始化
       public bool uninitialized { get { Update(); return pose.eTrackingResult == ETrackingResult.Uninitialized; } }

      
// These values are only accuratefor the last controller state change (e.g. trigger release), and by definition,will always lag behind
       // the predicted visualposes that drive SteamVR_TrackedObjects since they are sync'd to the inputtimestamp that caused them to update.

    控制器的位置。注释说,这些值只对最后一次控制器状态改变时是精确的,更精确     的是使用预测的位置?更精确性能更好应该使用“new_poses”通知。这里是主动       获取,有这样需求的并不太在意精度
       public SteamVR_Utils.RigidTransform transform { get { Update(); return new SteamVR_Utils.RigidTransform(pose.mDeviceToAbsoluteTracking);} }

    控制器的速度
       public Vector3 velocity { get { Update(); return new Vector3(pose.vVelocity.v0,pose.vVelocity.v1, -pose.vVelocity.v2); } }

    控制器的角速度
       public Vector3 angularVelocity { get { Update(); return new Vector3(-pose.vAngularVelocity.v0,-pose.vAngularVelocity.v1, pose.vAngularVelocity.v2); } }

    获取原始的状态参数
       public VRControllerState_t GetState() { Update(); return state; }

    获取前一个状态参数(前一次Update调用)
       public VRControllerState_t GetPrevState() {Update(); return prevState; }

    获取原始的姿态参数
       public TrackedDevicePose_t GetPose() { Update(); return pose; }

      
VRControllerState_t state, prevState;
      
TrackedDevicePose_t pose;
      
int prevFrameCount = -1;

       public void Update()
       {
          
if (Time.frameCount !=prevFrameCount)
           {

           Time.frameCount是从开始计数以来的总帧数。所谓的开始计数是指所           有的脚本的Awake结束时开始计数。类似于WindowsGetTickCount
              prevFrameCount= Time.frameCount;
              prevState= state;

             
var system = OpenVR.System;
             
if (system != null)
              {

                每次调用Update都会去获取即时的控制器状态和姿态
                  valid= system.GetControllerStateWithPose(SteamVR_Render.instance.trackingSpace, index, ref state, ref pose);

                更新微力扳机状态。所谓微力扳机就是指用很小的力就能扣动扳机
                  UpdateHairTrigger();
              }
           }
       }

    返回指定按键是否被按下。如果这个状态的判断会被频繁调用(比如在            MonoBehaviour.Update中调用),真不建议用这个,还是用回调比较好。不过按      键好像没有回调?——没有回调。不过这样没有问题,在Unity里面都是这样去       判断按键状态的
       public bool GetPress(ulong buttonMask) { Update(); return (state.ulButtonPressed& buttonMask) != 0;}

    按键是否持续按下(前一个按键状态也是按下的)
       public bool GetPressDown(ulong buttonMask) { Update(); return (state.ulButtonPressed& buttonMask) != 0 && (prevState.ulButtonPressed & buttonMask) == 0; }

     按键是否弹起(当前状态是没有按下,而前一个状态是按下的)
       public bool GetPressUp(ulong buttonMask) { Update(); return (state.ulButtonPressed& buttonMask) == 0 && (prevState.ulButtonPressed & buttonMask) != 0; }

    直接传入按钮Id的情况
       public bool GetPress(EVRButtonId buttonId) { return GetPress(1ul << (int)buttonId); }
     
public bool GetPressDown(EVRButtonId buttonId) { return GetPressDown(1ul << (int)buttonId); }
      
public bool GetPressUp(EVRButtonId buttonId) { return GetPressUp(1ul << (int)buttonId); }

    获取按键(touchpad)的触摸状态
       public bool GetTouch(ulong buttonMask) { Update(); return (state.ulButtonTouched& buttonMask) != 0;}
      
public bool GetTouchDown(ulong buttonMask) { Update(); return (state.ulButtonTouched& buttonMask) != 0 && (prevState.ulButtonTouched & buttonMask) == 0; }
      
public bool GetTouchUp(ulong buttonMask) { Update(); return (state.ulButtonTouched& buttonMask) == 0 && (prevState.ulButtonTouched & buttonMask) != 0; }

    获取按键(touchpad)的触摸状态
      
public bool GetTouch(EVRButtonId buttonId) { return GetTouch(1ul << (int)buttonId); }
     
public bool GetTouchDown(EVRButtonId buttonId) { return GetTouchDown(1ul << (int)buttonId); }
      
public bool GetTouchUp(EVRButtonId buttonId) { return GetTouchUp(1ul << (int)buttonId); }

    获取轴数据。轴数据为触控板、摇杆、扳机等有两个方向自由度(扳机只有一个)      的游戏输入设备。有xy两个值,范围都在0-1之间。总共支持5种轴设备。参     openvr.h中的描述
       public Vector2 GetAxis(EVRButtonId buttonId = EVRButtonId.k_EButton_SteamVR_Touchpad)
       {
           Update();
          
var axisId = (uint)buttonId - (uint)EVRButtonId.k_EButton_Axis0;
          
switch (axisId)
           {
             
case 0: return new Vector2(state.rAxis0.x, state.rAxis0.y);
             
case 1: return new Vector2(state.rAxis1.x, state.rAxis1.y);
             
case 2: return new Vector2(state.rAxis2.x, state.rAxis2.y);
             
case 3: return new Vector2(state.rAxis3.x, state.rAxis3.y);
             
case 4: return new Vector2(state.rAxis4.x, state.rAxis4.y);
           }
          
return Vector2.zero;
       }

    触发指定按键的触觉脉冲——经测试,确实是震动,但第二个参数只针对touchpad      有用(可能是针对vive手柄的情况,其它手柄可能其它按键也能震动)
       public void TriggerHapticPulse(ushort durationMicroSec = 500, EVRButtonId buttonId = EVRButtonId.k_EButton_SteamVR_Touchpad)
       {
          
var system = OpenVR.System;
          
if (system != null)
           {
             
var axisId = (uint)buttonId - (uint)EVRButtonId.k_EButton_Axis0;
              system.TriggerHapticPulse(index,axisId, (
char)durationMicroSec);
           }
       }

    微力扳机力道定义。注意这里定义的是delta,也就是变化值,如果以极慢的速度       扣动扳机,也是不能触发的
       public float hairTriggerDelta = 0.1f; // amount trigger must bepulled or released to change state

    这个limit其实就是扳机上次的值
       float hairTriggerLimit;

    C#未初始化是有缺省初始值的吗?比如bool缺省为false
       bool hairTriggerState,hairTriggerPrevState;
      
void UpdateHairTrigger()
       {
           hairTriggerPrevState= hairTriggerState;

        当前扳机扳动的距离
           var value = state.rAxis1.x; // trigger
           if (hairTriggerState)
           {

       
             
if (value 0.0f)

                换一种写法就是hairTriggerLimit - value >                             hairTriggerDelta,就是由触发状态变成非触发状态
                  hairTriggerState= false;
           }
          
else
           {
             
if (value >hairTriggerLimit + hairTriggerDelta || value >= 1.0f)

                换一种写法就是value - hairTriggerLimit >                             hairTriggerDelta
                  hairTriggerState= true;
           }

        从这里面hairTriggerLimit本质上就是上次的value
           hairTriggerLimit= hairTriggerState ? Mathf.Max(hairTriggerLimit, value) : Mathf.Min(hairTriggerLimit, value);
       }

    获取微力扳机状态
       public bool GetHairTrigger() { Update(); return hairTriggerState; }
      
public bool GetHairTriggerDown() {Update(); return hairTriggerState&& !hairTriggerPrevState; }
      
public bool GetHairTriggerUp() {Update(); return !hairTriggerState&& hairTriggerPrevState; }
    }

设备列表
    private static Device[] devices;

静态方法。调用SteamVR_Controller.Input创建并获取Device对象
    public static Device Input(int deviceIndex)
    {
      
if (devices == null)
       {

        第一次调用,创建好对象。有冗余,通常不会有16个跟踪设备,浪费了
           devices= new Device[OpenVR.k_unMaxTrackedDeviceCount];
          
for (uint i = 0; i < devices.Length; i++)
              devices[i]=
new Device(i);
       }

      
return devices[deviceIndex];
    }

提供给外部接口,在SteamVR_Render.Update中调用,更新所有跟踪设备的状态,也   很浪费
    public static void Update()
    {
      
for (int i = 0;i < OpenVR.k_unMaxTrackedDeviceCount;i++)
           Input(i).Update();
    }

   
// This helper can be used in a variety of ways.  Bewarethat indices may change
    // as new devices aredynamically added or removed, controllers are physically
    // swapped between hands,arms crossed, etc.

设备之间的位置关系。这种关系是可以变化的,比如左手右交换、交叉,设备还可以移   
    public enum DeviceRelation
    {

    按索引顺序的第1
       First,
      
// radially
       Leftmost,
       Rightmost,
      
// distance - also seevr.hmd.GetSortedTrackedDeviceIndicesOfClass
       FarthestLeft,
       FarthestRight,
    }

 

获取指定位置的设备索引。从缺省值也可以看出,基本上有用的就是获取手柄的关系了,  那个手柄在左边,哪个在右边。relativeTo-1时为绝对空间中的位置。这个方法   是根据设备的位置算出来的,而上面提到的    IVRSystem.GetSortedTrackedDeviceIndicesOfClass得到的是从右至左的设备列   表。其实也可以实现这个功能了
    public static int GetDeviceIndex(DeviceRelation relation,
      
ETrackedDeviceClass deviceClass = ETrackedDeviceClass.Controller,
      
int relativeTo = (int)OpenVR.k_unTrackedDeviceIndex_Hmd) // use -1for absolute tracking space
    {
      
var result = -1;

      
var invXform = ((uint)relativeTo < OpenVR.k_unMaxTrackedDeviceCount) ?
           Input(relativeTo).transform.GetInverse():
SteamVR_Utils.RigidTransform.identity;

      
var system = OpenVR.System;
      
if (system == null)
          
return result;

      
var best = -float.MaxValue;
      
for (int i = 0;i < OpenVR.k_unMaxTrackedDeviceCount;i++)
       {
          
if (i == relativeTo ||system.GetTrackedDeviceClass((uint)i) != deviceClass)
             
continue;

          
var device = Input(i);
          
if (!device.connected)
             
continue;

          
if (relation == DeviceRelation.First)

            看起来first就是索引顺序的第1个,与左右关系无关
              return i;

          
float score;

       TODO 下面有一些矩阵变换操作,大概是算距离与方向,暂时看不懂,后面再       看,有为每个设备打分,最终得分最高的获胜
           var pos = invXform *device.transform.pos;
          
if (relation == DeviceRelation.FarthestRight)
           {
              score= pos.x;
           }
          
else if (relation == DeviceRelation.FarthestLeft)
           {
              score= -pos.x;
           }
          
else
           {
             
var dir = new Vector3(pos.x, 0.0f, pos.z).normalized;
             
var dot = Vector3.Dot(dir, Vector3.forward);
             
var cross = Vector3.Cross(dir, Vector3.forward);
             
if (relation == DeviceRelation.Leftmost)
              {
                  score= (cross.y >
0.0f) ? 2.0f - dot : dot;
              }
             
else
              {
                  score= (cross.y <
0.0f) ? 2.0f - dot : dot;
              }
           }
          
          
if (score > best)
           {
              result= i;
              best= score;
           }
       }

      
return result;
    }
}

 

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