Quantcast
Channel: 英特尔开发人员专区文章
Viewing all articles
Browse latest Browse all 583

以 Pirate Cove 为例:如何将 Steam*VR 集成至 Unity*

$
0
0

查看 PDF [1065 kb]

本文的目标受众是谁?

本文主要面向希望将 Steam*VR 集成至场景之中的 Unity* 开发人员。我将假设读者已安装 HTC Vive* 且能够正常运行。如果没有,请访问 SteamVR 网站查看具体说明

我为何创建 Pirate Cove VR 场景?

我的工作重点随时变化,需要逐渐倾斜至虚拟现实 (VR),使其融于 Unity 环境之中。我想将 SteamVR 集成至 Unity 之中、设计场景,并支持远距传动以便在场景之中灵活移动。

本文主要介绍我在 VR 集成过程中所积累的经验,并展示如何实现 VR 和场景的有机结合。本文的重点不是如何设计场景以及如何使用 Unity 实现一切正常运行,而是介绍如何将 VR 集成至场景之中。

我的创建目标是什么?

我尝试创建虚拟现实视觉体验。尽管使用 Unity 来创建体验,但它不是游戏本身。我创建的视觉体验模拟以海盗为主题的小型热带岛屿。我选择对我有利的事物;毕竟我生活在雨水充沛的太平洋西北地区。所以我希望我的体验带有一些热带风情。

我使用的工具有哪些?

我使用 Unity* 5.6。我从 Unity 资产商店购买了一些资产。资产的选择均围绕如何设置生活着大量海盗的热带岛屿:

  • Pirates Island
  • SteamVR
  • Hessburg Navel Cutter
  • Aquas

还有一些其他的免费零星物品。

我为创建该项目积累了哪些知识?

我曾使用过 Unity,以及英特尔® 实感™ 技术。我们的 SDK 包含英特尔实感 Unity 插件,而且我编写过插件,并创建了有关该插件的培训示例。

到目前为止我没有真正尝试过在 Unity 中设计第一人称类型的游戏,也从不担心性能、每秒帧数 (FPS) 等相关问题。使用英特尔实感技术和其他简单支持工具时编写过脚本。但没有设计过大型项目或场景,也没有处理过与此相关的问题。

该项目的终极目标是什么?

我希望通过项目创建更好地了解 Unity 以及如何打造 VR 场景。我希望了解正常运行 VR 的难度。如果成功将会有哪些收获?性能是否可接受?如果存在帧速率低的问题,我是否能够保持稳定,继续创建该项目?

以及从中获得乐趣。我希望从该项目创建过程中获得乐趣,并积累经验,这也是我为何选择创建以海盗为主题的热带岛屿的原因。我个人对从前的加勒比海盗时代非常着迷。

我对此存在哪些误解?

如前所述,在使用 Unity 方面,经验并不丰富。

第一个误解是关于渲染对象和渲染时机。这是什么意思?由于某种原因,我假设地形中包含譬如 3D 模型(比如巨大的悬崖),如果仅将悬崖的一半放在地形之上,Unity 不会尝试渲染不可见部分。我认为是某种类型的渲染算法阻止了 Unity 渲染处于地形之下的对象。显然不是这样。Unity 仍然渲染整个 3D 悬崖模型。

这种相同的概念适用于两个不同的 3D 悬崖模型。例如,如果有两个悬崖游戏对象,我假设将其中一座悬崖推入另一座悬崖之中,以创造出一座巨大悬崖的幻象,那么任何不可见的几何体或纹理将不会被渲染。同样不是这么回事。

显然,如果有几何体和纹理,无论是否隐藏在其他对象之中,Unity 都会将其渲染。我们需要考虑到这一点。这对我的工作没有多大影响,也没有找到解决办法,我只是在正常使用 Unity 的过程中发现了这一问题。

性能

