65.9K
CodeProject 正在变化。 阅读更多。
Home

XNA 中的碰撞检测

2009 年 4 月 20 日

CPOL

4分钟阅读

viewsIcon

38404

XNA 中的碰撞检测

[本文是关于碰撞检测的序言的延续,该序言发布在此处。它刷新并完成了较早的 XNA 碰撞检测帖子 - 第一部分第二部分第三部分,这些部分是很久以前写的,并且多次要求完成。 终于,它来了!]

简单碰撞检测

XNA 包括对形状的简单相交测试,例如:边界球、AABB(轴对齐边界框)、平面、射线、矩形、视锥体等,以及它们的任意组合。 更有用的是,3D 模型已经为其各个部分设置了边界球。

使用这些测试,几乎可以实现任何类型的游戏类相交。 您必须记住,网格-任何东西的相交都很昂贵(当然,取决于多边形的数量),应该留给需要非常高的相交精度的特殊情况。 因此,通常首选使用一堆球体或盒子来近似复杂的几何体,而不是使用真实的三角形(参见第 1 部分)。

在 Sharky 的博客中有一篇关于 XNA 碰撞的非常好的帖子,特别是专注于使用边界球近似通用形状。 您可以在此处找到它。

精确碰撞检测

Despacho_2_Low

正如前面所评论的,有时需要更精确的相交方法。 例如,对于光照计算(其中光线追踪是模拟光照的最佳和最常用的方法——参见右图),射线-网格相交测试似乎是最佳选择。 在 D3D 中,有一个 Mesh.Intersect 方法可供您使用,该方法执行所需的相交测试,但在 XNA 中,没有这样的方法,我们将不得不自己完成。

为此,我们需要一个几何体的系统内存副本。 不幸的是,网格通常在 XNA 中使用 ReadOnly 标志创建(以使其管理快速),这不允许我们在运行时访问它们的几何体。 为此,我们将不得不处理自定义内容处理

注意此处,您将找到自定义内容处理的介绍。

为碰撞检测实现自定义内容处理器

解决先前问题的方法是创建一个自定义内容处理器,该处理器在构建时存储几何体信息的副本,此时仍然可以访问它。 所有需要的信息将存储在一个名为 MeshData 的新类中

public class MeshData
{
    public VertexPositionNormalTexture[] Vertices;
    public int[] Indices;
    public Vector3[] FaceNormals;

    public MeshData(VertexPositionNormalTexture[] Vertices, int[] Indices,
        Vector3[] pFaceNormals)
    {
        this.Vertices = Vertices;
        this.Indices = Indices;
        this.FaceNormals = pFaceNormals;
    }
}

您可以在此处放入您需要的所有信息。 目前,存储顶点、索引和面法线就足够了。

当 VisualStudio 使用我们的 ContentProcessor 处理每个模型时,它会将模型的数据写入 XNB 文件。 当它找到一个 MeshData 对象时,它会搜索一个能够序列化它的写入器,因此我们必须为 MeshData 类编写我们自己的自定义 ContentTypeWriter

[ContentTypeWriter]
public class ModelVertexDataWriter : ContentTypeWriter<MeshData>
{
    protected override void Write(ContentWriter output, MeshData value)
    {
        output.Write((int)value.Vertices.Length);
        for (int x = 0; x < value.Vertices.Length; x++)
        {
            output.Write(value.Vertices[x].Position);
            output.Write(value.Vertices[x].Normal);
            output.Write(value.Vertices[x].TextureCoordinate);
        }

        output.Write(value.Indices.Length);
        for (int x = 0; x < value.Indices.Length; x++)
            output.Write(value.Indices[x]);

        output.Write(value.FaceNormals.Length);
        for (int x = 0; x < value.FaceNormals.Length; x++)
            output.Write(value.FaceNormals[x]);
    }

    public override string GetRuntimeType(TargetPlatform targetPlatform)
    {
        return typeof(MeshData).AssemblyQualifiedName;
    }
    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return "ContentProcessors.ModelVertexDataReader, ContentProcessors," +
            "Version=1.0.0.0, Culture=neutral";
    }
}

以类似的方式,当 ContentPipeline 尝试读回 XNB 文件时,它将搜索 MeshData 类型的反序列化器,因此我们必须编写我们自己的 ContentTypeReader

public class ModelVertexDataReader : ContentTypeReader<MeshData>
{
    protected override MeshData Read(ContentReader input, MeshData existingInstance)
    {
        int i = input.ReadInt32();
        VertexPositionNormalTexture[] vb = new VertexPositionNormalTexture[i];
        for (int x = 0; x < i; x++)
        {
            vb[x].Position = input.ReadVector3();
            vb[x].Normal = input.ReadVector3();
            vb[x].TextureCoordinate = input.ReadVector2();
        }

        i = input.ReadInt32();
        int[] ib = new int[i];
        for (int x = 0; x < i; x++)
            ib[x] = input.ReadInt32();

        i = input.ReadInt32();
        Vector3[] normals = new Vector3[i];
        for (int x = 0; x < i; x++)
            normals[x] = input.ReadVector3();

        return new MeshData(vb, ib, normals);
    }
}

最后,我们的自定义内容处理器会为通过它的每个模型填充 MeshData 对象。 注意:某些部分取自ZiggyWare。 

