#HTC VIVE #进行VR开发的环境

因为一些原因,要求上周的博客缺了一篇,于是在这周补上。好了,让我们开始吧。

HTC VIVE 开发中最重要的基石是steamVR,只有充分理解steamVR,才能在开发中得心应手,创造最佳的虚拟现实体验。

知名游戏公司Valve将OpenVR和Steam平台结合在一起开发了SteamVR,这是一个非常有远见的战略,未来的VR内容平台必然是炙手可热的。让我们看一下SteamVR是如何结合HTC Vive工作的。

首先,HTC Vive提供了两个基站,我们将他们分别架设在一个空间的两端互为犄角,从而建立起一个定位空间。我们将这个空间称为Light House,因为在这个空间中布满了我们看不到的激光。SteamVR会根据激光数据精确地定位头盔和两个手柄的位置,让玩家更精确地在游戏中进行交互,从而得到更棒的VR体验。

然后,Valve为了让Unity的开发者更好地开发出VR游戏,在SteamVR的基础上为Unity开发出了SteamVR Plugin。可以在Unity的Asset Store里找到它。下载后就可以看到



在Scripts文件夹里,我们可以找到SteamVR各脚本的功能

SteamVR/Scripts/下脚本各功能的实现 

1、SteamVR.cs 单例管理类,管理SteamVR程序的运行和终止。 2、SteamVR_Camera.cs 
给场景添加一个最基本可运行的SteamVR组。 3、SteamVR_CameraFlip.cs 使用Shader将屏幕图像反转得到最终图像。 
4、SteamVR_CameraMask.cs 将头盔中看不到的屏幕像素遮盖。 5、SteamVR_Controller.cs 
管理类,管理所有设备的输入控制 6、SteamVR_ControllerManager.cs 管理类,管理场景中的设备活动 
7、SteamVR_Ears.cs 控制Audio Listener的方向 8、SteamVR_ExternalCamera.cs 
用于渲染外部摄像机 9、SteamVR_Fade.cs 屏幕渐变功能 10、SteamVR_Frustum.cs 生成用于渲染的面片 
11、SteamVR_GameView.cs 处理除眼图像之外的渲染 12、SteamVR_IK.cs 手柄IK的控制 
13、SteamVR_LoadLevel.cs 用于场景之间的平滑切换 14、SteamVR_Menu.cs 给出一个范例菜单 
15、SteamVR_Overlay.cs 提供和控制2D图像的绘制 16、SteamVR_PlayArea.cs 对移动空间的设置 
17、SteamVR_Render.cs 控制眼图像的渲染 18、SteamVR_RenderModel.cs 渲染手柄模型 
19、SteamVR_Skybox.cs 设置天空盒 20、SteamVR_SphericalProjection.cs 
应该是应用畸变投影矩阵 21、SteamVR_Stats.cs 通过GUI Text显示头盔状态 22、SteamVR_Status.cs 
由事件控制的渐变效果的基类 23、SteamVR_StatusText.cs 继承22的文字渐变 
24、SteamVR_TestController.cs 测试手柄每个按钮的输入 25、SteamVR_TrackedCamera.cs 
提供记录相机的位置的功能 26、SteamVR_TrackedObject.cs 使场景中的物体和控制器的Pose保持一致 
27、SteamVR_UpdatePose.cs 当使用OpenVR接口时用此更新Pose 28、SteamVR_Utils.cs 
一些公共方法和数据结构 SteamVR/Extras/脚本下功能的实现

详细脚本解析:

SteamVR_GazeTracker.cs脚本解析 
这个脚本的作用是判断当前物体是否被用户(头显)所注视,进入注视和离开注视都会有回调。处于注视状态的物体与实际注视点的距离范围定义为小于0.15米,而离开注视状态的距离范围为大于0.4米。之所以有一个大概的范围,并且使用了一个平面来相交,是因为注视这个动作是比较粗略的,玩家比较难能精确注视。 
  Gaze回调的事件结构体,只有一个参数,即距离,表示凝视点与物体(中心)的距离

public struct GazeEventArgs
{
    public float distance;
}

public delegate void GazeEventHandler(object sender, GazeEventArgs e);

public class SteamVR_GazeTracker : MonoBehaviour
{
    //当前是否处于gaze状态
    public bool isInGaze = false;
    //入gaze状态回调,使用者可以通过代码添加自己的事件处理方法(在Inspector      中不会出现)
    public event GazeEventHandler GazeOn;
    //离开gaze状态回调
    public event GazeEventHandler GazeOff;
    //定义的进入gaze与离开gaze的距离范围
    public float gazeInCutoff = 0.15f;
    public float gazeOutCutoff = 0.4f;