性能是我积累最多经验的一个方面。使用 Unity 的地形工具来设计场景非常简单。添加资产也非常简单。并不是说能够非常简单地设计出色的场景,只是说你可以轻松地明白如何设计场景中的对象。尽管我自己认为 Pirates Cove场景十分漂亮,但其他人可能会嗤之以鼻。但这是我第一次设计场景,而且我并不是尝试创建第一人称射击游戏。它是一种虚拟的视觉体验。

FPS:与他人探讨 VR 时我了解到适用于 VR 的 FPS 是 90。我一开始尝试使用 Unity Stats 窗口。在 Unity 论坛上与他人交谈时我发现该窗口并不是能够达到真正 FPS 性能的最佳工具。别人建议我使用 FPS Display 脚本。显然这种方法比较准确。

遮挡剔除:这种情况十分有趣。我正尝试解决一个完全不相关的问题时,一个同事过来帮忙。我们谈到 FPS 和可以帮助解决渲染问题的技巧,他介绍我使用遮挡剔除。他向我展示了一种人工操作方法:如何确定盒子的大小和形状。我必须承认,我只是简单地打开 Unity 的遮挡剔除窗口,让它自己算出遮蔽范围。这似乎帮助我解决了性能问题。

植被:我没有意识到为地形添加草地会对场景产生巨大的影响。我看过在其他场景中大片草丛在风中摇曳。因此我非常兴奋;将 FPS 将至 0,Unity 因此超载。我没有解决这一问题,而是简单地移除草丛,并使用三叶草纹理让场景看起来比较美观,但没有任何绘制调用。

如何让 Vive* 在场景中运行

我在文章开头提过,假设读者的工作站设置并运行 Vive。这是我找到的现有文章的浓缩版 HTC Vive Tutorial for Unity。我不打算深入介绍控制器条目,因为本文将介绍重点远距传动。我修改了远距传动版本,改动较小,因为我认为适度更好,更确切地说是在操作的过程中有所发现。

在进行 HTC Vive* 开发之前,必须下载和导入 SteamVR 插件。

screenshot of Steam*VR plugin logo

导入插件后,项目中将出现以下文件结构:

screenshot of file structure in unity

在项目中,我在名为 VR 的层级结构中创建了根文件夹。在这里我拷贝了 SteamVR 和 CameraRig 预制件。你无需创建 VR 文件夹;我这里只是为了便于项目整理。

screenshot of folder organization

只需将 SteamVR 插件添加至项目层级结构,无需进行额外操作;看 CameraRig,

我将 CameraRig 放在场景中我希望一开始启动的地方。

screenshot of game environment

放置 SteamVR CamerRig 预制件后,我需要删除主摄像头,以避免冲突。这时我可以启动场景,四处观察。我不能移动,但可以从静止的点四处观察,可以看到手中的控制器。不能随意走动,但至少可以看到任何地方。

screenshot of game environment

获得追踪对象

追踪对象包含手部控制器和耳机。有了这个代码示例,我完全不担心耳机;但需要从手部控制器获得输入。这对追踪按钮点击等事物来说非常有必要。

首先我们必须获得带有脚本的追踪对象实例。这里追踪对象是控制器;可通过 Awake 函数获得实例。

void Awake( )

{

    _trackedController = GetComponent( );

}

然后,如果想测试是否能从手部控制器获得输入,可以通过以下 Get 函数选择特定控制器。它使用该脚本连接的追踪对象(手部控制器):

private SteamVR_Controller.Device HandController
{
    get
    {
        return SteamVR_Controller.Input( ( int )_trackedObj.index );
    }
}

创建远距传动系统

现在我们想添加在场景中四处走动的功能。为此,我们必须创建脚本,知道如何读取手部控制器的输入。我创建了一个新的 Unity C# 脚本,名为 TeleportSystem.cs。

我们不仅需要脚本,还需要激光指示器,在本特定项目中为十字线。十字线不是强制性的,只是为了增添场景中的辨别力,因为用户可将该十字线用作视觉反馈工具。我只创建了一个带骷髅头的简单圆圈。

