问题
你想访问模型中每个顶点的位置。但你想要一个三角形对象的数组,每个三角形对象包含三个Vector3向量,而不是想前面的教程中那样将所有Vector3放在一个大数组中。
或者,更普遍的情况,你想将一个自定义内容处理器类对象传递到XNA程序中,但内容管道无法知道应该如何串行化和反串行化这个类对象,所以你必须定义自己的TypeWriter和TypeReader。
解决方案
本教程的代码主要建立在前一个教程的基础上,但这次你将每三个顶点创建一个简单的三角形对象,然后将所有生成的三角形对象添加到一个集合中,并将这个集合存储在模型的Tag属性中。
最主要的区别是你将在自定义内容管道中定义一个Triangle类。默认的XNA内容管道无法将你的自定义类串行化为一个文件或从文件反串行化为一个对象。所以你将自己定义 TypeWriter和TypeReader。
工作原理
这个教程也可以作为编写自定义TypeWriter和TypeReader的教程。看一下图4-20找到 TypeWriter和TypeReader在内容管道中的位置。
图4-20 内容管道中的自定义处理器、TypeWriter和TypeReader
浏览一下教程3-9中的创建自定义内容管道的步骤,将这个类添加到你的新的内容管道项目中:
public class Triangle { private Vector3[] points; public Triangle(Vector3 p0, Vector3 p1, Vector3 p2) { points = new Vector3[3]; points[0] = p0; points[1] = p1; points[2] = p2; } public Vector3[] Points { get { return points; } } public Vector3 P0 { get { return points[0]; } } public Vector3 P1 { get { return points[1]; } } public Vector3 P2 { get { return points[2]; } } }
这是个非常简单的类,用来存储三个Vector3。这个类还提供了getter方法,可以一次获取一个Vector3或包含三个Vector3的数组。
对模型的每个三角形来说,你想创建一个自定义类的对象,将这些对象储存在一个数组中,并将这个数组存储在模型的Tag属性中,这样就可以用于XNA的实时代码。
Process类的代码几乎和教程4-13中的一样,但这次你保存的是Triangle对象的集合而不是上一个教程的Vector3的集合。在Triangle类之后添加Processor类:
[ContentProcessor] public class ModelTriangleProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Triangle> triangles = new List<Triangle>(); triangles = AddVerticesToList(input, triangles); usualModel.Tag = triangles.ToArray(); return usualModel; } }
AddVerticesToList方法会遍历整个模型结构,为每三个Vector3生成一个Triangle对象并将所有的Triangle对象添加到集合中:
private List<Triangle> AddVerticesToList(NodeContent node, List<Triangle>triangleList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { //generate Triangle objects... } } foreach (NodeContent child in node.Children) triangleList = AddVerticesToList(child, triangleList); return triangleList; }
上面的代码完全来自于教程4-13,下面你将编写生成Triangle对象的代码:
int triangles = geo.Indices.Count / 3; for (int currentTriangle = 0; currentTriangle < triangles; currentTriangle++) { int index0 = geo.Indices[currentTriangle * 3 + 0]; int index1 = geo.Indices[currentTriangle * 3 + 1]; int index2 = geo.Indices[currentTriangle * 3 + 2]; Vector3 v0 = geo.Vertices.Positions[index0]; Vector3 v1 = geo.Vertices.Positions[index1]; Vector3 v2 = geo.Vertices.Positions[index2]; Vector3 transv0 = Vector3.Transform(v0, absTransform); Vector3 transv1 = Vector3.Transform(v1, absTransform); Vector3 transv2 = Vector3.Transform(v2, absTransform); Triangle newTriangle = new Triangle(transv0, transv1, transv2); triangleList.Add(newTriangle); }
上述代码很短,但简单的代码看起来更难。
模型是根据索引缓冲中的数据绘制的,每个索引对应顶点缓冲中的一个顶点(可见教程5-3学习更多索引的知识)。首先你需要知道模型中三角形的总数,它等于索引数除以3。
然后,对于每个三角形,你首先找到索引。每个三角形是由三个连续的索引定义的,所以首先你要将这些索引存储在变量中。一旦你有了这些索引值,就可以获取对应的顶点,并通过节点的绝对变换矩阵变换这些顶点(见教程4-9),这样可以使顶点位置变成相对于模型初始位置而不是相对于ModelMesh的初始位置。最后,你基于三个顶点创建一个新的Triangle对象并将这个对象存储在集合中。
在Process方法的最后,这个集合被转换为一个Triangle对象的数组并将这个数组存储在模型的Tag属性中。
这就是上一个教程结束时所做的:你已经将额外的数据存储在了模型的Tag属性中了。但是当你导入模型,选则这个Processor处理这个模型并试图编译时会发生错误:
Unsupported type. Cannot find a ContentTypeWriter implementation for ModelTriaPipeline.Triangle
编写自定义内容TypeWriter
之所以会发生这个错误只因为内容管道不知道如何将Triangle对象串行化为一个二进制文件!所以,你必须编写一个简单的TypeWriter处理Triangle对象如何被串行化。当在处理器中生成的一个ModelContent对象处理到Triangle对象时,XNA就会调用这个TypeWriter将 Triangle保存为二进制文件。
在内容管道项目中添加这个新类,这个类需要放置在Processor类的外面:
[ContentTypeWriter] public class TriangleTypeWriter : ContentTypeWriter<Triangle> { protected override void Write(ContentWriter output, Triangle value) { output.WriteObject<Vector3>(value.P0); output.WriteObject<Vector3>(value.P1); output.WriteObject<Vector3>(value.P2); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TriangleTypeReader).AssemblyQualifiedName; } }
首先两行代码表示你将定义一个知道如何串行化Triangle 类对象的ContentTypeWriter。
首先你需要重写Write方法,它以你要保存的每个Triangle对象作为参数。这个数组以值参数的形式传递给Write方法。Output变量包含一个ContentWriter对象让你可以保存到二进制文件。
当对一个确定对象编写TypeWriter时,你首先需要考虑存储什么东西,让你可以在加载二进制文件时重建这些对象。然后,你需要将这个对象分解成更多的简单对象,直到内容管道知道如何串行化这些简单对象。
对Triangle来说,存储三个Vector3就可以让你可以重建Triangle了。你很幸运,,因为内容管道知道如何串行化Vector3,所以Write方法只需简单地将Triangle分解成三个Vector3并进行串行化。
技巧:你可以遍历output的不同重载方法,Write方法可以知道哪种数据类型可以被默认内容管道串行化。
注意:你也可以使用output. Write(value. P0); 这是因为Vector3类型是默认被支持的。但是,上述代码中的output. WriteObject方法更普遍,因为这个方法允许写入自定义TypeWriter中的自定义类对象。
当开始XNA程序时,二进制文件还需要被反串行化。默认内容管道也不知道如何反串行化Triangle对象,所以你还要自定义一个TypeReader。
要让XNA程序找到对应的自定义TypeReader,你的TypeWriter需要在GetRuntimeReader方法中指定TypeReader的位置。
这个GetRuntimeReader方法(在前面的代码中也定义了)只是简单地返回一个字符串,表示在哪可以找到用于Triangle对象的TypeReader。
如果运行代码,会产生一个错误:ModelTriaPipeline. TriangleReader class cannot be found,这可能是因为你还没有编写这个类。所以在ModelTriaPipeline命名空间中添加最后一个类:
public class TriangleTypeReader : ContentTypeReader<Triangle> { protected override Triangle Read(ContentReader input, Triangle existingInstance) { Vector3 p0 = input.ReadObject<Vector3>(); Vector3 p1 = input.ReadObject<Vector3>(); Vector3 p2 = input.ReadObject<Vector3>(); Triangle newTriangle = new Triangle(p0, p1, p2); return newTriangle; } }
你从ContentTypeReader类继承,让你的自定义TypeReader可以反串行化Triangle类对象。
注意:确保你的自定义reader的名称与你在writer中GetRuntimeReader方法中指定的名称是一致的!否则,XNA仍然无法找到对应的TypeReader。
在XNA项目运行开始时,在二进制文件中每次碰到Triangle对象都会调用这个类的Read方法。你在每个三角形中存储了三个Vector3,所以在TypeReader中,你只需简单地从二进制文件中读出三个Vector3,然后基于这三个Vector3创建三角形对象,最后返回三角形。
在内容管道项目中添加引用
编写完TypeReader后,就做好了运行程序的准备。但是虽然项目可以正确生成,但在运行时仍会遇到一个错误:
Cannot find ContentTypeReader ModelTriaPipeline.Triangle, ModelTriaPipeline, Version=1.0.0.0, Culture=neutral
发生这个错误的原因是XNA项目无法访问TypeReader 中的ModelTriaPipeline命名空间。要解决这个问题,你需要在内容管道中添加一个引用。打开XNA主项目,选择Project→Add Reference,在弹出的对话框中选择Projects选项卡,可以看到列表中的内容管道,如图4-21所示,选择并点击OK。
图4-21 添加对自定义内容管道项目的引用
注意:如果你的内容管道不在列表中,请确保你已经生成了内容管道项目(可见教程4-9中步骤列表的第6步)。
这里你需要在XNA项目的Content 目录和XNA项目本身中添加内容管道的引用,如图4-22所示。第一个引用让你可以为模型选择自定义处理器,第二个引用让XNA项目可以实时调用TypeReader。
图4-22 需要两个自定义内容管道的引用
当你选择ModelTriangleProcessor处理模型并运行程序后,你的模型就在Tag属性中包含了Triangle对象的数组。
添加内容管道的命名空间
在XNA项目中,当你想访问存储在Tag属性中的Triangle对象时,你总有将Tag属性中的内容转换为你想要的类型,本例中是Triangle [] (三角形的数组)。但是,因为Triangle类是定义在另一个命名空间中的,你需要在Triangle名称之前加上它的命名空间:
ModelTriaPipeline.Triangle[] modelTriangles = (ModelTriaPipeline.Triangle[])myModel.Tag;
这看起来不好。如果你使用using将内容管道命名空间(本例中是ModelTriaPipeline)添加到XNA项目中:
using ModelTriaPipeline;
那么自定义内容管道的所有类都能被XNA项目知道,可以让代码变得更短:
Triangle[] modelTriangles = (Triangle[])myModel.Tag;
注意:存储在Triangle中的位置信息是相对于模型的初始位置的。当对模型的一部分施加动画时如何使用这些位置信息,可见教程4-14。
使用自定义类对象扩展处理器的步骤清单
这里你可以找到让包含自定义类的内容管道运行的步骤。与教程4-9做的一样,我会总结一个清单,你可以将它作为参考。教程4-9中的初始化列表扩展成两个部分,这在前面已经讨论过了。
1.在解决方案中添加一个内容管道项目
2.在新项目中,添加Microsoft. XNA. Framework. Content. Pipeline的引用。
3.在using代码块添加Pipeline命名空间。
4.表明你将扩展哪个部分(这部分的方法需要重写)。
5.编译新内容管道项目。
6.在主项目中添加新创建的引用
7.选择新创建的处理器处理一个素材。
8.设置项目依赖项。
9.在XNA主程序中,添加自定义内容管道的引用。
10.在XNA主程序中的using代码块中添加内容管道命名空间。
初始化后进行以下步骤:
1.定义自定义类
2.编写第4步中的代码
3.对每个处理器中使用的自定义类,创建一个TypeWriter。
4.对每个处理器中使用的自定义类,创建一个TypeReader。
注意:内容导入器,处理器和typewriters只在编译项目时才会被invoke。所以,这些类无法部署在Xbox 360平台上,因为TypeReader类和你的自定义类无法被Game类找到。要解决这个问题,可将这两个类移至主XNA项目中,并添加引用。可见本教程的示例代码(译者注:是指XboxPipeline文件夹中的示例)。
代码
下面是自定义内容管道命名空间下完整代码。这个命名空间包括一个带有辅助方法的自定义模型处理器,一个自定义类,一个TypeReader (将自定义类存储为一个二进制文件),一个TypeReader (反串行化自定义类对象)。
namespace TrianglePipeline { public class Triangle { private Vector3[] points; public Triangle(Vector3 p0, Vector3 p1, Vector3 p2) { points = new Vector3[3]; points[0] = p0; points[1] = p1; points[2] = p2; } public Vector3[] Points { get { return points; } } public Vector3 P0 { get { return points[0]; } } public Vector3 P1 { get { return points[1]; } } public Vector3 P2 { get { return points[2]; } } } [ContentProcessor] public class ModelTriangleProcessor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Triangle> triangles = new List<Triangle>(); triangles = AddVerticesToList(input, triangles); usualModel.Tag = triangles.ToArray(); return usualModel; } private List<Triangle> AddVerticesToList(NodeContent node, List<Triangle> triangleList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { int triangles = geo.Indices.Count / 3; for (int currentTriangle = 0; currentTriangle < triangles; currentTriangle++) { int index0 = geo.Indices[currentTriangle *3+ 0]; int index1 = geo.Indices[currentTriangle *3+ 1]; int index2 = geo.Indices[currentTriangle *3+ 2]; Vector3 v0 = geo.Vertices.Positions[index0]; Vector3 v1 = geo.Vertices.Positions[index1]; Vector3 v2 = geo.Vertices.Positions[index2]; Vector3 transv0 = Vector3.Transform(v0, absTransform); Vector3 transv1 = Vector3.Transform(v1, absTransform); Vector3 transv2 = Vector3.Transform(v2, absTransform); Triangle newTriangle = new Triangle(transv0, transv1, transv2); triangleList.Add(newTriangle); } } } foreach (NodeContent child in node.Children) triangleList = AddVerticesToList(child, triangleList); return triangleList; } } [ContentTypeWriter] public class TriangleTypeWriter : ContentTypeWriter<Triangle> { protected override void Write(ContentWriter output, Triangle value) { output.WriteObject<Vector3>(value.P0); output.WriteObject<Vector3>(value.P1); output.WriteObject<Vector3>(value.P2); } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return typeof(TriangleTypeReader).AssemblyQualifiedName; } } public class TriangleTypeReader : ContentTypeReader<Triangle> { protected override Triangle Read(ContentReader input, Triangle existingInstance) { Vector3 p0 = input.ReadObject<Vector3>(); Vector3 p1 = input.ReadObject<Vector3>(); Vector3 p2 = input.ReadObject<Vector3>(); Triangle newTriangle = new Triangle(p0, p1, p2); return newTriangle; } } }
在主XNA项目中,加载模型,访问它的Triangles,,并放置一个断点让你可以检查modelTriangles数组的内容:
myModel = Content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; Triangle[] modelTriangles = (Triangle[])myModel.Tag; System.Diagnostics.Debugger.Break();