#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回撥的事件結構體,只有一個引數,即距離,表示凝視點與物體(中心)的距離
這個指令碼的作用與上面的SteamVR_GazeTracker相關及類似。GazeTracker是通過頭顯的正視方向與物體相交來計算交點的。而這裡是通過所謂的鐳射束來與物體相交的。鐳射束就是手柄指向的方向,可以在遊戲裡面把這個方向渲染出一條鐳射束出來,特別是在通過手柄進行選單的UI操作的時候。在github openvr的sample目錄下的unity_teleport_sample示例有使用,它被加到右手柄上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); } } } } }
同上面的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);
}
}