创建激光

将立方体扔在场景中即可创建激光;激光在场景中足够高,不会干扰场景中的其他资产。从这里将其坐标设为:x = 0.005,y = 0.005,z = 1。这样形成一个细长立方体形状的激光。

screenshot of model in the unity envrionment

激光创建完成后,以预制件的形式保存并删除场景中的原始立方体,因为已不再需要。

创建十字线

我希望激光的末端带有一个定制的十字线,这点不是必需的,只是比较酷而已。我创建了一个预制件,即上面带贴花的圆形网格。

screenshot of pirate flag decal

screenshot of the unity inspector panel

设置层级

这一步骤非常重要。你需要告诉激光/十字线哪些可以远距传动,哪些不可以。例如,可能不支持用户在水上远距传动,或者不希望支持他们在悬崖边上远距传动。你可以使用层级,将他们限制在场景中特定的区域。我创建了两个层级 — Teleportable 和 NotTeleportable。

screenshot of game layers in unity

我将可以远距传动的事物,比如地形本身、草棚和楼梯,放在 Teleportable 层。用户无法远距传动的事物,比如场景中的悬崖或其他事物,放在 NotTeleportable 层。

定义变量时,我确定了双层掩码。一个掩码包含所有层级。另一个是非远距传动掩码,表示这些层级无法远距传动。

// Layer mask to filter the areas on which teleports are allowed
public LayerMask _teleportMask;

// Layer mask specifies which layers we can NOT teleport to.
public LayerMask _nonTeleportMask;

确定公共层级掩码时,它们均包含在脚本中。包含多个下拉列表,你可以从中选择不允许远距传动的层级。

screenshot of unity CSharp script

设置层级与 LayerMatchTest 函数搭配使用。

/// <summary>
/// Checks to see if a GameObject is on a layer in a LayerMask.
/// </summary>
/// <param name="layers">Layers we don't want to teleport to</param>
/// <param name="objInQuestion">Object that the raytrace hit</param>
/// <returns> true if the provided GameObject's Layer matches one of the Layers in the provided LayerMask.</returns>
private static bool LayerMatchTest( LayerMask layers, GameObject objInQuestion )
{
    return( ( 1 << objInQuestion.layer ) & layers ) != 0;
}

调用 LayerMatchTest()时,将发送包含无法远距传动层级列表的层级掩码,以及 HitTest 检测到的游戏对象。测试将表明该对象是否在非远距传动层级列表中。

更新帧

void Update( )
{
    // If the touchpad is held down
    if ( HandController.GetPress( SteamVR_Controller.ButtonMask.Touchpad ) )
    {
        _doTeleport = false;

        // Shoot a ray from controller.If it hits something make it store the point where it hit and show
        // the laser.Takes into account the layers which can be teleported onto
        if ( Physics.Raycast( _trackedController.transform.position, transform.forward, out _hitPoint, 100, _teleportMask ) )
        {
            // Determine if we are pointing at something which is on an approved teleport layer.
            // Notice that we are sending in layers we DON'T want to teleport to.
            _doTeleport = !LayerMatchTest( _nonTeleportMask, _hitPoint.collider.gameObject );

            if( _doTeleport )
            {
                PointLaser( );
            }
            else
            {
                DisplayLaser( false );
            }
        }
    }
    else
    {
        // Hide _laser when player releases touchpad
        DisplayLaser( false );
    }
    if( HandController.GetPressUp( SteamVR_Controller.ButtonMask.Touchpad ) && _doTeleport )
    {
        TeleportToNewPosition();
        ResetTeleporting( );
    }
}

更新时,代码将测试是否按下了控制器触控板按钮。如果按下了,将获得 Raycast 命中。请注意,我将发送包含一切事物的远距传动掩码。然后对命中点进行层级匹配测试。通过调用 LayerMatchTest 函数,确定是否命中了可远处传动或不可远距传动的事物。请注意,我将发送我不希望远距传动的层级列表。它将返回一个布尔值,之后该值用于确定我们能否远距传动。

