SteamVR(HTC Vive) Unity插件深度分析(九)
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是否加到了Unity的VR 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.
这里通过尝试获取IVRCompositor及IVROverlay接口判断是否可用,并没有 实际使用
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
本类主要封装了三个接口:CVRSystem、CVRCompositor、CVROverlay。只允许外部 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度之间,通常认为Vive和Oculus的视场角都是110度(理论上最佳的 视场角为120度)。
至于为什么Vive的视场左边缺一块,看下面的图片,Vive的镜片本来就是这样的形状 (左边为oculus,右边为vive):
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的类型,OpenGL或DirectX
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,这里得到的也是全0,Vive不关注这个参数,由插件或者运行时, 或者硬件处理
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()
{
通过OpenVR的System属性获取到CVRSystem对象。前面调用了OpenVR.Init后 就会获取CVRSystem等各种接口了。这里可以看到对CVRSystem对象的命名为 hmd,也就是IVRSystem接口其实就是与HMD(头显)相关的接口了,这是最复 杂的一个接口
hmd= OpenVR.System;
这里虽然是打log,但会触发对hmd属性的一些获取
Debug.Log("Connected to " + hmd_TrackingSystemName+ ":" + hmd_SerialNumber);
同样获取到CVRCompositor和CVROverlay接口
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;
获取左右眼的原始投影参数,也就是左右眼的视场参数,如图(大图见上面):
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.7028455995023251及http://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 的非静态成员都是自动序列化的。注:属性是不会序列化的,比如下面的head、offset、 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(禁用)、2、4、8四种设置。可以看到这 里强制使用反走样,即使在设置中禁用了反走样,也会使用1倍像素采样来抗锯齿
int aa = QualitySettings.antiAliasing == 0 ? 1 : QualitySettings.antiAliasing;
这里定义了RenderTexture的格式。HDR是相机中的术语(high-dynamic range, 高动态光照渲染),这里的hdr是Camera对象中的一个参数。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_Camera。SteamVR_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建立所谓的相机骨骼,即将eye、ear、head、origin关系建 立起来
Expand();
这里应该与OpenVR的不太一样,这里将eye的父亲直接设成了origin,那 eye与head是同级的
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,相对于head与eye进行了交换
_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中所描述的,MSAA(MultiSampleAnti-Aliasing, 即多重采样反走样)只支持前向渲染路径,而SteamVR的MSAA支持是通过 Unity的Quality设置的。
Debug.LogWarning("MSAA only supportedin Forward rendering path. (disabling MSAA)");
QualitySettings.antiAliasing = 0;
}
// Ensure game view camera hdrsetting matches
Head中也有一个Camera,head上的Camera就是PC上的伴随窗口
var headCam =head.GetComponent<Camera>();
if (headCam != null)
{
将两个相机的参数(HDR和渲染路径)设为一致
headCam.hdr= camera.hdr;
headCam.renderingPath= camera.renderingPath;
}
#endif
if (ears == null)
{
在新的插件中,ears和eyes是在同一级的,并且会在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))
保存public及SerializeField变量的值,也就是所有可以序 列化的值都会保存起来,留着后面恢复(只需要保存可以序列化 的值就可以了,因为像关闭场景再恢复也只是会保存可以序列化 的值)
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_GameView和SteamVR_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;
上面并没有看到添加Camera到head当中,但因为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。原因大概是DirectX与OpenGL的Y 坐标定义是反的,然后只需要在最后渲染的相机中将图像翻转过来就可以了
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会通过Unity的native插件机制通知到openvr_api.dll当中(参看:http://docs.unity3d.com/ScriptReference/GL.IssuePluginEvent.html,openvr_api.dll相当于是Unity的native插件。所以它与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里面)。而 如果直接使用OpenVR的C++版的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);
将原始图像作为纹理贴到绘制的四边形上,四边形大小为1米x1米
GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1, 1, 0);
GL.TexCoord2(1.0f, 0.0f); GL.Vertex3( 1, 1, 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选的shader是SteamVR插件中的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是一样的,只不过使用的shader是SteamVR_BlitFlip这个shader来做最后的图像处理,而这个shader的作用可以明确效果是将图像在Y方向颠倒了一下。原因是DirectX和OpenGL对Y方向的定义不同,这个脚本只在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 中的shader将src拷贝到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中自动创建并添加。它的作用显然可以提高性能。
依赖于MeshFilter和MeshRenderer组件,意思是如果没有会自动添加。这两个就是用来控制显示的
[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当中。嗯,原来Unity的MeshFilter提供隐藏网格的功能,但 从没有见到用过。关于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结束时开始计数。类似于Windows的GetTickCount
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); }
获取轴数据。轴数据为触控板、摇杆、扳机等有两个方向自由度(扳机只有一个) 的游戏输入设备。有x、y两个值,范围都在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
换一种写法就是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;
}
}