HTC vive 手柄轉動閥門功能

HTC vive設備結合unity開發手柄轉動閥門功能
現在需求是:使用手柄握住一個閥門,進行旋轉。
如下圖:
 


所有的交互都是要在兩個互動的物體之間做文章,VIVE裏也是一樣,所有要在手柄和閥門兩個方面進行“加工”。

先看手柄需要做哪些“加工”
程序現在都在走“短小快”的路線。所以插件VRTK肯定是很好的選擇
在手柄上加上VRTK裏的交互必要的腳本,這些腳本插件裏都有,如下圖(藍色箭頭標記爲必須加的腳本)。
在本案例中我使用的是Grab的方式進行轉動閥的,所以添加的是VRTK_Interact Grab的腳本。也可以根據需求自己修改。修改方法爲在Events腳本里有各種觸發方式的進行對應按鍵的選擇。如下圖:
 


有了這些腳本手柄的交互功能就已經具備了。只剩下被觸碰的物體了。

接受觸碰的物體需要進行的準備:
因爲需要交互所以collider是必不可少的,還有rigidbody,記住不要勾選重力選項。因爲這個要配合下面的VRTK_Knob腳本使用。Device_Value是我自己寫的傳值腳本,此處只講轉動方法不需要添加該腳本。如下圖:

 


上圖中的Clickpress腳本繼承了VRTK_InteractableObject腳本,這個腳本也是VRTK插件裏的。如果只是單純實現本案例的轉動功能完全可以使用VRTK_InteractableObject腳本。此處要注意轉動的原理是採用unity裏的鉸鏈的方法,所以在該腳本里有一次選擇抓取機制方法的地方要選擇Spring_Joint的方法。同樣既然是要抓取那肯定要勾選抓取的選項 ,如下圖:
 



如果要添加其他功能,需要繼承該腳本重寫某些方法。下面的代碼是最常用 的幾個方法也是我的腳本Clickpress裏用的方法:
 


VRTK_Knob腳本是一個用來轉動跟隨的腳本。
既然轉動那可得要選擇轉動的物體和軸向,如圖:
 

DIrection就是要轉動的軸向,下面的兩個參數是轉動最大小的限度,step size是轉動數值的精確度。

根據需求本案例選擇Y軸,如圖:
 


