问题
在XNA项目中,你想实时访问每个顶点的位置,如果你需要非常精确的碰撞检测或想找到模型上的特定点时,这一步非常有用。
虽然你可以使用常规代码访问模型的VertexBuffer,但因为顶点数据需要从显卡传递到系统内存,所以这种方法需要笨拙的代码并会拖慢程序。
解决方案
通过提取你想要的数据并把它们存储在模型的Tag属性中扩展默认模型处理器。本教程中,你将编写一个自定义模型处理器创建一个数组,这个数组包含模型中每个三角形的三个Vector3,并将这个数组存储在模型的Tag属性中。因为这个处理器只是默认模型处理器的扩展,所以你可以使用默认的模型导入器和TypeWriter,如图4-18所示。
图4-18 扩展内容管道中的模型处理器
工作原理
首先添加一个内容管道项目,具体步骤可见教程4-15,并起一个名称。
然后从默认ModelProcessor 继承定义你的处理器。因为你想改变模型对象(你需要在Tag属性中存储某些东西),所以需要重写Process方法:
[ContentProcessor] public class ModelVector3Processor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); usualModel.Tag = vertices.ToArray(); return usualModel; } }
这个处理器首先将input传递到基类的Process方法中,这会返回一个默认的ModelContent对象,做好被串行化成一个二进制文件的准备。
但是,在你返回这个对象前,你需要添加一点自定义的东西。所以首先创建一个集合用来存储顶点的位置。在下一段落,你将创建AddVerticesTo List方法,这个方法会遍历整个模型结构并将所有顶点添加到集合中。返回这个集合,并转换为一个数组,然后将这个数组存储在ModelContent对象的Tag属性中。
你的自定义处理的“input”变量包含指向模型根节点(root node)的链接,接下来是几何和材质数据,这个根节点包含指向其子节点的链接等,直到遍历整个模型。如果你想创建一个方法将模型中的所有顶点都保存在一个集合中,那么首先需要保存根节点中的顶点,然后递归调用保存这个根节点的子节点,直至模型中所有顶点都被保存到集合中。
以上就是AddVerticesToList方法所做的工作,将这个方法添加到ModelVector3Processor 类中:
private List<Vector3> AddVerticesToList(NodeContent node, List<Vector3> vertList) { MeshContent mesh = node as MeshContent; if (mesh != null) { //This node contains vertices, add them to the list } foreach (NodeContent child in node.Children) vertList = AddVerticesToList(child, vertList); return vertList; }
一个节点不一定包含顶点,有时一个节点只是用来作为二个或二个以上子节点的父节点而不包含顶点数据。这意味着你需要首先判断一个节点是否是一个MeshContent对象,如果是说明它包含顶点。这里的“as”关键字进行了这种检查,如果不成功则说明对象(这里是mesh)为null。所以当程序运行到if代码块时,你可以判断节点一定包含几何数据,就需要把这些顶点存储在集合中。
注意:一个不包含顶点信息的节点只会作为模型对象的一个Bone,而不包含几何信息的节点看作是Bone和链接到Bone的一个ModelMesh(实际上是一个ModelMeshPart而不是ModelMesh)。
在当前节点的所有顶点都存储了之后,你需要调用同样的方法处理子节点,通过这种方法代码可以遍历整个模型的结构。下列代码获取节点中所有位置信息并将它添加到集合中,这个代码要放在if语句块中:
Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; Vector3 transVertex = Vector3.Transform(vertex, absTransform); vertList.Add(transVertex); } }
一个节点中的所有顶点的位置被它的父节点引用,要获取相对于根节点的位置(=下一个模型的初始节点),你需要通过这个节点的绝对变换矩阵将位置进行转换(可见教程4-9的相关知识)。每个节点都在它的AbsoluteTransform属性中存储了绝对变换矩阵。
接下来你需要找到实际的位置数据。模型中的所有三角形是从IndexBuffer绘制的,而这个IndexBuffer链接到VertexBuffer (见教程5-3)。每个索引代表一个在VertexBuffer 中的顶点,因此对一个三角形你需要找到三个索引。
注意:模型中的所有三角形都是以索引化的三角形列表的形式绘制的(见教程5-3和 5-4)。
你遍历了节点的索引,从VertexBuffer中查询对应的顶点,将这个顶点的位置进行转换,这样这个位置就是相对于模型的初始位置而不是相对于ModelMesh的初始位置,然后将结果添加到集合中。
在将所有位置添加到集合中之后,这个集合返回到父节点,做好传递到下一个节点的准备,直到将所有节点都添加到集合中。之后,最终的集合返回到Process方法中,将这个集合转换为数组并将这个数组存储在模型的Tag属性中。
好了!当你使用自定义模型处理器将一个模型导入到XNA后,你会发现模型的Tag属性中有一个包含Vector3的数组:
myModel = content.Load<Model>("tank"); modelTransforms = new Matrix[myModel.Bones.Count]; Vector3[] modelVertices = (Vector3[])myModel.Tag; System.Diagnostics.Debugger.Break();
因为Tag属性可以包含任何东西,你需要使用(Vector3[])指定类型。最后一行代码让程序暂停,这样你可以检查modelVertices的内容,如图4-19所示。
图4-19 模型的所有Vector3在实时都可以访问
代码
你的自定义管道项目包含了自定义模型处理器,这个处理器从默认的ModelProcessor继承但重写了Process方法,可以将所有顶点存储在一个Vector3数组中:
namespace Vector3Pipeline { [ContentProcessor] public class ModelVector3Processor : ModelProcessor { public override ModelContent Process(NodeContent input, ContentProcessorContext context) { ModelContent usualModel = base.Process(input, context); List<Vector3> vertices = new List<Vector3>(); vertices = AddVerticesToList(input, vertices); usualModel.Tag = vertices.ToArray(); return usualModel; } private List<Vector3> AddVerticesToList(NodeContent node, List<Vector3> vertList) { MeshContent mesh = node as MeshContent; if (mesh != null) { Matrix absTransform = mesh.AbsoluteTransform; foreach (GeometryContent geo in mesh.Geometry) { foreach (int index in geo.Indices) { Vector3 vertex = geo.Vertices.Positions[index]; Vector3 transVertex = Vector3.Transform(vertex, absTransform); vertList.Add(transVertex); } } } foreach (NodeContent child in node.Children) vertList = AddVerticesToList(child, vertList); return vertList; } } }