    // Contains a HMD tracked object that we can use to find the user's gaze
    //头显的transform对象
    Transform hmdTrackedObject = null;
    // Use this for initialization
    void Start ()
    {

    }
    public virtual void OnGazeOn(GazeEventArgs e)
    {
        //如果有注册GazeOff回调,调用它
        if (GazeOn != null)
            GazeOn(this, e);
    }
    public virtual void OnGazeOff(GazeEventArgs e)
    {

        //如果有注册GazeOff回调,调用它
        if (GazeOff != null)
            GazeOff(this, e);
    }
    // Update is called once per frame
    void Update ()
    {
        // If we haven't set up hmdTrackedObject find what the user is looking at
        if (hmdTrackedObject == null)
        {
            //首次调用会去查找头显,方法是查找所有SteamVR_TrackedObject对象。所有的跟踪对象(比如头显、手柄、基站)都是SteamVR_TrackedObject对象(相应的对象上附加了SteamVR_TrackedObject脚本)
            SteamVR_TrackedObject[] trackedObjects = FindObjectsOfType<SteamVR_TrackedObject>();
            foreach (SteamVR_TrackedObject tracked in trackedObjects)
            {
                if (tracked.index == SteamVR_TrackedObject.EIndex.Hmd)
                {
                    //找到头显设备,取其transform对象。头显设备的索引是0号索引
                    hmdTrackedObject = tracked.transform;
                    break;
                }
            }
        }
        if (hmdTrackedObject)
        {
            //构造一条从头显正方向的射线
            Ray r = new Ray(hmdTrackedObject.position, hmdTrackedObject.forward);
            //构造一个头显正方向、在当前物体位置的平面
            Plane p = new Plane(hmdTrackedObject.forward, transform.position);

            float enter = 0.0f;
            //射线与物体平面正向相交,返回的enter为沿射线的距离。如果不相交,或者反向相交,则下面的Raycast返回false
            if (p.Raycast(r, out enter))
            {
                //intersect为射线与物体平面在三维空间的交点
                Vector3 intersect = hmdTrackedObject.position + hmdTrackedObject.forward * enter;
                //计算空间两点的距离,即物体当前位置与交点的距离
                float dist = Vector3.Distance(intersect, transform.position);
                //Debug.Log("Gaze dist = " + dist);
                if (dist < gazeInCutoff && !isInGaze)
                {
                    //当前物体与凝视点的距离小于0.15米,则认为进入gaze状态
                    isInGaze = true;
                    GazeEventArgs e;
                    e.distance = dist;
                    OnGazeOn(e);
                }
                else if (dist >= gazeOutCutoff && isInGaze)
                {
                    //当前物体与凝视点的距离超过0.4米,则认为离开gaze状态
                    isInGaze = false;
                    GazeEventArgs e;
                    e.distance = dist;
                    OnGazeOff(e);
                }
            }
        }
    }
}
这个脚本的作用与上面的SteamVR_GazeTracker相关及类似。GazeTracker是通过头显的正视方向与物体相交来计算交点的。而这里是通过所谓的激光束来与物体相交的。激光束就是手柄指向的方向,可以在游戏里面把这个方向渲染出一条激光束出来,特别是在通过手柄进行菜单的UI操作的时候。在github openvr的sample目录下的unity_teleport_sample示例有使用,它被加到右手柄上 

 同上面的GazeTracker一样,触发的事件所带的参数

public struct PointerEventArgs
{
    //控制器(手柄)索引
    public uint controllerIndex;
    //目前好像并没有用到
    public uint flags;
    //激光原点到命中点(交点)的距离
    public float distance;
    //命中物体的transform对象
    public Transform target;
}
public delegate void PointerEventHandler(object sender, PointerEventArgs e);


public class SteamVR_LaserPointer : MonoBehaviour
{
    //这个变量并未使用
    public bool active = true;
    // 激光的颜色
    public Color color;
    //激光束的粗细(创建了一个立方体,按下面的scale,x、y是0.002,z是100,就能看 到是一条很长的细线了)
    public float thickness = 0.002f;
    //一个空的GameObject,用于作激光束的parent
    public GameObject holder;
    //激光束本身,是用一个立方体拉长来模拟的(为啥不用圆柱体?显然立方体要比圆柱体渲染简单得多,在很细的情况下,用立方体是明智的选择)
    public GameObject pointer;
    //用来判断是否为第一次调用
    bool isActive = false;
    //这个是暴露在inspector中的属性,用于控制是否给激光束(长方体)添加刚体。本身光是没有重量的,没有必要添加刚体吧。所以这里缺省是false
    public bool addRigidBody = false;
    //这个变量并未使用
    public Transform reference;
    //同上面的GazeTracker一样,用于触发激光命中和离开事件
    public event PointerEventHandler PointerIn;
    public event PointerEventHandler PointerOut;
    //上次激光命中的物体的transform对象,用于判断是否命中同一个物体
    Transform previousContact = null;


