steamVR插件的基本功能

2023-10-27

本文转载自http://blog.csdn.net/tyuiof/article/details/52789283



从这一节起我开始介绍一些vive的交互实现方式,比如手柄发出的射线,凝视,瞬移等等。SteamVR插件内都有这三种交互的辅助类。

Extras文件夹里面的SteamVR_GazeTracker是凝视的工具类,SteamVR_LaserPointer是射线的工具类,SteamVR_Teleporter是瞬移的工具类,下面我们来分析这三种交互是如何实现的。

SteamVR_GazeTracker(凝视)

凝视是一种在没有手柄等输入设备的情况下,可以通过眼睛盯着某个物体看来与物体进行交互的体验。

我们只需要将个辅组类添加到我们想要凝视的物体上,比如菜单等,就可以实现凝视的功能。现在我们来看看凝视的实现原理。

    void Update ()
    {
        if (hmdTrackedObject == null)
        {
            /*查找全部的SteamVR_TrackedObject组件,我们知道这个组件是用来跟踪设备位置的,手柄,头盔上都有这个组件*/
            SteamVR_TrackedObject[] trackedObjects = FindObjectsOfType<SteamVR_TrackedObject>();
            /*循环遍历trackedObject,找到头盔的trackedObject*/
            foreach (SteamVR_TrackedObject tracked in trackedObjects)
            {
                if (tracked.index == SteamVR_TrackedObject.EIndex.Hmd)
                {
                    /*获取头显的transform*/
                    hmdTrackedObject = tracked.transform;
                    break;
                }
            }
        }

        if (hmdTrackedObject)
        {
            /*从头显发出一条向前的射线*/
            Ray r = new Ray(hmdTrackedObject.position, hmdTrackedObject.forward);
            Plane p = new Plane(hmdTrackedObject.forward, transform.position);

            float enter = 0.0f;
            if (p.Raycast(r, out enter))
            {
                Vector3 intersect = hmdTrackedObject.position + hmdTrackedObject.forward * enter;
                float dist = Vector3.Distance(intersect, transform.position);
                /*如果凝视的点与凝视目标在gazeIncutoff的范围内,则目标为凝视状态,并调用OnGazeOn()回调方法*/
                if (dist < gazeInCutoff && !isInGaze)
                {
                    isInGaze = true;
                    GazeEventArgs e;
                    e.distance = dist;
                    OnGazeOn(e);
                }
                /*如果凝视的点与凝视目标大于gazeIncutoff这个范围,则目标为非凝视状态,并调用OnGazeOff()回调方法*/
                else if (dist >= gazeOutCutoff && isInGaze)
                {
                    isInGaze = false;
                    GazeEventArgs e;
                    e.distance = dist;
                    OnGazeOff(e);
                }
            }

        }

    }

通过上面的代码我们知道了凝视的原理实际上是从头盔的位置发出一条射线判断是否与物体相交来做选中或者交互的。而且因为凝视的精确度不高,所以没有做直接与物体相交,而是在物体的位置创建了一个平面,通过射线与平面相交的交点的位置与物体的距离来大概判断的。这个距离值是可以调的,缺省是0.15到0.4米之间就算选中了。

我们现在知道了凝视的交互是如何实现的,实现的方式其实还是挺简单的,下面我们在来看看射线这种交互方式。

SteamVR_LaserPointer(激光束)

SteamVR_LaserPointer的作用是从指定位置(通常是手柄)发出一条射线,它会将这条射线显示出来,然后也是判断这条视线与场景中的物体是否相交。与凝视不一样的是,它可以精确操作,所以不需要一个辅助平面。用法和凝视也不太一样,需要将这个组件添加发出射线的物体上,比如手柄。

我们来分析一下这个类的代码

/*射线事件触发的回调参数,凝视也是类似的用法*/
public struct PointerEventArgs
{
    /*手柄的索引*/
    public uint controllerIndex;
    /*暂时无用的参数*/
    public uint flags;
    /*射线源到目标的距离*/
    public float distance;
    /*射线射中的transform对象*/
    public Transform target;
}

