本文主要是介绍【翻译】Managed DirectX(第八章),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
第八章 理解资源(Understanding Resoruces)
翻译:clayman
clayman_joe@yahoo.com.cn
仅供个人学习之用,勿用于任何商业用途,转载请注明作者^_^
资源是在Direct3D中渲染复杂对象的重要部分。在这一章的学习中,我们将会讨论关于资源的一些高级特性,包括:
n 静态和动态的资源
n Updating the buffers included in meshes
n 使用各种锁定标志。
n 使用不安全代码最优化性能。
初识Resource类
Resource是一个很小的类,同时也是Direct3D中其他资源类的基类,因此,先把这个类所有方法及其用途列出来作为本章开始将是一个不错的主意。由于Resource是一个抽象类,你不
会直接创建或使用这个类,但你会在其他其他的资源类中使用到这些方法:
如你所见,resource类主要用来处理提高托管资源的性能。除了上表和Resource所列出的条目外,还需要注意Direct3D中的每个资源都还包括一个使用状态(Usage)和一个内存池(memorey pool)成员(定义在Usage枚举和Pool枚举中)。这几个属性定义了如何使用资源,以及资源存放在内存的什么区域(可以是系统内存,video memory,或者AGP memory)。
资源类通过一个方法,允许CPU直接访问他所储存的数据,通常我们把这种机制称为锁定(locking)。迄今为止所举的例子中,只使用过SetDate方法来填充缓冲,并且都是通过文件来创建纹理;但是,通过锁定这些资源,则可以完成恢复数据,或者只填充数据中的一小部分的任务。我们会在这一章详细讨论锁定。先来看看我们所介绍的第一种资源吧。
使用顶点和索引缓冲
顶点缓冲是Direct3D中储存顶点的主要数据结构,同样,索引缓冲也是储存索引的主要数据结构。这些类都源自Resource类,所以除了继承基类所有方法外,还添加了一些额外的方法。
第一个新属性就是对缓冲自身的描述。通过这个熟悉返回的结果可以告诉你关于缓冲是如何创建的所有信息,包括格式(format),使用状态,内存池,大小,以及顶点格式。虽然大多数情况下你是知道了这些信息才创建缓冲,但如果获得了来自外部资源的未知缓冲,那么这些信息还是很有用的。
特别提示:静态及动态缓冲
在讨论关于缓冲的其他方法前,最好先说说静态缓冲和动态缓冲的区别。这里所讨论的内容对顶点缓冲和索引缓冲都是可行的。
好了,这里第一个问题就是“静态缓冲和动态缓冲有什么区别呢?”好在我已经准备好了答案。除了使用Usage.Dynamic标志所创建的缓冲是动态的,其他所有缓冲都是静态。静态缓冲是为不经常改变的数据设计的,相反,对于经常改变的数据来说,使用动态缓冲则更方便。这就是两者名字的由来。
然而,静态缓冲并不意味着不能修改数据。但是,锁定一块已经在使用的静态缓冲时,会带来巨大的性能损失。在修改数据之前,GPU必须完成对当前这组数据的读取,这可是很费时的。如果同一帧做几次这样的操作,情况还会更遭,因为你阻止了驱动器所能做的缓冲,并且强迫GPU闲置下来等待你完成对数据的修改。如今的图形卡已经相当强大了,不应该把它浪费在等待数据上,而是要不停绘图。
如果数据时要经常改动的,就应该用Usage.Dynamic标志来创建缓冲。这个标志允许Direct3D为了经常性的修改数据而优化管道。另外,创建一个永远不会被修改的动态缓冲,并不会带来性能提升,事实上,它比简单创建一个静态缓冲还要慢。
总是应该根据每一帧在缓冲中所做的改动来创建缓冲。为了最优化性能,建议你为每一种需要渲染的顶点格式都创建一个大的静态缓冲。只有在静态缓冲不能满足需要的情况下再使用动态缓冲。
锁定缓冲
锁存机制也许是Direct3D中被误解最多的技术,特别是在Managed DirectX中。人们经常会问如何锁定以及如何高效的完成锁定。
那么,究竟什么是“锁定”缓存呢?它其实就是允许CPU直接访问资源中一定范围内数据的操作。你不能直接访问图形硬件,因此需要一种方法来控制程序中的顶点数据,而锁存就是用来完成这个任务的。来看看可用在顶点和索引缓冲上的众多lock方法吧。
public System.Array Lock ( System.Int32 offsetToLock ,Microsoft.DirectX.Direct3D.LockFlags flags )
public System.Array Lock ( System.Int32 offsetToLock , System.Type typeVertex , Microsoft.DirectX.Direct3D.LockFlags flags , params int[] ranks )
public Microsoft.DirectX.Direct3D.GraphicsStream Lock ( System.Int32 offsetToLock , System.Int32 sizeToLock , Microsoft.DirectX.Direct3D.LockFlags)
如你所见,有3种方法可用来锁定缓冲。我们先从最简单的第一个开始。这个方法对只对通过使用System.Type以及一系列顶点或索引的构造函数创建的缓冲有用。实际上,这个方法只是使用构造函数所传入的数据来再调用第二个重载的方法而已。
接下来的两个重载就比较有意思了。他们的第一个参数都表示开始锁定的偏移值(以比特为单位)。如果需要锁定整个缓冲,把这个值设置为0就可以了。你可能已经注意到前两个重载的方法都把数据以数组的方式作为返回值。在第二个重载中,第二个参数可以设置所返回的数组的类型。最后一个参数决定了返回数组的大小。
由于某些原因“Rank”参数总是困扰着开发者,让我们来仔细讨论一下吧。假设有一个只包含了位置数据(Vector3)的顶点缓冲,并且它保存了1200个顶点。如果你想把它锁定为一个包含1200个元素的Vector3数组,那么应该这样调用方法:
Vector3[] data = (Vector3[])vb.Lock(0, typeof(Vector3), LockFlags.None, 1200);
注意到额外的参数没有?Rank参数实际上是一个参数数组。它还可以创建三维数组作为返回值。你可以使用这个参数来指定返回多大的数组。
特别提示:如何高效的锁定缓冲
LockFlag应该和最后一个重载放到一起来讨论,但首先,我想指出前两个以数组作为返回值的方法的缺点。第一位,也是最重要的,我们应该讨论一下性能。假设你使用“default”选项创建了一个顶点缓冲,并且没有lock flag,当调用这个方法的时,将发生以下情况:
n 顶点数据被锁定;保存下数据的内存地址。
n 根据rank参数指定的大小,在内存中定位一个类型正确的新数组。
n 数据从被锁定的内存中复制到新的缓冲。
n 新的缓冲被返回给用户进行修改。
n 调用Unlock方法的时,新缓冲中的数据再次被复制回锁定的内存中。
n 最后,解锁顶点数据。
不难明白为什么这个方法要比你所预计的慢一些。通过设置可以减少一次复制:创建缓冲时指定Usage.WriteOnly参数(避免第一次复制),或者在锁定缓冲时使用LockFlags.ReadOly
标志(避免第二次复制); 但没有方法可以把两次复制都消除。对一块ReadOnly/WriteOnly的缓冲能干些什么呢?
最后一种重载是最强大的。同时,他也有一个其他重载没有的参数,指定了我们想要锁定的数据大小。在其他的重载中,这个值是通过调用(sizeof(type)*NumberRanks)方法计算出来的。如果使用这个方法,则只需要传入需要锁定的数据大小(以比特为单位)就可以了。如果如要锁定整个缓冲,那么把前两个参数设为0就可以了。
这个方法将返回一个GraphicsStream类来让你直接控制锁定的数据,而不需要为数组分配额外的内存,也不需要额外的内存复制。你可以对这快内存作任意的修改。GraphicsStream对象有很多方法允许你把数据“写入”这块内存,这并不会带来很大的速度提升。但你可以通过直接控制内存来获得速度提升。
当然,我并不是想告诉你返回数组的方法会慢很多。合适的使用这两个方法是很方便的,差不多和返回数据流的方法一样快。但是,如果你想把系统里每一盎司性能都炸干的话,就应该使用返回数据流的方法。
控制如何锁定缓冲
最后,我们来讨论一下锁定内存时可以使用的标志(flags)。锁定顶点缓冲时,只有以下标志是有效的:
· LockFlags.None
· LockFlags.Discard
· LockFlags.NoOverwrite
· LockFlags.NoDirtyUpdate
· LockFlags.NoSystemLock
· LockFlags.ReadOnly
显然,当不使用锁定标致时,就使用默认的锁定机制。但是,如果你需要对锁定进行更多控制,其他标志就可以完成多种选项。
Discard标志只能用于动态缓冲。对顶点缓冲和索引缓冲来说,整个缓冲都会被丢弃,另外返回一块新的内存,以防还有其他程序在访问旧的数据。这个标志在填充动态的顶点缓冲时特别有用。一旦完成了对顶点缓冲的填充,锁定就结束了。它通常和其他标志一起使用。
NoOverWrite标志(同样也只对动态缓冲有用)告诉Direct3D你不会复写顶点和索引缓冲中的任何数据。时用这个标志,会使调用立即返回,并且继续使用缓冲。如果不使用这个标志,那么锁定调用直到完成了当前所有渲染之后才会返回。既然你不会修改但前缓冲中的任何数据,它只在向缓冲中添加顶点时才有用。
这两个标志使用最多的情况,是在填充大块的动态缓冲时。但持续向缓冲填充数据的时候,保持使用NoOverwrite标志,直到填充完毕。接下来,使用Discard标志刷新缓冲,然后再次开始填充。现在来看看随DirectX SDK一起发布的实例Point Sprites:
if (++numParticlesToRender == flush)
{
// Done filling this chunk of the vertex buffer. Lets unlock and
// draw this portion so we can begin filling the next chunk.
vertexBuffer.Unlock();
dev.DrawPrimitives(PrimitiveType.PointList, baseParticle,
numParticlesToRender);
// Lock the next chunk of the vertex buffer. If we are at the
// end of the vertex buffer, LockFlags.Discard the vertex buffer and start
// at the beginning. Otherwise, specify LockFlags.NoOverWrite, so we can
// continue filling the VB while the previous chunk is drawing.
baseParticle += flush;
if (baseParticle >= discard)
baseParticle = 0;
vertices = (PointVertex[])vertexBuffer.Lock(baseParticle *DXHelp.GetTypeSize(typeof(PointVertex)), typeof(PointVertex),
(baseParticle != 0) ? LockFlags.NoOverwrite : LockFlags.Discard,flush);
count = 0;
numParticlesToRender = 0;
}
在这段代码中,我们检测是否是应该“刷新”数据了。如果需要,我们就解锁并且渲染。接下来,检查是否到达了缓存的最后(baseParticle>=discard),最后再次锁定缓冲。我们使用NoOverwrite标志,从缓冲的尾部开始锁定,假如baseParticle是0,就使用Discard标志,从缓冲头开始锁定。这样就可以不停把新的point sprite添加到场景中,直到缓冲满了,这时候,就丢弃旧的缓冲,使用一块新的。
讨论完了2个“复杂”的标志之后,来看看剩下的标志。其中最简单的就是ReadOnly标志了,正如它名字所示,它会告诉Direct3D你不会写入缓冲。当使用返还数组的重载方法时,它也表示在最后解锁缓冲时,不需要复制更新过的数据。
NoDirtyUpdate标志防止对任何处于“dirty”状态的资源进行修改。没有这个标志,锁定资源时会自动对资源添加一个脏区域(dirty region)。
最后一个标志不太常用。一般来说,当你锁定显存中的资源时,会保留一个系统级别的临界区,在使用这个标志的锁定期间,不允许改变任何显示模式。使用systemLock标志,可以取消这种效果。它只有在你预计锁定时间很长,并且还需要保证很快的系统速度时使用。通常不推荐使用这个选项。
自然,在调用了lock方法之后,你还必须在某个时刻告诉Direct3D完成了锁定。Unlock就是用来完成这个任务的。这个函数没有其他的参数,而且必须在每锁定之后都调用unlock方法。有任何为解锁的数据都将导致渲染失败。
使用纹理资源
Managed DirectX中所有的纹理资源都继承于BaseTexture类,而BaseTexture类又是从Resource类继承而来的。下面列出了BaseTexture对象中的几个新方法。
特别提示:Mipmaps详解
我们一直再说的mipmap究竟是什么呢?很简单,mipmap其实就是一系列的纹理,其中,第一个成员拥有最多的细节,接下来的纹理大小减少为上一张的一半。举个例子,假设有一张原大小为256×256的“高分辨率”纹理,那么下一级别的mipmap就是128×128的,再下来就是64×64的,以此类推。Direct3D就通过mipmap链来控制渲染时纹理的质量,当然,代价是mipmap将会消耗更多的内存。当对象离摄像机很近的时候,使用高质量的纹理,而物体远离摄像机时,就是用低分辨率的纹理。
当创建纹理时,可以使用参数来指定有多少个层次包含在纹理中。这里层次的数目直接与mipmap链相对应。把这个参数设置为0,Direct3D则会自动在mipmap链中创建一系列纹理,从原始纹理的大小开始,一直递减到大小为1×1。在我们所举的例子中,在一张256×256的纹理上把这个参数设为0,将会创建9个纹理:256×256、128×128、64×64,32×32、16×16,8×8,4×4、2×2、1×1。
当调用SetTexture方法时,Direct3D会根据当前sampler states类中MipFilter的属性值,自动在众多的mipmap中进行筛选。如果还记得第六章所写的Dodger游戏,你可能还记得我们为赛道的纹理设置了ninify和magnify过滤器。他和mipmap过滤器的内容是一样的。
锁定纹理、获得纹理描述
与早先讨论的顶点以及索引缓冲一样,纹理也都有锁存机制,同时还包括用来控制锁定特性的各种标志。对这些方法来说,纹理有两个锁定几何资源所不具有的特性:称为脏区域以及“backing”对象。在讲解这2个参数之前,先来看看纹理的锁存机制。
纹理的锁存机制与几何缓冲的锁存机制很类似,但有两个区别。第一个区别在于lock方法接受一个额外的“level”参数,用来指定锁定时使用的mipmap级别。第二个则是用来指定锁定区域的矩形(对于体积纹理volume textures来说则是立方体)。可以使用不含这个参数的重载来锁定整个纹理表面。
当处理立体纹理(cube textures)时,还需要一个额外的参数:所需要锁定的立方体的面。可以使用CubeMapFace枚举中的值做为这个参数。
你可能注意到了,LockRectangle方法的一些重载会返回一个pitch参数作为输出。如果你需要知道每一行中的总数据量,pitch参数就会提供给你。If you specify this parameter,it will return the number of bytes in a row of blows
虽然锁定纹理时可以指定需要锁定的数据类型,但是纹理通常都只包含颜色信息。使用需要接收数据类型的重载时,必须保证所指定的类型大小与纹理的象素格式一样。比如,32-bit的象素格式,就应该用一个32为的整数来锁定,而16-bit象素格式就应使用16位的整数来锁定。
现在来看一个锁定纹理的例子。使用第五章的MeshFile文件作为开始。我们将创建一个动态的纹理来替换当前渲染模型的纹理。我们希望这个纹理有两种模式,一种是颜色循环变化的纹理,而另一种则是颜色完全随机的纹理。添加以下变量的声明:
private uint texColor = 0;
private bool randomTextureColor = false;
使用32-bit的象素格式,同眼,把循环颜色也储存为32位。我们还要指定创建随机颜色的纹理,还是使用颜色循环纹理,因此使用一个布尔变量。现在来编写真正完成锁定和更新纹理的方法。为了展示使用数组和数据流作为返回值的方法之间的区别,我们还将使用不安全代码。使用返回值为数组的方法如下所示:
private void FillTexture(Texture t, bool random)
{
SurfaceDescription s = t.GetLevelDescription(0);
Random r = new Random();
uint[,] data = (uint[,])t.LockRectangle(typeof(uint), 0,
LockFlags.None, s.Width, s.Height);
for (int i = 0; i < s.Width; i++)
{
for (int j = 0; j < s.Height; j++)
{
if (random)
{
data[i,j] = (uint)Color.FromArgb(
r.Next(byte.MaxValue),
r.Next(byte.MaxValue),
r.Next(byte.MaxValue)).ToArgb();
}
else
{
data[i,j] = texColor++;
if (texColor >= 0x00ffffff)
texColor = 0;
}
}
}
t.UnlockRectangle(0);
}
这段代码干了些什么呢?通过当前纹理,获得对它自身的描述:纹理的宽度,以及数据的大小,这样才能确定需要填充多少数据。有了数据的长度和宽度之后,就可以把数据锁定为一个二维数组。把数据锁定为32位无符号整数(因为使用32—bit的象素格式),并且关心锁定标志。因为只创建了一个级别的mipmap,所以锁定0级别。
锁定纹理之后,迭代所返回数组中的每一个元素,更新颜色:随机选一个颜色,或者每次增加一定的颜色值(即我们的循环颜色)。在所有数据更新完成之后,解锁纹理就可以了。
那么,使用使用不安全代码的版本将是什么样子呢?事实上非常简单,代码如下:
private unsafe void FillTextureUnsafe(Texture t, bool random)
{
SurfaceDescription s = t.GetLevelDescription(0);
Random r = new Random();
uint* pData = (uint*)t.LockRectangle(0,
LockFlags.None).InternalData.ToPointer();
for (int i = 0; i < s.Width; i++)
{
for (int j = 0; j < s.Height; j++)
{
if (random)
{
*pData = (uint)Color.FromArgb(
r.Next(byte.MaxValue),
r.Next(byte.MaxValue),
r.Next(byte.MaxValue)).ToArgb();
}
else
{
*pData = texColor++;
if (texColor >= 0x00ffffff)
texColor = 0;
}
pData++;
}
}
t.UnlockRectangle(0);
}
注意到没有,主要的区别只在于对不安全代码的声明,以及更新数据的方式。我们把缓冲锁定为一个图形流,使用“InternalData”成员(它实际是一个InPtr)来获得所要修改数据的指针。我们通过指针来直接控制数据,最后,解锁纹理。
为了使这段代码通过编译,必须在工程的属性中设置允许编译不安全代码。
好了,现在有了可以把纹理填充为奇异颜色的方法,因该修改一下LoadMesh方法了,通过上面的函数来获得纹理,而不是通过一个文件。如下所示,修改LoadMesh中控制材质的代码:
// Store each material and texture
for (int i = 0; i < mtrl.Length; i++)
{
meshMaterials[i] = mtrl[i].Material3D;
meshTextures[i] = new Texture(device,256, 256, 1, 0, Format.X8R 8G 8B8 ,Pool.Managed);
#if (UNSAFE)
FillTextureUnsafe(meshTextures[i], randomTextureColor);
#else
FillTexture(meshTextures[i], randomTextureColor);
#endif
}
如你所见,我们不再通过文件来加载纹理。对数组中的每一个纹理,我们都使用X8R 8G 8B8的像素格式创建一个新纹理。创建256*256,并且只有一个级别的纹理。接下来用我们刚才的方法填充纹理。你可能注意到了,我把填充纹理的方法包含在一个#if语句块中。你可以通过声明这是不安全代码来使用这个方法。
使用我们所写的方法填充了纹理之后,可以允许程序来看看了,模型现在看起来很漂亮吧。但很快,他就会变的很“呆板”,并且纹理不会变色了。我们应该每一帧都调用填充纹理的方法。同时解决这种呆板的效果。修改SetupCamera方法:
(注意:再强调一下我先前说过的东西:注意到当按下任意键选择随机颜色时模型的旋转速度改变没有?这是由于旋转是基于帧速率改变的而不是系统计时器)
private void SetupCamera()
{
device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,
this.Width / this.Height, 1.0f , 10000.0f );
device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, 580.0f ),
new Vector3(), new Vector3(0,1,0));
device.RenderState.Lighting = false;
if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyAnisotropic)
device.SamplerState[0].MinFilter = TextureFilter.Anisotropic;
else if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyLinear)
device.SamplerState[0].MinFilter = TextureFilter.Linear;
if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyAnisotropic)
device.SamplerState[0].MagFilter = TextureFilter.Anisotropic;
else if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyLinear)
device.SamplerState[0].MagFilter = TextureFilter.Linear;
foreach(Texture t in meshTextures)
#if (UNSAFE)
FillTextureUnsafe(t, randomTextureColor);
#else
FillTexture(t, randomTextureColor);
#endif
}
现在,关闭了灯光来看看没有经过灯光着色的模型有多漂亮吧。同时我们还打开了一个纹理过滤器(如过系统支持)来改变纹理呆板的效果。最后要做的只剩下添加一个方法来打开“随机”纹理颜色。使用一个按键事件来选择纹理模式,添加如下代码:
protected override void OnKeyPress(KeyPressEventArgs e)
{
randomTextureColor = !randomTextureColor;
}
使用随机颜色产生了几乎是动画的效果,看其来有些像当电视没有信号时的雪花点效果。
这篇关于【翻译】Managed DirectX(第八章)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!