2.6 检测相机与模型,墙或地形的碰撞

问题

你想检测相机是否已近靠近物体防止它穿过这个物体。

解决方案

要进行相机和模型的碰撞检测,你应将相机表示为一个包围球,使用这个包围球检查与模型的包围球或包围盒的碰撞。对应用户自定义的网格,诸如墙或地形,你必须首先找到墙或地形上的三角形任意位置的精确3D坐标。

工作原理

相机碰撞可分为与模型的碰撞和与已知顶点高度的三角形网格的碰撞。

相机与模型间的碰撞

3

这种情况中,你将相机作为一个球,只需调整球的半径就可以设置模型和相机间的最小距离。要检测相机球和模型的碰撞,你还需要模型的包围球或包围盒,这个球可以使用LoadModelWithBoundingSphere方法计算,这个方法在教程4-5中介绍,它把包围球存储在模型的Tag属性中。这个Tag属性可以存储模型的任意对象,也适合存储模型的包围球。在LoadContent方法中使用如下代码:

myModel = XNAUtils.LoadModelWithBoundingSphere(ref modelTransforms, "tiny",content);

无论是移动相机还是模型都需进行碰撞检测,如果检测到碰撞,你将通过将相机位置恢复到初始值取消相机的最后一次改变。在Update方法中,保证你已经保存了初始值:

protected override void Update(GameTime gameTime) 
{
    GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); 
    if (gamePadState.Buttons.Back == ButtonState.Pressed) 
        this.Exit(); 
    MouseState mouseState = Mouse.GetState(); 
    KeyboardState keyState = Keyboard.GetState(); 
    Vector3 lastCamPos = fpsCam.Position; 
    fpsCam.Update(mouseState, keyState, gamePadState); 
    base.Update(gameTime); 
}

更新相机后,你需要创建一个对应相机新位置的包围球,下面的代码中你将包围球的最小距离设置为1个单位:

float minimumDistance = 1.0f; 
BoundingSphere cameraSphere = new BoundingSphere(fpsCam.Position, minimumDistance);

有了更新过的相机包围球和模型包围球,就可以检测两者间的相交。如果检测到相交则将相机恢复到之前的位置:

BoundingSphere origModelSphere = (BoundingSphere)myModel.Tag; 
BoundingSphere transModelSphere = XNAUtils.TransformBoundingSphere (origModelSphere, worldMatrix); 
if (cameraSphere.Contains(transModelSphere) !=ContainmentType.Disjoint) 
    fpsCam.Position = lastCamPos;

如果模型通过世界矩阵移动到另一个位置或进行了缩放,你也应该相应地移动/缩放包围球,这可以通过使用TransformBoundingSphere实现。

接下来的代码检测相机的包围球与模型包围球的距离是否小于1个单位。

相机和地形间的碰撞

4

如果你想检测相机与你自定义的顶点网格(如地形)的碰撞,你需要使用三角形碰撞。本章使用的方法适用于顶点纵横坐标是等间距的情况。

如果顶点的高度信息存储在Y坐标中,这意味着所有顶点的X和Z坐标间的距离是相同的,因为这个原因,你可以知道相机是在哪个正方形的上方。例如,相机位置是(10.3f, 8.7f, 5.1f),你可以知道相机是在由四个顶点(10, .., 5),(11, .., 5), (10, ..., 6)和 (11, ..., 6)构成的正方形上方某高度,括号中的省略号表示这些顶点的高度,这些高度是在创建网格时定义的。你将这些高度存储在一个独立的数组中,或者你也可以从顶点缓冲获取这些高度(这不是首选方法)。

但是,如何知道相机与下方地形的高度差?最简单的方法是将相机位置截取到顶点的最近位置并检查相机是否在顶点的上方,如果相机在地形下方,则要相应调整相机的高度:

protected override void Update(GameTime gameTime) 
{
    GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); 
    if (gamePadState.Buttons.Back == ButtonState.Pressed) 
        this.Exit(); 
    MouseState mouseState = Mouse.GetState(); 
    KeyboardState keyState = Keyboard.GetState(); 
    fpsCam.Update(mouseState, keyState, gamePadState); 
    float treshold = 3.0f; 
    float terrainHeight = terrain.GetClippedHeightAt(fpsCam.Position.X, -fpsCam.Position.Z); 
    if (fpsCam.Position.Y < terrainHeight + treshold)
    {
        Vector3 newPos = fpsCam.Position; 
        newPos.Y = terrainHeight + treshold; 
        fpsCam.Position = newPos; 
    }
    base.Update(gameTime); 
}

GetClippedHeightAt方法返回地形顶点的高度,相关知识可见教程5-9。你可以通过改变threshold值设置相机和地形间高度差的最小阈值,以上代码会调整相机的高度,使相机与地形之间的高度差保持在threshold。

注意GetClippedHeightAt方法的两个参数都是正值,因为这两个参数要作为查询矩阵的索引,而向前方向是负Z轴方向,所以你需要将Z坐标前加个“-”号使它变为正值。

因为高度是从邻近顶点获取的,因此GetClippedHeightAt方法会返回不同的值,以上代码会导致相机在三角形上移动时产生跳跃。要保证过渡平滑,你可以使用地形对象的GetExactHeightAt方法,这个方法在教程5-9解释,此方法可以获取地形上低于相机位置的点的精确高度:

float terrainHeight = terrain.GetExactHeightAt(fpsCam.Position.X, -fpsCam.Position.Z);

当相机或模型是缓慢地在地形上移动时这个方法更好。

相机和墙之间的碰撞

墙通常只由两个三角形构成,所以它是相机-地形碰撞的简化形式。你可以使用教程5-9中介绍的GetExactHeightAt方法获取相机前方的墙的精确3D点。

代码

前面已经有了Update方法的全部代码了。