SteamVR(HTC Vive) Unity插件深度分析(十一)
10.12. SteamVR_IK.cs
注释的最开始已经说明了,它只是一个简单的两根骨头的模拟(可以用于手臂和腿)。这个脚本要放到模拟手臂的物体上(当然就是左右控制器了),比如在OpenVR插件的Extra/SteamVR_TestIK.unity场景里:
画了一个模拟手臂的层级关系:
public class SteamVR_IK : MonoBehaviour
{
这个target为肩的目标位置
public Transform target;
Start为Wrist(腕),joint为Elbow(肘),end为Shoulder(肩)。从实际经验来 看这好理解,腕带动肘,肘带动肩。但从实际的空间结构看,Wrist下面包含了Elbow, Elbow下面包含了Shoulder,这就很难理解了。不过Hierarchy里的父子关系并不 代表实际意义上的父子关系。其实是可以代表父子关系的,记住在Unity里面父子关 系的概念是针对Transform的相对关系的,父节点的移动会带动子节点的移动,子节 点的位置是相对于父节点的,那手腕为父节点,肘为子节点也就能理解了。可以看到各 个节点的位置差不多都是按照实际人体的关节的相对位置布置的。
public Transform start, joint,end;
public Transform poleVector,upVector;
public float blendPct = 1.0f;
HideInInspector这个顾名思义是在Inspector里面看不到。但这里为什么要用 public呢?
[HideInInspector]
public Transform startXform,jointXform, endXform;
LateUpdate是在调用了所有的Update之后再调用,也是每帧都会调用。因此适合于 对场景中的物体进行跟踪。所以这里正合适更新由于手柄位置的变化反向计算手臂的位 置。通过Solve静态方法计算出来的是肘部(上臂的位置)。TODO 里面有太多数学计 算,还没有注释,暂时看不明白。不过因为这个脚本是一个纯辅助数学计算脚本,与其 它SteamVR特性没有任何关系,因此有两个问题,一是它与手柄是如何关联的,也就 是这个过程是怎么由手柄的运动带动手臂的运动的(过程是因为这个脚本是 Controller的子物体上的,Controller的位置是由手柄更新的)。二是在vive环境 没准备好之前可以把这个脚本放到一个普通场景里面测试
void LateUpdate()
{
const float epsilon = 0.001f;
if (blendPct< epsilon)
return;
var preUp =upVector ? upVector.up : Vector3.Cross(end.position - start.position, joint.position -start.position).normalized;
var targetPosition = target.position;
var targetRotation = target.rotation;
Vector3 forward, up,result = joint.position;
Solve(start.position,targetPosition, poleVector.position,
(joint.position- start.position).magnitude,
(end.position- joint.position).magnitude,
ref result, out forward, out up);
if (up == Vector3.zero)
return;
var startPosition= start.position;
var jointPosition= joint.position;
var endPosition =end.position;
var startRotationLocal = start.localRotation;
var jointRotationLocal = joint.localRotation;
var endRotationLocal = end.localRotation;
var startParent =start.parent;
var jointParent =joint.parent;
var endParent =end.parent;
var startScale =start.localScale;
var jointScale =joint.localScale;
var endScale =end.localScale;
if (startXform== null)
{
startXform= new GameObject("startXform").transform;
startXform.parent= transform;
}
startXform.position= startPosition;
startXform.LookAt(joint,preUp);
start.parent= startXform;
if (jointXform== null)
{
jointXform= new GameObject("jointXform").transform;
jointXform.parent= startXform;
}
jointXform.position= jointPosition;
jointXform.LookAt(end,preUp);
joint.parent= jointXform;
if (endXform == null)
{
endXform= new GameObject("endXform").transform;
endXform.parent= jointXform;
}
endXform.position= endPosition;
end.parent= endXform;
startXform.LookAt(result,up);
jointXform.LookAt(targetPosition,up);
endXform.rotation= targetRotation;
start.parent= startParent;
joint.parent= jointParent;
end.parent= endParent;
end.rotation= targetRotation; // optionally blend?
// handleblending in/out
if (blendPct< 1.0f)
{
start.localRotation= Quaternion.Slerp(startRotationLocal,start.localRotation, blendPct);
joint.localRotation= Quaternion.Slerp(jointRotationLocal,joint.localRotation, blendPct);
end.localRotation= Quaternion.Slerp(endRotationLocal,end.localRotation, blendPct);
}
// restorescale so it doesn't blow out
start.localScale= startScale;
joint.localScale= jointScale;
end.localScale= endScale;
}
public static bool Solve(
Vector3 start, // shoulder /hip
Vector3 end, // desiredhand / foot position
Vector3 poleVector, // point toaim elbow / knee toward
float jointDist, // distancefrom start to elbow / knee
float targetDist, // distancefrom joint to hand / ankle
ref Vector3 result, // originaland output elbow / knee position
out Vector3 forward, out Vector3 up) // planeformed by root, joint and target
{
var totalDist =jointDist + targetDist;
var start2end =end - start;
var poleVectorDir= (poleVector - start).normalized;
var baseDist =start2end.magnitude;
result= start;
const float epsilon = 0.001f;
if (baseDist< epsilon)
{
// movejointDist toward jointTarget
result+= poleVectorDir * jointDist;
forward= Vector3.Cross(poleVectorDir, Vector3.up);
up= Vector3.Cross(forward,poleVectorDir).normalized;
}
else
{
forward= start2end * (1.0f / baseDist);
up= Vector3.Cross(forward,poleVectorDir).normalized;
if (baseDist +epsilon < totalDist)
{
// calculatethe area of the triangle to determine its height
var p =(totalDist + baseDist) * 0.5f; // half perimeter
if (p >jointDist + epsilon && p > targetDist + epsilon)
{
var A = Mathf.Sqrt(p * (p -jointDist) * (p - targetDist) * (p - baseDist));
var height = 2.0f * A /baseDist; // distance of joint from line between root and target
var dist = Mathf.Sqrt((jointDist* jointDist) - (height * height));
var right = Vector3.Cross(up,forward); // no need to normalized - already orthonormal
result+= (forward * dist) + (right * height);
return true; // in range
}
else
{
// movejointDist toward jointTarget
result+= poleVectorDir * jointDist;
}
}
else
{
// moveelboDist toward target
result+= forward * jointDist;
}
}
return false; // edge cases
}
}
现在看来这个脚本应用于控制器的话,需要让Wrist随着控制器移动,然后带动Elbow和Shoulder运动。但有点很奇怪是它需要指定Shoulder和Elbow的target,即目标位置,既然都指定了目标位置,那还要IK干什么?
10.13. SteamVR_LoadLevel.cs
这个脚本的作用是辅助平滑进行场景的变换,直观地说LoadLevel就是加载关卡,那切换关卡的时候通常就会涉及到场景的变换,这个脚本的作用就是让场景变换更平顺。因为在VR里面场景的跳变会让人觉得不舒服。这个脚本在示例中没有使用,只有在进行大场景切换的时候才需要用到
public class SteamVR_LoadLevel : MonoBehaviour
{
用一个静态变量表示一个加载过程
private static SteamVR_LoadLevel _active = null;
是否正在加载
public static bool loading { get { return _active != null; } }
异步加载进度
public static float progress
{
get { return (_active != null &&_active.async != null) ? _active.async.progress : 0.0f; }
}
进度纹理(界面)
public static Texture progressTexture
{
get { return (_active != null) ?_active.renderTexture : null; }
}
要加载的关卡的名字——实际上就是场景名称了
// Name of level to load.
public string levelName;
是否要加载一个外部app
// If loading an externalapplication
public bool loadExternalApp;
外部app的路径
// Name of external applicationto load
public string externalAppPath;
外部app的启动参数
// The command-line argsfor the external application to load
public string externalAppArgs;
以附加的方式加载关卡。调用的是Unity的API Application.LoadLevelAdditiveAsync,它与LoadLevelAsync的区别是不会销毁 当前场景中的物体,这样带来的是一种虽然进入不同关卡,但给玩家的体验是不同关卡 是连续的
// If true, callLoadLevelAdditiveAsync instead of LoadLevelAsync.
public bool loadAdditive;
是否异步加载。注释说,异步加载对于某些app可能导致crash
// Async load causescrashes in some apps.
public bool loadAsync = true;
大概是闪屏的意思吧。加载关卡的时候显示一个闪屏
// Optional logo texture.
public Texture loadingScreen;
定制进度条的纹理,分别表示0%和100%
// Optional progress bartextures.
public Texture progressBarEmpty, progressBarFull;
加载界面及进度条的大小,米为单位
// Sizes of overlays.
public float loadingScreenWidthInMeters = 6.0f;
public float progressBarWidthInMeters = 3.0f;
如果指定,加载界面就会显示在这个距离上
// If specified, theloading screen will be positioned in the player's view this far away.
public float loadingScreenDistance = 0.0f;
还可以指定加载界面和进度条的位置(这就不仅有距离了,还可以定制角度)。如果不 指定的话就缺省使用当前脚本所有物体的位置
// Optional overrides forwhere to display loading screen and progress bar overlays.
// Otherwisedefaults to using this object's transform.
public Transform loadingScreenTransform, progressBarTransform;
还可以定制天空盒的纹理
// Optional skyboxoverride textures.
public Texture front, back,left, right, top, bottom;
没有天空盒的话,可以指定背景颜色。颜色的渐变是直接交给合成器来做的
// Colors to use whendropping to the compositor between levels if no skybox is set.
public Color backgroundColor = Color.black;
这个表示是否显示网格。如果不显示网格,则会使用上面指定的颜色作为合成器的前景 颜色。如果指定了天空盒,这个参数则不起作用。所谓的合成器界面(带网格)的就是 下面这个界面了:
// If false,the background color above gets applied as the foreground color in thecompositor.
// This doesnot have any effect when using a skybox instead.
public bool showGrid = false;
渐出及渐入的时间(从这里的文字细节来看,就是有一个从渐出到渐入的过程,渐出就 是从当前场景渐出到合成器界面,渐入则是从合成器界面渐入到新的场景)
// Time to fade fromcurrent scene to the compositor and back.
public float fadeOutTime = 0.5f;
public float fadeInTime = 0.5f;
这个时间是在渐出之后渐入之前的一个额外时间,用于在某些场景在开始的时候有一些 比较慢的操作。
// Additional time towait after finished loading before we start fading the new scene back in.
// This is tocover up any initial hitching that takes place right at the start of levels.
// Most scenesshould hopefully not require this.
public float postLoadSettleTime = 0.0f;
加载界面渐入渐出的时间(进度条的渐入渐出也使这两个参数)
// Time to fade loadingscreen in and out (also used for progress bar).
public float loadingScreenFadeInTime = 1.0f;
public float loadingScreenFadeOutTime = 0.25f;
渐变的速度,也就是alpha每帧变化的大小,初始值为1.0,后面会根据时间重新计 算
float fadeRate = 1.0f;
初始及当前alpha值(范围是0.0-1.0)
float alpha = 0.0f;
注意上面两个参数是针对加载界面及进度条的渐变的,而场景及合成器界面的渐变是由 合成器自身来完成的
Unity的异步操作类,用于跟踪关卡加载过程
AsyncOperation async; // used totrack level load progress
用于渲染进度条的渲染纹理
RenderTexture renderTexture; // used to render progress bar
Overlay的句柄。加载界面及进度条都是通过Overlay实现的
ulong loadingScreenOverlayHandle = OpenVR.k_ulOverlayHandleInvalid;
ulong progressBarOverlayHandle= OpenVR.k_ulOverlayHandleInvalid;
是否在OnEnable的时候就触发切换
public bool autoTriggerOnEnable = false;
void OnEnable()
{
if (autoTriggerOnEnable)
Trigger();
}
public void Trigger()
{
触发加载过程,采用了协程的方式
if (!loading&& !string.IsNullOrEmpty(levelName))
StartCoroutine("LoadLevel");
}
一个帮助函数用于启动一个加载过程。里面会新创建一个名字为“loader”的空物体, 然后添加SteamVR_LoadLevel脚本,然后直接触发。这个函数的作用是,不需要在场 景中将这个脚本在Inspector中添加到一个物体上,而是直接可以在另外一个脚本中 调用SteamVR_LoadLevel.Begin就可以了
// Helper function toquickly and simply load a level from script.
public static void Begin(string levelName,
bool showGrid = false, float fadeOutTime = 0.5f,
float r = 0.0f, float g = 0.0f, float b = 0.0f, float a = 1.0f)
{
var loader = new GameObject("loader").AddComponent<SteamVR_LoadLevel>();
loader.levelName= levelName;
loader.showGrid= showGrid;
loader.fadeOutTime= fadeOutTime;
loader.backgroundColor= new Color(r, g, b, a);
loader.Trigger();
}
// Updates progress bar.
void OnGUI()
{
在OnGUI里面更新进度条。进度条是使用UGUI直接绘制的
if (_active != this)
return;
如果指定了进度条纹理,则独立于加载界面创建一个进度条overlay
// Optionallycreate an overlay for our progress bar to use, separate from the loadingscreen.
if (progressBarEmpty != null && progressBarFull != null)
{
if (progressBarOverlayHandle == OpenVR.k_ulOverlayHandleInvalid)
progressBarOverlayHandle= GetOverlayHandle("progressBar", progressBarTransform != null ?progressBarTransform : transform, progressBarWidthInMeters);
if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
{
从AsyncOperation取进度
var progress =(async != null) ? async.progress : 0.0f;
// Use thefull bar size for everything.
var w =progressBarFull.width;
var h =progressBarFull.height;
// Create aseparate render texture so we can composite the full image on top of the emptyone.
创建一个RenderTexture用于将完成的进度条纹理叠加到未完成的纹理 上以生成进度
if (renderTexture == null)
{
renderTexture= new RenderTexture(w, h, 0);
renderTexture.Create();
}
var prevActive = RenderTexture.active;
设为当前活动的RenderTexture,后面的绘图都会画到这个纹理上
RenderTexture.active =renderTexture;
if (Event.current.type== EventType.Repaint)
GL.Clear(false, true, Color.clear);
开始在一个区域内绘图了
GUILayout.BeginArea(new Rect(0, 0, w, h));
先画进度条的底部
GUI.DrawTexture(new Rect(0, 0, w, h),progressBarEmpty);
画已完成部分
// Reveal thefull bar texture based on progress.
GUI.DrawTextureWithTexCoords(new Rect(0, 0, progress *w, h), progressBarFull, new Rect(0.0f, 0.0f, progress, 1.0f));
GUILayout.EndArea();
切换回原来的RenderTexture,当前的RenderTexture中已经有绘制好 的进度条了
RenderTexture.active =prevActive;
需要每帧都重新设置到overlay当中,因为底层做了一份拷贝
// Textureneeds to be set every frame after it is updated since SteamVR makes a copyinternally to a shared texture.
var overlay = OpenVR.Overlay;
if (overlay != null)
{
可以学习这里往overlay设置unity纹理的方法
var texture = new Texture_t();
texture.handle= renderTexture.GetNativeTexturePtr();
texture.eType= SteamVR.instance.graphicsAPI;
texture.eColorSpace= EColorSpace.Auto;
overlay.SetOverlayTexture(progressBarOverlayHandle, ref texture);
}
}
}
下面这段屏蔽了,作用是同时把加载界面和进度条也绘制到伴随窗口上
看下面的做法就是在OnGUI里面直接绘制就是会绘制到伴随窗口上了。如果在 SteamVR_GameView中的drawOverlay设为true,则overlay会被自动绘制到 伴随窗口上面去了
#if false
//Draw loading screen and progress bar to 2d companion window as well.
if(loadingScreen != null)
{
varscreenAspect = (float)Screen.width / Screen.height;
vartextureAspect = (float)loadingScreen.width / loadingScreen.height;
floatw, h;
以加载界面的长宽比为准,保证画面不变形。画面不充满屏幕,最大只到90%
if(screenAspect < textureAspect)
{
//Clamp horizontally
w= Screen.width * 0.9f;
h= w / textureAspect;
}
else
{
//Clamp vertically
h= Screen.height * 0.9f;
w= h * textureAspect;
}
GUILayout.BeginArea(newRect(0, 0, Screen.width, Screen.height));
居中显示
varx = Screen.width / 2 - w / 2;
vary = Screen.height / 2 - h / 2;
GUI.DrawTexture(newRect(x, y, w, h), loadingScreen);
GUILayout.EndArea();
}
if(renderTexture != null)
{
画进度条
varx = Screen.width / 2 - renderTexture.width / 2;
vary = Screen.height * 0.9f - renderTexture.height;
GUI.DrawTexture(newRect(x, y, renderTexture.width, renderTexture.height), renderTexture);
}
#endif
}
// Fade our overlaysin/out over time.
void Update()
{
if (_active != this)
return;
根据渐变速度和当前时间计算出当前帧的实际alpha值
alpha= Mathf.Clamp01(alpha+ fadeRate * Time.deltaTime);
var overlay = OpenVR.Overlay;
if (overlay != null)
{
通过IVROverlay接口直接设置overlay的alpha值
if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
overlay.SetOverlayAlpha(loadingScreenOverlayHandle,alpha);
if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
overlay.SetOverlayAlpha(progressBarOverlayHandle,alpha);
}
}
LoadLevel协程
// Corourtine to handleall the steps across loading boundaries.
IEnumerator LoadLevel()
{
// Optionallyrotate loading screen transform around the camera into view.
// We assumehere that the loading screen is already facing toward the origin,
// and thatthe progress bar transform (if any) is a child and will follow along.
if (loadingScreen != null && loadingScreenDistance > 0.0f)
{
// Wait untilwe have tracking.
var hmd = SteamVR_Controller.Input((int)OpenVR.k_unTrackedDeviceIndex_Hmd);
while (!hmd.hasTracking)
等待头显建立跟踪,直接返回null在下一帧的Update之后会再次调用
yield return null;
根据头显转动的角度(仅绕Y轴)及指定的显示距离设置当前脚本所在物体的 transform。只利用了头显的绕Y轴转动的角度,这样界面只会随头显左右摆 动而动,不会上下摆动
var tloading =hmd.transform;
tloading.rot= Quaternion.Euler(0.0f,tloading.rot.eulerAngles.y, 0.0f);
tloading.pos+= tloading.rot * new Vector3(0.0f, 0.0f, loadingScreenDistance);
var t =loadingScreenTransform != null ? loadingScreenTransform : transform;
t.position= tloading.pos;
t.rotation= tloading.rot;
}
这个标记是否正在加载
_active= this;
发出一个“loading”自定义事件,感兴趣的可以监听
SteamVR_Utils.Event.Send("loading", true);
计算渐变的速度
// Calculaterate for fading in loading screen and progress bar.
if (loadingScreenFadeInTime > 0.0f)
{
fadeRate= 1.0f /loadingScreenFadeInTime;
}
else
{
没有指定渐变参数则一直以不透明方式显示
alpha= 1.0f;
}
var overlay = OpenVR.Overlay;
加载界面overlay是在这里创建的,因为是外面指定的纹理。而进度条纹理需要 经过一定的绘制操作的,所以放到了OnGUI中
// Optionallycreate our loading screen overlay.
if (loadingScreen != null && overlay != null)
{
loadingScreenOverlayHandle= GetOverlayHandle("loadingScreen", loadingScreenTransform!= null ?loadingScreenTransform : transform, loadingScreenWidthInMeters);
if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
{
var texture = new Texture_t();
texture.handle= loadingScreen.GetNativeTexturePtr();
texture.eType= SteamVR.instance.graphicsAPI;
texture.eColorSpace= EColorSpace.Auto;
overlay.SetOverlayTexture(loadingScreenOverlayHandle, ref texture);
}
}
bool fadedForeground = false;
发出开始渐出(当前场景)的自定义通知“loading_fade_out”
// Fade out tocompositor
SteamVR_Utils.Event.Send("loading_fade_out",fadeOutTime);
// Optionallyset a skybox to use as a backdrop in the compositor.
var compositor = OpenVR.Compositor;
if (compositor!= null)
{
if (front != null)
{
设置合成器的天空盒
SteamVR_Skybox.SetOverride(front,back, left, right, top, bottom);
渐显合成器网格。调用这句会渐变到下面这个带网格的合成器界面:
// Explicitlyfade to the compositor since loading will cause us to stop rendering.
compositor.FadeGrid(fadeOutTime, true);
等合成器完全显示出来后再进行后面的步骤
yield return new WaitForSeconds(fadeOutTime);
}
else if (backgroundColor!= Color.clear)
{
如果没有指定天空盒,但指定了背景颜色,则使用颜色渐变
// Otherwise,use the specified background color.
if (showGrid)
{
FadeToColor是将当前场景渐变到指定颜色,不过由于时间设为0, 是先直接变成指定颜色的,而且指定的是背景色
// Setcompositor background color immediately, and start fading to it.
compositor.FadeToColor(0.0f,backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, true);
仍然是调用FadeGrid来做渐变(渐显合成器,也就是上面指定的背 景色)
compositor.FadeGrid(fadeOutTime, true);
yield return new WaitForSeconds(fadeOutTime);
}
else
{
这里如果参数指定不显示网格,那么就是通过渐显前景色的方式。前 景色与背景色的区别就是会显示在场景前还是场景后。那么这里就是 会混合在场景前。最终完全变成前景色
// Fade theforeground color in (which will blend on top of the scene), and then cut to thecompositor.
compositor.FadeToColor(fadeOutTime,backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a, false);
等渐变过程完成
yield return new WaitForSeconds(fadeOutTime + 0.1f);
再完全切换到合成器(此时只有前景色),时间为0秒,也就是上面 英文注释中用的词cut,即瞬间切换
compositor.FadeGrid(0.0f, true);
fadedForeground= true;
}
}
}
到这里就已经完全切到合成器界面了,现在可以停止提交帧到合成器了
// Now thatwe're fully faded out, we can stop submitting frames to the compositor.
SteamVR_Render.pauseRendering= true;
接下来的过程为下一个场景的渐入。这里一直等待alpha值重新变成1。也就是等 加载界面及进度条完全显示出来
// Continuewaiting for the overlays to fully fade in before continuing.
while (alpha < 1.0f)
yield return null;
// Keep us fromgetting destroyed when loading the new level, otherwise this coroutine will getstopped prematurely.
transform.parent= null;
当前脚本位于上一个场景,这句可以避免被销毁(当然如果设置了loadAdditive 应该也不会销毁)。上面这句应该也很重要,断开关联关系
DontDestroyOnLoad(gameObject);
if (loadExternalApp)
{
如果是加载外部app的方式加载新场景(看来这也是一种方式,可以用在我 们后面聊天室的插件方式)
Debug.Log("Launchingexternal application...");
var applications= OpenVR.Applications;
if (applications== null)
{
Debug.Log("Failedto get OpenVR.Applications interface!");
}
else
{
使用IVRApplication.LaunchInternalProcess来运行外部app。不 过这里是叫内部进程,是不是要在SteamVR商店里的应用?
var workingDirectory = Directory.GetCurrentDirectory();
var fullPath = Path.Combine(workingDirectory, externalAppPath );
Debug.Log("LaunchingInternalProcess");
Debug.Log("ExternalAppPath= " + externalAppPath);
Debug.Log("FullPath= " + fullPath);
Debug.Log("ExternalAppArgs= " + externalAppArgs);
Debug.Log("WorkingDirectory= " + workingDirectory);
var error = applications.LaunchInternalProcess(fullPath,externalAppArgs, workingDirectory);
Debug.Log("LaunchInternalProcessError:" + error);
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
非编辑模式下还会结束当前进程,那当前对象怎么办?
System.Diagnostics.Process.GetCurrentProcess().Kill();
#endif
}
}
else
{
内部场景的加载,分5.0/5.1/5.2和其它。不同版本加载方式不一样,这API 也太不稳定和兼容了
#if !(UNITY_5_2 || UNITY_5_1 || UNITY_5_0)
var mode =loadAdditive ? UnityEngine.SceneManagement.LoadSceneMode.Additive :UnityEngine.SceneManagement.LoadSceneMode.Single;
if (loadAsync)
{
异步加载。直接使用Unity的SceneManager来加载。看起来这就和在 Project视图里双击切换场景一样。如果没有使用一些特别的技巧的话, 确实就会很突兀
Application.backgroundLoadingPriority= ThreadPriority.Low;
async= UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(levelName, mode);
// Performingthis in a while loop instead seems to help smooth things out.
//yield returnasync;
while (!async.isDone)
{
等异步加载完成
yield return null;
}
}
else
{
同步加载 UnityEngine.SceneManagement.SceneManager.LoadScene(levelName,mode);
}
#else
if(loadAsync)
{
其它版本的做法使用Application.LoadLevelxxx
async= loadAdditive ? Application.LoadLevelAdditiveAsync(levelName) :Application.LoadLevelAsync(levelName);
//Performing this in a while loop instead seems to help smooth things out.
//yieldreturn async;
while(!async.isDone)
{
yieldreturn null;
}
}
elseif (loadAdditive)
{
Application.LoadLevelAdditive(levelName);
}
else
{
Application.LoadLevel(levelName);
}
#endif
}
yield return null;
在下一帧执行回收
System.GC.Collect();
yield return null;
预加载所有的shader
Shader.WarmupAllShaders();
// Optionallywait a short period of time after loading everything back in, but before westart rendering again
// in order togive everything a change to settle down to avoid any hitching at the start ofthe new level.
在加载了场景后(渲染前)额外等一小段时间以避免在新场景开始时出现卡顿
yield return new WaitForSeconds(postLoadSettleTime);
SteamVR_Render.pauseRendering= false;
渐隐加载界面和进度条
// Fade outloading screen.
if (loadingScreenFadeOutTime > 0.0f)
{
fadeRate= -1.0f /loadingScreenFadeOutTime;
}
else
{
alpha= 0.0f;
}
发出一渐显(新场景)通知
// Fade out tocompositor
SteamVR_Utils.Event.Send("loading_fade_in", fadeInTime);
if (compositor!= null)
{
// Fade outforeground color if necessary.
if (fadedForeground)
{
如果前面是通过指定的前景色来渐显的,这里渐隐
compositor.FadeGrid(0.0f, false);
compositor.FadeToColor(fadeInTime, 0.0f, 0.0f, 0.0f, 0.0f, false);
yield return new WaitForSeconds(fadeInTime);
}
else
{
指定网格渐隐
// Fade sceneback in, and reset skybox once no longer visible.
compositor.FadeGrid(fadeInTime, false);
yield return new WaitForSeconds(fadeInTime);
if (front != null)
{
前面指定了天空盒,这里清理
SteamVR_Skybox.ClearOverride();
}
}
}
等待加载界面完全消失(透明,alpha变为0)
// Finally,stick around long enough for our overlays to fully fade out.
while (alpha > 0.0f)
yield return null;
if (overlay != null)
{
隐藏overlay
if (progressBarOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
overlay.HideOverlay(progressBarOverlayHandle);
if (loadingScreenOverlayHandle != OpenVR.k_ulOverlayHandleInvalid)
overlay.HideOverlay(loadingScreenOverlayHandle);
}
销毁当前脚本所有gameObject
Destroy(gameObject);
_active= null;
发出加载完成通知
SteamVR_Utils.Event.Send("loading", false);
}
创建一个overlay
// Helper to create (orreuse if possible) each of our different overlay types.
ulong GetOverlayHandle(string overlayName, Transform transform, float widthInMeters = 1.0f)
{
ulong handle = OpenVR.k_ulOverlayHandleInvalid;
var overlay = OpenVR.Overlay;
if (overlay == null)
return handle;
每个overlay都有一个绑定的关键字。这里的样式为:unity:companyName.productName.overlayName
var key = SteamVR_Overlay.key + "." + overlayName;
var error =overlay.FindOverlay(key, ref handle);
if (error != EVROverlayError.None)
没有的话就创建
error= overlay.CreateOverlay(key, overlayName, ref handle);
if (error == EVROverlayError.None)
{
overlay.ShowOverlay(handle);
overlay.SetOverlayAlpha(handle,alpha);
overlay.SetOverlayWidthInMeters(handle,widthInMeters);
// D3Dtextures are upside-down in Unity to match OpenGL.
if (SteamVR.instance.graphicsAPI== EGraphicsAPIConvention.API_DirectX)
{
DirectX的纹理方向与OpenGL(在垂直方向)是反过来,这里反转一下 以与OpenGL保持一致
var textureBounds= new VRTextureBounds_t();
textureBounds.uMin= 0;
textureBounds.vMin= 1;
textureBounds.uMax= 1;
textureBounds.vMax= 0;
overlay.SetOverlayTextureBounds(handle, ref textureBounds);
}
如果指定了loadingScreenDistance,那么overlay显示的位置就会根据 头显的方向来确定,否则就是固定位置(指定位置或者当前脚本所在物体位置)
// Convertfrom world space to tracking space using the top-most camera.
var vrcam =(loadingScreenDistance == 0.0f) ? SteamVR_Render.Top() : null;
if (vrcam != null &&vrcam.origin != null)
{
这里就是如果没有指定loadingScreenDistance,那么根据当前物体与 (顶层)相机(通常只有一个相机)相对位置设置overlay变换矩阵
var offset = new SteamVR_Utils.RigidTransform(vrcam.origin,transform);
offset.pos.x/= vrcam.origin.localScale.x;
offset.pos.y/= vrcam.origin.localScale.y;
offset.pos.z/= vrcam.origin.localScale.z;
var t =offset.ToHmdMatrix34();
overlay.SetOverlayTransformAbsolute(handle, SteamVR_Render.instance.trackingSpace, ref t);
}
else
{
否则直接使用当前物体的变换矩阵作为overlay的变换矩阵(前面已经 根据头显位置计算了)
var t = new SteamVR_Utils.RigidTransform(transform).ToHmdMatrix34();
overlay.SetOverlayTransformAbsolute(handle, SteamVR_Render.instance.trackingSpace, ref t);
}
}
return handle;
}
}
注:通过这里复杂的协程的使用可以看出协程的优势在于它是自动从yield语句恢复执行的。那如果说自己直接在Update语句里面通过各种条件判断也能做到协程的效果的话,那协程的方式真是太方便了。
10.14. SteamVR_Menu.cs
这是一个在OnGUI中绘制菜单的例子,支持overlay。其实这不能叫一个菜单示例,它演示的是如何通过overlay的方式显示一个GUI界面,并且有一些参数可以控制。最终的界面是这样的(通过按键盘上的Esc键可以显示和隐藏):
在[Status]预制体下面的_Stats子物体上有使用:
public class SteamVR_Menu : MonoBehaviour
{
可以指定一个随鼠标而动的光标
public Texture cursor,background, logo;
Logo的高度,菜单的间隙
public float logoHeight,menuOffset;
这里只是利用Vector2来保存两个数值。它实际上表示相机允许的缩放范围
public Vector2 scaleLimits = new Vector2(0.1f, 5.0f);
缩放的速度。指通过快捷键进行缩放时的速度
public float scaleRate = 0.5f;
保存overlay实例
SteamVR_Overlay overlay;
保存临时的overlay纹理的相机
Camera overlayCam;
保存overlay的uvOffset配置,用于恢复
Vector4 uvOffset;
保存overlay的distance配置,用于恢复
float distance;
Overlay的渲染纹理
public RenderTexture texture { get { return overlay ?overlay.texture as RenderTexture : null; } }
当前的(相机)缩放比例
public float scale { get; private set; }
UI上相应文本框的字符串值,分别对应最小缩放比例、最大缩放比例和缩放速度
string scaleLimitX,scaleLimitY, scaleRateText;
保存鼠标的锁定状态
CursorLockMode savedCursorLockState;
保存光标是否可见
bool savedCursorVisible;
void Awake()
{
初始化文本框内容
scaleLimitX= string.Format("{ 0:N1}",scaleLimits.x);
scaleLimitY= string.Format("{ 0:N1}",scaleLimits.y);
scaleRateText= string.Format("{ 0:N1}", scaleRate);
var overlay = SteamVR_Overlay.instance;
if (overlay != null)
{
保存overlay的部分参数
uvOffset= overlay.uvOffset;
distance= overlay.distance;
}
}
void OnGUI()
{
if (overlay == null)
return;
绘制界面,直接绘到overlay的纹理上
var texture =overlay.texture as RenderTexture;
将当前的渲染纹理设为overlay的纹理,则下面的绘制会直接绘制到overlay上, 而不会影响当前渲染的场景
var prevActive = RenderTexture.active;
RenderTexture.active =texture;
if (Event.current.type== EventType.Repaint)
GL.Clear(false, true, Color.clear);
var area = new Rect(0, 0,texture.width, texture.height);
// Account forscreen smaller than texture (since mouse position gets clamped)
if (Screen.width
纹理比屏幕尺寸要大的情况,对纹理进行裁剪,计算纹理的映射范围(0-1)
area.width= Screen.width;
overlay.uvOffset.x= -(float)(texture.width- Screen.width) / (2 *texture.width);
}
if (Screen.height
area.height=
overlay.uvOffset.y= (float)(texture.height- Screen.height) / (2 *texture.height);
}
开始绘制界面了
GUILayout.BeginArea(area);
if (background!= null)
{
画背景,居中对齐(并未拉伸)
GUI.DrawTexture(new Rect(
(area.width- background.width) / 2,
(area.height- background.height) / 2,
background.width,background.height), background);
}
GUILayout.BeginHorizontal();
让控件右对齐
GUILayout.FlexibleSpace();
GUILayout.BeginVertical();
if (logo != null)
{
Logo占据高度方向一半的空间,上部以空白填充
GUILayout.Space(area.height/ 2 -logoHeight);
GUILayout.Box(logo);
}
留白(相当于margin)
GUILayout.Space(menuOffset);
一个按钮,用于隐藏菜单
bool bHideMenu = GUILayout.Button("[Esc] -Close menu");
一个slider用于直接设置相机的缩放比例
GUILayout.BeginHorizontal();
GUILayout.Label(string.Format("Scale: { 0:N4}", scale));
{
var result = GUILayout.HorizontalSlider(scale,scaleLimits.x, scaleLimits.y);
if (result !=scale)
{
SetScale(result);
}
}
GUILayout.EndHorizontal();
两个编辑框分别设置缩放的最小和最大的比例
GUILayout.BeginHorizontal();
GUILayout.Label(string.Format("Scalelimits:"));
{
var result = GUILayout.TextField(scaleLimitX);
if (result !=scaleLimitX)
{
if (float.TryParse(result, out scaleLimits.x))
scaleLimitX= result;
}
}
{
var result = GUILayout.TextField(scaleLimitY);
if (result !=scaleLimitY)
{
if (float.TryParse(result, out scaleLimits.y))
scaleLimitY= result;
}
}
GUILayout.EndHorizontal();
一个设置缩放速率的编辑框,可以通过快捷键进行缩放,每按一次快捷键缩放多少
GUILayout.BeginHorizontal();
GUILayout.Label(string.Format("Scalerate:"));
{
var result = GUILayout.TextField(scaleRateText);
if (result !=scaleRateText)
{
if (float.TryParse(result, out scaleRate))
scaleRateText= result;
}
}
GUILayout.EndHorizontal();
if (SteamVR.active)
{
只要在其它地方引用了SteamVR的实例,那么就是active状态
var vr = SteamVR.instance;
显示一个调节场景质量(即分辨率的缩放比例)的“Scene quality”的slider
GUILayout.BeginHorizontal();
{
var t = SteamVR_Camera.sceneResolutionScale;
int w = (int)(vr.sceneWidth* t);
int h = (int)(vr.sceneHeight* t);
int pct = (int)(100.0f * t);
GUILayout.Label(string.Format("Scenequality: { 0}x{ 1} ({ 2}%)", w, h, pct));
var result = Mathf.RoundToInt(GUILayout.HorizontalSlider(pct, 50, 200));
if (result !=pct)
{
SteamVR_Camera.sceneResolutionScale= (float)result / 100.0f;
}
}
GUILayout.EndHorizontal();
}
显示一个“Highquality”的check box,对应于overlay的highquality参 数,表示高质量?
overlay.highquality= GUILayout.Toggle(overlay.highquality, "Highquality");
if (overlay.highquality)
{
如果highquality为true,则再显示对应于overlay的curved及antilias 的checkbox,分别表示曲线(面)overlay和反走样
overlay.curved= GUILayout.Toggle(overlay.curved, "Curvedoverlay");
overlay.antialias= GUILayout.Toggle(overlay.antialias, "OverlayRGSS(2x2)");
}
else
{
overlay.curved= false;
overlay.antialias= false;
}
var tracker = SteamVR_Render.Top();
if (tracker != null)
{
控制SteamVR_Camera的Wireframe参数
tracker.wireframe= GUILayout.Toggle(tracker.wireframe, "Wireframe");
加一个切换坐姿和站姿的按钮(设置到SteamVR_Render当中)
var render = SteamVR_Render.instance;
if (render.trackingSpace == ETrackingUniverseOrigin.TrackingUniverseSeated)
{
if (GUILayout.Button("Switchto Standing"))
render.trackingSpace= ETrackingUniverseOrigin.TrackingUniverseStanding;
增加一个“Center View”按钮用于重置当前位置为坐姿原点
if (GUILayout.Button("CenterView"))
{
var system = OpenVR.System;
if (system != null)
system.ResetSeatedZeroPose();
}
}
else
{
if (GUILayout.Button("Switchto Seated"))
render.trackingSpace= ETrackingUniverseOrigin.TrackingUniverseSeated;
}
}
#if !UNITY_EDITOR
在非编辑器模式下,加一个退出按钮,可以退出整个应用
if(GUILayout.Button("Exit"))
Application.Quit();
#endif
菜单结束,再加一个margin
GUILayout.Space(menuOffset);
显示两个系统信息,一个显示VR_OVERRIDE环境变量,这个变量是用来设置 SteamVR安装目录的,可以同时安装多个版本,用VR_OVERRIDE指定使用的版本)
var env = System.Environment.GetEnvironmentVariable("VR_OVERRIDE");
if (env != null)
{
GUILayout.Label("VR_OVERRIDE=" + env);
}
显示显卡信息
GUILayout.Label("Graphicsdevice: " + SystemInfo.graphicsDeviceVersion);
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.EndArea();
if (cursor != null)
{
如果指定了光标纹理,根据鼠标的位置显示一个光标
float x = Input.mousePosition.x,y = Screen.height - Input.mousePosition.y;
float w =cursor.width, h = cursor.height;
GUI.DrawTexture(new Rect(x, y, w, h),cursor);
}
RenderTexture.active =prevActive;
如果用户点击了退出菜单,隐藏菜单
if (bHideMenu)
HideMenu();
}
显示菜单
public void ShowMenu()
{
var overlay = SteamVR_Overlay.instance;
if (overlay == null)
return;
var texture =overlay.texture as RenderTexture;
if (texture == null)
{
Debug.LogError("Menurequires overlay texture to be a render texture.");
return;
}
在显示菜单之前保存当前的一些状态,隐藏菜单后再还原
保存之前的鼠标状态,包括鼠标是否可见及锁定状态,显示菜单后会修改
SaveCursorState();
鼠标状态设为可见
Cursor.visible = true;
锁定状态不做改变
Cursor.lockState = CursorLockMode.None;
设置了overlay才会绘制菜单
this.overlay =overlay;
保存uvOffset和distance参数
uvOffset= overlay.uvOffset;
distance= overlay.distance;
下面的做法是如果overlay的纹理是作为某个相机的渲染纹理,那么要将这个相 机暂时设为disable,否则相机会清理掉纹理上的内容
// If anexisting camera is rendering into the overlay texture, we need
// totemporarily disable it to keep it from clearing the texture on us.
var cameras = Object.FindObjectsOfType(typeof(Camera)) as Camera[];
foreach (var cam in cameras)
{
if (cam.enabled&& cam.targetTexture == texture)
{
overlayCam= cam;
overlayCam.enabled= false;
break;
}
}
初始化的相机缩放比例取自origin x方向的localScale
var tracker = SteamVR_Render.Top();
if (tracker != null)
scale= tracker.origin.localScale.x;
}
隐藏菜单
public void HideMenu()
{
恢复鼠标的状态
RestoreCursorState();
如果之前有临时禁用的相机,恢复之
if (overlayCam!= null)
overlayCam.enabled= true;
if (overlay != null)
{
恢复overlay的uvOffset和distance参数
overlay.uvOffset= uvOffset;
overlay.distance= distance;
overlay= null;
}
}
void Update()
{
处理一些快捷键
Esc键可以显示/隐藏菜单。摇杆的第7个按键也可以,摇杆的按键图如下:
if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Joystick1Button7))
{
if (overlay == null)
{
ShowMenu();
}
else
{
HideMenu();
}
}
else if (Input.GetKeyDown(KeyCode.Home))
{
按Home键,缩放比例重置为原始比例
SetScale(1.0f);
}
else if (Input.GetKey(KeyCode.PageUp))
{
PageUp可以放大,根据指定的速率
SetScale(Mathf.Clamp(scale +scaleRate * Time.deltaTime, scaleLimits.x, scaleLimits.y));
}
else if (Input.GetKey(KeyCode.PageDown))
{
PageDown可以缩小
SetScale(Mathf.Clamp(scale -scaleRate * Time.deltaTime, scaleLimits.x, scaleLimits.y));
}
}
设置缩放比例,设置到SteamVR_Camera相机的origin的localeScale上面,x/y/z 三个方向设置相同缩放比例
void SetScale(float scale)
{
this.scale =scale;
var tracker = SteamVR_Render.Top();
if (tracker != null)
tracker.origin.localScale= new Vector3(scale, scale,scale);
}
保存鼠标状态
void SaveCursorState()
{
savedCursorVisible= Cursor.visible;
savedCursorLockState= Cursor.lockState;
}
恢复鼠标状态
void RestoreCursorState()
{
Cursor.visible =savedCursorVisible;
Cursor.lockState =savedCursorLockState;
}
}
10.15. SteamVR_Overlay.cs
Overlay就是VR中的2D界面。这个脚本对overlay进行了一些简单的封装。在实际使用中,开发者可以直接使用OpenVR.Overlay或者SteamVR.overlay来直接操作overlay,像前面讲的SteamVR_LoadLevel脚本中就直接使用了这个接口创建了两个overlay。而SteamVR_Overlay脚本是当作应用中的一个单例,如果需要借助它来显示overlay的话,需要将需要显示的内容绘制到SteamVR_Overlay.overlay这个纹理上,比如SteamVR_Menu、[Status]预制体中_Stats中的做法。在OpenVR的示例中,在[Status]预制体的Overlay子对象上有。
public class SteamVR_Overlay : MonoBehaviour
{
Overlay绘制的纹理,这个需要指定插件目录下的overlay.rendertexture这个纹 理
public Texture texture;
高质量overlay的两个参数,antialias显然是反走样,curved貌似是生成弧形的 曲面overlay环绕用户
public bool curved = true;
public bool antialias = true;
高质量overlay。参看:https://github.com/ValveSoftware/openvr/wiki/IVROverlay::SetHighQualityOverlay
在变形的时候合成,质量高是因为它直接在源纹理上采样,而不是先光栅化(像素化?) 到每只眼睛的渲染纹理上
public bool highquality = true;
名为scale,实际是overlay视图的宽度(单位为米)
public float scale = 3.0f; // size ofoverlay view
这个距离应该就是指与相机(原点)的距离
public float distance = 1.25f; // distancefrom surface
透明度
public float alpha = 1.0f; // opacity0..1
用一个Vector4表示uv纹理的偏移,其中x、y表示u、v偏移,z、w表示uv缩放 倍数?只是借用了一个有4个值字段的结构体布局,与向量并没有关系
public Vector4 uvOffset = new Vector4(0, 0, 1, 1);
鼠标x、y坐标的缩放因子,是指对鼠标移动距离的放大或缩小?相当于PC上调鼠标 的灵敏度?TODO 实际调节并看不出变化
public Vector2 mouseScale = new Vector2(1, 1);
这个是曲面overlay时的最小距离和最大距离。TODO 这两个参数实际调节也看不出实 际意义是什么——有点作用,感觉可以改变曲率,但效果很小
public Vector2 curvedRange = new Vector2(1, 2);
输入方式,有无输入事件和鼠标输入事件两种。TODO 不知道干嘛的
public VROverlayInputMethod inputMethod = VROverlayInputMethod.None;
Overlay的单一实例
static public SteamVR_Overlay instance { get; private set; }
可以有多个overlay,每个overlay使用唯一关键字,这里采用的是“unity:公司名 称.产品名称”
static public string key { get { return "unity:" + Application.companyName + "." + Application.productName;} }
overlay的标识——句柄
private ulong handle = OpenVR.k_ulOverlayHandleInvalid;
void OnEnable()
{
在Enable的时候就创建了一个以“unity:公司名称.产品名称”的overlay
var overlay = OpenVR.Overlay;
if (overlay != null)
{
var error =overlay.CreateOverlay(key, gameObject.name, ref handle);
if (error != EVROverlayError.None)
{
Debug.Log(overlay.GetOverlayErrorNameFromEnum(error));
enabled= false;
return;
}
}
实例使用的就是当前对象本身
SteamVR_Overlay.instance = this;
}
void OnDisable()
{
禁用的时候销毁overlay
if (handle != OpenVR.k_ulOverlayHandleInvalid)
{
var overlay = OpenVR.Overlay;
if (overlay != null)
{
overlay.DestroyOverlay(handle);
}
handle= OpenVR.k_ulOverlayHandleInvalid;
}
SteamVR_Overlay.instance = null;
}
在SteamVR_Render的渲染循环里面调用来更新overlay
public void UpdateOverlay()
{
var overlay = OpenVR.Overlay;
if (overlay == null)
return;
if (texture != null)
{
有指定纹理的话,显示overlay
var error =overlay.ShowOverlay(handle);
if (error == EVROverlayError.InvalidHandle|| error == EVROverlayError.UnknownOverlay)
{
if (overlay.FindOverlay(key, ref handle) != EVROverlayError.None)
return;
}
将纹理设置到overlay当中
var tex = new Texture_t();
tex.handle= texture.GetNativeTexturePtr();
tex.eType= SteamVR.instance.graphicsAPI;
tex.eColorSpace= EColorSpace.Auto;
overlay.SetOverlayTexture(handle, ref tex);
设置透明度、宽度、弧形曲面的距离范围
overlay.SetOverlayAlpha(handle,alpha);
overlay.SetOverlayWidthInMeters(handle,scale);
overlay.SetOverlayAutoCurveDistanceRangeInMeters(handle,curvedRange.x, curvedRange.y);
设置纹理映射坐标
var textureBounds= new VRTextureBounds_t();
textureBounds.uMin= (0 + uvOffset.x)* uvOffset.z;
textureBounds.vMin= (1 + uvOffset.y)* uvOffset.w;
textureBounds.uMax= (1 + uvOffset.x)* uvOffset.z;
textureBounds.vMax= (0 + uvOffset.y)* uvOffset.w;
overlay.SetOverlayTextureBounds(handle, ref textureBounds);
设置鼠标坐标缩放因子
var vecMouseScale= new HmdVector2_t();
vecMouseScale.v0= mouseScale.x;
vecMouseScale.v1= mouseScale.y;
overlay.SetOverlayMouseScale(handle, ref vecMouseScale);
var vrcam = SteamVR_Render.Top();
if (vrcam != null &&vrcam.origin != null)
{
有SteamVR_Camera相机,取overlay与相机之间的偏移,根据相机的 缩放因子及指定的距离计算overlay相对于跟踪原点的变换矩阵
var offset = new SteamVR_Utils.RigidTransform(vrcam.origin,transform);
offset.pos.x/= vrcam.origin.localScale.x;
offset.pos.y/= vrcam.origin.localScale.y;
offset.pos.z/= vrcam.origin.localScale.z;
offset.pos.z+= distance;
var t =offset.ToHmdMatrix34();
overlay.SetOverlayTransformAbsolute(handle, SteamVR_Render.instance.trackingSpace, ref t);
}
设置输入方式
overlay.SetOverlayInputMethod(handle,inputMethod);
只要指定了曲面或者反走样其中的一个就认为是高质量的overlay
if (curved ||antialias)
highquality= true;
if (highquality)
{
将当前overlay设为高质量overlay。同一时刻只能有一个高质量 overlay
overlay.SetHighQualityOverlay(handle);
设置曲面或者反走样
overlay.SetOverlayFlag(handle, VROverlayFlags.Curved,curved);
overlay.SetOverlayFlag(handle, VROverlayFlags.RGSS4X,antialias);
}
else if (overlay.GetHighQualityOverlay() == handle)
{
这个意思是取消所有的高质量overlay了? overlay.SetHighQualityOverlay(OpenVR.k_ulOverlayHandleInvalid);
}
}
else
{
没有指定纹理的话就隐藏overlay了
overlay.HideOverlay(handle);
}
}
供外部使用的获取一条overlay事件。搜代码并没有人调用
public bool PollNextEvent(ref VREvent_t pEvent)
{
var overlay = OpenVR.Overlay;
if (overlay == null)
return false;
var size = (uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(Valve.VR.VREvent_t));
return overlay.PollNextOverlayEvent(handle, ref pEvent, size);
}
public struct IntersectionResults
{
public Vector3 point;
public Vector3 normal;
public Vector2 UVs;
public float distance;
}
计算指定的射线(源点+方向)与overlay的交点,没有交点返回false。可以使用这 个方法通过手柄来操作overlay
public bool ComputeIntersection(Vector3 source, Vector3 direction, ref IntersectionResults results)
{
var overlay = OpenVR.Overlay;
if (overlay == null)
return false;
实际调用的是IVROverlay.ComputeOverlayIntersection,这里仅做一些参数 的转换
var input = new VROverlayIntersectionParams_t();
input.eOrigin= SteamVR_Render.instance.trackingSpace;
input.vSource.v0= source.x;
input.vSource.v1= source.y;
input.vSource.v2= -source.z;
input.vDirection.v0= direction.x;
input.vDirection.v1= direction.y;
input.vDirection.v2= -direction.z;
var output = new VROverlayIntersectionResults_t();
if (!overlay.ComputeOverlayIntersection(handle, ref input, ref output))
return false;
results.point= new Vector3(output.vPoint.v0,output.vPoint.v1, -output.vPoint.v2);
results.normal= new Vector3(output.vNormal.v0,output.vNormal.v1, -output.vNormal.v2);
results.UVs= new Vector2(output.vUVs.v0,output.vUVs.v1);
results.distance= output.fDistance;
return true;
}
}