[ContentProcessor(DisplayName = "Custom Mesh Processor")]
 public class PositionNormalTexture : ModelProcessor
 {
     public override ModelContent Process(NodeContent input,
         ContentProcessorContext context)
     {
         ModelContent model = base.Process(input, context);
         foreach (ModelMeshContent mesh in model.Meshes)
         {
             // Put the data in the tag.
             VertexPositionNormalTexture[] vb;
             MemoryStream ms = new MemoryStream(mesh.VertexBuffer.VertexData);
             BinaryReader reader = new BinaryReader(ms);

             VertexElement[] elems = mesh.MeshParts[0].GetVertexDeclaration();
             int num =
                  mesh.VertexBuffer.VertexData.Length / VertexDeclaration.GetVertexStrideSize(
                  elems, 0);

             vb = new VertexPositionNormalTexture[num];
             for (int i = 0; i < num; i++)
             {
                 foreach (VertexElement e in elems)
                 {
                     switch (e.VertexElementUsage)
                     {
                     case VertexElementUsage.Position:
                         vb[i].Position.X = reader.ReadSingle();
                         vb[i].Position.Y = reader.ReadSingle();
                         vb[i].Position.Z = reader.ReadSingle();
                         break;
                     case VertexElementUsage.Normal:
                         vb[i].Normal.X = reader.ReadSingle();
                         vb[i].Normal.Y = reader.ReadSingle();
                         vb[i].Normal.Z = reader.ReadSingle();
                         break;
                     case VertexElementUsage.TextureCoordinate:
                         if (e.UsageIndex != 0)
                             continue;
                         vb[i].TextureCoordinate.X = reader.ReadSingle();
                         vb[i].TextureCoordinate.Y = reader.ReadSingle();
                         break;
                     default:
                         Console.WriteLine(e.VertexElementFormat.ToString());
                         switch (e.VertexElementFormat)
                         {
                         case VertexElementFormat.Color:
                             reader.ReadUInt32();
                             break;
                         case VertexElementFormat.Vector3:
                             reader.ReadSingle();
                             reader.ReadSingle();
                             reader.ReadSingle();
                             break;
                         case VertexElementFormat.Vector2:
                             reader.ReadSingle();
                             reader.ReadSingle();
                             break;

                         }
                         break;
                     }
                 }
             } // for i < num

             reader.Close();

             int[] ib = new int[mesh.IndexBuffer.Count];
             mesh.IndexBuffer.CopyTo(ib, 0);
             Vector3[] normals = new Vector3[mesh.IndexBuffer.Count / 3];
             for (int i = 0, conta = 0; i < mesh.IndexBuffer.Count; i += 3, conta++)
             {
                 Vector3 v0 = vb[mesh.IndexBuffer[i]].Position;
                 Vector3 v1 = vb[mesh.IndexBuffer[i + 1]].Position;
                 Vector3 v2 = vb[mesh.IndexBuffer[i + 2]].Position;
                 Vector3 edge1 = v1 - v0;
                 Vector3 edge2 = v2 - v0;
                 Vector3 normal = Vector3.Cross(edge1, edge2);
                 normal.Normalize();
                 normals[conta] = normal;
             }

             mesh.Tag = new MeshData(vb, ib, normals);
         } // foreach mesh
         return model;
     }
 }

现在我们有了所有需要的信息,我们将专注于碰撞检测本身的实现。

使用 MeshData 实现射线-网格测试

许多人认为 D3D Mesh.Intersect 方法会进行某种优化的“魔术”来测试相交,但实际上它只是循环遍历网格的所有三角形,进行三角形-射线相交测试,并跟踪最近的碰撞点(或所有点,具体取决于您使用的重载版本)。 当然,它应用了一些众所周知的优化,例如快速丢弃多边形、背面等。 这正是我们现在必须使用在内容处理器中生成的信息来做的事情。

以下方法执行射线-网格测试,并将先前内容处理器生成的 MeshData 对象作为参数获取。 请注意,此处可以进行大量优化,快速丢弃三角形。 只需要在 Google 上搜索一下即可。

public static bool RayMesh(Vector3 orig, Vector3 dir, MeshData pMesh,
    ref Vector3 pContactPoint, ref float pDist, ref int pFaceIdx)
{
    Vector3 maxContactPoint = Vector3.Zero;
    int maxFaceIdx = -1;
    float minT = float.MaxValue;
    for (int i = 0, countFace = 0; i < pMesh.Indices.Length; i += 3, countFace++)
    {
        int ia = pMesh.Indices[i];
        int ib = pMesh.Indices[i + 1];
        int ic = pMesh.Indices[i + 2];
        Vector3 v0 = pMesh.Vertices[ia].Position;
        Vector3 v1 = pMesh.Vertices[ib].Position;
        Vector3 v2 = pMesh.Vertices[ic].Position;

        double t = 0f;
        double u = 0f;
        double v = 0f;
        if (RayTriangle(orig, dir, v0, v1, v2, ref t, ref u, ref v))
        {
            Vector3 appPoint = orig + (dir * (float)t);
            if (t < minT)
            {
                minT = (float)t;
                maxFaceIdx = countFace;
                maxContactPoint = appPoint;
            }
        }
    }
    pContactPoint = maxContactPoint;
    pFaceIdx = maxFaceIdx;
    pDist = minT;
    return (minT < float.MaxValue);
}

剩下的唯一部分是射线-三角形相交测试,但是网上有很多关于此问题的信息,所以我将其留给您。 但是,您可以查看以下链接

希望以上四个参考文献足够,并且您喜欢这篇文章。

尽情享用!

© . All rights reserved.