/*定义命中事件委托函数*/
public delegate void PointerEventHandler(object sender, PointerEventArgs e);


public class SteamVR_LaserPointer : MonoBehaviour
{
    /*光线颜色*/
    public Color color;
    /*光线厚度*/
    public float thickness = 0.002f;
    /*空的GameObject,用来存放极光的gameobject*/
    public GameObject holder;
    public GameObject pointer;
    bool isActive = false;
    /*是否给激光束添加刚体*/
    public bool addRigidBody = false;
    /*激光束命中和离开的委托事件*/
    public event PointerEventHandler PointerIn;
    public event PointerEventHandler PointerOut;

    Transform previousContact = null;

    void Start ()
    {
        /*一些初始化操
        1,创建激光束父GameObject(holder)
        */

        holder = new GameObject();
        /*2,将holder的transform的parent设为当前脚本所在的物体(手柄)上面*/
        holder.transform.parent = this.transform;
        /*3,将holder本地坐标初始为0*/
        holder.transform.localPosition = Vector3.zero;
        /*4,创建激光束,用长方体模拟(这一点其实不太合理,用圆柱模拟会更好一点)*/
        pointer = GameObject.CreatePrimitive(PrimitiveType.Cube);
        /*5,将激光束父亲设为holder*/
        pointer.transform.parent = holder.transform;
        /*6,设置激光束locale为(0.002,0.002,100),使它看起来像一条很长的线*/
        pointer.transform.localScale = new Vector3(thickness, thickness, 100f);
        pointer.transform.localPosition = new Vector3(0f, 0f, 50f);
        /*7,是否添加刚体*/
        BoxCollider collider = pointer.GetComponent<BoxCollider>();
        if (addRigidBody)
        {
            if (collider)
            {
                collider.isTrigger = true;
            }
            Rigidbody rigidBody = pointer.AddComponent<Rigidbody>();
            rigidBody.isKinematic = true;
        }
        else
        {
            if(collider)
            {
                Object.Destroy(collider);
            }
        }
        /*8,设置激光束的材质*/
        Material newMaterial = new Material(Shader.Find("Unlit/Color"));
        newMaterial.SetColor("_Color", color);
        pointer.GetComponent<MeshRenderer>().material = newMaterial;
    }

    public virtual void OnPointerIn(PointerEventArgs e)
    {
        if (PointerIn != null)
            PointerIn(this, e);
    }

    public virtual void OnPointerOut(PointerEventArgs e)
    {
        if (PointerOut != null)
            PointerOut(this, e);
    }


