【翻译】Managed DirectX(第七章)

** Using Advanced Mesh Features **


** 翻译:clayman **

** [email protected] **

** 仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^ **

这一章,我们将要调论一些关于 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)

{

</spa

Published At
Categories with Web编程
Tagged with
comments powered by Disqus