如果可以远距传动,将使用 PointLaser 函数显示激光。在该函数中,我将告诉激光预制件看向 HitTest 的位置。接下来将激光预制件从控制器拉伸至 HitTest 位置。同时重新定位激光末端的十字线。

private void PointLaser( )
{
    DisplayLaser( true );

    // Position laser between controller and point where raycast hits.Use Lerp because you can
    // give it two positions and the % it should travel.If you pass it .5f, which is 50%
    // you get the precise middle point.
    _laser.transform.position = Vector3.Lerp( _trackedController.transform.position, _hitPoint.point, .5f );

    // Point the laser at position where raycast hits.
    _laser.transform.LookAt( _hitPoint.point );

    // Scale the laser so it fits perfectly between the two positions
    _laser.transform.localScale = new Vector3( _laser.transform.localScale.x,
                                            _laser.transform.localScale.y,
                                            _hitPoint.distance );

    _reticle.transform.position = _hitPoint.point + _VRReticleOffset;
}

如果 HitTest 指向非远距传动层级,我将确保通过 DisplayLaser 函数关闭激光指示器。

在该函数的末端,如果触控板已按下,且 shouldTeleport 变量为真值,我调用 Teleport 函数将用户远距传动至新的位置。

private void TeleportToNewPosition( )
{
    // Calculate the difference between the positions of the camera's rig's center and players head.
    Vector3 difference = _VRCameraTransform.position - _VRHeadTransform.position;

    // Reset the y-position for the above difference to 0, because the calculation doesn’t consider the
    // vertical position of the player’s head
    difference.y = 0;

    _VRCameraTransform.position =  _hitPoint.point + difference;
}

总结

这就是我准备和运行场景的全过程。涉及从互联网获取资源、阅读他人发布的文章,以及测试和纠错。希望大家能够通过本文有所收获,随时欢迎大家与我联系。

出于完整性考虑,下面我提供完整的脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// <summary>
/// Used to teleport the players location in the scene.Attach to SteamVR's ControllerRig/Controller left and right
/// </summary>
public class TeleportSystem :MonoBehaviour
{
    // The controller itself
    private SteamVR_TrackedObject _trackedController;

    // SteamVR CameraRig transform
    public Transform _VRCameraTransform;

    // Reference to the laser prefab set in Inspecter
    public GameObject _VRLaserPrefab;

    // Ref to teleport reticle prefab set in Inspecter
    public GameObject _VRReticlePrefab;

    // Stores a reference to an instance of the laser
    private GameObject _laser;

    // Ref to instance of reticle
    private GameObject _reticle;

    // Ref to players head (the camera)
    public Transform _VRHeadTransform;

    // Reticle offset from the ground
    public Vector3 _VRReticleOffset;

    // Layer mask to filter the areas on which teleports are allowed
    public LayerMask _teleportMask;

    // Layer mask specifies which layers we can NOT teleport to.
    public LayerMask _nonTeleportMask;

    // True when a valid teleport location is found
    private bool _doTeleport;

    // Location where the user is pointing the hand held controller and releases the button
    private RaycastHit _hitPoint;




    /// <summary>
    /// Gets the tracked object.Can be either a controller or the head mount.
    /// But because this script will be on a hand controller, don't have to worry about
    /// knowing if it's a head or hand controller, this will only get the hand controller.
    /// </summary>
    void Awake( )
    {
        _trackedController = GetComponent<SteamVR_TrackedObject>( );
    }


    /// <summary>
    /// Initialize the two prefabs
    /// </summary>
    void Start( )
    {
        // Spawn prefabs, init the classes _hitPoint

        _laser      = Instantiate( _VRLaserPrefab );
        _reticle    = Instantiate( _VRReticlePrefab );

        _hitPoint   = new RaycastHit( );

    }


