現在需求是:使用手柄握住一個閥門,進行旋轉。
如下圖:
所有的交互都是要在兩個互動的物體之間做文章,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; } } } |
這樣手柄和被接觸物體需要的東西都滿足了就實現了該功能。