深入Managed DirectX9(十二)

(接深入Managed DirectX9(十一))

  现在可以正式添加代码来优化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。

好了,现在运行程序看看吧

~~~~~~~~~~~~~~~~~第七章完~~~~~~~~~~~~~~~~~~

这一章作者的源代码SimplifyMesh文件有些小小的问题,使用他所提供的tiny.x作为mesh执行 Simplify()方法会抛出异常。附件里保留了作者的3个源文件,同时也包括了我修改之后可以运行的文件。为了减小附件的大小 ,大家需要自己吧2个资源文件复制到程序文件夹中^_^

档案下载

转载于:https://www.cnblogs.com/yurow/articles/951618.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值