HTC VIVE开发教程(四)

2024-05-01 12:08
文章标签 教程 开发 htc vive

本文主要是介绍HTC VIVE开发教程(四),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

从这一节起我开始介绍一些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 framevoid 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的开发中,这三种交互是很常见的。同样,我们也可以根据这几种交互的实现原理,设计出我们自己想要的交互。

这篇关于HTC VIVE开发教程(四)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/951648

相关文章

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

如何为Yarn配置国内源的详细教程

《如何为Yarn配置国内源的详细教程》在使用Yarn进行项目开发时,由于网络原因,直接使用官方源可能会导致下载速度慢或连接失败,配置国内源可以显著提高包的下载速度和稳定性,本文将详细介绍如何为Yarn... 目录一、查询当前使用的镜像源二、设置国内源1. 设置为淘宝镜像源2. 设置为其他国内源三、还原为官方

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

使用Python开发一个带EPUB转换功能的Markdown编辑器

《使用Python开发一个带EPUB转换功能的Markdown编辑器》Markdown因其简单易用和强大的格式支持,成为了写作者、开发者及内容创作者的首选格式,本文将通过Python开发一个Markd... 目录应用概览代码结构与核心组件1. 初始化与布局 (__init__)2. 工具栏 (setup_t

Python虚拟环境终极(含PyCharm的使用教程)

《Python虚拟环境终极(含PyCharm的使用教程)》:本文主要介绍Python虚拟环境终极(含PyCharm的使用教程),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录一、为什么需要虚拟环境?二、虚拟环境创建方式对比三、命令行创建虚拟环境(venv)3.1 基础命令3

使用Node.js制作图片上传服务的详细教程

《使用Node.js制作图片上传服务的详细教程》在现代Web应用开发中,图片上传是一项常见且重要的功能,借助Node.js强大的生态系统,我们可以轻松搭建高效的图片上传服务,本文将深入探讨如何使用No... 目录准备工作搭建 Express 服务器配置 multer 进行图片上传处理图片上传请求完整代码示例

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

python连接本地SQL server详细图文教程

《python连接本地SQLserver详细图文教程》在数据分析领域,经常需要从数据库中获取数据进行分析和处理,下面:本文主要介绍python连接本地SQLserver的相关资料,文中通过代码... 目录一.设置本地账号1.新建用户2.开启双重验证3,开启TCP/IP本地服务二js.python连接实例1.

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.