深入Managed DirectX9(十一)
Using Advanced Mesh Features
这一章,我们将要调论一些关于Mesh对象的高级特性,包括:
优化(Optimizing)mesh数据
简化(Simplifying)mesh
使用新的顶点数据元素创建mesh
连接(Welding)顶点
克隆Mesh数据
终于,你可以在场景里加载并渲染mesh了。虽然场景里只有少量的灯光,而且mesh看起来基本是黑的。观察一下mesh的属性,注意到顶点格式并没有包含计算光照必须的法线数据。我们需要一个简单的方法为现有的顶点数据添加法线信息。如果你猜想DirectX已经有这样的一个转换机制,那么恭喜,你猜对了。观察以下代码:
//Check if mesh doesn’t include normal data
If((mesh.VertexFormat & VertexFormats.Normal) != VertexFormatslNormal)
{
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。
minvalue参数是你希望mesh中的面或顶点(根据你所传的标志)所简化到的最小值。这个值越小,最后的mesh细节也越少;但是,需要注意的是即使这个方法正确执行了,也不一定能到达所要求的最小值。这个参数应该是一个期望最小值,而不是绝对的最小值。
这个方法的最后一个参数只能是两个值之中的一个。如果需要简化顶点,就使用MeshFlags.SimplifyVertex,否则,需要简化索引,则使用MeshFlags.SimplifyFace。
~~~~~~~~~~~~~~~~~未完待续~~~~~~~~~~~~~~~~~~~~
这一章,我们将要调论一些关于Mesh对象的高级特性,包括:
优化(Optimizing)mesh数据
简化(Simplifying)mesh
使用新的顶点数据元素创建mesh
连接(Welding)顶点
克隆Mesh数据
终于,你可以在场景里加载并渲染mesh了。虽然场景里只有少量的灯光,而且mesh看起来基本是黑的。观察一下mesh的属性,注意到顶点格式并没有包含计算光照必须的法线数据。我们需要一个简单的方法为现有的顶点数据添加法线信息。如果你猜想DirectX已经有这样的一个转换机制,那么恭喜,你猜对了。观察以下代码:
//Check if mesh doesn’t include normal data
If((mesh.VertexFormat & VertexFormats.Normal) != VertexFormatslNormal)
{
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。
minvalue参数是你希望mesh中的面或顶点(根据你所传的标志)所简化到的最小值。这个值越小,最后的mesh细节也越少;但是,需要注意的是即使这个方法正确执行了,也不一定能到达所要求的最小值。这个参数应该是一个期望最小值,而不是绝对的最小值。
这个方法的最后一个参数只能是两个值之中的一个。如果需要简化顶点,就使用MeshFlags.SimplifyVertex,否则,需要简化索引,则使用MeshFlags.SimplifyFace。
~~~~~~~~~~~~~~~~~未完待续~~~~~~~~~~~~~~~~~~~~