#创新实训#VR漫游项目汇报3

在这次项目中,我的任务是实现手柄轨迹的监测,并判定它围成的形状;其中就需要用到手柄的方向参数,但HTC vive的官方API中的Touchpad的方向只是很粗略的大方向,达不到我想要的较为细致的方向判定。所以,进行运动方向的判定成为了一个主要问题。

我认为路线判定方法有两种——射线追踪判定,以及,获取起点与终点,判断他们之间的矢量方向。


实现原理

原理很简单,就是根据路线,在一定时间内,记录路线数据,然后把数据映射到相机平面上,在根据取第一个点、中间点以及最后一个点,计算角度和斜率,在一定度数之内,都认为是某个方向的运行识别成功。
这里写图片描述

脚本实现

在脚本中增加了一些参数的设置,如图所示——

这里写图片描述
这样就可以更加灵活的通过参数进行调节。
挥动超过距离:手柄在识别过程中的路线,必须大于一定长度才算数。这里设置的为0.8f.

数据跟踪的最短距离: 在同一个位置,间隔太小的手柄位置,不作为参考数据。手柄运动之间间隔大于这个值,才有效的位置数据。

检测周期:数值越大识别越容易。但是过大,就会提高误识别率。默认为0.15秒。测试结果到0.3s也是可以的,这个值作为参数吧。数据过了这个时间,就重新清零检测。

检测角度:我们本来检测的是八个方向,在把个方向一定角度内都算是某个方向的,这就是冗余,因为没有冗余,必须直直的路线才可以的。

using UnityEngine;
using System.Collections;
using SLQJ;
using System.Collections.Generic;
using UnityEngine.UI;
/// <summary>
/// 主要功能:
/// 1.实现8中不同方向上的姿势识别
/// 2.实现输入矢量来判断是否完成对应路线。 
/// 注意:每次只能识别一个姿势,需要等识别完毕,才能下一个。
/// @cartzhang
/// </summary>

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

    /// <summary>
    /// 射线检测
    /// </summary>
    /// <param name="getstureVec"></param>
    /// <param name="TimeToDectect"></param>
    /// <returns></returns>    
    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);
    }

    /// <summary>
    /// 射线来检测碰撞体标签
    /// </summary>
    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

    /// <summary>
    /// 每隔一帧采样数据,然后在时间间隔内
    /// </summary>
    /// <param name="getstureVec"></param>
    /// <param name="TimeToDectect"></param>
    /// <returns></returns>
    private bool bInitialOnce = false;
    private int linkListMaxLength;
    private List<Vector3> recordPosList;
    private List<Vector3> 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<Vector3>(linkListMaxLength);
            recordWorldToViewPosList = new List<Vector3>(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());
        }
    }
    /// <summary>
    /// 姿势判断。
    /// </summary>
    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;
    }

    /// <summary>
    /// 计算与X轴夹角
    /// </summary>
    /// <param name="pos3D"></param>
    /// <returns></returns>
    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
}


/// <summary>
/// 动作识别的调用和结果返回。
/// </summary>
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
    }

其次,我们需要标识出手柄轨迹的路线——

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

    }
}

实现了两种方法,一个是用射线来判断的,对应函数CheckGestureByRay(),而另一种方法,是根据开头文章说的,根据路线方向通过矢量判断。这二者在准确度上也有各有优劣。

  • 射线追踪判定

使用了先判断射线射中的对象,通过层过滤和标签过滤来判断是否通过A点,然后在通过B点,这里就返回了。

优点是,几乎可以识别相机平面的所有方向,不在意朝向和位置,只要碰撞盒对就没有问题。

缺点就是,需设置层和标签。还有就是移动速度也不能过高,因为他会来不及计算或穿透过碰撞体,也可能会发生的。

  • 路径轨迹判定

路径跟踪计算,根据记录来判断有限长度序列中的点,判断与X轴

优点是,可以屏蔽转向中,不会误识别成功。计算量其实也不大,效率也可以。可以根据需要随时调节参数,得到想要的结果。

缺点:算法需要转换为相机矩阵下的平面,因为手柄在移动过程中,相机也在移动,造成数据点在不同坐标平面下,可能会造成误识别。若每个点都计算在当前下,在整体计算过程中,就会得到记录的坐标点大家都不在一个坐标下生成的尴尬情况,这样更不能保证是否精确了。

猜你喜欢

转载自blog.csdn.net/zys91011_muse/article/details/80026493
今日推荐