SteamVR(HTC Vive) Unity插件深度分析(十二)
10.16. SteamVR_PlayArea.cs
它的主要作用是在Unity编辑器中画一个游玩区的长方体,与实际的游玩区并没有关系,即并不会影响实际的游玩区设置。在相机骨骼中的origin上面就有这个脚本:
显示的样式为(隐藏了相机骨骼其它部分后的效果):
ExecuteInEditMode表示可以在编辑器执行,在运行的时候并看不到,主要是用来供开发者调试观察用的。需要有MeshRenderer和MeshFilter
[ExecuteInEditMode, RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
public class SteamVR_PlayArea : MonoBehaviour
{
边界区域(图中蓝色的部分)的宽度,15cm
public float borderThickness = 0.15f;
游玩区的高度,2米
public float wireframeHeight = 2.0f;
只在选中所有的gameobject时在显示框架(即长方体)
public bool drawWireframeWhenSelectedOnly = false;
这个表示在游戏视图中也画出来(这个缺省应该为false的,在正式运行时显示出来 会干扰玩家,在相机骨骼预制体中是为false的)
public bool drawInGame = true;
游玩区的大小设置的几个枚举
public enum Size
{
根据测量出来的区域大小(从Chaperone中读取)
Calibrated,
4m x 3m
_400x300,
3m x 2.25m
_300x225,
2m x 1.5m
_200x150
}
游玩区大小枚举
public Size size;
边界的颜色
public Color color = Color.cyan;
用于生成网格的顶点数组,不在Inspector中显示。没有必要用public嘛
[HideInInspector]
public Vector3[] vertices;
获取游玩区边界,地面上的4个点
public static bool GetBounds( Size size, ref HmdQuad_t pRect )
{
if (size == Size.Calibrated)
{
如果设置是测量出来的,则从Chaperone取
var initOpenVR =(!SteamVR.active&& !SteamVR.usingNativeSupport);
if (initOpenVR)
{
先初始化。这里感觉不太好,为什么在这里初始化,理论上应该在一个最 开始的地方统一初始化。这里有可能会重复调用,应该可以先判断一下是 否初始化了。不过应该重复初始化也没有问题
var error = EVRInitError.None;
OpenVR.Init(ref error, EVRApplicationType.VRApplication_Other);
}
var chaperone = OpenVR.Chaperone;
直接从Chaperone中取
bool success =(chaperone != null) && chaperone.GetPlayAreaRect(ref pRect);
if (!success)
Debug.LogWarning("Failedto get Calibrated Play Area bounds! Make sure you have tracking first,and that your space is calibrated.");
if (initOpenVR)
呀,取完后立即shutdown,这有必要吗?
OpenVR.Shutdown();
return success;
}
else
{
try
{
其它情况根据在Inspector中的选择来确定,格式为:_300x200这样 的
var str =size.ToString().Substring(1);
var arr =str.Split(new char[] {'x'} , 2);
// convert tohalf size in meters (from cm)
var x = float.Parse(arr[0]) / 200;
var z = float.Parse(arr[1]) / 200;
顶点顺序是这样的(顺时针):
pRect.vCorners0.v0= x;
pRect.vCorners0.v1= 0;
pRect.vCorners0.v2= z;
pRect.vCorners1.v0= x;
pRect.vCorners1.v1= 0;
pRect.vCorners1.v2= -z;
pRect.vCorners2.v0= -x;
pRect.vCorners2.v1= 0;
pRect.vCorners2.v2= -z;
pRect.vCorners3.v0= -x;
pRect.vCorners3.v1= 0;
pRect.vCorners3.v2= z;
return true;
}
catch {}
}
return false;
}
生成游玩区的网格(地面上的边界区域的网格,即上面图中看到的蓝色部分)
public void BuildMesh()
{
var rect = new HmdQuad_t();
if ( !GetBounds(size, ref rect ) )
return;
var corners = new HmdVector3_t[] {rect.vCorners0, rect.vCorners1, rect.vCorners2, rect.vCorners3 };
由顶点生成网格,总共8个顶点,顺序如图:
vertices= new Vector3[corners.Length* 2];
for (int i = 0; i
先确定了里面的4个顶点
var c =corners[i];
在地面上抬升了1cm
vertices[i]= new Vector3(c.v0, 0.01f, c.v2);
}
if (borderThickness == 0.0f)
{
必须指定边界的宽度
GetComponent<MeshFilter>().mesh = null;
return;
}
下面是计算外面的4个顶点的坐标,计算过程如下图(以顶点4为例):
for (int i = 0; i
后一个顶点
int next = (i + 1) %corners.Length;
前一个顶点
int prev = (i +corners.Length - 1) % corners.Length;
下一个顶点的方向(单位化)
var nextSegment =(vertices[next] - vertices[i]).normalized;
前一个顶点的方向(单位化)
var prevSegment =(vertices[prev] - vertices[i]).normalized;
var vert =vertices[i];
先移到一条边上,图中的①。叉乘的意义是得到垂直方向的向量(unity中左 手坐标系),大小为夹角sin值,因为这里是90度,因此得到的也是单位向 量,乘以宽度意思就是沿这个向量方向移动指定距离
vert+= Vector3.Cross(nextSegment, Vector3.up) *borderThickness;
再沿边上移动,图中的②
vert+= Vector3.Cross(prevSegment, Vector3.down) *borderThickness;
vertices[corners.Length+ i] = vert;
}
有了顶点,再按顺序组成三角形,就形成网格了,如图:
为逆时针方向
var triangles = new int[]
{
0, 1, 4,
1, 5, 4,
1, 2, 5,
2, 6, 5,
2, 3, 6,
3, 7, 6,
3, 0, 7,
0, 4, 7
};
为每个顶点定义了一个纹理uv
var uv = new Vector2[]
{
new Vector2(0.0f, 0.0f),
new Vector2(1.0f, 0.0f),
new Vector2(0.0f, 0.0f),
new Vector2(1.0f, 0.0f),
new Vector2(0.0f, 1.0f),
new Vector2(1.0f, 1.0f),
new Vector2(0.0f, 1.0f),
new Vector2(1.0f, 1.0f)
};
每个顶点的颜色
var colors = new Color[]
{
color,
color,
color,
color,
new Color(color.r,color.g, color.b, 0.0f),
new Color(color.r,color.g, color.b, 0.0f),
new Color(color.r,color.g, color.b, 0.0f),
new Color(color.r,color.g, color.b, 0.0f)
};
定义mesh
var mesh = new Mesh();
GetComponent<MeshFilter>().mesh =mesh;
mesh.vertices= vertices;
mesh.uv= uv;
mesh.colors= colors;
mesh.triangles= triangles;
var renderer =GetComponent<MeshRenderer>();
使用Sprites-Default.mat材质,不同的版本及在编辑器中获取的方法不一样
#if UNITY_EDITOR && !(UNITY_5_3 || UNITY_5_2 || UNITY_5_1|| UNITY_5_0)
renderer.material=UnityEditor.AssetDatabase.GetBuiltinExtraResource
renderer.material= Resources.GetBuiltinResource<Material>("Sprites-Default.mat");
#endif
renderer.reflectionProbeUsage= UnityEngine.Rendering.ReflectionProbeUsage.Off;
renderer.shadowCastingMode= UnityEngine.Rendering.ShadowCastingMode.Off;
renderer.receiveShadows= false;
#if !(UNITY_5_3|| UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
renderer.lightProbeUsage= LightProbeUsage.Off;
#else
renderer.useLightProbes= false;
#endif
}
#if UNITY_EDITOR
在unity编辑器中,能动态修改参数并能更新网格,而在运行时或者游戏窗口,则不 能动态修改
有一个hashtable记录了每个字段对应的上次的值,有修改后则会重新生成网格
Hashtable values;
void Update()
{
if (!Application.isPlaying)
{
应用没有运行的时候才能修改(指修改网格,但那个长方体的线框是可以运行 时修改的)
这里看起来是获取所有public的变量(通过反射)
var fields =GetType().GetFields(System.Reflection.BindingFlags.Instance |System.Reflection.BindingFlags.Public);
bool rebuild = false;
if (values == null || (borderThickness!= 0.0f &&GetComponent<MeshFilter>().sharedMesh == null))
{
第一次运行
rebuild= true;
}
else
{
foreach (var f in fields)
{
if (!values.Contains(f) || !f.GetValue(this).Equals(values[f]))
{
rebuild= true;
break;
}
}
}
if (rebuild)
{
参数变化,重建网格
BuildMesh();
values= new Hashtable();
foreach (var f in fields)
values[f]= f.GetValue(this);
}
}
}
#endif
画Gizmo,游玩区的那个方框是以Gizmo方式画出来的。所谓的Gizmo为场景中一些 可视化的调试及辅助装置。也说是说画一些东西辅助调试、观察什么的。终于知道Unity 窗口中Gizmos相拉菜单是干什么的了:
显然如果在一个脚本中实现了OnDrawGizmos就会显示在这个菜单里。而可以看到像相 机会在场景中画一个相机,选中后还会画一个视锥体,AudioSource会画一个喇叭,这 些都是Gizmo。取消勾选会隐藏相应的Gizmo,效果和在Inspector里面折叠相应的脚 本一样。
void OnDrawGizmos()
{
if (!drawWireframeWhenSelectedOnly)
DrawWireframe();
}
选中时候可以比如用另一种颜色显示。这里有一个参数控制是否只在选中状态下显示
void OnDrawGizmosSelected()
{
if (drawWireframeWhenSelectedOnly)
DrawWireframe();
}
public void DrawWireframe()
{
if (vertices == null || vertices.Length== 0)
return;
TransformVector和TransformPoint是将坐标由局部坐标转换到世界坐标
Offset是底面到顶面的偏移向量
var offset =transform.TransformVector(Vector3.up * wireframeHeight);
for (int i = 0; i < 4; i++)
{
int next = (i + 1) % 4;
var a =transform.TransformPoint(vertices[i]);
var b = a +offset;
var c =transform.TransformPoint(vertices[next]);
var d = c +offset;
Gizmos.DrawLine(a,b);
Gizmos.DrawLine(a,c);
Gizmos.DrawLine(b,d);
}
}
public void OnEnable()
{
if (Application.isPlaying)
{
在运行的时候是否显示由drawInGame参数决定
GetComponent<MeshRenderer>().enabled= drawInGame;
// No need toremain enabled at runtime.
// Anyone thatwants to change properties at runtime
// should callBuildMesh themselves.
在运行的时候自身是禁用的。如果需要在运行时修改参数,需要手动调用 BuildMesh。因为它的网格是赋给了脚本所在物体的MeshFilter上,所以禁 用了也能显示
enabled= false;
// If we wantthe configured bounds of the user,
// we need towait for tracking.
如果游玩区的大小是根据测量出来的,那么需要异步等设备连接(准确地说是 等Chaperone接口可用),采用了协程的方式
if (drawInGame&& size == Size.Calibrated)
StartCoroutine("UpdateBounds");
}
}
协程,等Chaperone接口可用后取网格信息,生成网格
IEnumerator UpdateBounds()
{
GetComponent<MeshFilter>().mesh = null; // clearexisting
var chaperone = OpenVR.Chaperone;
if (chaperone == null)
yield break的意思是不执行下面的语句了,也就是说协程结束了。这里的 意思是如果取不到chaperone接口,那就直接结束了。这里协程的作用只是 异步等待测量结束
yield break;
while (chaperone.GetCalibrationState() != ChaperoneCalibrationState.OK)
yield return null;
BuildMesh();
}
}
10.17. SteamVR_Render.cs
这个就是最重要的对SteamVR_Camera(准确地说是对整个场景,包括各种Camera、Overlay)进行渲染的类了。这个脚本是一个MonoBehaviour,因此必须(还是可以?)挂在一个物体上,但这个脚本只在SteamVR这个预制体上有用到(后面可以看到,如果有引用的话,会自动创建一个物体并添加这个脚本)。
public class SteamVR_Render : MonoBehaviour
{
显示控制面板时是否暂停游戏(这个暂停只是相当于时间暂停)
public bool pauseGameWhenDashboardIsVisible = true;
锁定物理更新速度为头显渲染频率
public bool lockPhysicsUpdateRateToRenderFrequency = true;
外部相机。外部相机用于拍摄现实世界与虚拟世界的混合视频。不需要手动指定,项目 目录下有下面这个配置文件就会自动创建
public SteamVR_ExternalCamera externalCamera;
外部相机配置文件
public string externalCameraConfigPath = "externalcamera.cfg";
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
在5.x版本中可以为左右眼分别指定要显示的层
public LayerMask leftMask,rightMask;
用于设置渲染时需要隐藏的网格
SteamVR_CameraMask cameraMask;
#endif
跟踪空间类型,缺省为站姿
public ETrackingUniverseOrigin trackingSpace= ETrackingUniverseOrigin.TrackingUniverseStanding;
当前渲染的是左眼还是右眼
static public EVREye eye { get; private set; }
单例
static private SteamVR_Render _instance;
static public SteamVR_Render instance
{
get
{
if (_instance == null)
{
第一次被调用时,如果场景中已经有SteamVR_Render了([SteamVR] 预制体中就放了一个,把它放到场景里就有了),则直接取出。
_instance= GameObject.FindObjectOfType<SteamVR_Render>();
if (_instance == null)
如果场景中还没有,则自动添加一个名为“[SteamVR]”的空物体并 且添加SteamVR_Render脚本。
_instance= new GameObject("[SteamVR]").AddComponent<SteamVR_Render>();
}
return _instance;
}
}
void OnDestroy()
{
_instance= null;
}
应用退出时设置一个标记,用于安全释放一些东西
static private bool isQuitting;
void OnApplicationQuit()
{
isQuitting= true;
SteamVR.SafeDispose();
}
添加SteamVR_Camera。在SteamVR_Camera.OnEnable中会调用
static public void Add(SteamVR_Camera vrcam)
{
if (!isQuitting)
instance.AddInternal(vrcam);
}
移除SteamVR_Camera,在SteamVR_Camera.OnDisable中调用
static public void Remove(SteamVR_Camera vrcam)
{
if (!isQuitting&& _instance != null)
instance.RemoveInternal(vrcam);
}
获取顶层相机(最后渲染,深度最大)
static public SteamVR_Camera Top()
{
if (!isQuitting)
return instance.TopInternal();
return null;
}
SteamVR_Camera数组
private SteamVR_Camera[] cameras = new SteamVR_Camera[0];
添加SteamVR_Camera相机
void AddInternal(SteamVR_Camera vrcam)
{
SteamVR_Camera总是依赖于一个Unity相机
var camera =vrcam.GetComponent<Camera>();
扩充相机数组
var length =cameras.Length;
var sorted = new SteamVR_Camera[length + 1];
int insert = 0;
相机数组是按深度排序的,深度越小排在越前面(索引越小)。主相机的深度通常 默认为-1。通常情况下只会有一个相机,为什么有的时候会有多个相机呢?参看: http://www.aiweibang.com/yuedu/89820078.html
for (int i = 0; i
if (i == insert&& c.depth > camera.depth)
sorted[insert++]= vrcam;
sorted[insert++]= cameras[i];
}
if (insert ==length)
sorted[insert]= vrcam;
cameras= sorted;
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
在5.x版本才启用本脚本(场景中至少有一个SteamVR_Camera才会启用)
enabled= true;
#endif
}
移除一个SteamVR_Camera
void RemoveInternal(SteamVR_Camera vrcam)
{
var length =cameras.Length;
int count = 0;
for (int i = 0; i
if (c == vrcam)
还可能重复添加?
++count;
}
if (count == 0)
return;
var sorted = new SteamVR_Camera[length -count];
int insert = 0;
for (int i = 0; i
if (c != vrcam)
sorted[insert++]= c;
}
cameras= sorted;
}
取顶层相机,也就是最后一个相机,最后渲染,深度最大
SteamVR_Camera TopInternal()
{
if (cameras.Length > 0)
return cameras[cameras.Length- 1];
return null;
}
所有跟踪设备的姿态
public TrackedDevicePose_t[] poses = new TrackedDevicePose_t[OpenVR.k_unMaxTrackedDeviceCount];
这个是预测的后两帧的姿态,只不过这里声明的是空数组,也就是不关心预测的结果
public TrackedDevicePose_t[] gamePoses = new TrackedDevicePose_t[0];
暂停渲染(在比如进行场景切换时),这个比pauseGameWhenDashboardIsVisible 更彻底,它会完全停止渲染
static private bool _pauseRendering;
static public bool pauseRendering
{
get { return _pauseRendering; }
set
{
_pauseRendering= value;
#if !(UNITY_5_3|| UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
对于非5.x版本,还调用合成器的暂停渲染
varcompositor = OpenVR.Compositor;
if(compositor != null)
compositor.SuspendRendering(value);
#endif
}
}
渲染协程,就是有条件的Update
private IEnumerator RenderLoop()
{
while (true)
{
这里有个死循环,每次循环的开头都是等一帧渲染完即将显示前。 WaitForEndOfFrame的意思是等所有相机和GUI都渲染完,但在显示之前。 因此可以在后面对要显示的内容进行操作。
yield return new WaitForEndOfFrame();
if (pauseRendering)
如果暂停渲染了就继续等
continue;
合成器上场。这里没有使用SteamVR全局的合成器,而直接从OpenVR中取 的,当然取出来实际上是同一个
var compositor = OpenVR.Compositor;
if (compositor!= null)
{
if (!compositor.CanRenderScene())
如果当前合成器不能渲染场景(能渲染场景的前提就是当前进程拥有 场景焦点),则继续等
continue;
设置跟踪空间类型,坐姿还是站姿,它影响的是视角的高度
compositor.SetTrackingSpace(trackingSpace);
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
QueueEventOnRenderThread的作用是通过Unity的native插件机制异步通知到 openvr_api.dll中。关于 k_nRenderEventID_WaitGetPoses的意义需要再看 SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_WaitGetPoses);
// Hack toflush render event that was queued in Update (this ensures WaitGetPoses hasreturned before we grab the new values).
感觉EventWriteString的作用是打log
SteamVR.Unity.EventWriteString("[UnityMain]GetNativeTexturePtr - Begin");
这是里只是随便调用了一个 SteamVR_Camera.GetSceneTexture.GetNativeTexturePtr,大概是上面注释说的, 只是一个强制flush渲染事件的小技巧,它可以确保前一个WaitGetPoses返回,从 而可以获取新的值。EventWriteString的作用看起来像是打log SteamVR_Camera.GetSceneTexture(cameras[0].GetComponent<Camera>().hdr).GetNativeTexturePtr();
SteamVR.Unity.EventWriteString("[UnityMain]GetNativeTexturePtr - End");
GetLastPoses获取的是上一个WaitGetPoses返回的值,gamePoses 是预测的姿态,这里并不关心。获取到跟踪器姿态后,通过自定义事件 “new_poses”发送给所有监听者
compositor.GetLastPoses(poses,gamePoses);
SteamVR_Utils.Event.Send("new_poses", poses);
SteamVR_Utils.Event.Send("new_poses_applied");
#endif
}
更新Overlay
var overlay = SteamVR_Overlay.instance;
if (overlay != null)
overlay.UpdateOverlay();
渲染外部相机
RenderExternalCamera();
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
5.x版本,渲染左右眼
var vr = SteamVR.instance;
RenderEye(vr, EVREye.Eye_Left);
RenderEye(vr, EVREye.Eye_Right);
// Movecameras back to head position so they can be tracked reliably
重置SteamVR_Camera的局部坐标(相对于head),使可以被跟踪。这里其 实没有必要重置的,后面对这个坐标的改变是在RenderEye中,采用的是直 接赋值,不是累加的,所以不影响。而且后面其实赋的值其实也是(0,0,0) 和(0,0,0,1),至于原因,后面另说
foreach (var c in cameras)
{
c.transform.localPosition= Vector3.zero;
c.transform.localRotation= Quaternion.identity;
}
if (cameraMask!= null)
如果指定了CameraMask,则清除(MeshFilter)的网格
cameraMask.Clear();
#endif
}
}
整理一下整个渲染循环的流程(每帧都会执行)为:等待Unity渲染完成——设置跟踪空间——获取跟踪设备姿态并通知——刷新Overlay——渲染外部相机——渲染左右眼——重置相机位置——清理相机Mask
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
void RenderEye(SteamVR vr, EVREye eye)
{
int i = (int)eye;
SteamVR_Render.eye = eye;
if (cameraMask!= null)
这个会创建网格过滤
cameraMask.Set(vr,eye);
foreach (var c in cameras)
{
修改相机的位置,它们的位置来自于SteamVR.eyes数组,它的数据又来自 IVRSystem.GetEyeToHeadTransform,在SteamVR.OnNewPose里面更新。 理论上来说眼睛与头部(head,如果头显代表头部位置的)本身就有一定的 偏移,然后为了左右眼看世界会有一定的视差,这样才能形成立体图像。所以 理论上来说,下面的vr.eyes数组对于左右眼来说是不一样的,但实际打印 出来的结果是,这左右眼的值是一样的,并且都是(0,0,0)及(0,0,0,1)。这 是为什么呢?这是因为这个偏移操作是在openvr_api.dll(或者运行时,或 者硬件)里面做的——这里只能猜测了,这里是根据vive得出的结论,其它 的硬件,比如oculus是不是这样的就说不定了
c.transform.localPosition= vr.eyes[i].pos;
c.transform.localRotation= vr.eyes[i].rot;
// Updateposition to keep from getting culled
为什么将相机的位置赋给CameraMask(当然也就相当于赋给了MeshFilter 和MeshRender)就能避免被裁剪?
cameraMask.transform.position= c.transform.position;
var camera =c.GetComponent<Camera>();
将SteamVR_Camera中的纹理设置为实际渲染相机的纹理,那么渲染出来的 场景就会绘制到这个纹理上
camera.targetTexture= SteamVR_Camera.GetSceneTexture(camera.hdr);
临时保存原始相机的剔除设置,渲染完后恢复。相机的cullingMask是可以 分层显示的一个配置,如图:
最多支持32层,通过一个32位的整型描述每层是否显示。通过每个对象的 Inspector中的Layer设置可以设置到特定的层中,也可以添加新的层:
int cullingMask =camera.cullingMask;
if (eye == EVREye.Eye_Left)
{
渲染左眼时把右眼特有的mask去掉,加上左眼特有的mask
camera.cullingMask&= ~rightMask;
camera.cullingMask|= leftMask;
}
else
{
渲染右眼时把左眼特有的mask去掉,加上右眼特有的mask
camera.cullingMask&= ~leftMask;
camera.cullingMask|= rightMask;
}
手动调用相机的Render,这样渲染出来的图像就在SteamVR_Camera的纹理 中了。这里要手动调用相机渲染,是因为在SteamVR_Camera.OnEnable中 相机被禁用了
camera.Render();
还原cullingMask
camera.cullingMask= cullingMask;
还应该还原相机的位置,可以在这里还原。不过它是在上面的RenderLoop 中还原的
}
}
#endif
渲染外部相机
void RenderExternalCamera()
{
if (externalCamera == null)
return;
if (!externalCamera.gameObject.activeInHierarchy)
return;
外部相机有一个跳帧的参数,意思是外部相机的帧率不需要这么高(Vive的渲染 帧率为90帧,而视频并不需要这么高),那么可以通过设置一个跳帧参数,让外 部相机每隔几个Vive渲染帧再渲染,这样可以提高性能
var frameSkip = (int)Mathf.Max(externalCamera.config.frameSkip, 0.0f);
这里就是跳帧,Time.frameCount是从所有Awake以来的总帧数
if (Time.frameCount %(frameSkip + 1) != 0)
return;
// Keepexternal camera relative to the most relevant vr camera.
将外部相机与顶层相机关联起来(其实不算关联,就是拷贝了一份这个相机而已)
externalCamera.AttachToCamera(TopInternal());
分别渲染近端和远端,或者说前景和背景,是直接绘制到PC的窗口上的
externalCamera.RenderNear();
externalCamera.RenderFar();
}
float sceneResolutionScale = 1.0f, timeScale = 1.0f;
当前应用获取到了输入焦点(焦点发生变化)
private void OnInputFocus(params object[] args)
{
bool hasFocus = (bool)args[0];
if (hasFocus)
{
获取焦点后恢复时间及场景缩放比例
if (pauseGameWhenDashboardIsVisible)
{
Time.timeScale =timeScale;
}
SteamVR_Camera.sceneResolutionScale= sceneResolutionScale;
}
else
{
失去焦点后,如果有设置显示控制面板后暂停游戏,则将时间缩放设为0。时 间比例可以用来加快或减慢游戏进程。这里将这个比例设为0,相当于时间停 止了,游戏也就暂停了
if (pauseGameWhenDashboardIsVisible)
{
timeScale= Time.timeScale;
Time.timeScale = 0.0f;
}
这里将场景分辨率缩放比例减小到一半。场景缩小一半,看到的物体的效果就 是放大一倍
sceneResolutionScale= SteamVR_Camera.sceneResolutionScale;
SteamVR_Camera.sceneResolutionScale= 0.5f;
}
}
退出,它是“Quit”这个自定义事件的响应函数。“Quit”事件是在下面的Update中 由PollNextEvent取出并发出来的,事件源应该是来自于SteamVR本身。
void OnQuit(params object[] args)
{
#if UNITY_EDITOR
下面是通过反射的方式让Unity编辑器停止播放,可以学习一下
foreach (System.Reflection.Assembly a in System.AppDomain.CurrentDomain.GetAssemblies())
{
var t =a.GetType("UnityEditor.EditorApplication");
if (t != null)
{
t.GetProperty("isPlaying").SetValue(null, false, null);
break;
}
}
#else
非编辑器模式的话,就是直接退出应用
Application.Quit();
#endif
}
void OnEnable()
{
脚本启用,启动RenderLoop协程
StartCoroutine("RenderLoop");
添加事件监听
SteamVR_Utils.Event.Listen("input_focus",OnInputFocus);
SteamVR_Utils.Event.Listen("Quit", OnQuit);
}
void OnDisable()
{
停止所有协程
StopAllCoroutines();
SteamVR_Utils.Event.Remove("input_focus",OnInputFocus);
SteamVR_Utils.Event.Remove("Quit", OnQuit);
}
void Awake()
{
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
5.x版本会自动添加CameraMask组件,把它加到当前对象下面
var go = new GameObject("cameraMask");
go.transform.parent= transform;
cameraMask= go.AddComponent<SteamVR_CameraMask>();
#endif
if (externalCamera == null && System.IO.File.Exists(externalCameraConfigPath))
{
如果存在“externalcamera.cfg”文件(游戏根目录或Unity工程的根目 录),则自动创建SteamVR_ExternalCamera这个预制体
var prefab = Resources.Load<GameObject>("SteamVR_ExternalCamera");
var instance =Instantiate(prefab);
instance.gameObject.name= "ExternalCamera";
SteamVR_ExternalCamera脚本挂在预制体的一个子节点Controller上面。 这是因为外部相机需要借助控制器来定位
externalCamera= instance.transform.GetChild(0).GetComponent<SteamVR_ExternalCamera>();
读取配置
externalCamera.configPath= externalCameraConfigPath;
externalCamera.ReadConfig();
}
}
void FixedUpdate()
{
#if (UNITY_5_3 ||UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
// We want tocall this as soon after Present as possible.
TODOk_nRenderEventID_PostPresentHandoff的作用是什么? SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_PostPresentHandoff);
#endif
}
#if !(UNITY_5_3|| UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
非5.x版本使用SteamVR_UpdatePoses来更新跟踪设备位置。5.x版本是在 RenderLoop里面做的
private SteamVR_UpdatePosesposeUpdater;
#endif
void Update()
{
#if !(UNITY_5_3|| UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
非5.x版本添加一个SteamVR_UpdatePoses对象。这个为啥不放到OnEnable 里面
if(poseUpdater == null)
{
vargo = new GameObject("poseUpdater");
go.transform.parent= transform;
poseUpdater= go.AddComponent
}
if (cameras.Length == 0)
{
如果场景中没有SteamVR_Camera,则不需要特别渲染了。放到这里判断并不 好,不如缺省为false,Add Camera时enable,Remove Camera时disable
enabled= false;
return;
}
// If ourFixedUpdate rate doesn't match our render framerate, then catch the handoffhere.
在FixedUpdate和Update里面都添加了这个事件到队列。上面注释说有可能 FixedUpdate的更新频率与渲染的帧率不一致才这里面再次添加到队列。至于如何实 现的得理解这个事件的作用是什么 TODO SteamVR_Utils.QueueEventOnRenderThread(SteamVR.Unity.k_nRenderEventID_PostPresentHandoff);
#endif
// Forcecontroller update in case no one else called this frame to ensure prevStategets updated.
更新所有跟踪设备的状态及位置。可以更新的位置很多,比如RenderLoop、 FixedUpdate,还有这里的Update
SteamVR_Controller.Update();
// Dispatchany OpenVR events.
下面获取并分发OpenVR事件,转换成自定义事件
var system = OpenVR.System;
if (system != null)
{
var vrEvent = new VREvent_t();
var size = (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(VREvent_t));
for (int i = 0; i < 64; i++)
{
if (!system.PollNextEvent(ref vrEvent, size))
break;
switch ((EVREventType)vrEvent.eventType)
{
其它app获取到了焦点(比如控制面板)
case EVREventType.VREvent_InputFocusCaptured: //another app has taken focus (likely dashboard)
if (vrEvent.data.process.oldPid == 0)
{
这里为啥要判断是否进程id为0?为0大概表示当前进程。 所以只有当失去焦点的是当前进程的时候才需要通知。因为 有可能有两个以上的app正在运行
发出自定义的焦点事件
SteamVR_Utils.Event.Send("input_focus", false);
}
break;
其它app释放焦点事件
case EVREventType.VREvent_InputFocusReleased: //that app has released input focus
if (vrEvent.data.process.pid == 0)
{
获取焦点的app的进程id为0(表示当前进程)
SteamVR_Utils.Event.Send("input_focus", true);
}
break;
显示渲染模型(TODO 为什么这还有个事件?)
case EVREventType.VREvent_ShowRenderModels:
SteamVR_Utils.Event.Send("hide_render_models", false);
break;
隐藏渲染模型
case EVREventType.VREvent_HideRenderModels:
SteamVR_Utils.Event.Send("hide_render_models", true);
break;
default:
其它情况直接使用事件的名字作为自定义事件发出去。所以在代 码中看到很多看不到谁发出的事件,应该就是在这里发出来的。 比如:TrackedDeviceRoleChanged、Quit。也可以看到这些外部事 件的命名规则是不一样的
var name =System.Enum.GetName(typeof(EVREventType), vrEvent.eventType);
if (name != null)
SteamVR_Utils.Event.Send(name.Substring(8) /*stripVREvent_*/, vrEvent);
break;
}
}
}
// Ensurevarious settings to minimize latency.
一些减小延迟的设置项,在这里配置,相当于SteamVR_Render可以认为是 Application类
这个是预期的帧率,-1表示缺省帧率
Application.targetFrameRate= -1;
是否可以在后台运行。也可以使得伴随窗口不需要获得焦点
Application.runInBackground= true; // don'trequire companion window focus
显卡可以允许排队的最大帧数
QualitySettings.maxQueuedFrames= -1;
VSync数量。0表示不需要等待VSync。这个是针对伴随窗口的
QualitySettings.vSyncCount = 0; // thisapplies to the companion window
if (lockPhysicsUpdateRateToRenderFrequency && Time.timeScale> 0.0f)
{
根据渲染帧率计算固定刷新时间
var vr = SteamVR.instance;
if (vr != null)
{
var timing = new Compositor_FrameTiming();
timing.m_nSize= (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(Compositor_FrameTiming));
vr.compositor.GetFrameTiming(ref timing, 0);
这个值正是在Edit->Project Settings->Time里面设置的Fixed Timestep,也就是设置FixedUpdate调用频率的
Time.fixedDeltaTime= Time.timeScale /vr.hmd_DisplayFrequency;
Time.maximumDeltaTime= Time.fixedDeltaTime* timing.m_nNumFramePresents;
}
}
}
}
可以看到SteamVR_Render的作用,一是控制渲染流程,最终的渲染后的图像位于SteamVR_Camera的纹理当中,二是事件分发,三是做一些全局设置