问题

前面的教程中让你可以访问模型的所有顶点的相对于模型初始位置的位置。但是,如果你想对模型的一部分施加动画,例如,旋转一个人的手臂,那么你还想变换手臂,手,手指的位置。使用上一个教程的结果是不可能的,因为Vector3集合不包含Vector3属于模型哪个部分的信息,只包含相对于模型初始位置的信息。

解决方案

模型的独立变换部分是存储在ModelMeshe中的。ModelMesh有一个Tag属性,你可以在Tag中存储有用的信息。在本教程中,你将在Tag属性中存储ModelMesh的所有顶点的位置信息。

你将存储相对于ModelMesh初始位置的顶点位置信息(而不是像前一个教程中的相对于模型的初始位置)。当XNA程序更新Bone矩阵并计算绝对矩阵时(见教程4-9),你可以使用与渲染模型相同的绝对Bone矩阵变换位置,获得相对于模型初始位置的顶点位置。相对于前一个教程,这个方法最大的好处是,这次你将处理独立部分的当前变换。工作原理这个教程在前一个教程的基础上再加以扩展。你仍需要遍历模型的整个结构,但这次,无需将所有顶点存储在一个大数组中,你将为每个节点生成独立的数组。

在遍历了整个模型并存储了数组后,你使用默认模型处理器从节点产生一个ModelContent。最后,你遍历ModelContent中的所有ModelMeshContents,然后在对应的ModelMeshContent的Tag属性中存储每个数组。

注意:ModelContent对象是内容处理器的输出,然后它被写入一个二进制文件。当程序运行时,这个文件从磁盘被加载,这个对象被转换为一个模型。

以下是处理器的开始部分的代码:

public override ModelContent Process(NodeContent input,ContentProcessorContext context) 
{
    List<Vector3[]> modelVertices = new List<Vector3[]>(); 
    modelVertices = AddModelMeshVertexArrayToList (input, modelVertices); 
} 

你创建了一个集合存储独立的Vector3数组,然后,将这个集合和根节点传递到AddModelMeshVertexArrayToList方法。

这个方法首先调用自己,传递它的子节点,这样可以将子节点的数组添加到集合中。

当所有子节点的数组添加到集合之后,AddModelMeshVertexArrayToList方法检查当前节点是否包含顶点。如果包含,则创建一个包含所有顶点位置的Vector3数组并将它添加到集合中。

private List<Vector3[]> AddModelMeshVertexArrayToList(NodeContent node, List<Vector3[]>modelVertices)
{
    foreach (NodeContent child in node.Children) 
        modelVertices = AddModelMeshVertexArrayToList(child, modelVertices); 
    MeshContent mesh = node as MeshContent; 
    if (mesh != null) 
    {
        List<Vector3> nodeVertices = new List<Vector3>(); 
        foreach (GeometryContent geo in mesh.Geometry) 
        {
            foreach (int index in geo.Indices) 
            {
                Vector3 vertex = geo.Vertices.Positions[index]; 
                nodeVertices.Add(vertex); 
            }
        }
        modelVertices.Add(nodeVertices.ToArray()); 
    }
    return modelVertices; 
}

注意:与上一个教程相反,这次位置不进行变换。这是因为你想存储相对于ModelMesh初始位置的顶点位置,而不是相对于模型初始位置。你可以在以后通过ModelMesh的绝对矩阵转换它们。

注意:AddModelMeshVertexArrayToList方法与上一个教程的AddVerticesToList方法最主要的不同之处在于:你在添加当前节点的数据之前首先将子节点的数据添加到集合中。在前一个教程中在添加子节点的数据之前首先添加当前节点的数据,这样看起来可能更加直观。但是,以ModelMesh保存到模型的相同顺序将ModelMesh的数组保存是很重要的,这样你可以很容易地在Process方法的最后加载数组。

最后,将下列代码添加到Process方法的最后:

ModelContent usualModel = base.Process(input, context); 
int i = 0; 
foreach(ModelMeshContent mesh in usualModel.Meshes) 
    mesh.Tag = modelVertices[i++]; 
