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

发表于2017-05-10
评论0 7k浏览

10.12.       SteamVR_IK.cs

注释的最开始已经说明了,它只是一个简单的两根骨头的模拟(可以用于手臂和腿)。这个脚本要放到模拟手臂的物体上(当然就是左右控制器了),比如在OpenVR插件的Extra/SteamVR_TestIK.unity场景里:

画了一个模拟手臂的层级关系:

 

public class SteamVR_IK : MonoBehaviour
{

这个target为肩的目标位置
       public Transform target;

StartWrist(腕),jointElbow(肘),endShoulder(肩)。从实际经验来    看这好理解,腕带动肘,肘带动肩。但从实际的空间结构看,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随着控制器移动,然后带动ElbowShoulder运动。但有点很奇怪是它需要指定ShoulderElbowtarget,即目标位置,既然都指定了目标位置,那还要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;

以附加的方式加载关卡。调用的是UnityAPI    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;

这个表示是否显示网格。如果不显示网格,则会使用上面指定的颜色作为合成器的前景       颜色。如果指定了天空盒,这个参数则不起作用。所谓的合成器界面(带网格)的就是       下面这个界面了:

说明: {0B40198F-7EEC-4516-8131-E66BB7152A5A}
       // 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接口直接设置overlayalpha
                     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);

            渐显合成器网格。调用这句会渐变到下面这个带网格的合成器界面:

            说明: {0B40198F-7EEC-4516-8131-E66BB7152A5A}
                            // 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)
                     {

            异步加载。直接使用UnitySceneManager来加载。看起来这就和在                       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键可以显示和隐藏):

说明: {A0805DEE-D949-4717-8C50-8486F3C33FCC}

 

[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;

保存overlayuvOffset配置,用于恢复
       Vector4 uvOffset;

保存overlaydistance配置,用于恢复
       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=
Screen.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,对应于overlayhighquality        数,表示高质量?
              overlay.highquality= GUILayout.Toggle(overlay.highquality, "Highquality");

             
if (overlay.highquality)
              {

        如果highqualitytrue,则再显示对应于overlaycurvedantilias                    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_CameraWireframe参数
                     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;

    保存uvOffsetdistance参数
              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)
              {

        恢复overlayuvOffsetdistance参数
                     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相机的originlocaleScale上面,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纹理的偏移,其中xy表示uv偏移,zw表示uv缩放     倍数?只是借用了一个有4个值字段的结构体布局,与向量并没有关系
       public Vector4 uvOffset = new Vector4(0, 0, 1, 1);

鼠标xy坐标的缩放因子,是指对鼠标移动距离的放大或缩小?相当于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;
       }
}

 

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