使用BoxCastAll计算混合现实对象位置


介绍

很难给这篇文章命名。通常,我试图找到一个或多或少描述了一个搜索词的标题,这个搜索词是我在查找手边主题的信息时使用的,但我并不能真正找到我要找的东西。这里的代码计算对象在其他对象和/或空间网格前面或顶部的位置。对于这个项目,我使用BoxCastAll,我以前试过用过,但不是很成功。I have tried using Rigidbody.SweepTest,虽然它适用于某些场景,但并不适用于所有场景。我的浮动信息屏幕最终落到了半座山之外Walk the World),或者机场由于一些微小的障碍物阻挡而无法在地面上移动(在AMS HoloATC)。所以,我尝试了一种新的方法。

这是一篇包含两篇文章的博客文章的第一部分。在这篇文章中,我将解释BoxCast是如何工作的,以及需要哪些额外的技巧和计算才能使其正常工作。

方框施放魔法

那么,实际上什么是BoxCast?和正常的光线相差无几。但是,当光线投射给你一个线而一个障碍物,一个BoxCast能做到--让人惊讶的是--用一个盒子。实际上,你从一个点沿着一个向量扔一个盒子,直到它击中某物--或某物事情-因为BoxCastAll可能返回一个以上的命中。如果你取离你的相机最近的一个(击中有一个方便的“距离”属性),你潜在地有一个可以放置物体的地方。

但是,它没有考虑到以下几件事:

  • 一个对象的(中心)位置和它的边界框的中心并不总是相同的;这将使BoxCast不总是发生在您希望它发生的地方。
  • 从相机到命中的向量可以平行于也可以不平行于箱型投射的方向;因此,我们需要项目向量从相机到命中的向量的方框。
  • BoxCast命中检测发生在边缘对象的位置由其中间。所以,我们需要搬家后退向摄影机靠近一点;否则,大约一半的物体--由它的实际方位决定--将最终内部障碍物。

我的代码把所有这些都考虑进去了。在我发现所有可爱的陷阱之前,这是一个来之不易的知识。

首先,一种新的效用方法

为了使BoxCast工作,您需要一个盒子。您通常通过获取要强制转换的对象中所有呈现器的边界,并将它们组合到一个大的边界框中来完成这一任务。我讨厌不止一次地键入或复制代码,所以我创建了这个名为GameObject

public static class GameObjectExtensions
{
    public static Bounds GetEncapsulatingBounds(this GameObject obj)
    {
        Bounds totalBounds = new Bounds();

        foreach (var renderer in obj.GetComponentsInChildren<Renderer>())
        {
            if (totalBounds.size.magnitude == 0f)
            {
                totalBounds = renderer.bounds;
            }
            else
            {
                totalBounds.Encapsulate(renderer.bounds);
            }
        }

        return totalBounds;
    }
}


方框施放魔法

LookingDirectionHelpers,一个静态类,包含用于计算方向和放置用户正在查看的方向的实用程序(duh)。我创建了一个方法来实现BoxCast魔术。它做了很多事情,我将带你一步一步地了解它。它是这样开始的:

