纹理
纹理是增强计算机生成的三维图像的真实感的有力工具。Microsoft? Direct3D?支持广泛的纹理特性,并使开发人员可以很方便地使用高级纹理技术。
本节讲述如何使用纹理。
以下主题将更详细地介绍另外的纹理功能。
要提高性能,可以考虑使用动态纹理。动态纹理在每一帧都可以被锁定,写入及解锁。更多信息请参阅使用动态纹理。
纹理的基本概念
早期计算机生成的三维图像看起来往往像是发亮的塑料,虽然这在当时也是比较先进的,但是它们缺乏各种纹路——如磨损、裂痕、指纹和污渍等,而这些纹路会增加三维物体的真实感。近年来,纹理已经在开发人员中得到普及并作为增强计算机生成的三维图像的真实感的工具。
词语“纹理”在日常使用中表示物体的光滑度或粗糙度,但是在计算机图形学中,纹理指的是一张表示物体表面细节的位图。
因为Direct3D中所有纹理都是位图,所以可以把任何位图贴到Direct3D图元的表面。例如,应用程序可以创建物体并使它们的表面看起来有木纹的样式。可以把草、泥土和岩石等纹理贴在构成山的图元的表面,这样就能得到看起来很真实的山坡。应用程序也可以用纹理创建其它的效果,如:路边的路标,悬崖边的岩层,或是地面上的大理石。
另外,Direct3D支持更高级的纹理技术,如纹理混合(包含或不含透明度)和光照贴图。更多信息请参阅纹理混合和用纹理实现光照贴图。
如果应用程序创建一个HAL设备或软件设备,那么可以使用8、16、24或是32位纹理。
以下主题包含了更多的信息。
Microsoft? Direct3D?应用程序可以把纹理坐标值赋给任何图元的任何顶点。更多细节,请参阅纹理坐标。一般来说,应用程序赋给顶点的u、v纹理坐标值在0.0到1.0范围内,闭区间。但是,通过把纹理坐标值赋为此范围外的值,应用程序可以创建某些特殊纹理效果。
通过设置纹理寻址模式,应用程序可以控制当纹理坐标位于范围[0.0, 1.0]外时希望Direct3D执行何种操作。例如,应用程序可以设置寻址模式,使纹理平铺于图元表面。下面的主题包含了更多的细节。
Direct3D使应用程序可以进行纹理环绕,很重要的一点是要注意把纹理寻址模式设为D3DTADDRESS_WRAP与进行纹理环绕并不相同。把纹理寻址模式设为D3DTADDRESS_WRAP会使源纹理的多个复本被贴到当前图元的表面,而启用纹理环绕则会改变系统对贴有纹理的多边形进行光栅化的方式。更多细节,请参阅纹理环绕。
启用纹理环绕实际上使位于[0.0, 1.0]范围之外的纹理坐标无效,在这种情况下,对无效的纹理坐标进行光栅化操作将导致未定义的结果。当启用纹理环绕时,不会使用纹理寻址模式,同时应用程序应该注意不要给出小于0.0或大于1.0的纹理坐标。
应用程序可以通过调用IDirect3DDevice9::SetSamplerState方法设置每个纹理层的纹理寻址模式,只需把纹理层的标识作为第一个参数,并把第二个参数设置为D3DSAMP_ADDRESSU,D3DSAMP_ADDRESSV或D3DSAMP_ADDRESSW,就可以分别改变u,v或w寻址模式。IDirect3DDevice9::SetSamplerState方法的第三个参数决定要设置的模式,这是一个D3DTEXTUREADDRESS枚举类型值。要取得某一纹理层当前的纹理寻址模式,只需调用IDirect3DDevice9::GetSamplerState方法,把第二个参数设置为D3DTEXTURESTAGESTATETYPE枚举类型值,并把第三个参数设置为用于保存返回的寻址模式的变量的地址。
虽然系统允许纹理坐标位于闭区间0.0到1.0之外,但硬件限制通常会决定纹理坐标究竟可以超出该范围多少。当应用程序取得设备能力时,渲染设备通过D3DCAPS9结构的MaxTextureRepeat成员表明这个限制,这个成员的值描述了设备允许的纹理坐标的全部范围。例如,若这个值为128,则输入的纹理坐标必须保持在从-128.0到+128.0之间,若输入顶点的纹理坐标位于这个范围外,则纹理坐标是无效的。对自动生成的纹理坐标和由纹理坐标变换得到的纹理坐标也有同样的限制。
对MaxTextureRepeat的解释同时还受D3DPTEXTURECAPS_TEXREPEATNOTSCALEDBYSIZE能力位的影响。若设置了这个能力位,则D3DCAPS9结构的MaxTextureRepeat成员的值正如前面描述的一样。但是,若没有设置该能力位,则纹理循环的限制还取决于纹理坐标寻址的那个纹理的大小。在这种情况下,MaxTextureRepeat必须除以当前mipmap中最大纹理的大小。例如,若纹理大小为32,而MaxTextureRepeat的值为512,则实际有效的纹理坐标的范围是512/32 = 16,因此传给该设备的纹理坐标必须在范围-16.0到+16.0之间。
以下主题包含了更多有关纹理寻址的信息:
环绕纹理寻址模式
环绕纹理寻址模式由D3DTEXTUREADDRESS枚举类型的D3DTADDRESS_WRAP成员表示,它会使Microsoft? Direct3D?在纹理坐标的整数边界重复使用该纹理。例如,设想应用程序创建了一个方的图元并把纹理坐标指定为(0.0,0.0), (0.0,3.0), (3.0,3.0)和(3.0,0.0),把纹理寻址模式设置为D3DTADDRESS_WRAP会使纹理在u和v方向都重复三次。
这种纹理寻址模式的效果与镜像纹理寻址模式有些相似,却又明显不同。更多信息,请参阅镜像纹理寻址模式。
镜像纹理寻址模式
镜像纹理寻址模式由D3DTEXTUREADDRESS枚举类型的D3DTADDRESS_MIRROR成员表示,它会使Microsoft? Direct3D?在纹理坐标的整数边界先对纹理进行镜像然后再重复使用。例如,设想应用程序创建了一个方的图元并把纹理坐标指定为(0.0,0.0), (0.0,3.0), (3.0,3.0)和(3.0,0.0),把纹理寻址模式设置为D3DTADDRESS_MIRROR会使纹理在u和v方向都重复三次,每一行和每一列的纹理都是相邻行和列的纹理的镜像。
这种纹理寻址模式的效果与环绕纹理寻址模式有些相似,却又明显不同。更多信息,请参阅环绕纹理寻址模式。
截取纹理寻址模式
截取纹理寻址模式由D3DTEXTUREADDRESS枚举类型的D3DTADDRESS_CLAMP成员表示,它会使Microsoft? Direct3D?把纹理坐标截取到[0.0, 1.0]范围内,也就是说,这种模式只应用纹理一次,然后就重复使用纹理边缘处像素的颜色。例如,设想应用程序创建了一个方的图元并把纹理坐标指定为(0.0,0.0), (0.0,3.0), (3.0,3.0)和(3.0,0.0),把纹理寻址模式设置为D3DTADDRESS_CLAMP会使纹理只被应用一次,列的顶端和行的末端处像素的颜色被相应地延伸至图元的顶端和右边。
下图描绘了截取寻址模式。
边框颜色纹理寻址模式
边框颜色纹理寻址模式由D3DTEXTUREADDRESS枚举类型的D3DTADDRESS_BORDER成员表示,该寻址模式会使Microsoft? Direct3D?对于位于[0.0, 1.0]范围之外的纹理坐标使用一个被称为边框颜色的指定颜色。
下图描绘了边框颜色纹理寻址模式,这里应用程序指定红色为纹理的边框颜色。
应用程序可以通过调用IDirect3DDevice9::SetSamplerState方法设置边框颜色。在调用时要把第一个参数设为想要设置的纹理层的标识,把第二个参数设为D3DSAMP_BORDERCOLOR纹理层状态值,并把第三个参数设为以RGBA形式表示的新的边框颜色。
无效纹理区域
通过给纹理指定无效区域,应用程序可以对需要复制纹理的哪些子集进行优化,只有那些被标记为无效的区域才会被IDirect3DDevice9::UpdateTexture方法更新。当创建纹理时,整个纹理被标记为无效的。只有以下几种操作可以改变纹理的无效状态。
对于mipmap纹理而言,无效区域被设在最高一级的纹理上,为了最小化对mipmap纹理中每一级的纹理更新所需复制的字节数,IDirect3DDevice9::UpdateTexture方法可以扩展无效区域并沿mipmap链更新子纹理。注意子级中无效区域的纹理坐标被向上舍入,也就是说,它们的小数部分被向上取整到纹理中最近的像素。
因为每种类型的纹理具有不同类型的无效区域,所以每种类型的纹理都有相应的方法表示无效区域。二维纹理使用矩形,立体纹理使用立方体。
把以上方法的pDirtyRect或pDirtyBox参数设置为NULL会扩大无效区域并使之覆盖整个纹理。
每种锁定方法都有D3DLOCK_NO_DIRTY_UPDATE标志,使用这个标志可以防止对纹理无效区域的改变。更多信息,请参阅锁定资源。
如果在锁定操作时可以得到已改变区域的完整集合,那么应用程序应该使用D3DLOCK_NO_DIRTY_UPDATE标志。注意,对纹理一个的子级的锁定或复制操作(也就是说,未对纹理的最高一级进行锁定或复制操作)不会更新该纹理的无效区域。当应用程序锁定了纹理的子级而没有锁定纹理的最高一级时,它同样有责任对无效区域进行更新。
纹理调色板
Microsoft DirectX? 9.0中的Microsoft? Direct3D?通过一组与IDirect3DDevice9对象相关联的256色调色板支持调色板纹理(paletted texture)。通过调用IDirect3DDevice9::SetCurrentTexturePalette方法可以设置当前调色板。当前调色板用于对所有已激活的纹理层中的所有调色板纹理进行颜色转换。IDirect3DDevice9::SetPaletteEntries方法可以更新调色板中的全部256个颜色项。每个颜色项都是一个用D3DFMT_A8R8G8B8格式表示的PALETTEENTRY结构,默认值为0xFFFFFFFF。
IDirect3DDevice9的调色板包含了一个阿尔法通道。若设备设置了D3DPTEXTURECAPS_ALPHAPALETTE能力位,则表示该设备支持调色板阿尔法,并可以使用该阿尔法通道。当纹理格式不含阿尔法通道时,就使用调色板阿尔法通道。若设备不支持调色板阿尔法,同时纹理格式也不含阿尔法通道,则使用0xFF作为阿尔法值。
系统中最多可以有65,536个调色板。因为调色板占用的内存资源与应用程序引用到的最大的调色板编号成正比,所以最好使用从零开始且连续的编号。
纹理坐标
大多数纹理,如位图,都是一个存放颜色值的二维数组,但立方体环境贴图除外,具体细节请参阅立方体环境贴图。数组中的每个颜色值被称为texel。每个texel在纹理中有唯一的地址,可以认为这个地址是行和列的编号,它们分别被标记为u和v。
纹理坐标位于纹理空间中,也就是说,它们相对于纹理中的位置(0,0)点。当把纹理贴到三维空间中图元的表面时,纹理的texel必须先被映射到对象坐标系,然后再变换到屏幕坐标系,或像素的位置。
将Texel映射到屏幕空间
Microsoft? Direct3D?直接把纹理中的texel映射到屏幕空间,这样就省略了中间步骤并极大地提高了效率。这个映射的过程实际上是一个反向映射,也就是说,系统根据每个像素在屏幕空间中的位置计算该像素在纹理空间中相应的texel的位置,然后对位于该点或该点附近的纹理颜色进行取样。取样的过程被称为纹理过滤。更多信息请参阅纹理过滤。
纹理中每个texel的位置可以用它的texel坐标表示。但是为了把texel贴到图元表面,Direct3D需要所有的纹理中的texel具有相同的地址范围,所以Direct3D使用了一种通用的寻址方法。在这种寻址方法中,所有texel的地址都在闭区间0.0到1.0内。Direct3D用u,v值表示纹理坐标,这和用x,y坐标表示二维笛卡尔坐标系非常相似。从技术上讲,系统事实上可以处理0.0到1.0范围外的纹理坐标,系统根据应用程序设置的纹理寻址模式来进行此类处理。更多信息,请参阅纹理寻址模式。
采用这种方法的结果是相同的纹理地址在不同的纹理中会映射到不同的texel坐标。在下图中,正在使用的纹理地址是(0.5,1.0)。但是,因为纹理的大小不同,所以该纹理地址映射到不同的texel。左边纹理的大小为5x5,纹理地址(0.5,1.0)映射到texel (2,4)。右边纹理的大小为7x7,纹理地址(0.5,1.0)映射到texel (3,6)。
下图描述了一个经简化的texel映射过程。这个示例显然非常简单,要了解更多细节信息,请参阅直接把Texel映射到像素。
本例中,图的左边显示了一个被理想化为正方形色块的像素。像素四角的地址被映射到对象空间中的三维图元,因为三维场景中图元的形状及观察角度的不同,像素的形状常常会被扭曲。像素四角所对应图元表面的区域然后被映射到纹理空间,这个映射过程会再次扭曲像素的形状。像素的最终颜色根据像素映射的区域所覆盖的texel计算得到。通过设置纹理过滤方法,应用程序可以控制让Direct3D使用何种方法计算像素颜色。更多信息,请参阅纹理过滤。
应用程序可以直接给顶点指定纹理坐标,这使应用程序可以控制把纹理的哪些部分贴到图元上。例如,设想应用程序创建了一个与下面的纹理大小完全相同的图元,本例中,如果应用程序希望把整张纹理贴到整面墙上,那么应用程序给图元的顶点指定的纹理坐标就应该是(0.0,0.0), (1.0,0.0), (1.0,1.0), 和 (0.0,1.0)。
如果应用程序决定把墙的高度减半,应用程序仍可以把整张贴图贴到稍小的墙上,但这会挤压纹理并使之扭曲,或者应用程序也可以重新指定纹理坐标,使Direct3D使用纹理的下半部分。
如果应用程序为了把纹理贴到稍小的墙上而决定挤压或拉伸纹理,那么应用程序使用的纹理过滤方法会影响最终图像的质量。更多信息,请参阅纹理过滤。
如果应用程序决定重新指定纹理坐标并使Direct3D使用纹理的下半部分,那么在本例中应用程序给图元的顶点指定的纹理坐标应该是(0.0,0.5), (1.0,0.5), (1.0,1.0), 和 (0.0,1.0),这样Direct3D就会把纹理的下半部分贴到墙上。
顶点的纹理坐标有可能大于1.0,如果应用程序给顶点指定的纹理坐标不在闭区间0.0到1.0范围内,那么应用程序还应该设置纹理寻址模式。更多信息,请参阅纹理寻址模式。
纹理坐标和纹理层
纹理坐标通过纹理层与纹理联系在一起。纹理通过SetTexture(stageIndex, pTextre) 被设定到某一纹理层。请参阅IDirect3DDevice9::SetTexture。
一个弹性顶点格式码最多可以定义八组纹理坐标,纹理坐标数据由用户在顶点数据中提供,数据通过索引值0到7来引用。最多可以有八个纹理混合层,一张纹理通过SetTexture(stageIndex, pTexture)与某一纹理层联系在一起。
完成以上操作后,任意一组纹理坐标可以被任意一纹理层使用。每一组纹理坐标通过SetTextureStageState(stageIndex, D3DTSS_TEXCOORDINDEX, textureCoordinateIndex)与某一纹理层联系在一起。请参阅IDirect3DDevice9::SetTextureStageState。通过这种方法,可以设置纹理混合层使它们使用任意一张纹理和任意一组纹理坐标。多个纹理层可以使用同一张纹理,或同一组纹理坐标。
以下主题包含了更多的信息。
直接把Texel映射到像素
应用程序经常需要把纹理贴到几何体上并使texel直接映射到屏幕上的像素。例如,以一个需要在纹理中显示文本的应用程序为例,为了使纹理中的文本能清晰地显示,应用程序需要以某种方式确保映射到几何体上的texel不受纹理过滤的影响。如果无法保证这一点,那么得到的图像通常会是模糊的,如果纹理过滤方法为最近点取样,那么可能会产生粗糙边缘。
为了统一像素和纹理取样,并同时支持图像和纹理过滤,Microsoft? Direct3D?的像素和纹理取样规则经过了精心定义,但这也使得把纹理中的texel直接映射到屏幕上的像素成为一个相当有意义却又艰难的挑战。要战胜这个挑战,需要透彻地理解Direct3D如何把用浮点数表示的纹理坐标映射到光栅化器使用的整数像素坐标。
为了把用浮点数表示的纹理坐标映射到texel地址,Direct3D执行下面的计算。
在这些公式中,Tx和Ty为水平/垂直方向的输出texel坐标,u和v为顶点提供的水平/垂直方向的纹理坐标。Mx和My元素表示当前mipmap级水平/垂直方向的texel的数量。本节剩余部分将主要讨论在水平方向上从texel到像素的映射,垂直方向上的映射与水平方向完全相同。
把纹理坐标0.0和1.0代入以上公式,会使纹理坐标0.0映射到本次纹理迭代的第一个texel和上次纹理迭代的最后一个texel的中间,纹理坐标1.0会被映射到本次纹理迭代的最后一个texel和下次纹理迭代的第一个texel的中间。对于一个宽度为4的迭代纹理,在mipmap的第0级,下图显示了系统把坐标0.0和1.0映射到哪里。
理解了这种映射方式,应用程序只需给几何体的屏幕空间坐标加上一个偏移量,就可以强制系统把每个texel映射到相应的像素。例如,要绘制一个四边形并使前面的纹理中的每个texel一一映射到屏幕上唯一的像素,应用程序必须使几何体坐标覆盖所有像素,并使每个texel的中心正好对应每个像素的中心,这样得到的结果就是应用程序常要追求的一对一映射。
为了把宽度为4的纹理映射到像素坐标0到3,可以用两个三角形绘制一个四边形,三角形在屏幕空间的坐标为-0.5到3.5,纹理坐标为0.0到1.0。以屏幕空间坐标为0.0的像素为例,因为0.0与第一个顶点,位于屏幕空间-0.5,相距半个像素,而总的宽度为4.0,迭代后的纹理坐标为0.125(译注:每个像素的宽度为0.25=1/4,半个像素的宽度为0.125),然后用纹理的大小,此处为4,对纹理坐标进行缩放,得到的结果坐标为0.5。再减去偏移量0.5即得到纹理地址为0.0,该地址完全对应于贴图中的第一个texel。
再概括一下,纹理坐标覆盖纹理贴图的两边,并平均分布在它们之间。下图显示了这种映射,纹理的宽度为4。
系统以相同的方法归一化像素坐标和纹理坐标,因此,如果顶点与要渲染的像素重叠,且顶点使用的纹理坐标为0.0和1.0,那么像素和顶点就可以对齐。如果它们的大小相同且排列整齐,那texel和像素就可以完全对应,如下图所示。
纹理坐标的格式
Microsoft? Direct3D?中的纹理坐标可以包含一个、两个、三个或四个用浮点数表示的元素,用来寻址不同大小的纹理。一维纹理——纹理表面的大小为1xn个texel——通过一个纹理坐标寻址。最常见的情况是二维纹理,通过两个被称为u和v的纹理坐标寻址。Direct3D支持两种三维纹理,立方体环境贴图和立体贴图。立方体环境贴图不是真正三维的,但使用一个三元素向量寻址。更多细节,请参阅立方体环境贴图。
如顶点格式中所述,应用程序把纹理坐标编码在顶点格式中。顶点格式可以包含多组纹理坐标。可以用FVF码D3DFVF_TEX0到D3DFVF_TEX8描述不包含纹理坐标或最多可包含八组纹理坐标的顶点格式。
每组纹理坐标可以有一到四个元素。D3DFVF_TEXTUREFORMAT1到D3DFVF_TEXTUREFORMAT4标志用来描述一组纹理坐标中元素的个数,但这些标志不能直接使用,D3DFVF_TEXCOORDSIZEn系列宏使用这些标志创建位掩码,用来在顶点格式中描述某组纹理坐标中元素的个数。这些宏带一个参数,表示要设置元素个数的那组纹理坐标的索引值。以下示例代码显示了如何使用这些宏。
// 这个顶点格式包含两组纹理坐标。第一组(索引为0)包含2个元素,
// 第二组包含一个元素。顶点格式描述应该是:
// dwFVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX2 |
// D3DFVF_TEXCOORDSIZE2(0) | D3DFVF_TEXCOORDSIZE1(1);
//
typedef struct CVF
{
D3DVECTOR position;
D3DVECTOR normal;
D3DCOLOR diffuse;
float u, v; // 第一组纹理坐标,二维
float t; // 第二组纹理坐标,一维
} CustomVertexFormat;
注意 除了立方体贴图和立体贴图外,光栅化器无法使用两个以上的元素寻址纹理。应用程序最多可以提供三个元素给一组纹理坐标,但只有当纹理是立方体贴图或立体贴图,或使用了D3DTTFF_PROJECTED纹理变换标志时多余的元素才会被用到。D3DTTFF_PROJECTED标志会使光栅化器把前两个元素除以第三个(或第n个)元素。更多信息,请参阅纹理坐标变换。
纹理坐标的处理
下图显示了纹理坐标从数据源,经过处理然后到达光栅化器的流程。
系统可以从两个数据源得到纹理坐标。对于某一层纹理,应用程序可以使用包含在顶点格式(D3DFVF_TEX1到D3DFVF_TEX8)中的纹理坐标,也可以使用Microsoft? Direct3D?自动生成的纹理坐标。对于后一种情况,请参阅自动生成的纹理坐标。如果当前纹理层的D3DTSS_TEXTURETRANSFORMFLAGS纹理层状态为D3DTTFF_DISABLE(默认值),那么系统不会对输入的纹理坐标进行变换。如果D3DTSS_TEXTURETRANSFORMFLAGS为任何其它值,那么系统会用该纹理层的变换矩阵对该输入纹理坐标进行变换。
D3DTEXTURETRANSFORMFLAGS枚举类型定义了D3DTSS_TEXTURETRANSFORMFLAGS纹理层状态的有效值。除了D3DTTFF_DISABLE标志不进行纹理坐标变换外,该枚举类型值用于设定系统传送至光栅化器的输出坐标的数量。D3DTTFF_COUNT1到D3DTTFF_COUNT4标志告诉系统把输出纹理坐标中的一个、两个、三个或四个元素传送至光栅化器。
D3DTTFF_PROJECTED标志有些特别:它告诉系统纹理坐标将用于经过投影的纹理。把D3DTTFF_PROJECTED标志与D3DTEXTURETRANSFORMFLAGS的其它成员一起使用可以告诉系统在光栅化操作前把所有元素除以最后一个元素。例如,当显式使用三元素纹理坐标,或纹理变换产生三元素纹理坐标时,应用程序可以同时使用D3DTTFF_COUNT3和D3DTTFF_PROJECTED标志,这会使光栅化器把前两个元素除以最后一个元素,并得到寻址二维纹理所需的二维纹理坐标。
注意 除了立方体贴图和立体贴图外,光栅化器无法使用多于两个元素的纹理坐标。如果应用程序提供的元素比寻址当前纹理层所需的多,那么多余的元素会被忽略。当把二维纹理坐标用于一维纹理时也是这样。
以下主题包含了更多的信息。
自动生成的纹理坐标
系统可以使用经过变换的摄像机空间中的位置或顶点的法向量作为纹理坐标,也可以计算用于寻址立方体贴图的三元素向量。与应用程序在顶点数据中明确给出的纹理坐标一样,应用程序可以使用自动生成的纹理坐标作为纹理变换的输入。
通过无需在顶点格式中显式地给出纹理坐标,自动生成的纹理坐标可以显著降低几何数据所需的带宽。在许多情况下,系统生成的纹理坐标可以和纹理变换一起使用以生成特效。当然这只是一种特殊用途,大多数情况下应用程序还是要用显式给出的纹理坐标。
设定自动生成的纹理坐标
C++应用程序用D3DTSS_TEXCOORDINDEX纹理层状态(来自D3DTEXTURESTAGESTATETYPE)控制系统如何产生纹理坐标。
一般来说,这个状态告诉系统使用编码在顶点格式中的某组特定的纹理坐标。当应用程序给这个状态指定的值包含D3DTSS_TCI_CAMERASPACENORMAL,D3DTSS_TCI_CAMERASPACEPOSITION,或D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR标志时,系统执行的操作会完全不同。如果使用了这些标志中的任意一个,那么纹理层会忽略在顶点格式中的给出的纹理坐标,并优先使用系统生成的坐标。下表列出了每个标志的意义。
使用变换到摄像机空间的顶点法向作为输入纹理坐标。
使用变换到摄像机空间的顶点位置作为输入纹理坐标。
使用变换到摄像机空间的反射向量作为输入纹理坐标。反射向量根据输入顶点位置和法向量计算得到。
前面的这些标志是互斥的。如果应用程序指定了其中一个标志,那么应用程序还可以指定一个索引值,系统用这个索引值决定纹理环绕模式。
以下示例代码显示了如何在C++应用程序中使用这些标志。
/*
* 在本例中,d3dDevice变量为指向IDirect3DDevice9接口的有效指针。
*
* 在当前纹理层使用顶点位置(摄像机空间)作为输入纹理坐标,
* 纹理环绕模式在D3DRENDERSTATE_WRAP1渲染状态中设置。
*/
d3dDevice->SetTextureStageState( 0, D3DTSS_TEXCOORDINDEX,
D3DTSS_TCI_CAMERASPACEPOSITION | 1 );
自动生成的纹理坐标在作为纹理坐标变换的输入,或使应用程序无需计算用于寻址立方体环境贴图的三元素向量时最为有用。
球形贴图使用一张预计算的(在建模时)纹理贴图,该贴图包含了一个反光的球体反射的整个环境。Microsoft? Direct3D?有一个纹理坐标自动生成特性,使用了D3DTSS_TCI_CAMERASPACENORMAL渲染状态,该特性会取得变换到摄像机空间的顶点法向,并对它进行纹理变换生成纹理坐标。更多信息请参阅Sphere Map示例。
相关主题
纹理坐标变换
Microsoft? Direct3D?设备可以用一个4x4矩阵对顶点的纹理坐标进行变换。系统使用相同的方法对纹理坐标和几何体进行变换。任何变换(缩放、旋转、投影、shear或这些变换的组合)可以用一个4x4矩阵完成。
注意 Direct3D不改变经过变换和光照处理的顶点,因此,使用经过变换和光照处理的顶点的应用程序无法让Direct3D变换顶点的纹理坐标。
支持硬件变换和光照的设备(TnLHAL设备)也会对纹理坐标变换进行硬件加速。如果设备不支持硬件变换和光照,那么Direct3D会使用几何流水线中与平台相关的优化进行纹理坐标变换。
纹理坐标变换非常有用,它在生成特效的同时避免了对几何体纹理坐标的直接修改。应用程序可以使用简单的平移或旋转矩阵给物体表面的纹理生成动画效果,也可以对Direct3D自动生成的纹理坐标进行变换,这样可以简化并可能加速如投影纹理和动态光照贴图等高级特效。另外,在多层纹理中,应用程序也可以用纹理坐标变换重复使用某一组纹理坐标并将之用于多种用途。
设置及取得纹理坐标变换的信息
和应用程序用于变换几何体的矩阵一样,应用程序可以通过IDirect3DDevice9::SetTransform和IDirect3DDevice9::GetTransform方法设置和取得纹理坐标变换的信息。在调用这些方法时,用D3DTRANSFORMSTATETYPE枚举类型的成员D3DTS_TEXTURE0到D3DTS_TEXTURE7标识纹理层0到7。
以下示例代码设置了一个矩阵,对纹理层0的纹理坐标进行变换。
// 本例中,假设d3dDevice变量为指向IDirect3DDevice9接口的有效指针。
D3DMATRIX matTrans = D3DXMatrixIdentity( NULL );
// 为希望的变换设置矩阵。
d3dDevice->SetTransform( D3DTS_TEXTURE0, &matTrans );
启用纹理坐标变换
D3DTSS_TEXTURETRANSFORMFLAGS纹理层状态控制对纹理坐标的变换。这个纹理层状态的值由D3DTEXTURETRANSFORMFLAGS枚举类型定义。
当D3DTSS_TEXTURETRANSFORMFLAGS为D3DTTFF_DISABLE(默认值)时,纹理坐标变换被禁用。假设纹理层0的纹理坐标变换已启用,以下代码将之禁用。
// 本例中,假设d3dDevice变量为指向IDirect3DDevice9接口的有效指针。
d3dDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS,
D3DTTFF_DISABLE );
D3DTEXTURETRANSFORMFLAGS定义的其它值用于启用纹理坐标变换,并控制把结果纹理坐标中的几个元素传送到光栅化器。以下为示例代码。
// 本例中,假设d3dDevice变量为指向IDirect3DDevice9接口的有效指针。
d3dDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS,
D3DTTFF_COUNT2 );
D3DTTFF_COUNT2值告诉系统对纹理层0进行变换,然后把得到的纹理坐标的前两个元素传送给光栅化器。
D3DTTFF_PROJECTED纹理变换标志表示用于投影纹理的坐标。当这个标志被设置时,光栅化器会把传入的元素除以最后一个元素。以下为示例代码。
// 本例中,假设d3dDevice变量为指向IDirect3DDevice9接口的有效指针。
d3dDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS,
D3DTTFF_COUNT3 | D3DTTFF_PROJECTED );
本例告诉系统传送三个纹理坐标元素到光栅化器。光栅化器把前两个元素除以第三个元素,得到寻址纹理所需的二维纹理坐标。
特效
本主题包含了可以用纹理坐标处理实现的特效。
给建模表面的纹理生成动画效果(通过变换或旋转)
· // 使用单纹理,二维纹理坐标。这个位掩码会根据需要
· // 被扩展为包含位置,法向和颜色信息。
DWORD dwFVFTex = D3FVF_TEX1 | D3DFVF_TEXCOORDSIZE2(0);
SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
// M为要设置的D3DMATRIX,用于在U和V方向对纹理坐标进行变换。
// 1 0 0 0
// 0 1 0 0
// du dv 1 0 (du和dv每帧都会改变)
// 0 0 0 1
D3DMATRIX M = D3DXMatrixIdentity(); // 在d3dutil.h中声明
M._31 = du;
M._32 = dv;
把纹理坐标作为建模在摄像机空间中的位置的线性函数创建
· // 为了节省带宽,输入顶点没有纹理坐标。三个纹理坐标用顶点在
· // 摄像机空间中的位置(x, y, z)产生。
SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);
· // 使用了两个输出纹理坐标。
SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2);
// 把纹理坐标作为线性函数创建,这样的话:
// u = Ux*x + Uy*y + Uz*z + Uw
// v = Vx*x + Vy*y + Vz*z + Vw
// 这种情况下矩阵M为:
// Ux Vx 0 0
// Uy Vy 0 0
// Uz Vz 0 0
// Uw Vw 0 0
SetTransform(D3DTS_TEXTURE0, &M);
用立方体贴图实现环境贴图
SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR);
SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT3);
实现投影纹理
SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, D3DTSS_TCI_CAMERASPACEPOSITION);
· // 使用了两个输出纹理坐标。
·
SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTF_PROJECTED | D3DTTFF_COUNT3);
纹理过滤
Microsoft? Direct3D?在渲染图元时,要把三维图元映射到二维屏幕上。如果图元贴有纹理,那么Direct3D必须用该纹理给图元在二维屏幕上对应的每个像素产生一个颜色。对于每个像素,Direct3D必须从纹理获得一个颜色值,这个过程被称为纹理过滤。
在执行纹理过滤操作时,正在使用的纹理一般会被放大或缩小,换句话说,就是纹理被贴到比它大或比它小的图元上。纹理放大会使多个像素映射到一个texel,得到的图像可能会有马赛克。纹理缩小会使一个像素映射到多个texel,得到的图像可能会模糊不清或有锯齿。要解决这些问题,必须在计算像素的颜色时对texel的颜色进行一些混合操作。
Direct3D把复杂的纹理过滤过程简化了,它给应用程序提供了三种类型的纹理过滤——线性过滤、各向异性过滤和mipmap过滤。如果应用程序不选择以上三种纹理过滤,那么Direct3D使用一种被称为最近点取样的技术。
每种类型的纹理过滤都各有优缺点。例如,线性纹理过滤可能会在最终图像中产生锯齿边缘或马赛克,但它是三种纹理过滤方法中计算量最小的。用mipmap纹理过滤通常可以得到最好的效果,尤其是和各向异性过滤一起使用的时候,但是在Direct3D支持的纹理过滤技术中,它对内存的需求也最大。
使用纹理接口指针的应用程序应该调用IDirect3DDevice9::SetSamplerState方法设置当前的纹理过滤方法。第一个参数为从0到7的整数,表示要设置纹理过滤方法的纹理层的索引值。第二个参数为D3DSAMP_MAGFILTER,D3DSAMP_MINFILTER或D3DSAMP_MIPFILTER,分别表示放大、缩小和mipmap过滤。第三个参数为D3DTEXTUREFILTERTYPE枚举类型值,为要设置的纹理过滤方法。
本节介绍了Direct3D支持的纹理过滤方法,并被划为以下主题。
注意 虽然D3DRENDERSTATETYPE枚举类型中定义的纹理过滤渲染状态已经被纹理层状态取代,但是如果应用程序试图使用这些渲染状态的话,IDirect3DDevice9与IDirect3DDevice2不同的是,IDirect3DDevice9::SetRenderState方法不会失败。相反,系统会把这些渲染状态映射到多重纹理的第一层,也就是索引值为0的那层。应用程序不应该把老的渲染状态和它们对应的纹理层状态混在一起,这样可能会产生无法预知的结果。
最近点取样
应用程序不一定要使用纹理过滤。应用程序可以让Microsoft? Direct3D?先计算纹理地址(通常都不是整数),然后使用离该值最近的整数地址处的texel的颜色,这个过程被称为最近点取样。如果纹理的大小与图元在屏幕上的大小相近的话,那么这将是快速且有效的纹理处理方法,但如果不是这样的话,得到的图像可能会有马赛克、锯齿或模糊不清。
C++应用程序可以调用IDirect3DDevice9::SetSamplerState方法选择最近点取样,只需把第三个参数设置为D3DTEXF_POINT即可。
应用程序在使用最近点取样时应该小心,因为当在两个texel间的边界处进行纹理取样时,这种方法有时会产生图形残留物。当使用最近点取样时,系统要么取样一个texel,要么取样另一个,这样当纹理地址从一个texel移向下一个texel时,取样得到的texel会突然改变。这种效果会导致在最终显示的纹理中出现不希望的图形残留物。当使用线性过滤时,取样得到的texel是根据所有邻近的texel计算得到的,当纹理地址在邻近的texel间移动时,线性过滤会根据当前纹理地址与邻近texel间的位置关系,把相邻texel混合。
当把非常小的纹理贴到非常大的多边形表面时可以看到这种效果:这个操作通常被称为纹理放大(magnification)。例如,当使用的纹理看起来像西洋跳棋的棋盘时,最近点取样会得到一个非常大的棋盘,格子之间有清晰的边缘,相比之下,线性纹理过滤得到的图像中棋盘的颜色会沿着多边形逐渐改变。
大多数情况下,为了得到最好的效果,应用程序应该尽量避免使用最近点取样。当今的大多数硬件都为线性过滤做了优化,所以应用程序不必担心因此导致的性能下降。如果应用程序想要的效果一定要用最近点取样——比如用纹理显示可读的文本——那么应用程序应该极度小心,避免在texel边界取样,因为那样会导致不想得到的效果。下图显示了这些图形残留物可能的样子。
注意这组图片中右上角的两个方块与其余的不同,可以在它们的对角线上看到明显的偏移。要避免此类图形残留物,开发人员必须熟悉Direct3D在最近点取样时使用的纹理取样规则。Direct3D把闭区间[0.0, 1.0]范围内的浮点纹理坐标映射到texel空间中的整数地址[–0.5, n – 0.5]范围内,这里n为给定纹理的大小。得到的纹理地址被舍入到最近的整数,这种映射方法在texel边界会产生取样误差。
举个简单的例子,设想应用程序用D3DTADDRESS_WRAP纹理寻址模式渲染多边形。根据Direct3D使用的映射方法,对于宽度为4个texel的纹理,纹理在u方向上的映射如下。
注意这张图中的纹理坐标0.0和1.0,正好在texel之间的边界。根据Direct3D的映射方法,纹理坐标的范围为[–0.5, 4 – 0.5],这里4为纹理的宽度。在这个例子中,纹理坐标为1.0处取样得到的texel是texel 0。但是,如果纹理坐标只比1.0稍微小一点,那么取样得到的就会是texel n而不是texel 0。
这就意味着用正好等于0.0和1.0的纹理坐标去放大较小的纹理,并把它贴到整齐排列在屏幕上的三角形时,如果过滤方法为最近点取样方法,那么得到的像素可能会在texel之间的边界进行取样。在纹理坐标计算过程中的任何误差,无论多小,都可能在渲染得到的图像上与texel边界对应的部分出现图形残留物。
要完全精确地执行从浮点纹理坐标到整数texel地址的映射是很难的,也很耗时,并且通常是不必要的。大多数硬件实现在给三角形内的每个像素计算纹理坐标时使用迭代法。迭代法趋向于隐藏误差,因为误差在迭代过程中被均匀地累积起来。
Direct3D参考光栅化器在给每个像素计算纹理地址时使用直接赋值法。直接赋值法与迭代法的不同之处在于这种方法产生的误差分布更随机。因为参考光栅化器不进行完全精确的计算,所以这种方法会导致在边界处的取样误差更容易被察觉。
最好的方法是只在必要的时候使用最近点取样。如果应用程序必须使用这种方法,那么最好把纹理坐标稍微偏移一些使之离开边界位置,这样就可以避免残留物的产生。
线性纹理过滤
Microsoft? Direct3D?使用一种被称为双线性过滤的线性纹理过滤方法。和最近点取样一样,双线性过滤首先计算一个texel地址,这通常都不会是整数,然后找到离该地址最近的整数地址。另外,Direct3D渲染模块还会根据最近取样点上、下、左、右的texel计算它们的加权平均值。
可以调用IDirect3DDevice9::SetSamplerState方法选择双线性过滤,只需把第三个参数设为D3DTEXF_LINEAR即可。
各向异性纹理过滤
因为三维物体表面与屏幕间的夹角而造成的纹理扭曲被称为各向异性。当把一个各向异性的图元所对应的像素映射到texel时,像素的形状会被扭曲。Microsoft? Direct3D?根据像素被反向映射到纹理空间中的伸长率——也就是长度除以宽度——计量屏幕上像素的各向异性属性。
为了提高渲染质量,应用程序可以把各向异性过滤与线性纹理过滤或mipmap纹理过滤结合在一起使用。应用程序可以调用IDirect3DDevice9::SetSamplerState方法启用各向异性纹理过滤,只需把第三个参数设为D3DTEXF_ANISOTROPIC即可。
应用程序必须同时把degree of anisotropy设为大于一的值。可以调用IDirect3DDevice9::SetSamplerState方法设置这个值。第一个参数为0到7的纹理层索引值,把第二个参数设为D3DSAMP_MAXANISOTROPY,第三个参数为要设置的degree of anisotropy(译注:原文为degree of isotropy)的值。
应用程序只需把degree of anisotropy(译注:原文为degree of isotropy)设为一即可禁用各向异性过滤。要确定degree of anisotropy的允许范围,可以检查D3DCAPS9结构的MaxAnisotropy成员。
Mipmap是一系列纹理,每一张纹理都表示同一幅图像,但是分辨率逐渐变低。Mipmap中的每张图像,或每一级,都比前一级小一半。Mipmap不必是正方形的。
较高分辨率的mipmap图像用于离用户近的物体,而较低分辨率的图像则用于远处的物体。使用Mipmap在消耗更多内存的情况下,提高了渲染得到的纹理的质量。
Microsoft? Direct3D?用一连串从属表面表示mipmap。分辨率最高的纹理在链的最前端,下一级mipmap是它的从属表面。依次,每一级mipmap的从属表面是它在mipmap中的下一级,一直到mipmap中分辨率最低的那级。
下图显示了这样的例子。该纹理表示在一个三维第一人称游戏中的一个容器上的标记。创建mipmap时,最高分辨率的纹理是mipmap中的第一个,mipmap中每个随后的纹理的宽度和高度都是原来的一半,在这个例子中,最高分辨率的mipmap为256x256。下一级纹理为128x128,最后一级纹理为64x64。
这个标记有一个最远可见距离。如果用户离标记很远,那么游戏就用mipmap链中最小的纹理显示,在这个例子中就是64x64的纹理。
随着用户移动视点并离标记越来越近,游戏会逐渐使用mipmap链中更高分辨率的纹理。下图中纹理的分辨率为128x128。
当视点离标记的距离为所允许的最近距离时,游戏就使用最高分辨率的纹理。
对于纹理而言,这是一种更有效的模拟透视的方法,与在不同的分辨率下用单张纹理进行渲染相比,在不同分辨率下使用多张纹理会更快。
Direct3D可以确定mipmap链中哪一级纹理的分辨率与当前需要的最为接近,并把像素映射到那一级纹理的texel空间。如果最终图像需要的分辨率位于mipmap链中两级纹理的分辨率之间,Direct3D会取得这两级mipmap中的texel并把它们的颜色值混合在一起。
要使用mipmap,应用程序必须创建一个mipmap链。应用程序只需把mipmap链设为当前纹理可以使用mipmap。更多信息,请参阅纹理混合。
下一步,应用程序必须设置Direct3D用于取样texel的纹理过滤方法。Mipmap过滤最快的方法就是让Direct3D选择最近的texel,D3DTEXF_POINT枚举类型值就是用来选择这种方法的。如果应用程序使用D3DTEXF_LINEAR枚举类型值,那么Direct3D可以产生更好的过滤效果,这会使Direct3D选择最近的那级mipmap,然后根据当前像素在那一级mipmap中所映射的texel及其附近的texel计算加权平均值。
Mipmap纹理用于减少渲染三维场景所需的时间,同时提高了场景的真实感。但是,mipmap通常需要大量的内存。
以下示例代码显示了应用程序如何调用IDirect3DDevice9::CreateTexture方法创建一条五级mipmap链:256x256, 128x128, 64x64, 32x32, 及16x16。
// 本例假设变量d3dDevice为指向IDirect3DDevice9接口的有效指针。
IDirect3DTexture9 * pMipMap;
d3dDevice->CreateTexture(256, 256, 5, 0, D3DFMT_R8G8B8, D3DPOOL_MANAGED, &pMipMap);
IDirect3DDevice9::CreateTexture的前两个参数为最高一级的纹理的宽和高。第三个参数为mipmap纹理的级数,如果应用程序把它设为零,那么Direct3D会创建一系列表面,每个都是前一个的一半,直到大小为1x1为止。第四个参数指定该资源的用途,本例中,零表示不为该资源指定特殊的用途。第五个参数指定纹理的表面格式,该参数为D3DFORMAT枚举类型值。第六个参数为D3DPOOL枚举类型值,表示在把创建的资源放在哪种类型的内存中,除非应用程序使用动态纹理,否则建议使用D3DPOOL_MANAGED。最后一个参数为指向IDirect3DTexture9接口指针的地址。
注意 Mipmap链中的每个表面的大小都是前一个表面的一半。如果最高一级mipmap的大小为256x128,那么第二级的mipmap就是128x64,第三级为64x32,依次类推,直到1x1(译注:最后几级分别为:4x2,,2x1,1x1)。应用程序在Levels中要求的mipmap级不能使链中的任何mipmap的宽和高小于1。举个简单的例子,如果最高一级的mipmap表面为4x2,那么Levels的最大允许值就是三,第三层的大小为1x1。如果Levels的值大于3,那么会导致第二级mipmap的高度值出现小数,而这是不允许的。(译注:原文表述不够准确,应该是log2 (max (width, height) ) < 0)
可以调用IDirect3DDevice9::SetTexture方法把mipmap纹理设置为当前纹理中的第一个纹理,更多信息请参阅纹理混合。
应用程序在选择mipmap纹理后,必须用D3DTEXTUREFILTERTYPE枚举类型值设置D3DSAMP_MIPFILTER取样器状态。之后Direct3D就可以自动执行mipmap纹理过滤。以下示例代码显了如何启用mipmap纹理过滤。
d3dDevice->SetTexture(0, pMipMap);
d3dDevice->SetTextureStageState(0, D3DSAMP_MIPFILTER, D3DTEXF_POINT);
应用程序也可以手工遍历mipmap链,只需调用IDirect3DTexture9::GetSurfaceLevel方法,并指定要取得的mipmap级即可。以下示例代码从mipmap链的最高一级遍历到最低一级。
IDirect3DSurface9 * pSurfaceLevel;
for (int iLevel = 0; iLevel < pMipMap->GetLevelCount(); iLevel++)
{
pMipMap->GetSurfaceLevel(iLevel, &pSurfaceLevel);
//Process this level.
pSurfaceLevel->Release();
}
为了把位图数据载入mipmap链中的每个表面,应用程序需要手工遍历mipmap链,一般来说这是遍历mipmap链的唯一原因。应用程序可以通过调用IDirect3DBaseTexture9::GetLevelCount取得mipmap的级数。
纹理资源
纹理资源在IDirect3DTexture9接口中实现。要得到一个指向纹理接口的指针,应该调用IDirect3DDevice9::CreateTexture方法或以下Direct3D扩展(D3DX)函数。
以下示例代码调用D3DXCreateTextureFromFile从文件Tiger.bmp中载入一张纹理。
// 以下示例代码假设D3dDevice为指向IDirect3DDevice9接口的有效指针。
LPDIRECT3DTEXTURE9 pTexture;
D3DXCreateTextureFromFile( d3dDevice, "tiger.bmp", &pTexture);
D3DXCreateTextureFromFile的第一个参数为指向IDirect3DDevice9接口的指针。第二个参数为文件名,告诉Direct3D从哪个文件载入纹理。第三个参数为指向IDirect3DTexture9接口的指针的地址,表示创建得到的纹理对象。
用纹理资源进行渲染
Direct3D通过纹理层的概念支持多重纹理混合,每个纹理层包含一张纹理以及可以在这张纹理上执行的操作。纹理层中的纹理组成了一个当前纹理的集合。更多信息请参阅纹理混合。每张纹理的状态被封装在对应的纹理层中。
C++应用程序必须调用IDirect3DDevice9::SetTextureStageState方法设置每张纹理的状态。第一个参数为从0到7的纹理层索引值,第二个参数为D3DTEXTURESTAGESTATETYPE枚举类型值,最后一个参数为要设置的纹理层状态值。
在使用纹理接口指针进行渲染时,应用程序最多可以把八张纹理混合。应用程序通过调用IDirect3DDevice9::SetTexture方法设置当前纹理。Direct3D会先把所有当前纹理混合,然后再贴到正在渲染的图元的表面。
注意 因为IDirect3DDevice9::SetTexture方法会增加正在设置的纹理表面的参考计数(reference count),所以当不再需要该纹理时,应用程序应该把相应纹理层的纹理设置为NULL。如果应用程序不这样做,那么表面就不会被释放,并造成内存泄漏。
应用程序可以调用IDirect3DDevice9::SetRenderState方法设置当前纹理的纹理环绕状态,只需把第一个参数设为从D3DRS_WRAP0到D3DRS_WRAP7的值,并把第二个参数设为D3DWRAPCOORD_0,D3DWRAPCOORD1,D3DWRAPCOORD2,及D3DWRAPCOORD3标志的组合,这样就可以启用在u, v, 或w方向上的纹理环绕。
应用程序还可以设置纹理透视和纹理过滤状态。请参阅纹理过滤。
纹理环绕
简而言之,纹理环绕用来改变Microsoft? Direct3D?用每个顶点的纹理坐标对贴有纹理的多边形进行光栅化的基本方法。在光栅化一个多边形时,为了确定多边形所覆盖的每个像素所需使用的 texel,系统要在每个多边形顶点的纹理坐标之间进行插值。一般来说,系统把纹理当做二维平面,为了在纹理中的点A和点B之间插值得到新的texel,系统会取两点间的最短路径。如果点A表示的u, v坐标为(0.8, 0.1),点B表示的u, v坐标为(0.1,0.1),那么插值的路径会如下图所示。
注意这张图中点A和点B间的最短路径大致穿越纹理的中央。启用u方向或v方向上的纹理坐标环绕会相应改变Direct3D在u方向或v方向上对纹理坐标间最短路径的理解。纹理环绕的定义使光栅化器在获取纹理坐标间的最短路径时,假设纹理坐标0.0和1.0是等价的。最后这一点是技巧所在:可以这样想象,在某一方向上启用纹理环绕会使系统把纹理当成在圆柱体表面环绕的纹理进行处理。例如,考虑下图。
这幅图显示了在u方向上的环绕是怎样影响系统对纹理坐标间的插值的。在一张普通的,或没有环绕的纹理上使用与前例中相同的点,我们会发现点A和点B间的最短路径不再穿越纹理的中央,而是穿越纹理的边框,也就是纹理坐标0.0和1.0重合的地方。在v方向上的环绕与此类似,唯一的不同在于圆柱体是平躺的。同时在u方向和v方向上进行环绕比较复杂,在这种情况下,可以把纹理想象成是一个立体圆环。
纹理环绕最通常的应用是在使用环境贴图时。通常,使用环境贴图的物体会显得比较反光,并反射出物体周围的场景。为了便于讨论,我们想象一个房间,房间有四面墙,每面墙上写着字母R, G, B, Y,分别对应颜色红,绿,蓝,黄。这样一个简单的房间使用的环境贴图可能如下图所示。
设想房屋的屋顶由一根完全反光的柱子支撑,柱子有四面。要把环境贴图贴到柱子上很简单,但是要让柱子看起来像是反射墙上的字母和颜色就没有那么容易了。下图显示了用线框模式绘制的柱子,并在顶部的顶点处列出了相应的纹理坐标。环绕将发生在纹理的接缝处,在下图中用虚线表示。
如果在u方向上启用纹理环绕,那么柱子会正确地显示出环境贴图上的颜色和字母,并且在纹理前面的接缝处,光栅化器会假设u坐标0.0和1.0是等价的并正确地选择纹理坐标间的最短路径。贴上纹理后的柱子如下图所示。
如果没有启用纹理环绕,那么光栅化器就不会产生可信的反射图像。相反,柱子前面的部分会包含经过挤压的位于u坐标0.175到0.875之间的texel,因为这些texel穿越纹理的中央。这样环绕效果被破坏了。
使用纹理环绕
要启用纹理环绕,应该调用IDirect3DDevice9::SetRenderState方法,如以下示例代码所示。
d3dDevice->SetRenderState(D3DRS_WRAP0, D3DWRAPCOORD_0);
IDirect3DDevice9::SetRenderState的第一个参数是要设置的渲染状态,应该设为从D3DRS_WRAP0到D3DRS_WRAP7的枚举类型值,表示要设置哪一个纹理层的环绕状态。把第二个参数设为从D3DWRAPCOORD_0到D3DWRAPCOORD_3的标志可以在对应的方向上启用纹理环绕,也可以组合使用这些标志在多个方向上启用纹理环绕。如果应用程序忽略其中某个标志,那么在相应的方向上的纹理环绕就被禁用,要禁用某组纹理坐标在所有方向上的纹理环绕,只需把第二个参数设为0。
不要把纹理环绕与名字相近的纹理寻址模式相混淆。纹理环绕在对纹理进行寻址之前进行。一定要保证用于纹理环绕的数据不包含[0.0, 1.0]范围外的纹理坐标,因为那样会导致不希望的结果。有关纹理寻址的更多信息,请参阅纹理寻址模式。
位移贴图的环绕
位移贴图由tessellation引擎解释,因为无法为tessellation引擎指定环绕模式,所以不能对位移贴图进行纹理环绕。应用程序可以用一组顶点,强制进行在任何方向上的环绕。应用程序也可以指定只进行简单的线性插值。
纹理混合
Microsoft? Direct3D?最多可以在一趟渲染过程中把八张纹理混合并贴到图元上。使用多重纹理可以极大地提高Direct3D应用程序的执行速度。应用程序可以用多重纹理混合在一趟渲染过程中产生纹理、影子、镜面反射光、漫反射光,以及其它特效。
要使用纹理混合,应用程序必须先检查硬件是否支持纹理混合。这个信息包含在D3DCAPAS9结构的TextureCaps成员中。有关如何查询硬件的纹理混合能力的细节,请参阅IDirect3DDevice9::GetDeviceCaps。
纹理层和纹理混合级联
通过使用纹理层,Direct3D支持在一趟渲染过程中完成多重纹理混合。一个纹理层有两个输入,并对它们执行一个混合操作,然后把结果用于进一步的处理或用于光栅化。可以把纹理层想象为如下图所示。
如上图所示,纹理层用一个指定的操作符把两个输入混合。常用的操作符包括对输入参数的颜色或阿尔法的简单调制或相加,但实际上Direct3D支持几十种混合操作。纹理层的输入可以是与该层关联的纹理,迭代后的颜色或阿尔法(在进行高洛德着色的过程中迭代得到),指定的颜色或阿尔法,或前一个纹理层的结果。更多信息,请参阅纹理混合操作和输入。
注意 Direct3D区分颜色混合和阿尔法混合。应用程序分别设置要对颜色和阿尔法执行的混合操作及相应的输入,并且这些设置互不影响。
多重混合层的参数和操作的组合定义了一种简单的基于流程的混合语言。每一层的结果流入下一层,依次类推。这个概念,也就是混合的结果在层与层之间流动,并最终被用来对多边形进行光栅化操作,通常被称为纹理混合级联。下图显示了各独立的纹理层如何组成纹理混合级联。
设备中的每个纹理层都有一个从零开始的索引值。虽然应用程序应该总是检查设备的能力以确定当前设备支持几层纹理,但是Direct3D最多允许有八个混合层。第一层的索引值为0,第二层的索引值为1,依次类推,直到索引值7。系统根据索引值的增长混合各纹理层。
最好只使用需要的那些混合层,默认情况下没有用到的混合层被禁用。因此,如果应用程序只使用前两个混合层,那么只需设置层0和层1的操作符和参数。系统会对这两层执行混合操作,并忽略其余被禁用的层。
有关性能的注意事项 如果应用程序在不同的情况下使用不同数量的纹理层——比如对一些物体使用四层纹理,而对其它物体只使用两层纹理——那么应用程序无需显式地禁止所有以前使用过的层。一种选择是对未曾用到的纹理层的第一层,禁用其颜色操作符,这样该纹理层及其后的纹理层将不会被用到。另一种选择是设置第一层纹理(层0)的颜色操作符,禁用所有纹理贴图。第三种选择是当纹理层的D3DTSS_COLORARG1等于D3DTA_TEXTURE时,只要该层的纹理指针为空,该层及其后的纹理层都不会被处理。
以下主题包含了更多信息。
纹理混合操作及参数
应用程序把混合层与当前纹理集合中的每张纹理相联系。Microsoft? Direct3D?按照顺序对每个混合层求值,从集合中的第一张纹理开始,至第八张结束。
Direct3D把当前纹理集合中每张纹理的信息应用于与之相联系的混合层。通过调用IDirect3DDevice9::SetTextureStageState,应用程序可以控制使用纹理层中的哪些信息。应用程序可以分别设置对颜色和阿尔法通道的操作,每个操作有两个参数。用D3DTSS_COLOROP纹理层状态指定要对颜色通道执行的操作,用D3DTSS_ALPHAOP纹理层状态指定要对阿尔法通道执行的操作,这两个纹理层状态都是D3DTEXTUREOP枚举类型值。
纹理混合的参数使用D3DTEXTURESTAGESTATETYPE枚举类型的D3DTSS_COLORARG1, D3DTSS_COLORARG2, D3DTSS_ALPHAARG1和D3DTSS_ALPHAARG2成员表示。对应的参数值由D3DTA指定。
注意 通过把某一层的颜色操作设置为D3DTOP_DISABLE,应用程序可以禁用该纹理层及纹理混合级联中所有的后续层。禁用颜色操作会同时禁用阿尔法操作。当颜色操作被启用时,阿尔法操作无法被禁用。当颜色混合被启用时,把阿尔法操作设置为D3DTOP_DISABLE会导致不确定的结果。
要测定一个设备支持的纹理混合操作,请查询D3DCAPS9结构的TextureCaps成员。
设置当前纹理
Microsoft? Direct3D?维护着一个当前纹理列表,最多可以有八张。Direct3D会把这些纹理混合到要渲染的图元上。只有作为纹理接口指针创建的纹理可以被用于当前纹理集合。
应用程序可以调用IDirect3DDevice9::SetTexture方法把纹理设置到当前纹理集合中。第一个参数必须是闭区间0到7之间的数字,第二个参数是纹理接口指针。
以下C++示例代码显示了如何把一张纹理加入到当前纹理集合中。
// 本示例代码假设变量lpd3dDev为指向IDirect3DDevice9接口的有效指针,
// 且pTexture为指向IDirect3DBaseTexture9接口的有效指针。
// 设置第三层纹理
d3dDevice->SetTexture(2, pTexture);
注意 软件设备不支持同时把一张纹理指定到一个以上的纹理层。
创建混合层
一个混合层是一个纹理操作及相应参数的集合,它定义了怎样混合纹理。C++应用程序在创建混合层时调用IDirect3DDevice9::SetTextureStageState方法。第一次调用指定要执行的操作,另外两次调用指定参数,Direct3D将用这两个参数执行指定的操作。以下示例代码描述了如何创建一个混合层。
// 本示例代码假设lpD3DDev为指向IDirect3DDevice9接口的有效指针。
// 设置要对第一个纹理进行的操作
d3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_ADD);
// 将参数1设置为纹理颜色
d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
// 将参数2设置为迭代后的漫反射色。
d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
纹理中的texel数据包含颜色和阿尔法值。应用程序可以在一个混合层中分别定义颜色和阿尔法操作。颜色操作和阿尔法操作分别有自己的参数。更多细节,请参阅D3DTEXTURESTAGESTATETYPE。
虽然下面的宏并不是Microsoft? Direct3D?应用程序编程接口(API)的一部分,但是应用程序可以用它们简化创建纹理混合层所需的代码。
#define SetTextureColorStage( dev, i, arg1, op, arg2 ) \
dev->SetTextureStageState( i, D3DTSS_COLOROP, op); \
dev->SetTextureStageState( i, D3DTSS_COLORARG1, arg1 ); \
dev->SetTextureStageState( i, D3DTSS_COLORARG2, arg2 );
#define SetTextureAlphaStage( dev, i, arg1, op, arg2 ) \
dev->SetTextureStageState( i, D3DTSS_ALPHAOP, op); \
dev->SetTextureStageState( i, D3DTSS_ALPHAARG1, arg1 ); \
dev->SetTextureStageState( i D3DTSS_ALPHAARG2, arg2 );
阿尔法纹理混合
Microsoft? Direct3D?在渲染一个图元时会根据图元的材质、或图元的顶点颜色,及光照信息为该图元产生一个颜色。更多细节,请参阅光照与材质。如果应用程序启用纹理混合,那么Direct3D必须把经过处理的多边形上的像素颜色与已经储存在帧缓存中的像素颜色进行混合。Direct3D使用以下公式计算图元的(渲染得到的)图像中每个像素的最终颜色。
FinalColor = TexelColor × SourceBlendFactor + PixelColor × DestBlendFactor
在这个公式中,FinalColor为输出到目标渲染表面的最终像素颜色。TexelColor为经过纹理过滤的输入颜色值,对应当前像素。有关Direct3D如何把texel映射到像素的细节,请参阅纹理过滤。SourceBlendFactor为一个经过计算的值,Direct3D用它计算输入颜色值在最终颜色中所占的百分比。PixelColor为当前存储在图元的图像中像素的当前颜色。DestBlendFactor为当前像素颜色在最终渲染得到的像素中所占的百分比。SourceBlendFactor和DestBlendFactor的值在闭区间0.0到1.0范围内。
正如我们从以上的公式中所看到的,如果SourceBlendFactor为D3DBLEND_ONE且DestBlendFactor为D3DBLEND_ZERO,那么最终渲染得到的像素是不透明的。如果SourceBlendFactor为D3DBLEND_ZERO且DestBlendFactor为D3DBLEND_ONE,那么得到的像素是完全透明的。如果应用程序把这些因子设置为为任何其它值,那么最终渲染得到的像素会具有一定的透明度。
经过纹理过滤后,每个像素的颜色值包含红、绿和蓝三种颜色值。默认情况下,Direct3D使用D3DBLEND_SRCALPHA作为SourceBlendFactor,使用D3DBLEND_INVSRCALPHA作为DestBlendFactor。因此,通过设置纹理中的阿尔法值,应用程序可以控制处理后的像素的透明度。
C++应用程序可以用D3DRS_SRCBLEND和D3DRS_DESTBLEND渲染状态控制这些因子,只需调用IDirect3DDevice9::SetRenderState方法,把这两个渲染状态其中之一作为第一个参数传入。第二个参数必须是D3DBLEND枚举类型成员。
多趟纹理混合
通过在多趟渲染的过程中将多个纹理贴到一个图元的表面,Microsoft? Direct3D?应用程序可以实现许多特效,这通常被称为多趟(multipass)纹理混合。多趟纹理混合的一个典型用途就是通过把几个不同纹理上的颜色混合,模拟复杂的光照和着色模型的效果。这种应用被称为光照贴图。更多信息,请参阅用纹理实现光照贴图。
注意 一些设备可以在一趟渲染过程中将多张纹理贴到图元表面。细节请参阅纹理混合。
如果用户的硬件不支持多重纹理混合,应用程序可以使用多趟纹理混合以达到同样的视觉效果。但是,与使用多重纹理混合相比,应用程序将无法保持相同的帧速率。
要进行多趟纹理混合,C++应用程序应该执行以下操作。
用纹理实现光照贴图
对于想要真实地渲染一个三维场景的应用程序来说,必须要考虑光源会对渲染得到的场景产生的效果。虽然诸如平面着色和高洛德着色之类的技术在这方面也是有用的工具,但它们可能无法满足应用程序的要求。Microsoft? Direct3D?支持多趟和多重纹理混合。与仅使用着色技术相比,这些能力使应用程序能够渲染更具真实感的场景。通过使用一张或多张光照贴图,应用程序可以把光影的范围映射到图元上。
光照贴图是包含三维场景中光照信息的一张纹理或一组纹理。应用程序可以把光照信息存放在光照贴图的阿尔法值中,颜色值中,或以上两者中。
如果应用程序用多趟纹理混合实现光照贴图,应用程序应该在第一趟渲染时把光照贴图贴到图元上,在第二次渲染时使用基本纹理。镜面反射光照贴图是个例外,在这种情况下,要先渲染基本纹理,再添加光照贴图。
多重纹理混合使应用程序能一次同时渲染光照贴图和基本纹理。如果用户的硬件支持多重纹理混合,应用程序应该在进行光照贴图时利用这一特性,这将极大地提高应用程序的性能。
如果使用光照贴图,Direct3D应用程序可以在渲染图元时得到多种光照效果。应用程序不仅可以在场景中使用单色光和有色光,还可以添加诸如镜面反射高光和漫反射光之类的细节。
以下主题介绍了用Direct3D纹理混合实现光照贴图的信息。
单色光照贴图
一些老的三维加速卡不支持使用目标像素的阿尔法值进行纹理混合,更多信息请参阅阿尔法纹理混合。一般来说这些加速卡也不支持多重纹理混合,如果应用程序在此类适配器上运行,那么可以用多趟纹理混合进行单色光照贴图。
要进行单色光照贴图,应用程序应该把光照信息存放在光照贴图的阿尔法数据中。应用程序使用Microsoft? Direct3D?的纹理过滤功能把图元的图像中的每个像素映射到光照贴图中的相应texel。应用程序应该把源混合因子设为相应texel的阿尔法值。
以下C++示例代码描述了应用程序如何把一张纹理用作单色光照贴图。
// 本例假设d3dDevice为指向IDirect3DDevice9接口的有效指针,
// 且lptexLightMap为指向包含单色光照贴图数据的纹理的有效指针。
// 把光照贴图设置为当前纹理。
d3dDevice->SetTexture(0, lptexLightMap);
// 设置颜色操作。
d3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
// 设置颜色操作的第一个参数。
d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1,
D3DTA_TEXTURE | D3DTA_ALPHAREPLICATE);
因为不支持目标阿尔法混合的适配器一般来说也不支持多重纹理混合,这个示例把光照贴图设为第一张纹理,而这在所有三维加速卡上都是可用的。示例代码先设置纹理混合层的颜色操作,让纹理数据与图元已有的颜色进行混合,然后选择第一张纹理和图元已有的颜色作为输入数据。
有色光照贴图
如果应用程序使用有色光照贴图,那么通常会渲染得到更具真实感的三维场景。一张有色光照贴图使用RGB数据存放光照信息。
以下C++示例代码显示了如何用RGB颜色数据进行光照贴图。
// 本例假设d3dDevice为指向IDirect3DDevice9接口的有效指针,
// 且lptexLightMap为指向包含单色光照贴图数据的纹理的有效指针。
// 把光照贴图设为第一张纹理。
d3dDevice->SetTexture(0, lptexLightMap);
d3dDevice->SetTextureStageState( 0,D3DTSS_COLOROP, D3DTOP_MODULATE );
d3dDevice->SetTextureStageState( 0,D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState( 0,D3DTSS_COLORARG2, D3DTA_DIFFUSE );
这个示例先把光照贴图设为第一张纹理,然后设置第一个混合层的状态,对输入数据进行调制(相乘),并把第一张纹理和图元的当前颜色用作调制操作的参数。
镜面反射光照贴图
在对发亮的物体——那些使用了高反射度材质的物体——进行光照计算时会产生镜面反射高光。在一些情况下,由光照模块产生的镜面反射高光不够精确,为了产生更吸引人的镜面反射高光,许多Microsoft? Direct3D?应用程序会给图元使用镜面反射光照贴图。
要进行镜面反射光照贴图,只需把镜面反射光照贴图与图元的纹理相加,然后再和RGB光照贴图进行调制(与结果相乘)操作。
以下C++示例代码描述了这个过程。
// 本例假设d3dDevice为指向IDirect3DDevice9接口的有效指针。
// lptexBaseTexture为指向纹理的有效指针。
// lptexSpecLightMap为指向包含RGB镜面反射光照贴图数据的纹理的有效指针。
// lptexLightMap为指向包含RGB光照贴图数据的纹理的有效指针。
// 设置基本纹理。
d3dDevice->SetTexture(0, lptexBaseTexture );
// 设置要对基本纹理执行的操作及参数。
d3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE );
d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
// 设置镜面反射光照贴图。
d3dDevice->SetTexture(1, lptexSpecLightMap);
// 设置要对镜面反射光照贴图执行的操作及参数。
d3dDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_ADD );
d3dDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT );
// 设置RGB光照贴图。
d3dDevice->SetTexture(2, lptexLightMap);
// 设置要对RGB光照贴图执行的操作及参数。
d3dDevice->SetTextureStageState(2,D3DTSS_COLOROP, D3DTOP_MODULATE);
d3dDevice->SetTextureStageState(2,D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState(2,D3DTSS_COLORARG2, D3DTA_CURRENT );
漫反射光照贴图
不光滑的表面在被光源照射时会显示漫反射光。漫反射光的亮度取决于表面到光源的距离及表面法向与光源的方向向量间的夹角。通过光照计算(译注:由光照模块执行)模拟漫反射光只能得到一般的效果。
应用程序可以用光照贴图模拟更为复杂的漫反射光照效果,只需在基本纹理的基础上再加一张漫反射光照贴图即可,如以下C++示例代码所示。
// 本例假设d3dDevice为指向IDirect3DDevice9接口的有效指针。
// lptexBaseTexture为指向纹理的有效指针。
// lptexLightMap为指向包含RGB光照贴图数据的纹理的有效指针。
// 设置基本纹理。
d3dDevice->SetTexture(0,lptexBaseTexture );
// 设置要对基本纹理执行的操作及参数。
d3dDevice->SetTextureStageState(0,D3DTSS_COLOROP, D3DTOP_MODULATE );
d3dDevice->SetTextureStageState(0,D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState(0,D3DTSS_COLORARG2, D3DTA_DIFFUSE );
// 设置漫反射光照贴图。
d3dDevice->SetTexture(1,lptexDiffuseLightMap );
// 设置混合层 。
d3dDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE );
d3dDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE );
d3dDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT );
表面
表面是显存的线性表示,虽然可以存在于系统内存中,但更通常都存在于显卡的显存中。IDirect3DSurface9接口包含了表面对象。
可以通过调用下列方法得到一个IDirect3DSurface9接口。
IDirect3DSurface9接口允许应用程序通过IDirect3DDevice9::UpdateSurface方法间接访问内存。该方法允许应用程序从一个IDirect3DSurface9接口复制一块矩形区域的像素到另一个IDirect3DSurface9接口。表面接口也提供了直接访问显存的方法。例如,应用程序可以用IDirect3DSurface9::LockRect方法锁定显存的一块矩形区域。很重要的一点是在完成对锁定的矩形区域的操作后,要调用IDirect3DSurface9::UnlockRect方法。
表面格式用于描述如何解释表面内存中的像素数据。Microsoft? Direct3D?使用D3DSURFACE_DESC结构的D3DFORMAT成员描述表面格式。应用程序可以通过调用IDirect3DSurface9::GetDesc方法取得一个现有的表面的格式。
以下主题包含了更多信息。
宽度与Pitch的比较
虽然对术语宽度和pitch的使用经常不很正式,但它们有着非常重要并且完全不同的意义。因此,开发人员应该理解它们的意义,以及如何解释Microsoft? Direct3D?用于描述它们的值。
Direct3D使用D3DSURFACE_DESC结构保存描述表面的信息。其中,这个结构包含了表面的大小,以及这些大小在内存中是如何表示的。结构使用Height和Width成员描述表面的逻辑大小,这两个成员都以像素为单位。因此,对于一个640x480表面来说,无论它是8位表面或24位RGB表面,它的Height和Width值都是相同的。
当应用程序使用IDirect3DSurface9::LockRect方法锁定一个表面时,该方法会填写一个D3DLOCKED_RECT结构,这个结构包含了表面的pitch值及一个指向被锁定数据的指针。Pitch成员的值描述了表面在内存中的pitch,也被称为跨度。Pitch是两个内存地址间以字节为单位的距离,两个内存地址分别表示一个位图某一行的起始地址以及下一行的起始地址。因为pitch是以字节为单位而非以像素为单位,所以一个640x480x8表面的pitch值与另一个大小相同但像素格式不同的表面的pitch值会大不相同。另外,pitch值有时反映出被Direct3D保留并用作高速缓存的字节数,因此简单地认为pitch就是宽度乘以每个像素所占的字节数是不保险的。如下图所示,对宽度和pitch之间的区别做一个直观的比较会更清楚。
在这张图中,前后缓存都是640x480x8,高速缓存为384x480x8。
当直接访问表面时要小心,不要访问为表面分配的内存以外的地方,更不要访问任何为高速缓存目的而保留的内存。另外,当应用程序锁定一个表面的一部分时,应用程序必须保持在锁定表面时指定的矩形区域内。不按这些指导方针行事将会导致无法预料的结果。当直接渲染到表面内存时,应该总是使用由IDirect3DSurface9::LockRect方法返回的pitch值。不要认为pitch仅取决于显示模式。如果应用程序在一些显示适配器上运行良好,但在另一些适配器上显示不正确的话,很可能就是pitch的问题。
更多信息,请参阅直接访问表面内存。
翻转表面
Microsoft? Direct3D?应用程序一般通过这种方式显示动画序列,即:先在后缓存中生成动画的各帧,然后按顺序把这些帧显示出来。后缓存是属于交换链的一部分。一个交换链是一系列后缓存,这些后缓存会一个接一个被“翻转”到屏幕上。这种方法可以用来在内存中渲染一个场景,当渲染完成后随即把场景翻转到屏幕上。这避免了画面撕裂的现象,并能生成更为平滑的动画。
在Direct3D中创建的每个设备至少有一个交换链。当应用程序初始化第一个Direct3D设备时,应用程序要设置D3DPRESENT_PARAMETER结构的BackBufferCount成员,告诉Direct3D交换链需要包含的后缓存的数量。随后对IDirect3DDevice9::CreateDevice的调用会创建Direct3D设备及相应的交换链。
当应用程序使用IDirect3DDevice9::Present方法要求一个翻转操作时,指向前缓存的指针和指向后缓存的指针被交换。翻转是通过切换显示设备用来引用内存的指针完成的,而不是复制表面的内存。当翻转链包含一个前缓存和一个以上的后缓存时,指针的切换以循环的方式进行,如下图所示。
通过调用IDirect3DDevice9::CreateAdditionalSwapChain,应用程序可以为设备创建附加的交换链。应用程序可以为每个视区创建一个交换链并将每个交换链与某个特定窗口相关联。应用程序在每个交换链的后缓存中渲染图像,然后分别显示它们。IDirect3DDevice9::CreateAdditionalSwapChain的两个参数分别为一个指向D3DPRESENT_PARAMETER结构的指针和一个指向IDirect3DSwapChain9接口的指针。应用程序可以使用IDirect3DSwapChain9::Present显示位于前缓存之后的那个后缓存的内容。注意一个设备只能有一个全屏交换链。
应用程序可以通过调用IDirect3DDevice9::GetBackBuffer或IDirect3DSwapChain9::GetBackBuffer方法取得对某个后缓存的访问权,这两个方法会返回一个指向IDirect3DSurface9接口的指针,代表被返回的后缓存表面。注意对这两个方法的调用会增加IDirect3DDevice9接口的内部引用计数,因此当完成对表面的操作后要记得调用IUnknown::Release,否则会导致内存泄漏。
记住,Direct3D通过交换交换链内指向表面内存的指针翻转表面,而不是交换表面本身。这意味着应用程序总是在下次将被显示的那个后缓存上进行渲染。
很重要的一点是要注意由显卡驱动程序执行的“翻转操作”和一个用D3DSWAPCHAIN_FLIP标志创建的交换链执行的“present”操作间的区别。
按照惯例,术语“翻转”表示改变显卡用来产生输出信号的视频内存地址的范围,这样就导致原先隐藏着的后缓存的内容将被显示。在Microsoft DirectX? 9.0中,这个术语更经常地是被用来描述把任何用D3DSWAPEFFECT_FLIP标志创建的交换链中的后缓存显示出来。
而当交换链为全屏模式时,“present”操作几乎总是通过翻转操作实现,当交换链为窗口模式时,“present”操作必然通过复制操作实现。此外,显卡驱动程序可能会根据D3DSWAPEFFECT_DISCARD和D3DSWAPEFFECT_COPY标志,用翻转实现全屏交换链的present操作。
以上讨论适用于常用的情况,也就是用D3DSWAPEFFECT_FLIP标志创建的全屏交换链。
有关窗口和全屏交换链的各种不同交换效果的讨论,请参阅D3DSWAPEFFECT。
页面翻转和后缓存
页面翻转是多媒体、动画和游戏软件中的关键,它和动画师用一叠纸产生动画的方法相似。在每张纸上,动画师对图片稍做改变,因此当动画师在页与页之间快速地翻动时,图片看起来就像是动了。
软件中的页面翻转与这个过程相似。Microsoft? Direct3D?通过交换链实现页面翻转功能,而交换链是设备的一个属性。开始时,应用程序先设置一系列Direct3D缓存,这些缓存会以和动画师相同的翻页方法翻转到屏幕。第一个缓存被称为前颜色缓存,它之后的缓存被称为后缓存。应用程序可以先写入到后缓存,然后翻转颜色缓存,这样后缓存就显示在屏幕上。当系统显示图像时,应用程序又可以写入到后缓存。这个过程可以一直持续,这样应用程序就可以高效地生成活动的图像。
Direct3D使建立一个页面翻转机制非常容易——从一个双缓存机制(一个前颜色缓存和一个后缓存)到使用额外后缓存的更为复杂的机制。
当使用IDirect3DDevice9::UpdateSurface时,要传入源表面中的一个矩形,或者用NULL表示整个表面,应用程序还需要传入目标表面中的一个点,源图像的左上角将被复制到这个位置。该方法不支持裁剪,除非源矩形和对应的目标矩形分别完全被包含在源和目标表面内,否则操作将会失败。这个方法不支持阿尔法混合、color key,及格式转换。注意目标表面和源表面不能是同一个表面。
其它有关UpdateSurface的使用限制,请参阅IDirect3DDevice9::UpdateSurface。
C++/C应用程序可以用下列方法把图像复制到一个Microsoft? Direct3D?表面。
术语blit是“位块传输(bit block transfer)”的缩写,表示把数据块从内存中的一处传输到另一处的过程。作为在每帧——IDirect3DDevice9::Present方法背后的面向复制的机制——中移动大块矩形中的像素的主要机制,blitting设备驱动程序接口(DDI)仍在继续使用。blit操作中对纹理数据的传输由IDirect3DDevice9::UpdateTexture方法执行。在DirectX 9.0中,纹理数据也可以用IDirect3DDevice9::UpdateSurface方法复制,该方法复制像素的一个矩形子集。
注意 DirectX 9.0提供了Direct3D扩展(D3DX)函数,这使应用程序可以从文件载入纹理,进行颜色转换,及调整纹理的大小。有关更多可供使用的函数的信息,请参阅与纹理相关的函数。
直接访问表面内存
通过使用IDirect3DSurface9::LockRect方法,应用程序可以直接访问表面内存。在调用这个方法时,pRect参数为指向RECT结构的指针,描述要直接访问表面上的哪一部分。如果要锁定整个表面,只需把pRect设为NULL即可。同时,应用程序可以指定一个只覆盖表面的一部分的RECT。如果提供两个不相交迭的矩形,那么两个线程或进程可以同时锁定一个表面中的多个矩形。注意一个多重取样的(multisample)后缓存不能被锁定。
IDirect3DSurface9::LockRect方法会填写一个D3DLOCKED_RECT结构,该结构中包含了访问表面内存所需的全部信息。该结构包含了pitch信息,及一个指向被锁定的数据的指针。当应用程序完成对表面内存的访问后,应该调用IDirect3DSurface9::UnlockRect方法将表面解锁。
应用程序在锁定了一个表面后,可以直接对其中的内容进行操作。下面给出了一些提示,说明如何避免在使用直接渲染表面内存(directly rendering surface memory)时遇到的一些问题。
私有表面数据
应用程序可以在表面中存储任何类型的应用程序特有的数据。例如,在一个游戏中,一个表示地图的表面可以包含有关地形的数据。
一个表面可以有一个以上的私有数据缓存。每个缓存用一个GUID标识,该GUID由应用程序在把数据连接到表面时提供。
要存储私有表面数据,应该使用SetPrivateData,并传入源缓存,数据的大小,及应用程序为数据定义的GUID。或者,源数据也可以以COM对象的形式存在,这种情况下,应用程序只需传入对象的IUnknown接口指针,并设置D3DSPD_IUNKNOWNPOINTER标志。
SetPrivateData会为数据分配一块内部缓存并把数据复制到其中。应用程序可以安全地释放源缓存或对象。当FreePrivateData被调用时,内部缓存或对接口的引用也会被释放。当释放一个表面时,系统会自动执行这个操作。
要得到表面的私有数据,应用程序必须先分配一块大小合适的缓存,然后调用GetPrivateData方法,并把原先赋给数据的GUID传入。应用程序有责任释放任何用于这块缓存的动态内存。如果数据是COM对象,那么该方法会取得IUnknown指针。
如果应用程序不知道应该分配多大的缓存,可以先把pSizeOfData设为零并调用GetPrivateData,如果调用失败并返回D3DERR_MOREDATA,那么该方法会在pSizeOfData中返回所需的字节数。
Gamma控制允许应用程序改变系统如何显示表面的内容,同时不会影响表面本身的内容。可以认为这些控制是很简单的过滤器,在把表面数据显示在屏幕上之前,Microsoft? Direct3D?会对这些数据进行过滤。
Gamma控制是交换链的一个属性。有了Gamma控制,动态改变如何把表面的红、绿和蓝色深映射到系统最终显示的实际色深就成为了可能。通过设置Gamma level,应用程序可以使用户的屏幕闪现不同的颜色——当用户控制的角色被击中时为红色,当角色捡起了新的物品时为绿色,等等——同时不必为了达到相同的效果而把新的图像复制到帧缓存中去。
由于在Microsoft DirectX? 9.0中,交换链是设备的一个属性,因此每个Direct3D设备都至少有一条交换链(隐式交换链)。正因为gamma ramp是交换链的一个属性,所以当交换链处于窗口模式下时,gamma ramp也可以使用。Gamma ramp会立刻生效,不存在等待VSYNC的操作。
IDirect3DDevice9::SetGammaRamp和IDirect3DDevice9::GetGammaRamp方法允许应用程序在把表面中的像素送到数模转换器(DAC)进行显示之前对ramp levels进行操作,这会影响到表面中的像素的红、绿和蓝色分量。
在Direct3D中,术语gamma ramp指的是一个组数值,这些数值用于把帧缓存中所有像素的某一颜色分量——红、绿、蓝——的level映射到被DAC接收并用于显示的新的色深。
以下是gamma ramp的工作方式:Direct3D从帧缓存中得到一个像素并分别计算每个红、绿和蓝颜色分量。每个分量由一个位于0到65535之间的值表示。Direct3D用这个原始值作为索引,在一个有256个元素的数组(即ramp)中查找,数组中每个元素包含一个值,用来替换原始值。Direct3D对帧缓存中每个像素的每个颜色分量进行这个查找并替换的过程,从而改变了所有最终显示在屏幕上的像素的颜色。
通过画图很容易就能把ramp值直观地表示出来。下面两张图中左图显示了一个完全不改变颜色的ramp,右图显示的ramp会给它所作用于的颜色分量加上负偏移。
左图中数组元素包含的值与它们的索引值相同——索引为0的元素的值为0,索引为255的元素的值为65535。这是默认的ramp类型,它不会在显示输入值之前改变它们。右图显示的ramp有较大变化,第一个元素的值为0,最后一个元素的值为32768,第一个和最后一个元素之间的元素的值在0到32768之间均匀分布。得到的效果就是使用这个ramp的颜色分量在显示器上会显得比较暗。Direct3D并没有限制必须使用线性映射,如果需要,应用程序可以指定任意的映射方式。应用程序甚至可以把所有元素都设为零,以把某一颜色分量从显示器上完全消除。
Gamma ramp levels是Direct3D用于把帧缓存中的颜色分量映射到将被显示新的level的快速查找表。应用程序可以通过调用IDirect3DDevice9::SetGammaRamp和IDirect3DDevice9::GetGammaRamp方法设置和取得主表面的ramp levels。IDirect3DDevice9::SetGammaRamp接收两个参数,第一个参数为D3DSGR_CALIBRATE或D3DSGR_NO_CALIBRATION,第二个参数pRamp为一指向D3DGAMMARAMP结构的指针。D3DGAMMARAMP结构包含了三个有256个元素的WORD数组,每个数组用来存放红、绿和蓝gamma ramp。IDirect3DDevice9::GetGammaRamp接收一个参数,为一指向D3DGAMMARAMP的指针,当前的gamma ramp将被填写到该指针指向的结构中。
应用程序可以把IDirect3DDevice9::SetGammaRamp的第一个参数设为DDSGR_CALIBRATE,这样在设置新的gamma levels时就会调用校正器。由于校正gamma ramp会增加一些开销,因此最好不要频繁地调用。无论显卡或显示器是何类型,设置一个校正过的gamma ramp会给用户提供完全一致的gamma值。
并非所有系统都支持gamma校正,要测定设备是否支持gamma校正,应该调用IDirect3DDevice9::GetDeviceCaps,然后检查由该方法返回的D3DCAPS9结构的Caps2成员。如果设置了D3DCAPS2_CANCALIBRATEGAMMA能力标志,那么设备就支持gamma校正。
在设置新的ramp levels时,谨记应用程序在数组中设置的levels只有当应用程序运行于全屏独占模式时才会被使用。一旦应用程序切换到正常模式,ramp levels就会被忽略,并在应用程序恢复到全屏模式时重新生效。
即使设备在当前presentation模式(全屏或窗口)下不支持gamma ramps,也不会返回错误码。应用程序可以检查D3DCAPS9结构的Caps2成员是否设置了D3DCAPS2_FULLSCREENGAMMA和D3DCAPS2_CANCALIBRATEGAMMA能力位,以确定设备的能力及是否安装了校正器。
Mipmap的自动生成
Mipmap的自动生成在创建纹理时利用了硬件过滤,这使得这项功能对应用程序而言是完全透明的。自动生成对mipmap渲染目标尤其有用,因为它们位于显存中,而这种情况下,软件过滤的效率很低。
要自动生成 mipmap,应该在创建纹理时设置D3DUSAGE_AUTOGENMIPMAP标志。后续的sublevel的生成对应用程序来说都是完全透明的。在某些情况下,某些硬件的自动mipmap生成可能会占用许多时间,这时应用程序可以适时地使用IDirect3DBaseTexture9::GenerateMipSubLevels,示意驱动程序生成相应数量的sublevels。
IDirect3DBaseTexture9::SetAutoGenFilterType用来控制自动生成时的过滤质量。改变过滤类型会导致mipmap的sublevels被标记为无效的并需要重新生成。
IDirect3DBaseTexture9::GetAutoGenFilterType用来取得当前的过滤类型。在创建纹理时使用的默认过滤类型是D3DTEXF_LINEAR。如果驱动程序不支持线性过滤,那么过滤类型将被设为D3DTEXF_POINT。
如果纹理不是用D3DUSAGE_AUTOGENMIPMAP标志创建的,那么调用这些方法不会产生任何效果,也不会返回任何错误。除了D3DTEXF_NONE外,驱动程序支持的所有可用于常规纹理过滤的过滤类型都可用于自动生成。对每种资源类型而言,驱动程序应该支持它在相应的纹理、立方体纹理和立体纹理过滤能力信息中提供的过滤类型。
当源纹理是自动生成的mipmap,而目标纹理不是时,IDirect3DDevice9::UpdateTexture是非法的,调用会失败。如果源纹理不是自动生成的mipmap且目标纹理是自动生成的mipmap,那么只有目标纹理中的最高的相匹配的那层被更新,该层的sublevels会重新被生成,而源纹理的sublevels将会被忽略。与此类似,如果源纹理和目标纹理都是自动生成的mipmap,那么只有目标纹理中的最高的相匹配的那层被更新,该层的sublevels会被重新生成,而源纹理的sublevels将被忽略。
在创建一个自动生成的mipmap时,Levels参数必须被设置为零或一。
要检查设备对自动mipmap生成的支持,应该检查设备是否设置了D3DCAPS2_CANAUTOGENMIPMAP能力位。如果是,那么应该用D3DUSAGE_AUTOGENMIPMAP作为参数调用IDirect3D9::CheckDeviceFormat方法。如果返回值为D3D_OK,那么可以保证mipmap是自动生成的。如果返回值是D3DOK_NOAUTOGEN,这意味着对创建纹理的调用会成功,但不会生成任何mipmap。
要知道设备支持哪些过滤类型,应该检查D3DCAPS9结构的TextureFilterCaps、CubeTextureFilterCaps(译注:及VolumeTextureFilterCaps)成员所包含的能力位。
最后,要注意D3DUSAGE_AUTOGENMIPMAP只是一个提示,在创建纹理或调用 IDirect3D9::CheckDeviceFormat的过程中指定这个标志不会在任何类型的设备驱动程序接口(DDI)上引起错误。
相关主题
自动纹理管理
纹理管理是确定在某一特定时刻需要用哪些纹理进行渲染,并确保这些纹理已经被载入显存的过程。同任何算法一样,不同的纹理管理机制在复杂度上会有所不同,但任何纹理管理机制都会涉及到以下一些关键任务。
为了保证纹理载入具有最佳的性能,Microsoft? Direct3D?内建了对纹理管理的支持。由Direct3D管理的纹理资源被称为由系统管理的资源。
纹理管理器用时间戳对纹理进行跟踪,时间戳记录了纹理最后被使用的时间。管理器然后用最近最少使用(least-recently-used)算法确定哪些纹理应该被移除。在准备把两张纹理从显存中移除时,纹理的优先级用来仲裁。如果两张纹理具有相同的优先级,那么最近最少使用的那张纹理会被移除。如果两张纹理具有相同的时间戳,那么优先级较低的那张纹理会先被移除。
应用程序可以在创建纹理表面时要求自动纹理管理。要在C++应用程序中得到一个由系统管理的纹理,应该调用IDirect3DDevice9::CreateTexture创建纹理资源,并把Pool参数指定为D3DPOOL_MANAGED。Direct3D不允许应用程序指定要在何处创建纹理。在创建由系统管理的纹理时,应用程序不能使用D3DPOOL_DEFAULT或D3DPOOL_SYSTEMMEM标志。创建完由系统管理的纹理后,应用程序可以调用IDirect3DDevice9::SetTexture方法把纹理设到渲染设备的纹理级联中。
应用程序可以通过调用IDirect3DDevice9::SetPriority方法给由系统管理的纹理设置优先级。
Direct3D根据需要自动把纹理载入显存。系统可能会根据非本地视频内存的可用性或其它因素把由系统管理的纹理放在本地或非本地视频内存中作为高速缓存。系统不会把由系统管理的纹理所用的缓存的位置和大小告诉应用程序,而且对使用自动纹理管理而言也无需了解该信息。如果应用程序使用的纹理超过了显存所能容纳的数量,那么Direct3D会把旧的纹理从显存中移除以给新的纹理腾出空间。如果应用程序再次用到被移除的纹理,那么系统会用原始的系统内存纹理表面把纹理重新载入到显存的高速缓存中。虽然重新载入纹理是必须的,但它同时降低了应用程序的性能。
通过更新或锁定纹理资源,应用程序可以动态地修改纹理位于系统内存中的原件。当系统检测到一个无效表面时——在更新操作完成后,或当表面被解锁时——纹理管理器会自动地更新纹理位于显存中的复本。由此导致的性能下降与重新载入一个被移除的纹理相似。
当进入游戏中新的一关时,应用程序可能需要清空显存中所有由系统管理的纹理,此时应该调用IDirect3DDevice9::EvictManagedResources。
有关资源管理的更多信息,请参阅管理资源。
纹理贴图是画在三维物体上的数字化图像,用来添加可视细节。它们在光栅化时被贴到物体表面,这个过程会消耗大量的系统带宽和内存。为了减少纹理所消耗内存的数量,Microsoft? Direct3D?支持对纹理表面的压缩。一些Direct3D设备本身就支持压缩纹理表面。在这些设备上,只要应用程序创建了压缩表面并将数据载入其中,该表面就可以和其它任何纹理表面一样,在Direct3D中使用。在把压缩纹理贴到三维物体表面时,Direct3D会进行解压。
所有纹理压缩的格式都是二的乘方。虽然这并不表示纹理一定要是方的,但确实表示X和Y都是二的乘方。例如,如果一个纹理原来是512×128,那下一级mipmap应该是256×64,依次类推,每一级都以两倍递减。到最低两级,纹理被过滤成16×2 和8×1,因为压缩块总是一个4×4的texel块,所以这里会浪费一些数据位。块中没有用到的部分被填满。虽然在最低几级会浪费一些数据位,但总体的收获还是显著的。理论上最差的情况是,一个2K×1的纹理。这里,每一块只用到一行像素,其余的都没有用到。
需要特别注意的是任何单个的纹理必须指明它的数据——每组16个texel——是以64位还是以128位存储的。如果是64位块——也就是说,纹理用了DXT1格式,那么在同一纹理内以块为单位,混用不透明和一位阿尔法格式是可以的。换句话说,对每个由16个texel组成的块,对color_00和color_1两个无符号整数的比较是单独进行的。
一旦使用了128位块,整个纹理的阿尔法通道必须被指定为直接模式(DXT2和DXT3格式)或插值模式(DXT4和DXT5格式)。和颜色一样,一旦选择了插值模式,就可以以块为单位,混合使用八位或六位插值阿尔法。对alpha_0和alpha_1大小的比较仍然是以块为单位进行的。
对用于三维建模的纹理,Direct3D提供了压缩表面的服务。本节提供了有关创建压缩纹理表面及操控表面中的数据的信息。
信息被分为以下主题。
DXT1纹理格式用于不透明的或只有一个透明色的纹理。
每个不透明或一位阿尔法块保存了两个16位颜色值(RGB 5:6:5格式)和一个4x4的位图,位图中的每个像素占用2位。这样16个texel一共占用64位,或每个像素占用四位。在位图块中,每个texel占用2位,可以选择四个颜色,其中两个直接存储在经过编码的数据中,另两个则通过线性插值从存储的颜色值导出。下图显示了这种布局。
可以通过对存储在块中的两个16位颜色值进行比较来区分一位阿尔法格式和不透明格式。两个16位颜色值被当作无符号整数。如果第一个颜色值大于第二个,那么就暗示这一块只定义了不透明texel。这意味着有四个颜色可以用来表示texel。在四色编码中,有两个是导出的颜色,四个颜色值在RGB颜色空间中均匀分布。这种格式和RGB 5:6:5格式相似。否则(第一个颜色值小于等于第二个),就是一位阿尔法格式,一位阿尔法格式可以使用三个颜色,第四个颜色被保留,用来表示透明的texel。
在三色编码中,有一个导出的颜色,第四个2位编码被保留,用来表示透明的texel(阿尔法信息)。这种格式与RGBA 5:5:5:1格式相似,RGBA 5:5:5:1格式的最后一位被用来编码阿尔法掩码。
以下示例代码描述了用来决定当前块是使用了三色编码还是四色编码的算法。
if (color_0 > color_1)
{
// 四色编码块:导出另两个颜色。
// 00 = color_0, 01 = color_1, 10 = color_2, 11 = color_3
// 这些二位编码对应于存储在64位块中的二位位域。
color_2 = (2 * color_0 + color_1 + 1) / 3;
color_3 = (color_0 + 2 * color_1 + 1) / 3;
}
else
{
// 三色编码:导出一个颜色。
// 00 = color_0, 01 = color_1, 10 = color_2,
// 11 = transparent.
// 这些二位编码对应于存储在64位块中的二位位域。
color_2 = (color_0 + color_1) / 2;
color_3 = transparent;
}
应用程序在进行混合操作之前,最好把透明像素的RGBA成员设为零。
下面这些表显示了八字节块的内存布局,这里假设第一个索引值对应y坐标,第二个索引值对应x坐标。例如,Texel[1][2]指的是纹理贴图中位于(x,y) = (2,1)处的像素。
下表显示了八字节(64位)块的内存布局。
字地址 |
16位字 |
0 |
Color_0 |
1 |
Color_1 |
2 |
位图数据Word_0 |
3 |
位图数据Word_1 |
Color_0和Color_1为位于两端的颜色,它们的布局如下所示。
位 |
颜色 |
4:0 (最低位) |
蓝色分量 |
10:5 |
绿色分量 |
15:11 |
红色分量 |
位图数据Word_0的布局如下所示。
位 |
Texel |
1:0 (最低位) |
Texel[0][0] |
3:2 |
Texel[0][1] |
5:4 |
Texel[0][2] |
7:6 |
Texel[0][3] |
9:8 |
Texel[1][0] |
11:10 |
Texel[1][1] |
13:12 |
Texel[1][2] |
15:14 (最高位) |
Texel[1][3] |
位图数据Word_1的布局如下所示。
位 |
Texel |
1:0 (最低位) |
Texel[2][0] |
3:2 |
Texel[2][1] |
5:4 |
Texel[2][2] |
7:6 |
Texel[2][3] |
9:8 |
Texel[3][0] |
11:10 |
Texel[3][1] |
13:12 |
Texel[3][2] |
15:14 (最高位) |
Texel[3][3] |
作为不透明编码的一个例子,假设位图左右两边的颜色为红色和黑色。红色为color_0,黑色为color_1。在它们之间有两个插值得到的颜色,四个颜色一起形成了均匀的颜色变化。以下计算用来确定4x4位图的值。
00 ? color_0
01 ? color_1
10 ? 2/3 color_0 + 1/3 color_1
11 ? 1/3 color_0 + 2/3 color_1
位图看起来如下图所示。
这看起来就是下面的一系列颜色。
当16位无符号整数color_0小于color_1时,就表示选择一位阿尔法格式。举个例子,这种格式可以用来显示在蓝天背景前的树叶,可以把一些texel标记成透明的,而树叶还可以使用三种深度的绿色。其中两个颜色位于两端,第三个颜色通过插值得到。
这里显示了这样一幅图片。
注意图像中白色的地方,texel会被编码成透明的。此外还要注意在进行混合前,应该把透明texel的RGBA分量设为零。
以下计算用来确定位图的颜色和透明度的编码。
00 ? color_0
01 ? color_1
10 ? 1/2 color_0 + 1/2 color_1
11 ? Transparent
位图看起来如下图所示。
可以用有两种方法对具有更为复杂的透明度的纹理贴图进行编码。每种方法都包含一个描述透明度的块,位于前述的64位块之前。透明度要么用4x4位图表示,位图中每像素占用4位(直接编码),要么占用更少的位(译注:3位)并进行线性插值,这与颜色编码相似。
透明度块和颜色块中数据的排列如下表所示。
字地址 |
64位块 |
3:0 |
透明度块 |
7:4 |
前述64位块 |
对于直接纹理编码(DXT2和DXT3格式),描述texel的透明度的阿尔法分量被编码在一个4x4位图中,每个texel占用4位。这四位阿尔法值可以通过各种方法得到,诸如抖动或使用阿尔法数据的最高四位。但无论它们是如何产生的,都只按原样使用,不做任何形式的插值。
下图显示了一个64位的透明度块。
注意 Microsoft? Direct3D?的压缩方法使用最高四位。
下面这些表描述了在每个16位字中,阿尔法信息的内存布局。
下表包含了字0的布局。
位 |
阿尔法 |
3:0 (最低位) |
[0][0] |
7:4 |
[0][1] |
11:8 |
[0][2] |
15:12 (最高位) |
[0][3] |
下表包含了字1的布局。
位 |
阿尔法 |
3:0 (最低位) |
[1][0] |
7:4 |
[1][1] |
11:8 |
[1][2] |
15:12 (最高位) |
[1][3] |
下表包含了字2的布局。
位 |
阿尔法 |
3:0 (最低位) |
[2][0] |
7:4 |
[2][1] |
11:8 |
[2][2] |
15:12 (最高位) |
[2][3] |
下表包含了字3的布局。
位 |
阿尔法 |
3:0 (最低位) |
[3][0] |
7:4 |
[3][1] |
11:8 |
[3][2] |
15:12 (最高位) |
[3][3] |
DXT2和DXT3之间的区别在于,在DXT2格式中,我们认为颜色数据已经预乘了阿尔法,而在DXT3格式中,我们认为颜色数据没有预乘阿尔法。之所以需要这两种是格式是因为大多数情况下,在使用一个纹理时,仅检查数据并不足以确定颜色数据是否已经预乘了阿尔法。因为运行的时候需要这样的信息,所以就用两个四字符码(FOURCC)来区分这两种情况。但是,这两种格式使用的数据和插值方法是相同的。
为了检测texel是否是透明的,在DXT1中要对颜色进行比较,而DXT2和DXT3格式没有使用种比较。我们认为在不需要颜色比较的情况下,颜色数据总是以四色模式进行处理。换句话说,DXT1代码中最上面的if语句应该是:
if ((color_0 > color_1) OR !DXT1) {
对DXT4和DXT5格式的透明度的编码基于线性插值,这和颜色编码中使用的线性编码相似。这两种编码方法中,第一个八字节块存储了两个8位阿尔法值和一个4x4位图,位图中的每个像素占用三位。存储的两个阿尔法值用来插值导出中间的阿尔法值。其它信息根据两个阿尔法值的存储方式有所不同。如果alpha_0大于alpha_1,那么插值会导出六个中间的阿尔法值。反之,插值会导出四个中间的阿尔法值,另外两个隐含的阿尔法值分别为0(完全透明)和255(完全不透明)。
以下示例代码描述了这种算法。
// 含8个阿尔法还是含6个阿尔法的块?
if (alpha_0 > alpha_1) {
// 含八个阿尔法的块:导出另外六个阿尔法值。
// 位编码000 = alpha_0, 001 = alpha_1, 其它值通过插值得到。
alpha_2 = (6 * alpha_0 + 1 * alpha_1 + 3) / 7; // 位编码010
alpha_3 = (5 * alpha_0 + 2 * alpha_1 + 3) / 7; // 位编码011
alpha_4 = (4 * alpha_0 + 3 * alpha_1 + 3) / 7; // 位编码100
alpha_5 = (3 * alpha_0 + 4 * alpha_1 + 3) / 7; // 位编码101
alpha_6 = (2 * alpha_0 + 5 * alpha_1 + 3) / 7; // 位编码110
alpha_7 = (1 * alpha_0 + 6 * alpha_1 + 3) / 7; // 位编码111
}
else {
// 含六个阿尔法的块。
// 位编码000 = alpha_0, 001 = alpha_1, 其它值通过插值得到。
alpha_2 = (4 * alpha_0 + 1 * alpha_1 + 2) / 5; // 位编码010
alpha_3 = (3 * alpha_0 + 2 * alpha_1 + 2) / 5; // 位编码011
alpha_4 = (2 * alpha_0 + 3 * alpha_1 + 2) / 5; // 位编码100
alpha_5 = (1 * alpha_0 + 4 * alpha_1 + 2) / 5; // 位编码101
alpha_6 = 0; // 位编码110
alpha_7 = 255; // 位编码111
}
阿尔法块的内存布局如下所示:
字节 |
阿尔法 |
0 |
Alpha_0 |
1 |
Alpha_1 |
2 |
[0][2] (最低2位), [0][1], [0][0] |
3 |
[1][1] (最低1位), [1][0], [0][3], [0][2] (最高1位) |
4 |
[1][3], [1][2], [1][1] (最高2位) |
5 |
[2][2] (最低2位), [2][1], [2][0] |
6 |
[3][1] (最低1位), [3][0], [2][3], [2][2] (最低1位) |
7 |
[3][3], [3][2], [3][1] (最高2位) |
DXT4和DXT5之间的区别在于,在DXT4格式中,我们认为颜色数据已经预乘了阿尔法,而在DXT5格式中,我们认为颜色数据没有预乘阿尔法。之所以需要这两种是格式是因为大多数情况下,在使用一个纹理时,仅检查数据不足以确定颜色数据是否已经预乘了阿尔法。因为运行的时候需要这样的信息,所以就用两个四字符码(FOURCC)来区分这两种情况。但是,这两种格式使用的数据和插值方法是相同的。
为了检测texel是否是透明的,在DXT1中要对颜色进行比较,而DXT4和DXT5格式没有使用这种比较。我们认为在不需要颜色比较的情况下,颜色数据总是以四色模式进行处理。换句话说,DXT1代码中最上面的if语句应该是:
if ((color_0 > color_1) OR !DXT1) {
本节包含了有关压缩纹理格式的内部结构的信息。应用程序开发人员并不需要了解这些细节以使用压缩纹理,因为应用程序可以使用Direct3D扩展(D3DX)函数在压缩和未压缩纹理间进行转换。但是,如果应用程序需要直接对压缩表面中的数据进行操作,那么这些信息就很有用。
Microsoft? Direct3D?使用的压缩格式把纹理贴图分成4x4的texel块。如果纹理不包含透明度——也就是说是不透明的——或者透明度用一位阿尔法表示,那么就用一个8字节的块表示纹理贴图块。如果纹理贴图确实在阿尔法通道中包含了透明度信息,那么就用一个16字节的块表示纹理贴图块。
注意 任何单个纹理必须指定它的数据——每组16个texel——是以64位还是以128位存储的。如果纹理用了64位块——也就是DXT1格式,那么可以以块为单位在同一张纹理中混合使用不透明和一位阿尔法格式。换句话说,对每个由16个texel组成的块来说,对无符号整数color_0和color_1的比较是单独进行的。
在使用128位的块时,必须给整个纹理的阿尔法通道指定直接模式(DXT2或DXT3)或插值模式(DXT4或DXT5)。和颜色一样,当选择了插值模式时,可以以每一块为单位混合使用八阿尔法模式或六阿尔法模式。对alpha_0和alpha_1大小的比较仍然是以块为单位进行的。
当前DXTn格式的pitch与Microsoft DirectX? 7.0中返回的不同。该值现在指的是以块为单位的每一行的pitch。例如,如果应用程序有一个宽度为16的压缩纹理,那么pitch值会是四块(DXT1为4*8,DXT2-5为4*16)。
在应用程序创建一个渲染设备前,可以通过调用IDirect3D9::CheckDeviceFormat方法测定设备是否支持用压缩纹理进行纹理操作。该方法测定是否可以在设备上使用某种表面格式。
要测试适配器,应该把像素格式指定为DXT1, DXT2, DXT3, DXT4, 或DXT5四字符码。如果IDirect3D9::CheckDeviceFormat返回D3D_OK,那么设备可以从该格式的压缩纹理直接创建纹理。如果是这样,那么应用程序就可以通过调用IDirect3DDevice9::SetTexture方法,在Microsoft? Direct3D?中直接使用压缩纹理表面。以下示例代码显示了如何测定适配器是否支持压缩纹理格式。
BOOL IsCompressedTextureFormatOk( D3DFORMAT TextureFormat,
D3DFORMAT AdapterFormat ) {
HRESULT hr = pD3D->CheckDeviceFormat( D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
AdapterFormat,
0,
D3DRTYPE_TEXTURE,
TextureFormat);
return SUCCEEDED( hr );
}
如果设备不支持用压缩纹理表面进行纹理操作,那么应用程序仍然可以在压缩格式的表面中存储数据,但是必须在使用纹理之前,把任何压缩纹理转换为设备支持的格式。
在创建了一个支持压缩纹理格式的设备后,应用程序就可以创建压缩纹理资源了。只要调用IDirect3DDevice9::CreateTexture并在Format参数中指定一个压缩纹理格式即可。
在把图像载入纹理对象之前,应该调用IDirect3DTexture9::GetSurfaceLevel方法取得一个指向纹理表面的指针。
现在应用程序就可以调用任何D3DXLoadSurfacexxx函数,把图像载入先前用IDirect3DTexture9::GetSurfaceLevel取得的表面。
用DirectX?软件开发包(SDK)提供的DXTex工具,开发人员可以创建压缩纹理(DDS)文件并在不同的格式间进行转换。
这种方法的好处在于应用程序可以把压缩表面的内容存到文件中,而不必根据表面的格式以及纹理的宽度和高度计算所需的存储容量。
下表显示了五种压缩纹理类型。更多有关数据存储方式的信息,请参阅压缩纹理格式。开发人员只有在要写自己的压缩函数时才需要了解这些信息。
四字符码 |
描述 |
预乘阿尔法? |
DXT1 |
不透明/一位阿尔法 |
N/A |
DXT2 |
直接阿尔法 |
是 |
DXT3 |
直接阿尔法 |
否 |
DXT4 |
插值阿尔法 |
是 |
DXT5 |
插值阿尔法 |
否 |
注意 当应用程序要把数据从未预乘阿尔法格式传输到已预乘阿尔法格式时,Direct3D会根据阿尔法值缩放颜色值。Direct3D不支持把数据从已预乘阿尔法格式传输到未预乘阿尔法格式。如果应用程序试图把数据从已预乘阿尔法的源数据传输到未预乘阿尔法的目标数据时,被调用的方法会返回D3DERR_INVALIDCALL。如果应用程序要把数据从已预乘阿尔法的源数据传输到不含阿尔法的目标数据时,那么源数据中已预乘过阿尔法的颜色分量会被直接复制,不做任何改变。
正如对纹理表面的压缩,对压缩纹理的解压也是通过Direct3D的复制服务进行的。
要把压缩纹理复制到未压缩的纹理表面,应该使用函数D3DXLoadSurfaceFromSurface。这个函数会处理对表面的压缩和解压。
使用纹理时需要考虑的硬件问题
当前的硬件并不一定支持Microsoft? Direct3D?接口提供的全部功能。应用程序必须测试用户的硬件并相应地调整渲染策略。
许多三维加速卡不支持把经过迭代的漫反射色作为混合单元的参数,但是,应用程序可以在进行纹理混合时使用经过迭代的颜色数据。
一些三维硬件的第一层纹理可能没有相应的纹理混合层,在此类适配器上,应用程序应该在当前纹理集合的第二和第三纹理层进行混合操作。
因为当前大多数硬件的限制,很少有适配器可以通过IDirect3DDevice9提供的多重纹理混合接口执行三线性mipmap插值。应用程序可以用多趟纹理混合达到相同的效果,或者降级使用被广为支持的D3DTEXF_POINT mipmap过滤模式。
立体纹理资源
立体纹理是三维像素(texel)的集合,可以用来绘制诸如三角形或线之类的二维图元。对于每个使用立体纹理的图元来说,图元的每个顶点需要包含三元素纹理坐标。在绘制图元时,图元覆盖的每个像素的颜色来自立体纹理中的某个像素,这和二维纹理中的情况相一致。因为不存在可以用立体纹理进行绘制的真正的三维图元,所以立体纹理不能直接用来渲染。
应用程序可以把立体纹理用于诸如patchy fog,爆炸等特效。
立体纹理由多片组织而成,可以把它想象成把大小为width x height的二维表面叠在一起形成的大小为width x height x depth的立体纹理。每片为一行。立体纹理可以有后续的级,每一级的大小为前一级大小的一半。下图给出了一个多级立体纹理看起来的样子。
创建立体纹理
以下示例代码显示了使用立体纹理所需的步骤。
首先,定义一个包含三个纹理坐标的自定义顶点类型,如以下示例代码所示。
struct VOLUMEVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT tu, tv, tw;
};
#define D3DFVF_VOLUMEVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|
D3DFVF_TEX1|D3DFVF_TEXCOORDSIZE3(0))
然后,把数据填入顶点。
VOLUMEVERTEX g_vVertices[4] =
{
{ 1.0f, 1.0f, 0.0f, 0xffffffff, 1.0f, 1.0f, 0.0f },
{-1.0f, 1.0f, 0.0f, 0xffffffff, 0.0f, 1.0f, 0.0f },
{ 1.0f,-1.0f, 0.0f, 0xffffffff, 1.0f, 0.0f, 0.0f },
{-1.0f,-1.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 0.0f }
};
现在,创建一个顶点缓存,并填入顶点数据。
下一步是用IDirect3DDevice9::CreateVolumeTexture创建一个立体纹理,如以下示例代码所示。
LPDIRECT3DVOLUMETEXTURE9 pVolumeTexture;
d3dDevice->CreateVolumeTexture( 8, 4, 4, 1, 0, D3DFMT_R8G8B8,D3DPOOL_MANAGED,
&pVolumeTexture );
在渲染图元之前,把当前纹理设为前面创建的立体纹理。以下示例代码显示了渲染一个三角形带的整个过程。
if( SUCCEEDED( d3dDevice->BeginScene() ) )
{
// 用立体纹理绘制四边形。
d3dDevice->SetTexture( 0, pVolumeTexture );
d3dDevice->SetFVF( D3DFVF_VOLUMEVERTEX );
d3dDevice->SetStreamSource( 0, pVB, sizeof(VOLUMEVERTEX) );
d3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2);
// 结束绘制。
d3dDevice->EndScene();
}