GO物體就是要被旋轉的物體,使用時直接拖動過來就可以。這個GO物體原本腳本是沒有的,我把原本的腳本稍稍做了加工。
代碼如下:
[C#] 純文本查看 複製代碼
 
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
namespace VRTK
{
    using UnityEngine;
     
    public class VRTK_Knob : VRTK_Control
    {
 
 
        public GameObject go;
 
        public enum KnobDirection
        {
            x, y, z // TODO: autodetect not yet done, it's a bit more difficult to get it right
        }
         
        public KnobDirection direction = KnobDirection.x;
        public float min = 0f;
        public float max = 100f;
        public float stepSize = 1f;
 
        private static float MAX_AUTODETECT_KNOB_WIDTH = 3; // multiple of the knob width
 
        private KnobDirection finalDirection;
        private Quaternion initialRotation;
        private Vector3 initialLocalRotation;
        private Rigidbody rb;
        private VRTK_InteractableObject io;
 
        protected override void InitRequiredComponents()
        {
            initialRotation = transform.rotation;
            initialLocalRotation = transform.localRotation.eulerAngles;
            InitRigidBody();
            InitInteractable();
            SetContent(go,false);//cdl
        }
 
        protected override bool DetectSetup()
        {
            finalDirection = direction;
            SetConstraints(finalDirection);
 
            return true;
        }
 
        protected override ControlValueRange RegisterValueRange()
        {
            return new ControlValueRange() { controlMin = min, controlMax = max };
        }
 
        protected override void HandleUpdate()
        {
           
            value = CalculateValue();
        }
 
        private void InitRigidBody()
        {
            rb = GetComponent<Rigidbody>();
            if (rb == null)
            {
                rb = gameObject.AddComponent<Rigidbody>();
            }
            rb.isKinematic = false;
            rb.useGravity = false;
            rb.angularDrag = 10; // otherwise knob will continue to move too far on its own
        }
 
        private void SetConstraints(KnobDirection direction)
        {
            if (!rb) return;
 
            rb.constraints = RigidbodyConstraints.FreezeAll;
            switch (direction)
            {
                case KnobDirection.x:
                    rb.constraints -= RigidbodyConstraints.FreezeRotationX;
                    break;
                case KnobDirection.y:
                    rb.constraints -= RigidbodyConstraints.FreezeRotationY;
                    break;
                case KnobDirection.z:
                    rb.constraints -= RigidbodyConstraints.FreezeRotationZ;
                    break;
            }
        }
 
        private void InitInteractable()
        {
            io = GetComponent<VRTK_InteractableObject>();
            if (io == null)
            {
                io = gameObject.AddComponent<VRTK_InteractableObject>();
            }
            io.isGrabbable = true;
            io.precisionSnap = true;
            io.grabAttachMechanic = VRTK_InteractableObject.GrabAttachType.Spring_Joint;
           
        }
 
        private KnobDirection DetectDirection()
        {
            KnobDirection direction = KnobDirection.x;
            Bounds bounds = Utilities.GetBounds(transform);
 
            // shoot rays in all directions to learn about surroundings
            RaycastHit hitForward;
            RaycastHit hitBack;
            RaycastHit hitLeft;
            RaycastHit hitRight;
            RaycastHit hitUp;
            RaycastHit hitDown;
            Physics.Raycast(bounds.center, Vector3.forward, out hitForward, bounds.extents.z * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
            Physics.Raycast(bounds.center, Vector3.back, out hitBack, bounds.extents.z * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
            Physics.Raycast(bounds.center, Vector3.left, out hitLeft, bounds.extents.x * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
            Physics.Raycast(bounds.center, Vector3.right, out hitRight, bounds.extents.x * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
            Physics.Raycast(bounds.center, Vector3.up, out hitUp, bounds.extents.y * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
            Physics.Raycast(bounds.center, Vector3.down, out hitDown, bounds.extents.y * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
 
            // shortest valid ray wins
            float lengthX = (hitRight.collider != null) ? hitRight.distance : float.MaxValue;
            float lengthY = (hitDown.collider != null) ? hitDown.distance : float.MaxValue;
            float lengthZ = (hitBack.collider != null) ? hitBack.distance : float.MaxValue;
            float lengthNegX = (hitLeft.collider != null) ? hitLeft.distance : float.MaxValue;
            float lengthNegY = (hitUp.collider != null) ? hitUp.distance : float.MaxValue;
            float lengthNegZ = (hitForward.collider != null) ? hitForward.distance : float.MaxValue;
 
            // TODO: not yet the right decision strategy, works only partially
            if (Utilities.IsLowest(lengthX, new float[] { lengthY, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
            {
                direction = KnobDirection.z;
            }
            else if (Utilities.IsLowest(lengthY, new float[] { lengthX, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
            {
                direction = KnobDirection.y;
            }
            else if (Utilities.IsLowest(lengthZ, new float[] { lengthX, lengthY, lengthNegX, lengthNegY, lengthNegZ }))
            {
                direction = KnobDirection.x;
            }
            else if (Utilities.IsLowest(lengthNegX, new float[] { lengthX, lengthY, lengthZ, lengthNegY, lengthNegZ }))
            {
                direction = KnobDirection.z;
            }
            else if (Utilities.IsLowest(lengthNegY, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegZ }))
            {
                direction = KnobDirection.y;
            }
            else if (Utilities.IsLowest(lengthNegZ, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegY }))
            {
                direction = KnobDirection.x;
            }
 
            return direction;
        }
 
        private float CalculateValue()
        {
            float angle = 0;
            switch (finalDirection)
            {
                case KnobDirection.x:
                    angle = transform.localRotation.eulerAngles.x - initialLocalRotation.x;
                    break;
                case KnobDirection.y:
                    angle = transform.localRotation.eulerAngles.y - initialLocalRotation.y;
                    break;
                case KnobDirection.z:
                    angle = transform.localRotation.eulerAngles.z - initialLocalRotation.z;
                    break;
            }
            angle = Mathf.Round(angle * 1000f) / 1000f; // not rounding will produce slight offsets in 4th digit that mess up initial value
 
            // Quaternion.angle will calculate shortest route and only go to 180
            float value = 0;
            if (angle > 0 && angle <= 180)
            {
                value = 360 - Quaternion.Angle(initialRotation, transform.rotation);
            }
            else
            {
                value = Quaternion.Angle(initialRotation, transform.rotation);
            }
 
            // adjust to value scale
            value = Mathf.Round((min + Mathf.Clamp01(value / 360f) * (max - min)) / stepSize) * stepSize;
            if (min > max && angle != 0)
            {
                value = (max + min) - value;
            }
            
            return value;
        }
    }
}


這樣手柄和被接觸物體需要的東西都滿足了就實現了該功能。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章