public static Vector3 GetObjectBeforeObstruction(GameObject obj, float maxDistance = 2,
    float distanceFromObstruction = 0.02f, int layerMask = Physics.DefaultRaycastLayers,
    BaseRayStabilizer stabilizer = null, bool showDebugLines = false)
{
    var totalBounds = obj.GetEncapsulatingBounds();

    var headRay = stabilizer != null
        ? stabilizer.StableRay
        : new Ray(CameraCache.Main.transform.position, CameraCache.Main.transform.forward);

     var hits = Physics.BoxCastAll(GetCameraPosition(stabilizer),
                                  totalBounds.extents, headRay.direction,
                                  Quaternion.identity, maxDistance, layerMask)
                                  .Where(h => !h.transform.IsChildOf(obj.transform)).ToList();


如您所见,该方法接受相当多的参数,并且其中大多数参数是可选的:

  •  obj -要投下并放置在障碍物上或上方的实际物体
  •  maxDistance  -放置物体离相机的最大距离(如果它没有先击中另一个物体)
  •  distanceFromObstruction -物体与障碍物之间的距离
  •  layerMask -当我们寻找障碍物时,我们应该“击中”哪些层(默认为“所有”)
  •  stabilizer -用于获得比相机本身更稳定的位置和视点源
  •  showDebugLines -使用一些很棒的帮助类Inicked from the Unity Forums从“HiddenMonk以显示如何执行BoxCast。没有这些,我就无法确定我必须处理的所有问题。

首先,我们得到总的封装界。然后,我们检查是否可以使用稳定器来定义我们想要投射的方向上的光线。然后,我们计算摄像机正前方的一个点。

然后,我们进行实际的Boxcaster,或者实际上的Boxcastall。执行强制转换:

  • 从摄像机位置;
  • 使用对象的总范围;
  • 在观察光线的方向上(所以从你的头部到注视光标所在的位置是一条线);
  • 不使用循环(我们使用了呈现的界限,它已经考虑了任何旋转);
  • 在最大距离上;
  • 根据图层掩码描述的图层(默认为全部)。

请注意其中子句末尾。BoxCasts攻击所有内容,包括强制转换对象本身的子对象因为它可能在它自己的演员的路径上。所以,我们需要剔除所有与对象本身或其子级。

下一篇文章将可视化显示如何执行BoxCast。使用HiddenMonk的代码:

if (showDebugLines)
{
    BoxCastHelper.DrawBoxCastBox(GetCameraPosition(stabilizer),
        totalBounds.extents, headRay.direction,
        Quaternion.identity, maxDistance, Color.green);
}


这将使用debug。接下来,绘制这些线条,使它们仅在Unity editor和Play模式下可见。它们不会出现在游戏窗格中的场景窗格。这是有意义的,因为你可以从每个角度看结果,而不影响游戏中的实际场景。

它看起来会是这样的:

现在,为了解决我在本文顶部列出的问题,我们需要做一些事情。

给它最好的演员阵容

下一行是一个奇怪的,但可以解释的事实是,在实际包围盒的中心(以及因此的造型)和对象的中心之间可能存在差异,正如Unity报告的那样。我不完全确定这是为什么,但是,相信我,它发生在一些对象上。我们需要对此进行补偿。

var centerCorrection = obj.transform.position - totalBounds.center;


下面,您将看到这样一个对象的示例。当一个物体是由一个或多个偏离中心的物体组成时,特别是当物体是不对称的时,就像这个“浮动屏幕”。你会看到它是一个空的游戏对象,包含一个四边形和一个3dTextPrefab,它们在局部空间中向上移动。如果没有校正因子,您将得到左侧的情况--框形转换发生“太低”。

在右边,你看到了想要的效果。我选择将对象的位置更改为Boxcast的中心。但是,您也可以考虑更改箱型但那是个副作用:射线不会开始从用户的角度来看(但是,在本例中,有一点点上面它),这可能会使人困惑或产生不希望的结果。

命中或未命中:投射

我们得找到最接近的目标。但是,它可能并不就在我们面前,就像观看矢量一样。因此,我们需要创建一个从相机到命中的向量,然后创建一个(更长的)向量,跟随用户的注视,最后,将“命中向量”投影到“注视向量”。只有到那时,我们才知道有多大的空间在前面我们的。

if (hits.Any())
{
    var closestHit = hits.First(p => p.distance == hits.Select(q => q.distance).Min());
    var hitVector = closestHit.point - GetCameraPosition(stabilizer);
    var gazeVector = CalculatePositionDeadAhead(closestHit.distance * 2) - 
                       GetCameraPosition(stabilizer);
    var projectedHitVector = Vector3.Project(hitVector, gazeVector);


为了显示所发生的情况,我制作了一个屏幕截图,让Unity为每个计算的向量绘制调试线:

if (showDebugLines)
{
    Debug.DrawLine(GetCameraPosition(stabilizer), closestHit.point, Color.yellow);
    Debug.DrawRay(GetCameraPosition(stabilizer), gazeVector, Color.blue);
    Debug.DrawRay(GetCameraPosition(stabilizer), projectedHitVector, Color.magenta);
}


这就产生了下面的视图(为了清楚起见,我禁用了为这个屏幕快照绘制BoxCast的代码)

稍微放大一下,可以更好地显示感兴趣的区域:

你可以清楚地看到从相机到原始命中的黄线。蓝色线是用户的观看方向,洋红色线投射在上面。

请保持距离

现在,这一切都适用于一个平面的物体,比如一个四边形(这里是一个信息屏幕),但不适用于像这样的盒子(为了清晰起见,我把它部分变成半透明的)。

这里的问题很简单。虽然我花了一些时间才弄清楚是什么引起的,但撞击发生在边缘形状的。但是,对象的位置与它的居中。所以,如果我把物体的位置设置在命中位置,它就会停在障碍物的一半。QED。

我们需要做的是另一个从物体中心到边缘的光线,其方向与投射的击中矢量(品红色线)相同。现在,光线投射在物体内部不起作用,但幸运的是,还有另一种方法。bounds类支持IntersectRay方法。它的工作有点笨拙,但它确实有效:

var edgeRay = new Ray(totalBounds.center, projectedHitVector);
float edgeDistance;
if(totalBounds.IntersectRay(edgeRay,  out edgeDistance))
{
    if (showDebugLines)
    {
        Debug.DrawRay(totalBounds.center, 
            projectedHitVector.normalized * Mathf.Abs(edgeDistance + distanceFromObstruction),
            Color.cyan);
    }
}


我们将从边界中心到边界边缘的投影命中向量相交。这将给出从中心到物体撞击障碍物的部分的距离,我们可以将物体“向后”移动到所需的位置。因为我指定了一个‘distanceFromObstruction,“我们还可以把这个值加到物体需要向后移动的距离上,以保持与障碍物的距离,而不是接触障碍物(虽然这个物体的距离是0)。”然而,另一个调试行这次可以显示正在发生的情况:

青色线条是物体被移回的部分。现在,唯一剩下的就是计算新的位置然后还回去。这次,使用centerCorrection我们将使对象实际出现在Boxcast的轮廓中:

return GetCameraPosition(stabilizer) +
            projectedHitVector - projectedHitVector.normalized *             Mathf.Abs(edgeDistance + distanceFromObstruction) +
            centerCorrection;


人无完人

如果你认为“嘿,看起来不是完全完美对齐的”,你是对的!这是因为Unity在确定体积和包围盒时有其局限性。这可能是因为游戏的主要关注点是性能,而不是百分之百的准确率。如果我在代码中添加这一行:

BoxCastHelper.DrawBox(totalBounds.center, totalBounds.extents, Quaternion.identity, Color.red);


它实际上显示了边界框:

这解释了更多的事情。启用所有调试行后,如下所示:

展示和讲述

要正确地向您展示如何使用这种方法实际上并不容易。我会把它留到下一篇文章里。在此期间,我拼凑了a demo project使用GetObjectBeforeObstruction 用一种非常简单的方式。我创建了一个SimpleKeepInViewController 每隔这么多秒(默认为2秒)轮询用户查找的位置,然后调用GetObjectBeforeObstruction并将对象移到那里。这给出了一个有点紧张的结果,但你明白了。

public class SimpleKeepInViewController : MonoBehaviour
{
    [Tooltip("Max distance to display object before user")]
    public float MaxDistance = 2f;

    [Tooltip("Distance before the obstruction to keep the current object")]
    public float DistanceBeforeObstruction = 0.02f;

    [Tooltip("Layers to 'see' when detecting obstructions")]
    public int LayerMask = Physics.DefaultRaycastLayers;

    [Tooltip("Time before calculating a new position")]
    public float PollInterval = 2f;

    [SerializeField]
    private BaseRayStabilizer _stabilizer;

    [SerializeField]
    private bool _showDebugBoxcastLines = true;

    private float _lastPollTime;


    void Update()
    {
        if (Time.time > _lastPollTime)
        {
            _lastPollTime = Time.time + PollInterval;
            LeanTween.move(gameObject, GetNewPosition(), 0.5f).setEaseInOutSine();
        }
#if UNITY_EDITOR
        if (_showDebugBoxcastLines)
        {
            LookingDirectionHelpers.GetObjectBeforeObstruction(gameObject, MaxDistance,
                DistanceBeforeObstruction, LayerMask, _stabilizer, true);
        }
#endif
    }

    private Vector3 GetNewPosition()
    {
        return LookingDirectionHelpers.GetObjectBeforeObstruction(gameObject, MaxDistance,
            DistanceBeforeObstruction, LayerMask, _stabilizer);
    }
}


这里只有一个奇怪的地方--你看,我实际上把GetObjectBeforeObstruction 两次。但是,第一次仅在编辑器中发生,并且仅在选中“显示调试框转换行”复选框时发生:

如果我没有添加这个,你会看到每2秒一帧的线条闪烁,这几乎不是重点。这样,您就可以在编辑器中随时看到它们

the demo project,你会发现三个对象--在上面的图片中你已经看到了一个单独的块(默认),一个旋转的“信息屏幕”显示“Hello World”,还有一个在左边的复合对象(两个偏中心的立方体)。在这里,它是在启用所有调试行的情况下显示的。你可以通过说“toggle”或按“T”来切换这三个对象。如果你有蓝牙键盘,后者将在HoloLens中实际工作。而且,相信我--我试过了!

结论

下面是另一种方法,使一个物体出现在障碍物旁边或上面。这段代码实际上花了我太多的时间来完成,但是我从中学到了很多,并且在某些时候,它成为了一个荣誉的问题,一旦我得到它的工作!