return usualModel; 

有了数组后,你就可以使用默认模型处理器从节点创建一个默认的ModelContent对象。在每个ModelMeshes中你存储了正确的包含Vector3的数组。

但是,别忘了有些ModelMeshe是由超过一个ModelMeshPart构成的,每个ModelMeshPart 有自己的NodeContent,对于每个ModelMeshPart,AddModelMeshVertexArrayToList方法会将Vector3数组添加到集合。所以要让代码完整,你需要用以下代码替代前面的代码:

ModelContent usualModel = base.Process(input, context); 
int i = 0; 
foreach (ModelMeshContent mesh in usualModel.Meshes) 
{
    List<Vector3> modelMeshVertices = new List<Vector3>();
    foreach (ModelMeshPartContent part in mesh.MeshParts) 
    { 
        modelMeshVertices.AddRange(modelVertices[i++]); 
    } 
    mesh.Tag = modelMeshVertices.ToArray(); 
}
return usualModel; 

对于属于一个ModelMesh的所有ModelMeshPart,上面的代码将所有Vector3添加到一个集合,这个集合被转换到一个数组并存储在ModelMesh的Tag属性中。

注意:因为一个ModelMesh的所有ModelMeshPart使用相同的绝对变换矩阵,你可以一次性地处理所有Vector3仍能保证正确处理动画(见教程4-9)。你可能还想在ModelMeshPart 的Tag属性中存储每个ModelMeshPart的顶点,例如,创建一个更好的包围球处理快速的碰撞检测。

最后返回ModelContent,做好了串行化为一个二进制文件的准备。

当你使用自定义模型处理器导入一个模型时,你可以在模型的每个ModelMesh的Tag属性中找到一个数组,这个数组包含了ModelMesh的每个三角形的三个Vector3分量。在 LoadContent方法中加载模型:

myModel = Content.Load<Model>("tank"); 
modelTransforms = new Matrix[myModel.Bones.Count]; 

现在每个模型的ModelMesh的Tag属性中包含了Vector3的集合,你可以通过编译器访问储存在数组中的数据:

Vector3[] modelVertices = (Vector3[])myModel.Meshes[0].Tag; 
System.Diagnostics.Debugger.Break(); 

最后一行代码放置了一个断点,你可以观察Vector3的内容。

代码

下面是自定义内容管道:

namespace Vector3Pipeline 
{
    [ContentProcessor]
    public class ModelVector3Processor : ModelProcessor
    {
        public override ModelContent Process(NodeContent input, ContentProcessorContext context) 
        {
            List<Vector3[]> modelVertices = new List<Vector3[]>(); 
            modelVertices = AddModelMeshVertexArrayToList(input, modelVertices); 
            
            ModelContent usualModel = base.Process(input, context); 
            int i = 0; 
            foreach (ModelMeshContent mesh in usualModel.Meshes) 
            {
                List<Vector3> modelMeshVertices = new List<Vector3>(); 
                foreach (ModelMeshPartContent part in mesh.MeshParts) 
                {
                    modelMeshVertices.AddRange(modelVertices[i++]); 
                }
                mesh.Tag = modelMeshVertices.ToArray(); 
            }
            return usualModel; 
        }
        
        private List<Vector3[]> AddModelMeshVertexArrayToList(NodeContent node, List<Vector3[]> modelVertices)
        {
            foreach (NodeContent child in node.Children) 
                modelVertices = AddModelMeshVertexArrayToList(child, modelVertices); 
            MeshContent mesh = node as MeshContent; 
            if (mesh != null) 
            {
                List<Vector3> nodeVertices = new List<Vector3>(); 
                foreach (GeometryContent geo in mesh.Geometry) 
                {
                    foreach (int index in geo.Indices) 
                    {
                        Vector3 vertex = geo.Vertices.Positions[index]; 
                        nodeVertices.Add(vertex); 
                    }
                }
                modelVertices.Add(nodeVertices.ToArray()); 
            } 
            return modelVertices; 
        }
    }
}

1