一、VR运行环境配置:
- 安装steam,在steam上安装SteamVR驱动。
- 在Unity项目中需要导入VRTool插件包(已上传服务器),里面包含两个插件一个是SteamVR插件,一个是VRTK插件,这两个插件也可以直接在Unity的商店中进行下载。这两个插件要求的Unity最低版本要5.6。
二、VR项目开发:
1. 基础组件:
VR要实现人物在场景中的初始化需要以下物体:
这两个物体可以在VRTK中的任意示例场景中找到:
建议第二个例子中的物体。将这两个物体直接做成预制体,然后再将预制体放到我们自己的场景中即可。
这两个物体的结构如下:
VRTK_SDKManager下的StemVR对应的便是我们在场景中的人物。
在CameraRig上有 这个组件,这是人物游玩区域域的配置,就是人脚底圆圈的配置。一般使用默认配置即可
SteamVR_PlayArea属性:
Border Thickness:游玩区域底部颜色区域的宽度
Wireframe Height:游玩区域高度
Draw Wireframe ...:当被选中时绘制线框图
Draw In Game:是否在游戏中绘制
Size和Color:调整尺寸和颜色
基础组件中最主要的便是VRTK_Scripts这个物体下的两个物体,基本上所有交互脚本都会在这几个空物体上,这里你也可以不用预制体自己随意创建空物体即可,不过必须要与VRTK_SDKMangager建立如下关系:
在运行时,这两个脚本会成为 Controller(left)和Controller(Right)的子物体。
在这两个物体上都有一个事件响应脚本:
有了这个便可以给手柄添加各种事件。
VRTK_ControllerEvents这个脚本是使用VRTK手柄事件机制必备的脚本,需要在左右手柄控制器都添加上
Action Aias Button: 设定一些事件的触发按钮
Axis Refinement:设定按键点击事件的临界值,以Trigger(扳机键为例),当我们轻轻扣动扳机键时,会产生一个按键值,这边可以设定当按键值达到多少的时候表示扳机轻按事件的触发(扳机键有三种基本事件:轻按,触底,按下)
添加了这个脚本之后我们就可以在自己的脚本里面使用VRTK的手柄事件了,使用方式可以分为三步:
第一:去获取左右控制器上面的VRTK_ControllerEvents脚本
var controlEvents = GetComponent
这个是以该脚本正好挂载在手柄控制器上为例,可以直接去获取。
第二:为相应按钮的相应事件设定触发时的函数
controlEvents.TriggerClicked += new ControllerInteractionEventHandler(DoTriggerClicked);
这边主要是这个脚本ControllerInteractionEventHandler,参数是我们要设定的监听函数的函数名
第三:完成监听函数,参数是固定的写法
private void DoTriggerClicked(object sender, ControllerInteractionEventArgs e){}
sender:触发事件的是哪个手柄
e:有四个可以获得数属性
buttonPressure:按钮被按下的力度
controllerIndex:设备的ID。
touchpadAxis:touchpad被按下的位置点的坐标
touchpadAngle:touchpad被按下位置点的角度(顺时针,其他按钮被按下都是90)
手柄上按钮对应在代码中的名字:
2. 人物移动:
人物移动有三种实现方式:1.是利用射线进行移动2.是利用触摸板进行人物移动3.是人在原地踏步做出走的动作人物移动,这种移动方式主要配合万向跑步机使用,在此主要讲解利用射线进行人物移动。
射线:
如果想要做手柄上射出线则将组建挂载左手柄上,如果想要右手柄上射出线则将组建挂在右手柄上。具体示例可以查看VRTK中的场景 002;
投射射线需要用到两个脚本,一个是基本的VRTK_Pointer脚本,线控制器:
这个脚本需要挂载在手柄控制器上面,主要属性说明:
Enable Teleport:是否启用瞬移
Point Renderer:线渲染器(不能为空),需要指定一个挂载了线渲染器脚本的物体,这边使用的是射线,我们使用VRTK_StraightPointerRenderer脚本作为线渲染器(插件还提供一种贝塞尔曲线的线渲染器,后面会提到)
Target List Poslicy:可以选取一些点设置为可以移动或不能移动。需要配合
这个组件使用 Operation :Ingore时则下面的物体为不能移动的点,Include时则下面的物体为能移动的点。Check为控制的模式, 可以根据标签,脚本,层进行控制
Activation Button:发射射线的按钮
Activation Delay:发射延迟
Selection Button:选中的按钮
Point Interaction Settings:与物体交互的选项
线渲染器,这个渲染的是直线
射线脚本主要属性:
Layer To Ignore:被射线忽略的层
Valid Collision Volor:当射线选中有效物体时的颜色
Invalid Collistion Volor:当射线选中无效点时的颜色
Maximum Length:射线的最大长度
脚本为射线事件设定监听,射线一共有三种事件:射线有效、射线无效、选中(即按钮松开)
分为三步:
一:获取相应手柄上VRTK_DestinationMarker脚本,这个脚本是VRTK_Pointer父类
var destinationMarker = GetComponent
二:设定监听函数
destinationMarker.DestinationMarkerEnter += new DestinationMarkerEventHandler(DoPointerIn);
DestinationMarkerEnter射线有效,这个事件会在选中点有效的时候一直被触发
DestinationMarkerExit 射线无效,这个事件会在选中点从有效点到无效点和选中事件触发后被触发一次
DestinationMarkerSet 选中,按钮被松开是执行一次
三:完成监听函数,同样是固定写法
private void DoPointerIn(object sender, DestinationMarkerEventArgs e){}
sender:产生事件的手柄
e:有7中属性值可以获取
public float distance;距离
public Transform target;被选中点的Transform
public RaycastHit raycastHit;射线
public Vector3 destinationPosition;被选中点的坐标
public bool forceDestinationPosition;被选中点力的坐标
public bool enableTeleport;是否瞬移
public uint controllerIndex;当前设备的ID
线渲染器,曲线:
一般在瞬移时需要用曲线,在操作UI界面时需要用的直线,所以两个线渲染器需要同时使用,但是线控制器只有一个,这就需要在适当的时候对 当前线控制器控制的渲染器进行修改,对是否启动射线瞬移进行修改 直线时设为false,曲线时设为true。但是直线和曲线切换时一定要在射线绘制按钮抬起之后也就是不在绘制线时进行切换,否则会造成当前线渲染器渲染的线一直存在于场景中。现在先介绍利用曲线实现人物瞬移,后面会介绍利用直线进行UI操作。
利用曲线实现人物瞬移
利用曲线的示例场景为:VRTK下022场景。
需要在VRTK_Scripts下新建一空物体,上面配置一下脚本:
这些脚本主要用来处理人体碰撞,人物头部和场景中物体是否发生碰撞,为了防止穿模,也就是人物头部进入到物体内部时会让人物眼前景象变黑。对于人物瞬移位置限制,还有人物往高处,和高处坠落模拟实现
这个脚本上的数值一般不用改动,改动后可能造成眩晕, 其中Nav MesLimit Distance这个属性是用来控制在导航网格上瞬移的,到导航网格边缘多少距离时禁止瞬移,当值为0时,则停止使用导航网格控制瞬移。默认人物瞬移按键就是射线绘制按键,当按键抬起时就会瞬移。可以在线控制器中对按键进行修改。其余脚本的属性值直接利用即可,修改 后会造成眩晕。可以根据需求修改曲线的绘制距离,角度,以达到控制瞬移距离。
3. 物体抓取:
- 基础抓取:
物体抓取需要在控制器上挂载一下脚本:
Touch:触摸交互,Grab抓取物体,Use使用物体。
controllerAttachPoint: 被抓的物体,被附加在哪个物体上, 默认是手柄的圆环处
Grab Precognition 提前预判抓取物体 对应快速运动的物体,我们可能需要提早按下抓取按键才能抓住物体, 数值是提前的时间值,值越大, 可提前的抓取时间越长
Throw Multiplier: 把物体扔出去时,速度的倍增值
Create Rigid Body When Not Touch : 在碰到物体时才创建RigidBody 默认情况下手柄也创建Rigidbody,这就可以和物体在物理上产生碰撞
需要在被抓取物体上挂载以下脚本:
VRTK_InteractableObject 基本的脚本,一个物体只要挂载上这个脚本,就可以与手柄控制器交互
Touch Height Color:触碰高亮,默认为黑色,触碰时不高亮
Allowed Touch Controllers:允许触摸的手柄
Is Grabbable:是否允许抓取,配合物体上的抓取脚本使用,后面会提到
Stay Grabbed On Teleport:当抓取物体时是否可以瞬移
Valid Drop:有效的扔下的位置
Grab Override Button:指定抓取的按键事件,默认是侧边键
Allowed Grab Controller:允许抓取的手柄
Grab Attach Mechanic Script:指定才用抓取的方式(固定关节、父子关系等等),默认采用固定关节的方式
Use Options:物体使用的一些选项设置
Is Usable:是否可以被使用
Hold Button To Use:需要长按手柄按键使用
Use Only if Grabbled:在被抓取时才能使用
Point Activates Use Action:远程激活选项,如果被勾选,手柄激光选中物体时可以被使用(激活)
Use Override Button:指定使用物体的按键
Allowed Use Controllers:允许物体被使用的控制器
这些属性设置只有在初始化之前的设置有效,一但初始化完成在改变属性没有任何作用,所以一般会根据不同的物体,继承这个类,然后呢 在Awake中进行属性初始化,里面用大量的方法允许重写,最常用的有一下几种:
public class Test : VRTK_InteractableObject {
public override void StartTouching(VRTK_InteractTouch currentTouchingObject = null)
{
//当手柄和物体接触时执行一次,注意如果两个手柄都能和物体进行接触,则两个物体和手柄接触时会分别各执行一次
base.StartTouching(currentTouchingObject);
}
public override void StopTouching(VRTK_InteractTouch previousTouchingObject = null)
{
//当手柄和物体停止接触时执行一次。
base.StopTouching(previousTouchingObject);
}
public override void StartUsing(VRTK_InteractUse currentUsingObject = null)
{
//当物体允许使用时,手柄按下扳机键时会执行一次
base.StartUsing(currentUsingObject);
}
public override void StopUsing(VRTK_InteractUse previousUsingObject=null)
{
//当停止使用物体时会执行一次
base.StopUsing(previousUsingObject);
}
public override void Grabbed(VRTK_InteractGrab currentGrabbingObject = null)
{
//当物体被抓取时,会一直在Update中执行此函数
base.Grabbed(currentGrabbingObject);
}
}
注意:VRTK_InteractableObject这个脚本中的Update不会一直执行,在初始化完成之后会停止执行。
以固定关节实现物体抓取的脚本,抓取物体的位置一直在物体的中心点。
继承自VRTK_BaseJointGrabAttach
Right Snap Handle:如果设置了这一项 ,如果用右手柄抓取当前物体,则抓取的点为这个属性中物体的中心点。如果不设置抓取物体的位置一直在被抓取物体的中心点。
Destory Immediately On Throw:扔下物体时立刻销毁关节
Break Force:销毁时的力
可以作为抓取的方式脚本,这个脚本意味着使用的固定关节的方式实现抓取
VRTK_SwapControllerGrabAction
挂载这个脚本的物体就可以被控制器手柄抓取
VRTK_OutlineObjectCopyHighlighter
物体上挂载这个脚本之后就能进行描边高亮提示。
- 手柄抓取物体扩展进阶(示例场景VRTK下008)
VRTK_TrackObjectGrabAttach:
继承自VRTK_BaseGrabAttach,这个脚本的作用是抓取物体时的一些设置,使用这个脚本,抓取的位置是固定的
Detach Distance:分离再次抓取的距离
Velocity Limit:物体的最大速度
Andular Velocity Limit:旋转的最大速度
VRTK_ChildOfControllerGrabAttach
这个脚本表示采用父子关系的方式,即将被抓取的物体变成手柄模型的子物体
VRTK_InteractControllerAppearance
这个脚本用来设定手柄是否隐藏,隐藏的时机等
Touch Visibility:触碰的时候是否隐藏手柄模型
Grab Visibility:抓取的时候是否隐藏
Use Visibility:使用的时候是否隐藏
4.开关按钮实现:
原理:开关阀主要是利用了手柄与物体接触之后是否使用物体,如果使用了物体则代表按下了阀门。主要重写了 VRTK_Interactable Object脚本中的以下方法:
public override void StartTouching(VRTK_InteractTouch currentTouchingObject = null)
{
//当手柄和物体接触时执行一次,注意如果两个手柄都能和物体进行接触,则两个物体和手柄接触时会分别各执行一次
base.StartTouching(currentTouchingObject);
}
public override void StopTouching(VRTK_InteractTouch previousTouchingObject = null)
{
//当手柄和物体停止接触时执行一次。
base.StopTouching(previousTouchingObject);
}
public override void StartUsing(VRTK_InteractUse currentUsingObject = null)
{
//当物体允许使用时,手柄按下扳机键时会执行一次
base.StartUsing(currentUsingObject);
}
public override void StopUsing(VRTK_InteractUse previousUsingObject=null)
{
//当停止使用物体时会执行一次
base.StopUsing(previousUsingObject);
}
第二种实现方式则是检测 VRTK_InteractableObject.Touched属性,如果为true则表示手柄与物体接触,然后检测手柄是否按下了扳机键。
5.物体旋转:
开关阀和旋转阀理论上都用两种实现方式,第一种是直接利用内部自带的旋转方法 VRTK_Wheel,也就是利用Unity的物理引擎中铰链刚体等组件;第二种则是计算向量。
第一种 利用VRTK_Wheel:
VRTK_Wheel属性简介:
Default Events 在物体旋转时的回调事件,在当前版本中已经不再利用这个属性添加事件,在下面将会具体介绍添加事件监听的方法。
Interact Without Grab :是否允许不用抓住物体就可以旋转物体。
Grab Type:旋转方式。TrackObject:模拟的是类似于拧瓶盖的旋转方式,手柄在一个地方360度转动,物体跟着转动Rotaor Track:模拟阀门旋转。
Detatch Distance:手柄离与物体的距离超过这个距离则没法旋转物体。
Minimun Value :物体旋转的最小角度对应的最下值。
Maximum Value:物体旋转的最大角度对应的最大值。
Step Size: 值改变的差值。
Snap To Step :如果选中,则限制旋转物体的角度不能超过最大值,不能小于最小值。
Max Angle: 可以旋转的角度范围,最大旋转角度为359度。因为在Unity获取物体角度时,如果物体旋转超过360度,在获取欧拉角时会自动置为0度,暂时没有找到改进办法。
Rotate Axis 和RotateAnchor为脚本改进之后的新增属性,在VRTK原版的旋转中并没有这两个属性。
如果物体上挂载了这个脚本则不用挂载其他交互脚本,这个脚本会自动初始化其他交互脚本,这个脚本一旦初始化完成则无法再次更改其属性,所以属性的初始化通常继承VRTK_Wheel后重写InitWheel()方法,在初始化之前先进行属性修改,这就要求在代码动态加载VRTK_Wheel组件之前首先加载一个存储数据的脚本。然后再InitWheel中增添获取属性数据的方法。
VRTK_Wheel的深度改进
在实际项目开发中VRTK_ 并不能直接使用,因为这个组件只有围绕物体Y轴旋转的方法,我们需要给他自定义旋转轴,而其中有控制旋转轴向的隐藏属性,就是
这个HingeJoint中有两个属性,分别为 wheelHinge.anchor,wheelHinge.axis; 这两个属性设置同一个方向向量即可,即都是Vector3.up,或Vector3.right或Vector3.left。要实现物体围绕任意轴都能旋转则需要更改两个地方,一个是将 DetectSetup()方法更改。原本内容如下:
需要根据所旋转的轴向给予wheelHinge.anchor和wheelHinge.axi不同的方向向量。一般更改为如下:
第二个需要修改的地方为SetupHingeRestrictions()这个方法,
脚本在这个地放计算了一个角度值,这个角度值的具体作用暂时未搞懂,不过这个地方的角度值需要根据不同的旋转轴进行不同的计算,官方原脚本只有计算围绕Y轴的角度值。围绕x轴角度值的计算方法如下:
switch (Mathf.RoundToInt(initialLocalRotation.eulerAngles.y))
{
case 0:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x - minJointLimit, transform.localEulerAngles.y, transform.localEulerAngles.z);
break;
case 90:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x , transform.localEulerAngles.y, transform.localEulerAngles.z + minJointLimit);
break;
case 180:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x + minJointLimit, transform.localEulerAngles.y , transform.localEulerAngles.z);
break;
}
围绕z轴的计算方法如下:
switch (Mathf.RoundToInt(initialLocalRotation.eulerAngles.x))
{
case 0:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x, transform.localEulerAngles.y , transform.localEulerAngles.z - minJointLimit);
break;
case 90:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x , transform.localEulerAngles.y + minJointLimit, transform.localEulerAngles.z);
break;
case 180:
adjustedLimitsAngle = new Vector3(transform.localEulerAngles.x, transform.localEulerAngles.y , transform.localEulerAngles.z + minJointLimit);
break;
}
至此,这个脚本已经能够实现任意轴向物体的旋转。因为这个脚本上的属性只有在初始化之前更改才会有效,所以一般会结合一个数据类来记录当前脚本初始化时需要的设置。在使用这个脚本时,我们需要自定义一个类来继承这个脚本,然后重写脚本的InitWheel()方法,在脚本初始化之前获取数据类中的相关属性设置。模式基本如下所示:
protected override void InitWheel(){
//在此完成属性设置
base.InitWheel()
}
至此第一种阀门旋转方式完成。
添加旋转监听事件:
在代码里面设置监听函数:
VRTK_Control_UnityEvents这个脚本用于给控制事件设置监听,挂载继承自VRTK_Control的脚本后会在场景允许后自动挂载上这个脚本,也可以自己去手动在场景还没有开始前添加这个脚本,需要注意的是监听函数的参数是固定写法,使用这个脚本去设置监听函数可以获取一些详细的数值
注意: 在使用这种旋转方式时,VRTK中有一个错误,在VRTK_Control类中
这个地方需要先判断是否为空。
第二种计算向量实现阀门旋转的方法:
计算向量实现阀门旋转的大体思路:
第一步:初始化基础物体交互脚本,设置其属性为不可抓取,可以触摸。
第二步:检测手柄是否触摸到物体,如果为true,则检测手柄是否按住两边的侧键,如果按下则记录当前手柄到物体的方向向量。这期间如果松开了两边侧键或手柄离开了物体则判定停止旋转物体,如果没有则计算对应阀门方向。具体计算方式有待研究。
注意:这两种旋转阀门方式只适用于 0---360度之内的旋转,主要原因在于在unity中获取物体旋转角度只有 0—360度,当一个物体正向选装到 360度时获取到的是0度,逆向旋转到0度时再旋转获取到的是359度。
简单UI介绍
Canvas的渲染方式需要修改成World Space,为Canvas添加VRTK_UICanvas脚本即可
手柄上需要挂载 VRTK_UI Pointer
与UI的交互必须配合射线使用,所以必须挂载射线相关的脚本(见人物移动中射线介绍),这样就可以将射线当做鼠标来使用。默认的使用方式为,按住TouchPad键出现射线,将射线指向要交互的UI,点击Trigger按键==鼠标左击。
VR中2DUI的实现方式。
VR中2DUI的实现方式类似于NGUI的实现方式,实际上,VR中是将UI Canvas当做一个3D物体来进行处理的,然后将普通UGUI的鼠标点击事件改为射线输入点击。2DUI在VR中实现方式就是利用了 这一属性, 将ui的Render Camera改成主相机。这样Canvas永远会在相机的正前方,并且会自动缩放并填充整个视野,注意,在VR中的视野范围有点类似于屏幕视野的内切圆,在摆放UI时不能太靠近边缘。在相机移动时会出现UI抖动的现象,当PlaneDistance值越大时,UI的抖动越小,但不能超出视野的范围,这样就会有一个问题,3D物体有可能会遮挡住UI,这里就用到了组件上的材质:
材质的shader如下:
Shader "LZLUI/Canvas"
{
Properties
{
[PerRendererData] _MainTex("Font Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)
_StencilComp("Stencil Comparison", Float) = 8
_Stencil("Stencil ID", Float) = 0
_StencilOp("Stencil Operation", Float) = 0
_StencilWriteMask("Stencil Write Mask", Float) = 255
_StencilReadMask("Stencil Read Mask", Float) = 255
_ColorMask("Color Mask", Float) = 15
}
SubShader
{
LOD 100
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Stencil
{
Ref[_Stencil]
Comp[_StencilComp]
Pass[_StencilOp]
ReadMask[_StencilReadMask]
WriteMask[_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest Always
Offset -1,-1
Blend SrcAlpha OneMinusSrcAlpha
ColorMask[_ColorMask]
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "UnityUI.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float4 color : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
half2 texcoord : TEXCOORD0;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed4 _TextureSampleAdd;
v2f vert(appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
o.color = v.color * _Color;
#ifdef UNITY_HALF_TEXEL_OFFSET
o.vertex.xy += (_ScreenParams.zw - 1.0)*float2(-1,1);
#endif
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = (tex2D(_MainTex, i.texcoord) + _TextureSampleAdd) * i.color;
clip(col.a - 0.01);
return col;
}
ENDCG
}
}
}
创建一个Material,并将Material赋给相应的组件即可,一般为Image和Text组件这样这些组件就可以永远显示在3D物体的前面了,以实现类似于PC的2DUI。注意要实现射线在2Dui上的点击事件,要让射线忽略所有的3D物体, 利用线渲染器的层控制即可。
VR2DUI中实现物体提示标签
原理:原理就是利用相机到物体形成的向量和Canvas相交形成的交点,也就是标签的位置。
需要用的向量: 相机到物体的方向向量,Canvas的法线(取transform.forward即可),Canvas上的一个点(即Canvas的postion即可)。
假设Canvas的法线为(A,B,C),Canvas上一个点的坐标为(x0,y0,z0);
则根据点法公式可以得出Canvas的面方程为:
A(x-x0)+B(y-y0)+C(z-z0)=0 ----------------------①
假设相机到物体的方向向量为(a1,b1,c1)取线上一点取目标物体的位置点即可假设为(x1,y2,z2);
这样根据直线的点向式可以求出直线方程为:
(x-x1)/a1=(y-y2)/b1=(z-z2)/c1 --------------------------------②
方程①②组成方程组,解方程组为
x=(A*a1*x0 + B*a1*y0 + B*b1*x1 - B*a1*y2 + C*a1*z0 + C*c1*x1 - C*a1*z2)/(A*a1 + B*b1 + C*c1)
y=(A*b1*x0 - A*b1*x1 + A*a1*y2 + B*b1*y0 + C*b1*z0 - C*b1*z2 + C*c1*y2)/(A*a1 + B*b1 + C*c1)
z=(A*c1*x0 - A*c1*x1 + A*a1*z2 + B*c1*y0 + B*b1*z2 - B*c1*y2 + C*c1*z0)/(A*a1 + B*b1 + C*c1)
(x,y,z)即为提示标签的位置点。最后一步要获取提示标签的anchoredPosition3D将其中的z设为0。