    // Update is called once per frame
    void Update ()
    {
        /*第一次调用时将holder设为active*/
        if (!isActive)
        {
            isActive = true;
            this.transform.GetChild(0).gameObject.SetActive(true);
        }

        /*将激光束的最远距离设为100米*/
        float dist = 100f;
        /*获取当前物体(手柄)上的SteamVR_TrackedController脚本*/
        SteamVR_TrackedController controller = GetComponent<SteamVR_TrackedController>();
        /*构造一条射线*/
        Ray raycast = new Ray(transform.position, transform.forward);
        RaycastHit hit;
        bool bHit = Physics.Raycast(raycast, out hit);

        /*射线命中物体后移出,说明物体不在命中,调用OnPointerOut的通知*/
        if(previousContact && previousContact != hit.transform)
        {
            PointerEventArgs args = new PointerEventArgs();
            if (controller != null)
            {
                args.controllerIndex = controller.controllerIndex;
            }
            args.distance = 0f;
            args.flags = 0;
            args.target = previousContact;
            OnPointerOut(args);
            previousContact = null;
        }
        /*射线命中物体,调用OnPointerIn的通知*/
        if(bHit && previousContact != hit.transform)
        {
            PointerEventArgs argsIn = new PointerEventArgs();
            if (controller != null)
            {
                argsIn.controllerIndex = controller.controllerIndex;
            }
            argsIn.distance = hit.distance;
            argsIn.flags = 0;
            argsIn.target = hit.transform;
            OnPointerIn(argsIn);
            previousContact = hit.transform;
        }
        if(!bHit)
        {
            previousContact = null;
        }

        /*如果命中物体距离大于100,则无效,否则有效*/
        if (bHit && hit.distance < 100f)
        {
            dist = hit.distance;
        }


        if (controller != null && controller.triggerPressed)
        {
            /*当按下扳机键时,将光束的粗细增大5倍,长度会设为dist,通过这种方法让光线不会穿透物体*/
            pointer.transform.localScale = new Vector3(thickness * 5f, thickness * 5f, dist);
        }
        else
        {
            /*没按下扳机或者当前控制器没有添加SteamVR_TrackedController时,显示原始粗细的光束*/
            pointer.transform.localScale = new Vector3(thickness, thickness, dist);
        }
        /*将光束的位置设在光束长度的一半的位置,使得光束看起来是从手柄发出来的*/
        pointer.transform.localPosition = new Vector3(0f, 0f, dist/2f);
    }
}

看完了SteamVR_LaserPointer的代码,我们就知道了激光束实现的原理,其实激光束实现起来还是蛮简单的,但是在VR的交互中,使用起来非常的方便。

好了,我们接下来再看看最后一种交互方式,瞬移。

SteamVR_Teleporter(瞬移)

我们只需要将这个脚本添加到手柄上就能使用瞬移功能,这个类的面板如下图

可以看到,它只有两个可控制的参数

  • Teleport On Click:表示是否激活按扳机键瞬移功能
  • Teleport Type:瞬移类型,有三种
    • Teleport Type Use Terrain:表示在地形上做瞬移,地形有高低的区别
    • Teleport Type Use Collider:表示与场景中的任何碰撞体做相交瞬移
    • eleport Type Use Zero Y:表示在Y方向0坐标的平面上做瞬移,当地面为平面时可以使用

同样,我们再来分析瞬移的源码,为了精简,一些不太核心的源码我直接省去了

public class SteamVR_Teleporter : MonoBehaviour
{

    ……

    Transform reference
    {
        get
        {
            /*获取CameraRig的Transform,SteamVR_Render.Top实际就是头显的预制体*/
            var top = SteamVR_Render.Top();
            return (top != null) ? top.origin : null;
        }
    }

    void Start ()
    {
        /*获取SteamVR_TrackedController脚本,这个脚本是用来相应输入的触发回调的,比如手柄上的按键等*/
        var trackedController = GetComponent<SteamVR_TrackedController>();
        if (trackedController == null)
        {
            trackedController = gameObject.AddComponent<SteamVR_TrackedController>();
        }
        /*Trigger键的回调,实际上是通过按下Trigger来实现瞬移*/
        trackedController.TriggerClicked += new ClickedEventHandler(DoClick);

        if (teleportType == TeleportType.TeleportTypeUseTerrain)
        {

            /*这里的reference就是我们在上面获取的摄像机的位置
            这这里,会将头显的位置设置为地形地图上的采样高度,这么做是为了避免瞬移时钻入地里面*/
            var t = reference;
            if (t != null)
                t.position = new Vector3(t.position.x, Terrain.activeTerrain.SampleHeight(t.position), t.position.z);
        }
    }

