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

这样手柄和被接触物体需要的东西都满足了就实现了该功能。