Using Advanced Mesh Features
翻译:clayman
仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^
这一章,我们将要调论一些关于Mesh对象的高级特性,包括:
n 优化(Optimizing)mesh数据
n 简化(Simplifying)mesh
n 使用新的顶点数据元素创建mesh
n 合并(Welding)顶点
克隆Mesh数据
终于,你可以在场景里加载并渲染mesh了。虽然场景里只有少量的灯光,而且mesh看起来基本是黑的。观察一下mesh的属性,注意到顶点格式并没有包含计算光照必须的法线数据。我们需要一个简单的方法为现有的顶点数据添加法线信息。如果你猜想DirectX已经有这样的一个转换机制,那么恭喜,你猜对了。观察以下代码:
//Check if mesh doesn’t include normal data
If((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal)
{
Mesh tempMesh = mesh.Clone(mesh.Options.Value,mesh.VertexFormat | VertexFormats.Normal,device);
tempMesh.ComputeNormals()
//Replace existiong mesh
Mesh.Dispose()
Mesh = tempMesh;
}
这里,我们有一个已存在的名为“mesh”的Mesh对象,并检查他是否已经包含了法线数据。VertexFormat属性返回一个经过了逻辑或运算的属性格式的列表,因此,我们使用一个逻辑与运算来检查是否设置过了法线位(normal bit)。如果没有,就使用Clone方法通过现有mesh创建一个新的临时mesh。Clone方法有3个种重载:
public Mesh Clone(MeshFlags options,GraphicsStream declaration,Device device);
public Mesh Clone(MeshFlags options,VertexElement[] declaration,Device device);
public Mesh Clone( MeshFlags options,VertexFormats vertexFormat,Device device);
示例中,我们使用了最后一个方法。在每一个重载的方法中,第一个和第三个参数都是一样的。Options参数允许新创建的mesh对象与原来的mesh相比有一组不同的选项。如果要保留和原mesh相同的选项,那么就像示例所做的那样就可以了,使用现有mesh的options参数;当然,你也可以根据自己的需要来改变。比如你想把新的mesh分配到系统内存,而不是托管的内存。所有在创建mesh时可用的Meshflags枚举,在Clone方法中也是可用的。
Device允许你选择把mesh创建到哪一个设备上。大多数情况下这个device就是创建原mesh的device,当然,克隆到一个完全不同的device也是可能的。举个例子,比如你在写一个将运行在多显示器上的程序,,并且每个显示器都运行在全屏模式。当mesh对象只需要渲染在第一个显示器,那么对于第二个显示器来说,就没有必要保存这个mesh的“实例”了。而当第二个显示器也要渲染这个对象的时候再把它克隆过去。我们简单的示例就只使用了当前的device。
中间的参数决定了将以什么方式来渲染数据。我们所用的重载方法中,使用了所要克隆的mesh对象的顶点格式(这个格式可以与原mesh的不同)作为参数。其他的几种重载中,这个参数是作为新mesh的顶点声明(vertex declaration)来使用。这个顶点声明会在暂时还没讨论过可的编程管道(programmable pipeline)中用到,但是它本质上就是一些比固定功能顶点格式强大得多的选项。
你应该已经发现了,mesh类同样也有一个助手函数,可以自动为mesh计算法线。这个方法同样也有3种重载。我们使了没有参数的一个,其他的两个方法接收一个作为邻接信息(adjacency information)的参数,它可以是一个整型的数组,也可以是一个GraphicsStream。如果你有邻接信息,那么就自由的使用它吧。
你可以使用mesh克隆的能力在一个新设备上创建一个完全相同的mesh实例,也可以通过现有mesh改变一些选项来创建。比如使用新的顶点格式,甚至从mesh中删除一些现有的顶点格式信息。现有的mesh没有你希望的信息,就把它克隆为一个格式更适合的版本吧。
优化Mesh数据(Optimizing Mesh Data)
克隆Mesh并不是改变现有mesh的唯一方法。还有很多种方法可以优化mesh。Mesh类还有一个称为Optimize的方法,它和Clone方法类似,也可以创建有不同选项(option)的新mesh,除此之外,他还能在创建新mesh的时候进行优化。不能使用Optimize方法来添加或删除顶点数据,也不能把mesh克隆到其他device。以下是Optimize方法的主要重载之一:
public Mesh Optimize(MeshFlags flags,int[] adjacencyIn,out int adjacencyOut,out int faceRemap,out GraphicsStream vertexRemap);
其它的几种重载中,都使用了这组参数或其中的几个作为参数。这里需要注意的是adjacencyIn参数可以是一个整型的数组。也可以是一个GraphicsStream.
Flags参数决定如何来创建新的mesh。他的值可以是MeshFlags枚举(除了Use32Bit和WriteOnly)中的一个或多个。以下是众多可用的优化选项:
MeshFlags.OptimizeCompact Reorders the faces in the mesh to remove unused vertices and faces.
MeshFlags.OptimizeAttrSort Reorders the faces of the mesh so that there are fewer attribute state changes (i.e., materials), which can enhance DrawSubset performance.
MeshFlags.OptimizeDeviceIndependent Using this option affects the vertex cache size by specifying a default cache size that works well on legacy hardware.
MeshFlags.OptimizeDoNotSplit Using this flag specifies that vertices should not be split if they are shared between attribute groups.
MeshFlags.OptimizeIgnoreVerts Optimize faces only; ignore vertices.
MeshFlags.OptimizeStripeReorder Using this flag will reorder the faces to maximize the length of adjacent triangles.
MeshFlags.OptimizeVertexCache Using this flag will reorder the faces to increase the cache hit rate of the vertex caches.
AdjacencyIn参数也是必须的,它可以是为每个面含了3整型的整型数组,它指定了mesh中每个面所邻的三个面(an integer array containing three integers per face that specify the three neighbors for each face in the mesh);也可以是包含了同样数据的graphics stream。
(产生邻接信息(adjacency information):你可能已经注意到了,mesh类的很多高级方法都需要邻接信息。虽然可以在创建mesh的时候获得获得这些信息,但要是你获得的是一个 已经创建好的mesh怎么办呢?可以使用mesh中一个名为GenerateAdjacency的方法来获得这些信息。这个方法的第一个参数是一个float值,所有顶点间的距离小于这个值的都将按这个值来计算(vertices whose position differ by less than this amount will be treated as coincident),第二个参数是用来填充邻接信息的整型数组。这个数组的大小至少为mesh.NumberFaces的三倍。)
如果你选择了参数比较多的两种重载,那么最后的3个参数都是做为数据的返回值来使用的。其中的第一个是新的邻接信息,第二个是mesh中每个面的新索引值,最后一个作为GrapicsStream的值是每一个顶点的索引值。很多的应用程序现在都不使用这些参数了,所以基本可以忽略这几种重载。
下面的简短代码展示了使用mesh自身的属性缓冲来挑选mesh,并且保证它处于托管的内存中:
//Compact our mesh
Mesh tempMesh = mesh.Optimize(MeshFlages.Managed | MeshFlages.OptimizeAttrSort | MeshFlages.OptimizeDoNotSplit, adj);
mesh.Dispose();
mesh = tempMesh;
(适当的优化mesh:但是如果你不想创建一个全新的mesh,你并不想改变创建mesh的标志变量(various creation flags),只想添加一些有用的选项改怎么办呢?有一个成为OptimizeInPlace,接收同样参数的方法可以帮助你。但他有2个不同的地方,flags参数必须是optimization flags中的一个(不能使用其他任何creation flags),并且这个方法没有返回值。直接在调用这个方法的mesh上进行优化。
简化现有Mesh(Simplifying Existing Meshes)
现在, 假设三维设计师给了你一个可能放置在场景不同位置,不同层次(level)的mesh作为道具。根据场景层次的不同,这个mesh有可能作为背景来使用,就是说它不需要和其他靠近镜头层次中的物品显示相同的细节。你可以要求三维设计师给你两个不同的模型,一个高细节,一个低细节的,但是万一他忙不过来了怎么办?为什么不使用一些mesh类就能提供的方法来简化mesh呢?
简化Mesh表示使用现有mesh,根据所给的权重(using a set of provided weights),来移除尽可能多的面和顶点,获得一个低细节的mesh。在简化mesh之前,必须先对它进行一些整理(be cleaned)操作。
所谓整理就是在两个三角扇(注:还记得第四章讲的几种图元类型吗)共享顶点的地方添加一个顶点。来看一下clean方法的重载之一:
public static Mesh Clean(Mesh mesh,GraphicsStream adjacencyIn,GraphicsStream adjacencyOut,out string errorsAndWarnings);
(注:新版的MDX中这个方法已经改为:
public static Mesh Clean(CleanType cleanType,Mesh mesh,GraphicsStream adjacency,GraphicsStream adjacencyOut,out string errorsAndWarnings);)
你可能已经发现他和早些几个方法差不多,把所要clean的mesh作为参数之一,同时,也把邻接信息作为参数。但是注意到,adjacencyOut也是必须的参数。最常见的做法是把创建mesh时获得的邻接信息作为graphics stream,同时当作adjacencyIn和adjacencyOut参数。除此之外,还可以返回一个字符串制来提醒你是否在clean操作中发生了任何错误。最后需要知道的是adjacency参数可以是如上示例中的graphics stream,也可以是一个整型的数组。
为了生动的展示使用这种方法可以获得简化到什么效果,我们将使用第五章所写的“MeshFile”文件作为简化mesh的基础。首先,我将换到线框模式(wire-frame)来显示,这样你很容易就可以看出在顶点上发生的简化效果,在SetupCamera方法中添加如下代码;
device.RenderState.FillMode = FillMode.WireFrame;
接下来,如前面讨论的,clean mesh。因为clean操作需要邻接信息,还需要修改一下LoadMesh中创建mesh的方法:
ExtendedMaterial[] mtrl;
GraphicsStream adj;
//Load mesh
mesh = Mesh.FromFile(file,MeshFlags.Managed,device,out adj,out mtrl);
这里所做的就是加上用来保存邻接信息的adj变量而已。然后,我们调用FromFile方法的重载之一返回这个数据。现在可以调用Clean方法了,在LoadMesh方法最后,添加代码:
//clean mesh
Mesh tempMesh = Mesh.Clean(mesh,adj,adj);
//replace our existing mesh with this one
mesh.Dispose();
mesh = tempMesh;
(注:新版本的MDX中应为 Mesh tempMesh = Mesh.Clean(CleanType.Optimization,mesh,adj,adj); ,另外,偶调试的时候发现添加了mesh.Dispose();,程序运行的时有时会抛出异常)
在最终简化之前,我们应该先来看看Simplify方法,它众多的重载之一如下:
public static Mesh Simplify( Mesh mesh,int[] adjacency,AttributeWeights vertexAttributeWeights, float[] vertexWeights,int minValue, MeshFlags options);
这个方法的结构还是和之前方法的类似。所要简化的mesh作为第一个参数,接下来是邻接信息(同样可以为整型数组或graphics stream)。
之后的AttributeWeights结构是用来设置简化时,大量变量的权重。大部分的情况下都应该使用不带这个参数的重载,因为默认的结构都只考虑几何以及法线的调整(gemoetric and normal adjustment)。只有在特殊的情况下才需要修改其他成员。如果你没有使用这个参数,那么这个结构的默认值如下:
AttributeWeights weights = new AttributeWeights();
weights.Position = 1.0f;
weights.Boundary = 1.0f;
weights.Normal = 1.0f;
weights.Diffuse = 0.0f;
weights.Specular = 0.0f;
weights.Binormal = 0.0f;
weights.Tangent = 0.0f;
weights.TextrueCoordinate = new float[] {0.0f,0.0f 0.0f,0.0f,0.0f,0.0f 0.0f 0.0f};
接下来的参数是每一个顶点的权重表。如果你传入的参数为null,那么会假设每个点的权重为1。(注:sdk注释中说当参数设置为0时权重为1是错误的,此处的null才是正确用法)
minValue参数是你希望mesh中的面或顶点(根据你所传的标志)所简化到的最小值。这个值越小,最后的mesh细节也越少;但是,需要注意的是即使这个方法正确执行了,也不一定能到达所要求的最小值。这个参数应该是一个期望最小值,而不是绝对的最小值。
这个方法的最后一个参数只能是两个值之中的一个。如果需要简化顶点,就使用MeshFlags.SimplifyVertex,否则,需要简化索引,则使用MeshFlags.SimplifyFace。
现在可以正式添加代码来优化mesh了。我们希望同时保留原来的mesh(经过clean的那个)和简化之后的mesh,因此,首先田间一个变量保存简化之后的mesh。
private Mesh simplifiedMesh = null;
之后,添加创建简化过的mesh。在LoadMesh方法的最后,添加如下代码:
simplifiedMesh = Mesh.Simplify(mesh,adj,null,1,MeshFlags.SimplifyVertex);
Console.WriteLine("Number of vertices in original mesh: {0}",mesh.NumberVertices);
Console.WriteLine("Number of vertices in simplified mesh: {0}",simplifiedMesh.NumberVertices);
(注:原著中作者使用了sdk目录下..\ \Samples\Media\Tiny文件夹里的.x文件做为mesh,经过调试发现使用那个mesh在执行simplifiedMesh方法时会抛出异常。原因还在研究ing,汗-_-#,可能是由于那个mesh是带骨胳动画的,不能用这种方法简化。暂时使用sdk目录下..\ Samples\Media\Tiger中的文件作为mesh^o^)
在这里我们尝试把巨大的mesh简化为一个点。说到低分辨率的mesh,你不可能简化到比这个更低的程度了。接下来,在输出窗口中显示简化前后的顶点数量。现在运行程序还不能看到简化之后的mesh,但却能显示两个mesh的顶点数量(注:这个顶点数量是程序运行完之后在VS的输出窗口中显示的)。
Number of vertices in original mesh: 4445
Number of vertices in simplified mesh: 391
不一定能简化到我们所指定的级别,但我们已经把他简化到了原尺寸的8.8%。在远距离的情况下,你几乎分辨不出高分辨率和低分辨率模型的区别,所以节约下一些三角面到更有用的地方吧。
现在来更新代码,看看简化之后的mesh吧。我们想交替显示正常的以及简化之后的mesh,所以先添加一个变量来控制当前所渲染的mesh:
private bool isSimplified = false;
接下来,根据这个标志的值更新渲染代码来正确的绘制mesh子集,添加代码:
device.Material = meshMaterials[i];
device.SetTexture(0,meshTextures[i]);
if(!isSimplified)
{
mesh.DrawSubset(i);
}
else
{
simplifiedMesh.DrawSubset(i);
}
注意,无论我们绘制完整的绘制整个mesh还是绘制简化之后的mesh,都依然保留纹理和材质。所改变的只是使用哪个mesh来调用DrawSubset方法。最后只需添加一个bool值来控制渲染哪一个mesh。我们可以使用空格键来控制跳转,添加代码:
protected override void OnKeyUp(KeyEventArgs e)
{
if(e.KeyCode == Keys.Space)
isSimplified = !isSimplified;
}
第一次运行程序,你可以看到整个mesh是在线框模式下的。这个mesh确实有不少顶点,注意观察脸部,它几乎是实心的,顶点太密集了(注:使用tiny.x文件时确实是这样)。之后,点击空格键,可以看到大部分的顶点都消失了,当摄像机相当近的时候,这种效果是很明显的,但如果在对象很远的时候呢?再添加一个变量来控制摄像机的距离;
private bool isClose = true;
更新SetupCamera方法,根据这个标志的值来控制摄像机的远近:
if(isClose)
{
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 580.0f), new Vector3(), new Vector3(0,1,0));
}
else
{
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,8580.0f), new Vector3(),new Vector3(0,1,0));
}
(注:请根据实际情况来调整距离)
如你所见的,如果isClose为false就把摄像机后移8000个单位。同时,你可以注意到我关闭了线框模式,没有游戏是在线框模式下运行的,这样你才会能体验玩家所看到的最终效果。最后要做的是添加一个开关来控制摄像机,用m键来完成这个任务。在OnKeyPress方法的最后加上如下代码:
if(e.KeyCode == Keys.M)
isClose = !isClose;
好了,现在运行程序吧。尝试着看看不同距离,不同细节的mesh看起来有多大区别,在很远的距离下低细节的模型是否和高细节的没有区别呢?
特别提示:保存mesh
也许你还没有注意到,执行这种简化操作代价是很高的。对比一下程序使用原始mesh和简化之后mesh的启动时间就能发现。但即使艺术家们不能为我们单独创建一个低分辨率的模型,我们也不能让玩家经常性的在这种变化过程中等待。
那么折中的方案是什么呢?为什么不做一个额外的工具来完成简化操作,并把简化后的mesh保存到一个文件中呢?这样不仅可以解放艺术家的劳动,让他们专注于高质量的模型,而且每次加载游戏时可以快速的读取数据,而不需要每次都进行简化操作。
如果需要保存简化过的mesh,我们只需添加如下代码就可以了:
int[] simpleAdj = new int[simplifiedMesh.NumberFaces *3];
simplifiedMesh.GenerateAdjacency(0.0f, simpleAdj);
using(Mesh cleanedMesh = Mesh.Clean(simplifiedMesh, simpleAdj, out simpleadj))
{
cleanedMesh.Save(@”..\..\simple.x”, simpleAdj, mtrl, XfileFormat.Text);
}
你可能注意到了我们没获得任何关于简化过的mesh的邻接信息。但我们需要他,所以创建它。需要注意的是简化的操作通常会使mesh回到unclean的状态。Unclean状态下的mesh是不能保存的。因此需要对简化之后的mesh再次clean。这里使用了using语句来保证这个clean的mesh会在使用过后就释放了。
实际上,Save方法有8种不同的重载,但他们都相当类似。其中四种把数据保存到一个你传入的数据流中,其他的则保存到文件中。每一种方法都需要邻接信息和纹理作为参数。这里我们使用了自己创建的邻接信息和原来的纹理作为参数。当然,邻接信息可以是一个整型的数组,也可以是一个数据流。还有一半的方法需要接收一个EffectInstance结构作为参数,用来处理我们之后会讨论的HLSL效果文件。
最后一个参数则是你所要保存的文件类型。有文本格式(text format),二进制格式(binary format),以及压缩格式(compressed format)。选择你最方便的格式就可以了。
合并mesh中的顶点(Welding Vertices in a Mesh)
还有一种控制权较少,但速度比较快的方法可以简化mesh,就是把相似的顶点合并起来。在mesh类中还有一个称为“WeldVertices”的方法,可以把顶点合并合并到一起,并且这些被复制出来的顶点具有相同的属性值(to weld togerther vertices that are replicated and have equal attribute value)。以下是这个方法的原型:
public void WeldVertices(WeldEpsilonsFlags flags,WeldEpsilons epsilons, int[] adjacencyIn,out int adjacencyOut,out int faceRemap,out GraphicsStream vertexRemap);
最后四个参数我们已经详悉讨论过了,他们和前面讨论的clean函数基本相同,这里就不再重复。前面的两个参数控制着如何来把大量的顶点合并起来。第一个参数可以是下表中的任意一个:
WeldEpsilonsFlags.WeldAll
Welds all vertices marked by adjacency as being overlapping.
WeldEpsilonsFlags.WeldPartialMatches
If the given vertex is within the epsilon value given by the WeldEpsilons structure, modify the partially matched vertices to be identical. If all components are equal, then remove one of the vertices.
WeldEpsilonsFlags.DoNotRemoveVertices
Can only be used if WeldPartialMatches is specified. Only allows modification of vertices, but not removal.
WeldEpsilonsFlags.DoNotSplit
Can only be used if WeldPartialMatches is specified. Does not allow the vertices to be split.
WeldEpsilons结构和之前简化mesh时使用的AttributeWeights结构是很相似的。唯一的区别就是多了一个用来控制几何缩放(tessellation)选项的成员。对mesh进行几何缩放,实际上就是通过删除一些三角形,或者把一个三角形细分为更多的三角形来改变模型的细节程度。除此之外,其他的成员都是相同的。
为了展示这个方法的效果,我们再一次修改已有的MeshFile文件来把合并顶点。因为需要添加的只有一个方法,所以并不需要添加太多的代码。我们通过按下任意键来触发合并操作,使用如下代码更新OnKeyPress方法:
protected override void OnKeyPress(KeyPressEventArgs e)
{
Console.WriteLine("Before: {0}", mesh.NumberVertices);
mesh.WeldVertices(WeldEpsilonsFlags.WeldAll, new WeldEpsilons(), null, null);
Console.WriteLine("After: {0}", mesh.NumberVertices);
}
现在运行程序,随便点击一个按键(注:通过这种方法简化的效果是不太明显的),最后可以看到以下的文本输出:
Before: 4432
After: 3422
虽然不能像之前的方法获得91%的简化效果,却要比之前的方法快许多。但是,你注意到按键时纹理坐标的变化没有?这是由于移除了那块区域的一部分顶点造成的。近距离的话,这个缺陷还是比较明显的,但远一点,就很难注意到了。
特别提示:检验mesh
又没有什么方法可以检查mesh是否需要clean,或者可以进行优化呢?答案是肯定的。Mesh类还有一个称为“Validate”的方法,他有四种重载,其中的一个如下:
public void Validate(GraphicsStream adjacency,out string errorsAndWarnings);
他同样也有一个支持整型数组作为邻接信息的重载,此外,你也可以选择是否使用错误信息。
如果mesh通过了检验,那么这个方法就会执行成功,同时,错误输出的文本就为System.String.Empty。如果mesh没有通过验证(比如索引无效),那么这个方法的行为就取决于你是否选择使用错误信息。如果使用了,那个这个方法会成功执行,并且用字符串返回错误信息。如果没有,那么它将会抛出异常。
在进行任何的简化和优化前先进行检验,可以避免许多调用这些方法时的失败。
细分Mesh
如果你不想从Mesh中移出顶点,而是需要把一个大的mesh分为许多块应该怎么做呢?好了,让我们再一次使用MeshFile文件作为基础,把一个大的mesh分成一些小的,更容易控制的mesh。当细分mesh的时候,需要把一个单一的mesh放到一个mesh数组中来渲染它。在我们所举的例子里,我们会同时保留着原mesh和细分之后的mesh,并且同时渲染他们,让你能有一个比较。
首先,声明一个mesh数组来保存细分的mesh。还需要一些bool变量来控制哪些mesh片需要渲染。添加如下代码:
private Mesh[] meshes = null;
private bool drawSplit = false;
private bool drawAllSplit = false;
private int index = 0;
private int lastIndexTick = System.Environment.TickCount;
这些变量将会保存着这个细分之后的mesh片的列表,同时也包含了绘制原mesh还是细分之后mesh的标志(默认绘制未细分过的mesh)。如果绘制细分的mesh,那么还有一个标志决定了绘制整个数组中的mesh还是一次绘制一部份,默认情况下一次绘制一部份。你还需要记录下当前所绘mesh的索引值,此外,还有一个迷你计时器来决定什么时候绘制下一个mesh片。
有了这些变量,就可以创建mesh数组了。在LoadMesh方法下面添加如下代码:
meshes = Mesh.Split(mesh, null, 1000, mesh.Options.Value);
来看一下split方法所接受的参数吧。它只用2种重载,我们就看一下比较复杂的一个吧:
public static Mesh[] Split( Mesh mesh,int[] adjacencyIn,int maxSize,MeshFlags options,out GraphicsStream adjacencyArrayOut,out GraphicsStream faceRemapArrayOut,
out GraphicsStream vertRemapArrayOut);
这个方法把需要细分的mesh作为第一个参数。接下来的是邻接信息(如果不关心邻接信息的话,可以使用null作为参数)。第三个参数是新创建的mesh的最大顶点数。我们的例子里,每个新mesh包含1000个顶点。Options参数用来指定新创建的mesh的标志。最后三个参数以数据流的形式返回新创建的mesh的信息。我们使用了不需要这三个参数的重载。
有了这个新创建的mesh数组,就需要指定哪一个mesh需要绘制了:原mesh或是mesh数组。在这之前,还需要一个开关来控制这个工作。还是使用空格和M键,更新OnKeyPress的中的代码:
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (e.KeyChar == ' ')
{
drawSplit = !drawSplit;
}
else if (e.KeyChar == 'm')
{
drawAllSplit = !drawAllSplit;
}
}
这里控制了绘制原mesh或是mesh数组,是绘制整个数组,还是其中一个元素。现在使用这些变量来更新DrawMesh中的方法。添加如下代码:
if ((System.Environment.TickCount - lastIndexTick) > 500)
{
index++;
if (index >= meshes.Length)
index = 0;
lastIndexTick = System.Environment.TickCount;
}
device.Transform.World = Matrix.RotationYawPitchRoll(yaw, pitch, roll) * Matrix.Translation(x, y, z);
for (int i = 0; i < meshMaterials.Length; i++)
{
device.Material = meshMaterials[i];
device.SetTexture(0, meshTextures[i]);
if (drawSplit)
{
if (drawAllSplit)
{
foreach(Mesh m in meshes)
m.DrawSubset(i);
}
else
{
meshes[index].DrawSubset(i);
}
}
else
{
mesh.DrawSubset(i);
}
}
在所添加的这段代码里,每半秒增加一次索引值,并且保证当达到数组最后一个元素时能循环。接下来,如果绘制的是细分mesh,还需要检查是全部绘制,还是绘制一部份。否则,就绘制原mesh。
好了,现在运行程序看看吧