    /*Trigler的回调实现*/
    void DoClick(object sender, ClickedEventArgs e)
    {
        if (teleportOnClick)
        {
            var t = reference;
            if (t == null)
                return;

            float refY = t.position.y;

            Plane plane = new Plane(Vector3.up, -refY);
            /*发出一条射线,用来寻找瞬移的目的地*/
            Ray ray = new Ray(this.transform.position, transform.forward);

            bool hasGroundTarget = false;
            float dist = 0f;
            /*这里是对三种不同地形的处理*/
            if (teleportType == TeleportType.TeleportTypeUseTerrain)
            {
                RaycastHit hitInfo;
                TerrainCollider tc = Terrain.activeTerrain.GetComponent<TerrainCollider>();
                hasGroundTarget = tc.Raycast(ray, out hitInfo, 1000f);
                dist = hitInfo.distance;
            }
            else if (teleportType == TeleportType.TeleportTypeUseCollider)
            {
                RaycastHit hitInfo;
                Physics.Raycast(ray, out hitInfo);
                dist = hitInfo.distance;
            }
            else
            {
                hasGroundTarget = plane.Raycast(ray, out dist);
            }

            if (hasGroundTarget)
            {
                /*将头显的位置设置到移动的目的地*/
                Vector3 headPosOnGround = new Vector3(SteamVR_Render.Top().head.localPosition.x, 0.0f, SteamVR_Render.Top().head.localPosition.z);
                t.position = ray.origin + ray.direction * dist - new Vector3(t.GetChild(0).localPosition.x, 0f, t.GetChild(0).localPosition.z) - headPosOnGround;
            }
        }
    }
}

我们可以看到,瞬移的核心不是怎么移过去,而是如何确定瞬移的目标位置,确定了移动的目标位置后再将Camera的position设置成目标位置就行了,瞬移的难点在于对不同地形的处理。

现在我们已经知道这三种交互方式的用法和原理了,在VIVE的开发中,这三种交互是很常见的。同样,我们也可以根据这几种交互的实现原理,设计出我们自己想要的交互。



本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

steamVR插件的基本功能 的相关文章

  • 2020-02-26

    请教大家一个AD的问题困扰多少的问题 AD10原理图复制一个器件 比如R1 正常复制粘贴还是R1 通过SHIFT拖动是R2 那如果我原理图中原本就有R2了 还是会有重复的现象 怎样复制粘贴会生成一个原理图中没有的位号呢

