问题

给定一个3D空间中的点序列,你想构建一个漂亮的,光滑曲线可以通过所有这些点。图5-32中的黑色曲线显示了这样条曲线,灰色线段表示使用简单的线性插值的情况,可参见教程5-9。

image

图5-32 通过5点的Catmul-Rom样条(spline)

这在许多情况中是很用的。例如,你可以用它产生一个赛道,可参见教程5-17。当相机非常靠近模型或地形时,你可以使用Catmull-Rom插值产生额外的顶点,使模型或地形看起来更加光滑。

解决方案

如果你想在两个基点之间生成Catmull-Rom样条,你还需要知道两个邻近基点。在图5-32中,当你想在点1和点2之间生成曲线时,你需要知道基点0, 1, 2和3的坐标。

XNA已经带有一维的Catmull-Rom功能。你可以将任意四个基点传入MathHelper. CatmullRom方法,这个方法可以为你计算第二和第三个点之间的曲线。你还需要传递在0和1范围内的第五个参数,表示你想要第二和第三个点之间的哪个点。

在本教程中,你将这个功能扩展到3维并创建一个方法用来生成多个基点之间的一根样条。

工作原理

XNA提供了一维的单个值的Catmul-Rom插值算法。因为Vector3只是三个单个值的组合,所以你可以通过在Vector3的X, Y和Z分量上调用一维XNA方法实现Catmull-Rom插值 。下面的方法是XNA默认方法的3D扩展,它接受四个Vector3而不是四个单个值。

private Vector3 CR3D(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float amount) 
{
    Vector3 result = new Vector3(); 
    
    result.X= MathHelper.CatmullRom(v1.X, v2.X, v3.X, v4.X, amount); 
    result.Y = MathHelper.CatmullRom(v1.Y, v2.Y, v3.Y, v4.Y, amount); 
    result.Z = MathHelper.CatmullRom(v1.Z, v2.Z, v3.Z, v4.Z, amount); 
    
    return result; 
} 

这个方法会返回一个叫做result的Vector3,位于v2和v3之间的样条上。变量amount让你可以指定v2,result和v3之间的距离,当amount为0时返回v2,1返回v3。

使用CR3D方法计算样条

有了计算3维Catmull-Rom插值的方法,就可以生成样条的多个点了。给定3维空间中的四个基点v1,v2,v3和v4,下面的方法返回v2和v3之间的20个点的集合,第一个点位于v2。

private List<Vector3> InterpolateCR(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) 
{
    List<Vector3> list = new List<Vector3>(); 
    
    int detail = 20; 
    for (int i = 0; i < detail; i++) 
    {
        Vector3 newPoint = CR3D(v1, v2, v3, v4, (float)i / (float)detail); 
        list.Add(newPoint); 
    }
    return list; 
} 

如果你想添加/移除v2和v3之间的点,可以增加/减少detail值(或更好的做法,将detail值作为这个方法的参数)。

注意:点集合中的最后一个点不是v3。这是因为最后一个点调用CR3D方法时,传递的参数是19/20而不是1。

你需要手动添加v3,例如,添加这行代码:list.Add(v3);。

将样条的多个部分连接在一起

前面的代码生成了位于四个点中间两点之间的样条,下面的代码表示如何扩展样条:

points.Add(new Vector3(0,0,0)); 
points.Add(new Vector3(2,2,0)); 
points.Add(new Vector3(4,0,0)); 
points.Add(new Vector3(6,6,0)); 
points.Add(new Vector3(8,2,0)); 
points.Add(new Vector3(10, 0, 0)); 

List<Vector3> crList1 = InterpolateCR(points[0], points[1], points[2], points[3]); 
List<Vector3> crList2 = InterpolateCR(points[1], points[2], points[3], points[4]);
List<Vector3> crList3 = InterpolateCR(points[2], points[3], points[4], points[5]); 

straightVertices = XNAUtils.LinesFromVector3List(points, Color.Red); 
crVertices1 = XNAUtils.LinesFromVector3List(crList1, Color.Green); 
crVertices2 = XNAUtils.LinesFromVector3List(crList2, Color.Blue); 
crVertices3 = XNAUtils.LinesFromVector3List(crList3, Color.Yellow);

首先在3D空间定义七个(译者注:原文如此,不过好像是六个)点的集合。然后,使用前四个点生成points [ 1]和points [ 2]之间的额外点。然后切换到下一个点获取points [2]和points [ 3]之间的额外点。最后获取了points[3]和points[4]之间的额外点。

这意味着你最终获得了许多额外点,所有这些点都在从points[1]出发,经过points[2]和point[3],终止于points[4]的样条上(看一下图5-32加深理解)。

因为你想绘制这些点,所以要从样条的这三个部分生成顶点。首先要将七个基点转换为顶点,这样就可以绘制线段了。使用下面的方法进行此步:

public static VertexPositionColor[] LinesFromVector3List(List<Vector3> pointList, Color color) 
{
    myVertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements); 
    VertexPositionColor[] vertices = new VertexPositionColor[pointList.Count]; 
    
    int i = 0; 
    foreach (Vector3 p in pointList) 
        vertices[i++] = new VertexPositionColor(p, color); 
    
    return vertices; 
}

上述方法只是简单地将集合中的每个Vector3转换为一个顶点,并将它和选择的颜色存储在一个数组中。

你可以绘制位于这些顶点间的线段,可参加教程6-1:

basicEffect.World = Matrix.Identity; 
basicEffect.View = fpsCam.ViewMatrix; 
basicEffect.Projection = fpsCam.ProjectionMatrix; 
basicEffect.VertexColorEnabled = true; 
basicEffect.TextureEnabled = false; 

basicEffect.Begin(); 
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) 
{
    pass.Begin(); 
    device.VertexDeclaration = myVertexDeclaration; 
    device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineStrip, straightVertices, 0, straightVertices.Length - 1); 
    device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineStrip, crVertices1, 0, crVertices1.Length - 1);
    device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineStrip, crVertices2, 0, crVertices2.Length- 1); 
    device.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.LineStrip, crVertices3, 0, crVertices3.Length - 1); 
    pass.End(); 
}
basicEffect.End(); 
代码

这个教程介绍了两个方法。第一个是XNA自带的Catmull-Rom 插值算法在3维空间中的扩展:

private Vector3 CR3D(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float amount) 
{
    Vector3 result = new Vector3(); 
    
    result.X = MathHelper.CatmullRom(v1.X,v2.X, v3.X, v4.X, amount); 
    result.Y = MathHelper.CatmullRom(v1.Y, v2.Y, v3.Y, v4.Y, amount); 
    result.Z = MathHelper.CatmullRom(v1.Z, v2.Z, v3.Z, v4.Z, amount); 
    
    return result; 
} 

第二个方法可以计算通过四个基点的样条上额外点:

private List<Vector3> InterpolateCR(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)
{
    List<Vector3> list = new List<Vector3>(); 
    
    int detail = 20; 
    for (int i = 0; i < detail; i++) 
    {
        Vector3 newPoint = CR3D(v1, v2, v3, v4, (float)i / (float)detail); 
        list.Add(newPoint); 
    }
    list.Add(v3); 
    return list; 
} 

image