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