随机推荐

  • Splunk HEC 取发送数据 服务器的hostname

    1 背景 最近Client 发送数据到 Splunk HEC 发现对方hostname 没有取到 都是HEC 的VIP 地址 这个就不能发现是那个host 发过来的数据 下面查了下文档 发现Splunk 是可以跟踪发送数据的host 的 主
  • 【计算机网络】UDP协议

    目录 1 UDP协议头部格式 2 UDP协议的特点 2 1 无连接 2 2 不可靠 2 3 面向数据报 2 4 有接收缓冲区 没有发送缓冲区 2 5 大小受限 3 基于UDP的应用层协议 4 UDP协议与TCP协议对比 5 经典面试题 1
  • 基于.NET的企业级软件开发

    企业级开发最好基于一些成熟的框架 从而将主要精力集中到领域模型的设计上 1 UI与业务逻辑的隔离 在web领域可以采用ASP NET MVC框架 2 业务逻辑与DB的隔离 可以采用Entity Framework框架 3 业务逻辑中涉及工作
  • 毕业设计-基于机器视觉的水表读数智能识别系统-OpenCV

    目录 前言 课题背景和意义 实现技术思路 一 系统总体方案设计 二 图像预处理的研究与实现 三 识别区域定位及字符分割的研究与实现 实现效果图样例 最后 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备
  • 分治算法(Java)

    想必大家通过算法的名字就已经明白了 这个算法的过程 一个是分 一个是治 那么我为什么要使用这种算法呢 因为当前的问题是我们使用现有的方法是解决不了的 所以我们需要将一个复杂的问题分成两个或者是更多个相同或相似的子问题 然后再一我们已有的方法
  • 【detectron2】注册、训练、推断自己的数据集

    一 注册自己的数据集 使用detectron2训练自己的数据集 第一步要注册自己的数据集 首先保证自己的数据集标注是coco格式 就可以使用load coco json加载自己的数据集并转化为detectron2的专有数据格式 使用Data
  • C++中关于枚举的使用(enum)

    首先加入头文件 include
  • mysql中字段长度到底是字符数还是字节数?

    这个问题 困惑了很多新学者 今天就给大家来测试测试 首先来给个定长的字段类型 因为这样好看效果 超出的会被截取 create table test id int 10 not null auto increment test name ch
  • 在VMware ESXi服务器上配置NAT上网

    文章目录 前言 一 vSphere Client上操作 1 配置网络环境 2 创建软件路由 3 虚拟机设置 总结 相关文章 前言 在使用VMware workstation的时候 我们经常以NAT的方式配置虚拟机的网络 与桥接方式相比 这样
  • spring之application.yml配置

    server port 18080 context path netty logging config classpath logback xml 文件上传下载大小 速度配置 spring http multipart max file s
  • 15_弹性盒布局

    一 弹性盒子的基本概念 弹性盒属性的使用 概念 在父级元素设置设置弹性盒属性 容器 所有的子级元素会在父级容器的轴向上排列 项目 作用 控制所有的子级元素在父级元素上的排列位置 如何形成弹性盒 属性 display 显示方式 属性值 fle
  • 软件测试-兼容性测试

    1 兼容性测试综述 软件兼容性测试 检查软件之间是否能够正确交互和共享信息 交互可以在运行在同一台计算机上的应用程序之间发生 也可以在距离数千公里的不同计算机上的应用程序之间发生 还可以用软盘在两台计算机之间传输数据 兼容性测试要解决的问题
  • Visual Studio Code调试node.js:无法在PATH上找到运行时的node

    首先 环境变量Path中加入nodejs的路径 验证nodejs是否已经加入环境变量 接着 重新启动Visual Studio Code 试一下 是不是好了 附录 Visual Studio Code 调试 nodeJS launch js
  • 开源C++版本CGI库CGICC入门

    原发布在ChinaUnix 但未被自动搬迁过来 http blog chinaunix net uid 20682147 id 4895772 html PDF版本 https download csdn net download aque
  • 记一次在关于本地IDE能AC,牛客平台却显示数组越界问题——输入处理带来的问题!!!!

    在参加了百度 网易雷火 网易互娱 360笔试之后 被频繁遇到的数组越界问题深深困扰 为什么在eclipse上怎么测试 怎么看代码都找不到数组越界越在哪 不禁发出越你姥姥的惊叹 今天晚上再次遇到这个问题 在查看了各种遇到此类问题的帖子后 终于
  • 安装完Anaconda Navigator,“Mark for specfic version installation”显示灰色(不能更改包的版本)的解决方法

    最近安装完anaconda 出现了 Mark for specfic version installation 显示灰色的问题 解决方法很简单 只需要在 conda环境下输入以下命令即可 conda search python 仔细思考一下
  • 【C++】通过类来计算圆的周长

    include
  • 时域高通滤波算法(THPF)

    时域高通滤波算法 THPF 今天介绍一下图像非均匀校正中的场景校正算法 时域高通滤波算法 Temporal High Pass Filtering THPF 以及它后续的各种改进版本 空域和频域已经在之前的文章介绍过一些了 时域还没有 图像
  • RTKLIB源码解析(二)、 RTK定位(rtkpos.c)

    本博客是转载 感谢 rtklib代码详解 rtkpos c 博客园 哆啦A梦 博客园 主函数 rtkpos 1 设置基站位置 2 统计基站和流动站的卫星数量 3 单点定位解算 4 若定位模式为单点定位 直接返回 5 精密单点定位 6 动基线
  • steamVR插件的基本功能

    本文转载自http blog csdn net tyuiof article details 52789283 从这一节起我开始介绍一些vive的交互实现方式 比如手柄发出的射线 凝视 瞬移等等 SteamVR插件内都有这三种交互的辅助类