    // Use this for initialization
    void Start ()
    {
        //在脚本被加载的时候,做一些初始化
        //首先创建一个holder(即激光束的父物体)
        holder = new GameObject();
        //holder的transform的parent设为当前脚本所在的物体(通常这个脚本会加到控制器手柄上面)
        holder.transform.parent = this.transform;
        //位置设在0点(本地坐标系,相对于父亲)
        holder.transform.localPosition = Vector3.zero;
        holder.transform.localRotation = Quaternion.identity;
        //创建激光束,用长方体模拟
        pointer = GameObject.CreatePrimitive(PrimitiveType.Cube);
        //将父亲设为上面的holder
        pointer.transform.parent = holder.transform;
        //设置locale为(0.002,0.002,100),看起来就是一条很长的线
        pointer.transform.localScale = new Vector3(thickness, thickness, 100f);
        //位置设在父亲的(0,0,50)位置,因为对于立方体(长方体),其中心在立方体中心,因为上面被放大到了100倍,那移动位置到(0,0,50)可以让激光束的起点为父亲
        pointer.transform.localPosition = new Vector3(0f, 0f, 50f);

        pointer.transform.localRotation = Quaternion.identity;
        // 如果指定了addRigidBody为true,则为激光束添加一个刚体,对应的collider 则只设为触发器(不会执行碰撞,但会进入代码)。否则,会把collider销毁掉,也就是不需要collider
        BoxCollider collider = pointer.GetComponent<BoxCollider>();
        if (addRigidBody)
        {
            if (collider)
            {
                collider.isTrigger = true;
            }
            Rigidbody rigidBody = pointer.AddComponent<Rigidbody>();
            rigidBody.isKinematic = true;
        }
        else
        {
            if(collider)
            {
                Object.Destroy(collider);
            }
        }
        //新建纯色材质并添加到MeshRender中。Color值通过inspector设置
        Material newMaterial = new Material(Shader.Find("Unlit/Color"));
        newMaterial.SetColor("_Color", color);
        pointer.GetComponent<MeshRenderer>().material = newMaterial;
    }

    public virtual void OnPointerIn(PointerEventArgs e)
    {
        //回调激光命中委托
        if (PointerIn != null)
            PointerIn(this, e);
    }

    public virtual void OnPointerOut(PointerEventArgs e)
    {
        //回调激光不再命中委托
        if (PointerOut != null)
            PointerOut(this, e);
    }


    // Update is called once per frame
    void Update()
    {
        if (!isActive)
        {
            //第一次调用时将holder设为active(当前物体transform的第一个child就是holder)
            isActive = true;
            this.transform.GetChild(0).gameObject.SetActive(true);
        }
        //命中物体(或者说激光束)的最远距离记为100米
        float dist = 100f;
        //当前物体(手柄上)上还要挂一个SteamVR_TrackedController脚本
        SteamVR_TrackedController controller = GetComponent<SteamVR_TrackedController>();
        //  构造一条射线
        Ray raycast = new Ray(transform.position, transform.forward);
        RaycastHit hit;
        //计算射线命中的场景中的物体
        bool bHit = Physics.Raycast(raycast, out hit);

        if(previousContact && previousContact != hit.transform)
        {
            // 如果之前已经有一个命中的物体,而当前命中的物体发生了变化,那么说明前一个命中的物体就要收到一个不再命中的通知
            PointerEventArgs args = new PointerEventArgs();
            if (controller != null)
            {
                args.controllerIndex = controller.controllerIndex;
            }
            args.distance = 0f;
            args.flags = 0;
            args.target = previousContact;
            OnPointerOut(args);
            previousContact = null;

        }
        if(bHit && previousContact != hit.transform)
        {
            //通知命中新的物体
            PointerEventArgs argsIn = new PointerEventArgs();
            if (controller != null)
            {
                argsIn.controllerIndex = controller.controllerIndex;
            }
            // hit.distance为射线原点到命中点的距离
            argsIn.distance = hit.distance;
            argsIn.flags = 0;
            //target记录的是命中物体的transform
            argsIn.target = hit.transform;
            OnPointerIn(argsIn);
            // 记录上一次命中的物体的transform
            previousContact = hit.transform;
        }
        if(!bHit)
        {
            previousContact = null;
        }
        if (bHit && hit.distance < 100f)
        {
            //如果命中物体距离小于100,则记录下来,否则最远就是100米
            dist = hit.distance;
        }

        if (controller != null && controller.triggerPressed)
        {
            //当按下扳机键时,将光束的粗细增大5倍,同时长度会设为dist,这样看起来光束就会到命中点截止,不会穿透物体
            pointer.transform.localScale = new Vector3(thickness * 5f, thickness * 5f, dist);
        }
        else
        {
            //按下扳机或者当前控制器没有添加SteamVR_TrackedController时,显示原始粗细的光束
            pointer.transform.localScale = new Vector3(thickness, thickness, dist);
        }
        //光束的位置总是设在光束长度的一半的位置,使得光束看起来总是从手柄发出来的
        pointer.transform.localPosition = new Vector3(0f, 0f, dist/2f);
    }
}


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页