    /// <summary>
    /// Checks to see if player holding down touchpad button, if so, are the trying to teleport to a legit location
    /// </summary>
    void Update( )
    {
        // If the touchpad is held down
        if ( HandController.GetPress( SteamVR_Controller.ButtonMask.Touchpad ) )
        {
            _doTeleport = false;

            // Shoot a ray from controller.If it hits something make it store the point where it hit and show
            // the laser.Takes into account the layers which can be teleported onto
            if ( Physics.Raycast( _trackedController.transform.position, transform.forward, out _hitPoint, 100, _teleportMask ) )
            {
                // Determine if we are pointing at something which is on an approved teleport layer.
                // Notice that we are sending in layers we DON'T want to teleport to.
                _doTeleport = !LayerMatchTest( _nonTeleportMask, _hitPoint.collider.gameObject );

                if( _doTeleport )
                    PointLaser( );
                else
                    DisplayLaser( false );
            }
        }
        else
        {
            // Hide _laser when player releases touchpad
            DisplayLaser( false );
        }
        if( HandController.GetPressUp( SteamVR_Controller.ButtonMask.Touchpad ) && _doTeleport )
        {
            TeleportToNewPosition( );
            DisplayLaser( false );
        }
    }


    /// <summary>
    /// Gets the specific hand contoller this script is attached to, left or right controller
    /// </summary>
    private SteamVR_Controller.Device HandController
    {
        get
        {
            return SteamVR_Controller.Input( ( int )_trackedController.index );
        }
    }


    /// <summary>
    /// Checks to see if a GameObject is on a layer in a LayerMask.
    /// </summary>
    /// <param name="layers">Layers we don't want to teleport to</param>
    /// <param name="objInQuestion">Object that the raytrace hit</param>
    /// <returns>true if the provided GameObject's Layer matches one of the Layers in the provided LayerMask.</returns>
    private static bool LayerMatchTest( LayerMask layers, GameObject objInQuestion )
    {
        return( ( 1 << objInQuestion.layer ) & layers ) != 0;
    }


    /// <summary>
    /// Displays the lazer and reticle
    /// </summary>
    /// <param name="showIt">Flag </param>
    private void DisplayLaser( bool showIt )
    {
        // Show _laser and reticle
        _laser.SetActive( showIt );
        _reticle.SetActive( showIt );
    }



    /// <summary>
    /// Displays the laser prefab, streteches it out as needed
    /// </summary>
    /// <param name="hit">Where the Raycast hit</param>
    private void PointLaser( )
    {
        DisplayLaser( true );

        // Position laser between controller and point where raycast hits.Use Lerp because you can
        // give it two positions and the % it should travel.If you pass it .5f, which is 50%
        // you get the precise middle point.
        _laser.transform.position = Vector3.Lerp( _trackedController.transform.position, _hitPoint.point, .5f );

        // Point the laser at position where raycast hits.
        _laser.transform.LookAt( _hitPoint.point );

        // Scale the laser so it fits perfectly between the two positions
        _laser.transform.localScale = new Vector3( _laser.transform.localScale.x,
                                                    _laser.transform.localScale.y,
                                                    _hitPoint.distance );

        _reticle.transform.position = _hitPoint.point + _VRReticleOffset;
    }



    /// <summary>
    /// Calculates the difference between the cameraRig and head position.This ensures that
    /// the head ends up at the teleport spot, not just the cameraRig.
    /// </summary>
    /// <returns></returns>
    private void TeleportToNewPosition( )
    {
        Vector3 difference = _VRCameraTransform.position - _VRHeadTransform.position;
        difference.y = 0;
        _VRCameraTransform.position =  _hitPoint.point + difference;
    }
}

关于作者

Rick Blacker 任职于英特尔® 软件和服务事业部,主要负责以 Primer VR 应用开发为重点的虚拟现实。


Viewing all articles
Browse latest Browse all 583

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>