纹理过滤
Direct3D渲染一个图元时,会将三维图元映射到二维屏幕上。如果图元有纹理,Direct3D就必须用纹理来产生图元的二维渲染图象上每个像素的颜色。对于图元在二维屏幕上图象的每个像素来说,都必须从纹理中获得一个颜色值。我们把这一过程称为纹理过滤(texture filtering)。
进行纹理过滤时,正在使用的纹理通常也正在被进行放大或缩小。换句话说,这个纹理将被映射到一个比它大或小的图元的图象上。纹理的放大会导致许多像素被映射到同一个纹理像素上。那么结果看起来就会使矮矮胖胖的。纹理的缩小会导致一个像素被映射到许多纹理像素上。其结果将会变得模糊或发生变化。要解决这些问题,我们可以将一些纹理像素颜色融合到一个像素颜色上。
Direct3D提供了一些方法来简化纹理过滤的过程。它提供了三种类型的纹理过滤:线性过滤(linear filtering)、各向异性过滤(anisotropic filtering)和mipmap过滤(mipmap filtering)。如果不选择纹理过滤,Direct3D还会使用一种叫做最近点采样(nearest point sampling)的技术。
每种类型的纹理过滤都有各自的优缺点。例如,线性过滤会产生锯齿状的边缘和矮胖的效果。但是,它对系统的消耗却是最小的。另一方面,mipmap过滤的效果通常是最好的,特别是和各项异性过滤混合使用时。但是它却需要很大的内存消耗。
如果程序使用纹理句柄,可以调用IDirect3DDevice3::SetRenderState方法来设置当前的纹理过滤方法,同时要将第一个参数设置为D3DRENDERSTATE_TEXTUREMAG或D3DRENDERSTATE_TEXTUREMIN,第二个参数要设置为D3DTEXTUREFILTER枚举类型的一个成员。
程序也可以使用纹理接口指针来设置纹理过滤方法,这是要调用IDirect3DDevice3::SetTexture平台State方法,并将第一个参数设置为要进行纹理过滤的那个纹理的整数索引号(0-7),将第二个参数设置为D3DTSS_MAGFILTER、D3DTSS_MINFILTER或D3DTSS_MIPFILTER,将第三个参数设置为D3DTEXTUREMAGFILTER、D3DTEXTUREMINFILTER或D3DTEXTUREMIPFILTER枚举类型的一个成员。
下面我们来分别讨论几种纹理过滤方法:
4.1 最近点采样
4.2 线性纹理过滤
4.3 各向异性纹理过滤
4.4 mipmap纹理过滤
4.1 最近点采样
在程序中,我们并不是必须要使用纹理过滤。Direct3D可以被设置来计算纹理像素的地址,这个地址通常不是一个整数值,这时,可以简单的取一个与它最接近的整数地址来代替原地址,并使用这个整数地址上的纹理像素颜色。我们把这一过程称为最近点采样(nearest point sampling)。当纹理的大小与图元图象的大小差不多时,这种方法非常有效和快捷。如果大小不同,纹理就需要进行放大或缩小,这样,结果就会变得矮胖、变形或模糊。
调用IDirect3DDevice3::SetTexture平台State方法可以来选择最近点采样方法,将它的第一个参数设置为纹理的整数平台索引号(0-7),第二个参数设置为D3DTEXTUREMAGFILTER、D3DTEXTUREMIPFILTER或D3DTEXTUREMIPFILTER,将第三个参数设置为D3DTFG_POINT(设置放大过滤时)、D3DTFN_POINT(缩小时)或D3DTFP_POINT(mipmap时)。
使用最近点采样是要特别小心,因为在两个纹理像素的分界线上进行采样时,可能产生图象失真(graphic artifacts)。在进行采样时,系统会选择这个采样纹理像素或者是那个采样纹理像素,在穿越分界线时,得到的结果将会产生很大的变化,也许会得到我们不想要的结果。(在使用线性过滤时,当纹理索引穿越两个纹理像素的边界时,最终的纹理像素将会是这两个纹理像素的融合结果。)
当我们将一个很小的纹理映射到一个很大的多边形上——这被称为“放大(magnification)”——时,我们会看到这种效果。例如,当我们使用一个看起来象西洋跳棋盘一样的纹理时,最近顶采样会得到一个更大的西洋跳棋盘,并带有明显的边界;而采用线性过滤时,最后在多边形上,跳棋格颜色会平滑的进行过渡,不会有明显的边界出现。
大多数时候,不使用最近点采样往往能得到最好的结果。并且在现在,大部分的硬件都是针对线性过滤进行优化的。如果你确实想得到最近顶采样那样的效果——比如要清晰的显示一些文本特征——那么就要尽量避免在纹理像素分界处进行采样,否则,就会产生图象的失真。下图中向我们展示了一种图象的失真:
注意上面的两个靠右的方格,它们就产生了失真。要避免这样的失真,我们首先要了解Direct3D最近点采样的规则。Direct3D是将一个浮点纹理坐标,范围为[0.0, 1.0](包括它们),映射到一个整数的纹理像素空间,范围为[–0.5, n–0.5],n是一个给定大小的纹理中纹理像素的数量。最终的纹理索引是最近的整数。这样的映射会导致在纹理像素分界处进行采样。
下面我们来看一个例子,我们使用D3DTADDRESS_WRAP纹理寻址模式来对多边形进行渲染,使用Direct3D提供的映射对一个有4个纹理像素宽度的纹理进行映射,u纹理索引如下:
注意图中的纹理坐标0.0和1.0,它们正好在两个纹理像素的分界处。更具最近点采样法的规则,纹理坐标的范围为[–0.5, 4–0.5],4是纹理的宽度。这样,被采样的纹理像素就是第0个纹理像素,它的纹理索引值就为1.0。但是,如果纹理坐标只比1.0小一点,那么被采样的纹理像素就应该是第n个而不是第0个。
这也就表示在将一个小的纹理使用最近点采样方法放大映射到屏幕空间时,会产生在纹理像素分界处采样的情况,这样就会产生失真。
要将浮点纹理坐标映射到整数纹理像素上是很困难的,并且通常也没有必要这样做。大多数硬件在执行时使用一种迭代的方法来计算每个像素位置上的纹理坐标。这种方法往往会隐藏这些错误,因为它们在迭代过程中会逐渐积累起来。
Direct3D参考光栅使用直接估计(direct-evaluation)的方法来计算每个像素位置上的纹理索引。与迭代法不同,直接估计过程中的一些错误会比较自由的表现出来。这样,在使用最近点采样时出现的错误就会比较明显。
我们最好只在必需时再使用最近点采样法。在使用它时,最好能使纹理坐标明显的偏离纹理像素的分界处。
4.2 线性纹理过滤
Direct3D使用的线性过滤方法是双线性过滤(bilinear filtering)。和最近点采样一样,双线性过滤首先要计算一个纹理像素的地址,这个地址通常不是整数地址。然后,找到一个地址最接近的整数地址纹理像素。另外,Direct3D渲染模块还要计算与最近采样的点相邻的四个纹理像素的加权平均(weighted average)。
可以调用IDirect3DDevice3::SetTexture平台State方法来选择双线性纹理过滤,并将第一个参数设置为纹理的整数索引号(0-7),将第二个参数设置为D3DTEXTUREMAGFILTER、D3DTEXTUREMIPFILTER或D3DTEXTUREMIPFILTER,将第三个参数设置为D3DTFG_LINEAR(放大)、D3DTFN_LINEAR(缩小)或D3DTFP_LINEAR(mipmap)。
4.3 各向异性纹理过滤
各向异性是对一个三维物体纹理像素的可见的变形,这个物体的表面朝向屏幕平面,并与之有一定的角度。各向异性图元的像素在映射到纹理像素时,它的形状会发生变形。Direct3D用反映射到纹理空间的屏幕像素的延伸率(长度/宽度)来度量一个像素的各向异性(anisotropy)。
各向异性纹理过滤可以和线性过滤或mipmap过滤联合使用。调用IDirect3DDevice3::SetTexture平台State方法可以使各项异性过滤有效,同时要将第一个参数设置为纹理的整数索引值(0-7),将第二个参数设置为D3DTEXTUREMAGFILTER、D3DTEXTUREMINFILTER,将第三个参数设置为D3DTFG_ANISOTROPIC(放大)或D3DTFN_ANISOTROPIC(缩小)。
程序还要调用IDirect3DDevice3::SetTexture平台State方法将各向异性度(degree of anisotropy)设置在0到1之间(不包含它们),同时将诶一个参数设置为纹理的整数索引值(0-7),将第二个参数设置为D3DTSS_MAXANISOTROPY,最后一个参数就是各向同性度(degree of isotropy)。
将各向同性度(degree of isotropy)设置为1将使各向同性过滤无效(设置为任何比1大的值将会使它有效)。检查D3DPRIMCAPS结构中的D3DPRASTERCAPS_ANISOTROPY标志,以确定各向异性度的可能的范围。
4.4 Mipmap纹理过滤
Mipmap纹理技术用来降低场景渲染的时间消耗。同时也提高了场景的真实感。但它的缺点是要占用大量的内存空间。
下面我们来讨论mipmap纹理技术的有关内容:
4.4.1 什么是mipmap
4.4.2 创建一系列mipmap
4.4.3 选择和显示Mipmap
4.4.1 什么是mipmap?
一个mipmap就是一系列的纹理,每一幅纹理都与前一幅是相同的图样,但是分辨率都要比前一幅有所降低。mipmap中的每一幅或者每一级图象的高和宽都比前一级小二分之一。Mipmap并不一定必须是正方形。
高分辨率的mipmap图象用于接近观察者的物体。当物体逐渐远离观察者时,使用低分辨率的图象。Mipmap可以提高场景渲染的质量,但是它的内存消耗却很大。
Direct3D将mipmap描绘成一系列相互联系的表面。高分辨率的纹理位于开始处,并与下一级纹理相互联系。以此类推,纹理相互联系,逐渐排列到分辨率最小的一级。
下面这套插图显示了这样的一个例子。这套纹理是一个三维场景中一个集装箱的标签。当我们创建了一个mipmap时,分辨率最高的一幅纹理就是这一套纹理的第一个。这套mipmap中的每一个纹理宽高都是前一个纹理宽高的二分之一。这样,最大分辨率的纹理是256x256,接下来的纹理就是128x128,最后一个纹理就是64x64。
我们有一个能看到这个标签的最大距离。如果观察者从远处向标签走近,那么场景中首先会显示最小的一幅纹理,它的大小是64x64的。
当观察者走进标签时,我们就使用更高分辨率一幅纹理:
当观察者走到允许的最近距离时,我们使用分辨率最高的那幅纹理:
这是方法能够模拟纹理的透视效果并能够减少处理时的计算量。与将一幅纹理用于不同的分辨率相比,这种方法更加快速。
Direct3D能够访问mipmap中与我们想要输出的分辨率最接近的那个纹理设置,并将像素映射到它的纹理像素空间中。如果最终图象的分辨率在mipmap纹理的分辨率的中间,那么Direct3D会对两幅纹理中的纹理像素进行检查,并将它们的颜色值进行融合。
如果要使用mipmap,首先要创建一套mipmap。详细内容见“创建一系列mipmap”。如果程序使用的使纹理句柄,那么就必须将mipmap选择作为当前纹理。如果使用纹理接口指针,那么就要将mipmap选择作为当前纹理设置中的第一个纹理。详细内容见“多纹理融合”。
接下来,程序要设置用来对纹理像素采样的过滤方法。Mipmap过滤最快的方法就是让Direct3D选择最近的纹理像素。我们用D3DTFP_POINT枚举值来选择这一方法。如果程序使用D3DTFP_LINEAR枚举值,可以得到更好的过滤效果。它会选择最近的mipmap,然后找到当前像素映射在纹理中的位置,计算这个位置周围纹理像素的加权平均。
4.4.2 创建一系列Mipmap
要创建一个表示mipmap中某一级的表面,需要在DDSURFACEDESC结构中声明DDSCAPS_MIPMAP和DDSCAPS_COMPLEX标志,并将这个结构传递给IDirectDraw4::CreateSurface方法。由于所有的mipmap也是纹理,因此也要声明DDSCAPS_TEXTURE标志。
我们也可以自己来创建mipmap中的每一级,然后使用IDirectDrawSurface4::AddAttachedSurface方法将它们链接在一起。但是我们并不推荐使用这样的方法。许多3-D硬件都对IDirectDraw4::CreateSurface方法优化了它们的驱动程序。因此,通过调用IDirectDrawSurface4::AddAttachedSurface来建立mipmap链接的程序可能会发现mipmapping没有想象中的那么快捷。
下面的例子向我们展示了如何使用IDirectDraw4::CreateSurface方法来创建一个mipmap链接,这个mipmap分为5级,大小分别为256×256、128×128、64×64、32×32和16×16:
// This code fragment assumes that the variable lpDD is a
// valid pointer to a DirectDraw interface.
DDSURFACEDESC ddsd;
LPDIRECTDRAWSURFACE4 lpDDMipMap;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_MIPMAPCOUNT;
ddsd.dwMipMapCount = 5;
ddsd.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_MIPMAP | DDSCAPS_COMPLEX;
ddsd.dwWidth = 256UL;
ddsd.dwHeight = 256UL;
ddres = lpDD->CreateSurface(&ddsd, &lpDDMipMap);
if (FAILED(ddres))
…… ……
在用IDirectDraw4::CreateSurface方法创建一系列表面时,我们可以忽略mipmap的级数,这样每一级都是前一级大小的二分之一,直到最小的尺寸大小为止。我们也可以忽略高和宽,这样IDirectDraw4::CreateSurface就会创建我们所声明的级数,并将最小一级的大小设为1×1。
注:一个mipmap链接中的每一个表面的大小都是链接中前一个表面大小的二分之一。如果一mipmap中最顶端一级的大小为256×128,那么第二级的大小就是128×64,第三级为64×32,直到2×1为止。如果你声明了dwWidth和dwHeight成员的大小,就要注意一些限制条件。也就是要注意,在dwMipMapCount中声明的级数大小不能使任何一级mipmap的高或宽的值小于1。我们来看一个简单的最顶端一级大小为4×2的mipmap表面:dwMipMapCount所允许的最大值为2。任何大于2的值都会使高或宽变成小数,这是不允许的。
创建了mipmap表面之后,需要将表面与一个纹理相互联系起来。如果使用纹理句柄,就可以使用前面在“创建一个纹理句柄”中介绍的方法。如果使用的是纹理接口指针,请看“获得一个纹理接口指针”部分。
4.4.3 选择并显示Mipmap
如果程序使用纹理句柄,就要将mipmap纹理的句柄指派为当前纹理。详细内容见“当前纹理”部分。
如果程序使用纹理接口指针,就要将mipmap纹理设置作为当前纹理列表的第一个纹理。详细内容见“多纹理融合”部分。
程序选择了mipmap纹理设置之后,it must assign values from the D3DTEXTUREFILTER enumerated type to the D3DRENDERSTATE_TEXTUREMAG and D3DRENDERSTATE_TEXTUREMIN render states.然后,Direct3D会自动执行mipmap纹理过滤。
程序也可以自己来设置mipmap表面的链接,这是要使用IDirectDrawSurface4::GetAttachedSurface方法,并要在DDSCAPS结构中声明DDSCAPS_MIPMAP和DDSCAPS_TEXTURE标志。下面的例子中展示了这一过程:
LPDIRECTDRAWSURFACE lpDDLevel, lpDDNextLevel;
DDSCAPS ddsCaps;
HRESULT ddres;
lpDDLevel = lpDDMipMap;
lpDDLevel->AddRef();
ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_MIPMAP;
ddres = DD_OK;
while (ddres == DD_OK)
{
// Process this level.
ddres = lpDDLevel->GetAttachedSurface( &ddsCaps, &lpDDNextLevel);
lpDDLevel->Release();
lpDDLevel = lpDDNextLevel;
}
if ((ddres != DD_OK) && (ddres != DDERR_NOTFOUND))
{
// Code to handle the error goes here
}
程序还需要自己实现一个mipmap链接来将位图数据加载到链接中的每一个表面。
Direct3D会明确保存一个mipmap链接中的级数。当程序获得一个mipmap的表面描述时(调用IDirectDrawSurface4::Lock或IDirectDrawSurface4::GetSurfaceDesc方法),DDSURFACEDESC结构的dwMipMapCount成员就获得了mipmap的级数,包括最顶端一级。对于mipmap中的那些不是最顶端的级来说,dwMipMapCount成员详细说明了链接中的级数。