1. 程式人生 > >#HTC VIVE #進行VR開發的環境

#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);
    }
}