HTC Vive controller手柄运动方向识别
本文由catzhang 编写,转载请注明出处,所有权利保留。
博客地址:http://blog.csdn.net/cartzhang
github地址:https://github.com/cartzhang
HTC Vive相信已经有很多人体验过,那为什么HTC Vive手柄可以判断识别玩家的运动方法,下面就从HTC Vive controller手柄运动方向识别的原理和实现过程上给大家做讲解。
一、概要
使用Vive手柄,我们需要做一个简单的姿势识别,用来判断手柄的运动方向,然后我根据需要做了一个运动方向的识别,根据上下左右和各个夹角的方向,总共有八个方向。
功能:基本实现了手柄的八方向运动方向识别,也可以叫动作识别。识别精度和效率,可以根据参数来调整。 里面也附带了一个通过射线识别的方法,当然有缺点在后面也做了分析和说明,若有问题,还请不吝指教。 同样工程在后面会给出源码和unity包,还有图片路径地址。
1 实现原理
原理很简单,就是根据路线,在一定时间内,记录路线数据,然后把数据映射到相机平面上,在根据取第一个点、中间点以及最后一个点,计算角度和斜率,在一定度数之内,都认为是某个方向的运行识别成功。
使用了Unity的5.4.0f3版本,也使用了之前的vive的Event项目的消息传递机制。
项目地址,把它放在了图说VR的工程中
https://github.com/cartzhang/ImgSayVRabc/tree/master/ViveEventDemo
希望有需要可以迁出和修改,提交,前几天也做了一点点的修改和提交。
当然,根据惯例,本项目的源码也会在项目后面分享出来,并给出到处的unity包,贴心吧。
打开下载好的项目,打开DemoScene中SteamVR_motion_direction_recongnize这个场景。
可以看到检视板中,内容不算多。
看看工程中大致的内容,包括样例场景,预制体对象,脚本和steamVR原插件中的内容。 当然SteamVR中代码已经做了部分修改,这个之前Event消息解耦使用所添加的一点点代码。
若你需要了解可以参考:
http://blog.csdn.net/cartzhang/article/details/53915229
https://github.com/cartzhang/ImgSayVRabc/tree/master/ViveEventDemo
有不足之处还请不吝指教。
3. 对相机渲染层的设置
因为这里有Vive的头盔中项目camera(eye),需要把层设置为不渲染UI。
然后也同样对UI项目做了处理,让他只渲染UI。
4. 手柄运动方向识别
主要的脚本为:GestureJudge.cs。
我在测试场景中,只使用了右侧手柄来处理,这里没有在left左侧手柄挂载代码,若有需要,可以自己添加,是没有问题的。
可以通过调节参数来识别效果。
挥动超过距离:手柄在识别过程中的路线,必须大于一定长度才算数。这里设置的为0.8f.
数据跟踪的最短距离: 在同一个位置,间隔太小的手柄位置,不作为参考数据。手柄运动之间间隔大于这个值,才有效的位置数据。
检测周期:数值越大识别越容易。但是过大,就会提高误识别率。默认为0.15秒。测试结果到0.3s也是可以的,这个值作为参数吧。数据过了这个时间,就重新清零检测。
检测角度:我们本来检测的是八个方向,在把个方向一定角度内都算是某个方向的,这就是冗余,因为没有冗余,必须直直的路线才可以的。
5. 手柄路径标识
写了个小代码来标识手柄的运动路线。
就是在手柄的位置,每间隔一段距离就放置一个红色小球来表示。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | ///
/// 标记手柄路经。 /// @cartzhang /// public class CotrollerPathMark : MonoBehaviour { public GameObject spehere; public float StepDistance = 0.1f; private Vector3 currentPos; // Use this for initialization void Start () { currentPos = this .transform.position; } // Update is called once per frame void Update () { if ( Vector3.Distance(currentPos, this .transform.position) > StepDistance) { GameObject Tmpobj = Instantiate(spehere, currentPos,Quaternion.identity) as GameObject; currentPos = transform.position; Destroy(Tmpobj, 0.3f); } } } |
6. 结果
在键盘上按下S键后,就可以挥动手柄来测试结果了。结果会实时在头盔中文字提示。
基本在正常速度下,都可以识别出来。
识别出来的结果还是通知来实现。若需要在根据结果来操作,只需要简单的订阅消息就可以了。是不是很简单。
三、识别代码解析
1. 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | using UnityEngine; using System.Collections; using SLQJ; using System.Collections.Generic; using UnityEngine.UI; ///
/// 主要功能: /// 1.实现8中不同方向上的姿势识别 /// 2.实现输入矢量来判断是否完成对应路线。 /// 注意:每次只能识别一个姿势,需要等识别完毕,才能下一个。 /// @cartzhang /// public enum GestureType: int { None = 0, Left_Right, LeftDown_RightUp, Down_Up, RightDown_LeftUp, Right_Left, RightUp_LeftDown, Up_Down, LeftUp_RightDown } public partial class GuestureJudge : MonoBehaviour { public Text showState; private bool isStartRecongnize = false ; [Header( "挥动需要超过的距离 default 0.8" )] public float RecongnizeMinStepDistance = 0.8f; [Header( "数据跟踪的最短距离 default 0.08" )] public float addListMinStepDist = 0.08f; [Header( "检测周期时间,default 0.15" )] public float stepTime = 0.15f; [Header( "检测角度的最小冗余" )] [Range(5,15)] public float MaxAnlgeToConfirm = 15f; // 8个方向都有15度的间隔 int arrowLayer = 10; int layerMask; int step = 0; float currentStepTime; ///开始检测标志 private bool bStartCheck = false ; //检测结果标志 private bool bOutputResult = false ; private GestureType gestureType = GestureType.None; private Camera mainCamera; void Start() { arrowLayer = 10; layerMask = 1 << arrowLayer; currentStepTime = stepTime; isStartRecongnize = false ; bOutputResult = false ; gestureType = GestureType.None; NotificationManager.Instance.Subscribe(NotificationType.Gesture_Recongnize.ToString(), GestureRecongnize); StartUnderEditorTest(); mainCamera = Camera.main; } void Update() { UpdateUnderEditor(); #if UNITY_EDITOR Debug.DrawRay(transform.position, transform.forward, Color.red); #endif CheckGestureByRay(); CheckGetsturebyCoordinate(); } void GestureRecongnize(MessageObject obj) { object [] objArray = ( object [])obj.MsgValue; StartCoroutine(RecongnizeGetsture(Vector3.zero,( float )objArray[1])); } ///
/// 射线检测 /// /// /// /// IEnumerator RecongnizeGetsture(Vector3 getstureVec, float TimeToDectect) { Debug.Log( "start recongnize" ); bOutputResult = false ; gestureType = GestureType.None; bStartCheck = true ; currentStepTime = stepTime; while (TimeToDectect > 0) { if (bOutputResult || gestureType != GestureType.None) { Debug.Log( "jump out while" ); #if !UNITY_EDITOR //for test TimeToDectect = 0; #endif } yield return null ; TimeToDectect -= Time.deltaTime; } bStartCheck = false ; Debug.Log( "begin notify" ); NotificationManager.Instance.Notify( NotificationType.Gesture_Recongnize_Result.ToString(), bOutputResult); NotificationManager.Instance.Notify( NotificationType.Gesture_Recongnize_Result.ToString(), gestureType); } ///
/// 射线来检测碰撞体标签 /// private void CheckGestureByRay() { if (!bStartCheck) { return ; } currentStepTime -= Time.deltaTime; if (currentStepTime <= 0) { currentStepTime = stepTime; step = 0; } // 动作判断 RaycastHit hit; if (Physics.Raycast(transform.position, transform.forward, out hit, 6f, layerMask)) { if (hit.collider.tag == "ArrowA" ) { Debug.Log( "collison A" ); step = 1; } if (hit.collider.tag == "ArrowB" && step == 1) { Debug.Log( "collison B" ); step = 2; bOutputResult = true ; bStartCheck = false ; //Destroy(hit.transform.parent.gameObject, 0); } } } #region Test by use coordinate ///
/// 每隔一帧采样数据,然后在时间间隔内 /// /// /// /// private bool bInitialOnce = false ; private int linkListMaxLength; private List recordPosList; private List recordWorldToViewPosList; private Vector3[] SamplePos = new Vector3[3]; private int UpdateLenToCheck = 5; private int iNewAddCount = 0; private void CheckGetsturebyCoordinate() { if (!bStartCheck) { return ; } // 1. 初始化数据表,并把数据转换到相机视口坐标系上。 Vector3 currentRecordPos = transform.position; Vector3 currentWVPPos = mainCamera.WorldToViewportPoint(currentRecordPos); gestureType = GestureType.None; if (!bInitialOnce) { bInitialOnce = true ; linkListMaxLength = ( int )(RecongnizeMinStepDistance / addListMinStepDist); linkListMaxLength = linkListMaxLength < 5 ? 5 : linkListMaxLength; // 最小存5个数据 recordPosList = new List(linkListMaxLength); recordWorldToViewPosList = new List(linkListMaxLength); recordPosList.Add(currentRecordPos); recordWorldToViewPosList.Add(currentWVPPos); UpdateLenToCheck = ( int )(linkListMaxLength * 0.4f); iNewAddCount = 0; } //2. 数据加入,最小距离判断是否符合加入条件 if (Vector3.Distance(recordPosList[recordPosList.Count - 1], currentRecordPos) > addListMinStepDist) { recordPosList.Add(currentRecordPos); recordWorldToViewPosList.Add(currentWVPPos); iNewAddCount++; } //3. list 的数据的添加和刷新,每次新入多少数据,重新开启检测。 int currentListLen = recordPosList.Count; if (currentListLen >= linkListMaxLength) { // 刷新iNewAddCount个数据,再做检测。 if (iNewAddCount > UpdateLenToCheck) { iNewAddCount = 0; // 4. 移动行程换算角度来做判断 if (Vector3.Distance(recordPosList[0], recordPosList[currentListLen - 1]) > RecongnizeMinStepDistance * 0.3f) { int middleIndex = currentListLen >> 1; SamplePos[0] = (middleIndex <= 3) ? recordWorldToViewPosList[0]: (recordWorldToViewPosList[0] + recordWorldToViewPosList[1] + recordWorldToViewPosList[2])/3; SamplePos[1] = (middleIndex <= 3) ? recordWorldToViewPosList[middleIndex]: (recordWorldToViewPosList[middleIndex-1] + recordWorldToViewPosList[middleIndex] + recordWorldToViewPosList[middleIndex+1]) / 3; SamplePos[2] = (middleIndex <= 3) ? recordWorldToViewPosList[currentListLen - 1]: (recordWorldToViewPosList[currentListLen - 1] + recordWorldToViewPosList[currentListLen - 2]) / 2; CalcuolateDirection(); showState.text = gestureType.ToString(); if (gestureType != GestureType.None) { Debug.Log( "current recongnize gesture is " + gestureType.ToString()); } } } // 移除首位,并开始判断 recordPosList.RemoveAt(0); recordWorldToViewPosList.RemoveAt(0); //Debug.Log("current recongnize gesture is " + gestureType.ToString()); } } ///
/// 姿势判断。 /// private void CalcuolateDirection() { float anlge1 = GetAngleWithX(SamplePos[1] - SamplePos[0]); float anlge2 = GetAngleWithX(SamplePos[2] - SamplePos[1]); float stepAngle = 45; // 8平分角度间隔 int step = 0; //1. X轴0度夹角附近 if ((anlge1 < MaxAnlgeToConfirm + stepAngle * step || (anlge1 > stepAngle * 8 - MaxAnlgeToConfirm && anlge1 < 8 * stepAngle)) && // (anlge2 < MaxAnlgeToConfirm + stepAngle * step || (anlge2 > stepAngle * 8 - MaxAnlgeToConfirm && anlge2 < 8 * stepAngle)) ) { gestureType = GestureType.Left_Right; goto CDEND; } //2. X轴逆时针方向计算 for (step = 1; step < 8; step++) { if ((anlge1 > stepAngle * step - MaxAnlgeToConfirm && anlge1 < stepAngle * step + MaxAnlgeToConfirm) && (anlge2 > stepAngle * step - MaxAnlgeToConfirm && anlge2 < stepAngle * step + MaxAnlgeToConfirm)) { gestureType = (GestureType)(step + 1); break ; } } CDEND: return ; } ///
/// 计算与X轴夹角 /// /// /// private float GetAngleWithX(Vector3 pos3D) { float angleOutput = Vector2.Angle( new Vector2(pos3D.x, pos3D.y), Vector2.right); if (pos3D.y <= 0) { angleOutput = 360 - angleOutput; } if (angleOutput <= 0) { return 0; } return angleOutput; } #endregion } ///
/// 动作识别的调用和结果返回。 /// public partial class GuestureJudge { void StartUnderEditorTest() { NotificationManager.Instance.Subscribe( NotificationType.Gesture_Recongnize_Result.ToString(), GetRecongnizeResult); } void UpdateUnderEditor() { if (Input.GetKeyDown(KeyCode.S)) { NotificationManager.Instance.Notify( NotificationType.Gesture_Recongnize.ToString(), new Vector3(5,0,0),1000.0f); //Debug.Log("start to check gesture"); } } #if UNITY_EDITOR void OnDrawGizmosSelected() { Gizmos.color = Color.blue; Gizmos.DrawLine(transform.position, transform.position + (transform.forward * 100f)); } void GetRecongnizeResult(MessageObject obj) { Debug.Log( "recongnize output is " + obj.MsgValue); } #endif } |
识别的代码并算多,但也不少。我把类写成了两个部分,一部分就是常规的算法实现过程,另一部分就是辅助操作的代码,包括按下按键S开始识别运动方向,实时绘制射线,消息的通知和订阅等,结果的显示。
2. 方向枚举类
首先给八个方向定义了一个枚举类。
1 2 3 4 5 6 7 8 9 10 11 12 | public enum GestureType: int { None = 0, Left_Right, LeftDown_RightUp, Down_Up, RightDown_LeftUp, Right_Left, RightUp_LeftDown, Up_Down, LeftUp_RightDown } |
这里说明,我实现了两种方向判断的,一种是用射线来判断的,对应函数CheckGestureByRay(),而另一种方法,是根据开头文章说的,根据路线方向通过矢量判断。
3. 射线判断方法及其优缺点
射线判断方法,使用了先判断射线射中的对象,通过层过滤和标签过滤来判断是否通过A点,然后在通过B点,这里就返回了。
优点是,几乎可以识别相机平面的所有方向,不在意朝向和位置,只要碰撞盒对就没有问题。
缺点是,需设置层和标签。还有就是移动速度也不能过高,因为他会来不及计算或穿透过碰撞体,也可能会发生的。
说明,这个方法在的demo场景中并没有展示。有需要的可以找我或自己也可以根据代码稍微设置下tag和Layer,几乎无难度。
4. 路径跟踪计算及其优缺点
路径跟踪计算,根据记录来判断有限长度序列中的点,判断与X轴
优点是,可以屏蔽转向中,不会误识别成功。计算量其实也不大,效率也可以。可以根据需要随时调节参数,得到想要的结果。
缺点:算法需要转换为相机矩阵下的平面,因为手柄在移动过程中,相机也在移动,造成数据点在不同坐标平面下,可能会造成误识别。若每个点都计算在当前下,在整体计算过程中,就会得到记录的坐标点大家都不在一个坐标下生成的尴尬情况,这样更不能保证是否精确了。(希望大家有好的方法或想法,还请不吝指教。)
还有就是在代码中,
1 2 3 4 | ///
/// 姿势判断。 /// private void CalcuolateDirection() |
里面使用了很多人不太乐意的goto。其实,我大部分或说绝大部分时间都不希望这样用的,但是这里觉得可以用一些。
四、源码地址
源码地址:
https://github.com/cartzhang/vive_motion_direction_recongnize
整个工程包下载地址:
图片地址:
https://github.com/cartzhang/vive_motion_direction_recongnize/tree/master/img
五、 参考
[1] https://github.com/cartzhang/ImgSayVRabc/tree/master/ViveEventDemo
[2] http://blog.csdn.net/cartzhang/article/details/53915229