我们的Demo变得开始有趣,但真实世界的对象的细节通常比每个顶点颜色可以捕捉到的更多。纹理映射是一种将图像数据映射到三角网格的技术,从而增加场景的细节和真实感。如,我们可以通过在每一边映射一个箱子纹理来构建一个立方体,并把它变成一个箱子(图8.1)。
学习目标:
1.了解如何指定映射到三角网格的纹理。
2.了解如何创建和启用纹理。
3.了解如何过滤纹理以创建更平滑的图像。
4.发现如何用地址模式多次平铺纹理。
5.了解如何将多个纹理结合起来以创建新的纹理和特殊效果。
6.学习如何通过纹理动画创建一些基本的效果。
图8.1 箱子演示创建一个带有箱子纹理的立方体。
8.1纹理和资源重构
其实自从第4章以来,我们已经在使用纹理了; 特别是深度缓冲区和后台缓冲区是由ID3D11Texture2D接口表示的2D纹理对象。为了便于参考,在第一部分中,我们回顾了第4章中已经介绍的很多材质。
2D纹理是数据元素的矩阵。二维纹理的一个用途是存储二维图像数据,纹理中的每个元素存储像素的颜色。但是,这不是唯一的用法; 例如,在称为法线贴图的高级技术中,纹理中的每个元素都存储一个3D矢量,而不是一个颜色。因此,虽然通常认为纹理是存储图像数据,但它们实际上用途更广。一维纹理(ID3D11Texture1D)就像一个数据元素的一维数组,3D纹理(ID3D11Texture3D)就像一个3D数组元素。1D,2D和3D纹理接口都从ID3D11Resource继承。
正如本章后面将讨论的那样,纹理不仅仅是数据的数组, 他们可以有mipmap级别和GPU可以对它们进行特殊操作,如应用滤波和多重采样。但是,纹理不是任意的数据块; 它们只能存储某些类型的数据格式,这些数据格式由DXGI_FORMAT枚举类型描述。一些示例格式是:
1 . DXGI_FORMAT_R32G32B32_FLOAT:每个元素有三个32位浮点组件。
2 . DXGI_FORMAT_R16G16B16A16_UNORM:每个元素都有四个映射到[0,1]范围的16位组件。
3 . DXGI_FORMAT_R32G32_UINT:每个元素有两个32位无符号整数分量。
4 . DXGI_FORMAT_R8G8B8A8_UNORM:每个元素有四个8位无符号分量映射到[0,1]范围。
5 . DXGI_FORMAT_R8G8B8A8_SNORM:每个元素有四个8位有符号分量映射到[-1,1]范围。
6 . DXGI_FORMAT_R8G8B8A8_SINT:每个元素有四个8位有符号整数分量映射到[-128,127]范围。
7 . DXGI_FORMAT_R8G8B8A8_UINT:每个元素有四个映射到[0,255]范围的8位无符号整数分量。
请注意,R,G,B,A字母分别代表红色,绿色,蓝色和alpha。 但是,之前讲过,纹理不需要存储颜色信息; 例如格式
具有三个浮点组件,因此可以存储具有浮点坐标的3D矢量(不一定是颜色矢量)。 也有无类型的格式,我们只保留内存数据,然后指定在纹理绑定到渲染管线时,如何在以后重新解释数据(类似于强制转换); 例如,以下无类型格式保留具有四个8位分量的元素,但不指定数据类型(例如,整数,浮点,无符号整数):
纹理可以被绑定到渲染管线的不同阶段; 一个常见的例子是使用纹理作为渲染目标(即,Direct3D绘制到纹理中)和作为着色器资源(即纹理将在着色器中被采样)。 为这两个目的创建的纹理资源将被赋予绑定标志:
表明纹理将被绑定到两个流水线阶段。其实资源并不是直接绑定到流水线阶段,而是将其关联的资源视图绑定到不同的流水线阶段。对于每种我们要使用纹理的方式,Direct3D都要求我们在初始化时创建一个纹理的资源视图。这主要是为了提高效率,正如SDK文档指出的那样:“这允许在创建视图时在运行时和驱动程序中进行验证和映射,在绑定时最小化类型检查。”因此,使用纹理作为呈现的示例目标和着色器资源,我们需要创建两个视图:一个渲染目标视图( ID3D11RenderTargetView)和一个着色器资源视图( ID3D11ShaderResourceView)。资源视图基本上做了两件事:它们告诉Direct3D资源将被如何使用(即,你将绑定到哪个管道阶段),如果资源格式在创建时被指定为无类型,那么现在我们必须声明在创建视图时键入。因此,对于无类型格式,纹理的元素可能在一个流水线阶段被视为浮点值,在另一个流水线阶段被视为整数;这基本上等于重新解释数据的转换。
NOTE:8月份的SDK文档说:“创建完全类型的资源会将资源限制为其创建的格式。 这使得运行时可以优化访问[…]。“因此,如果你真的需要,你应该只创建一个无类型的资源。 否则,创建一个完全类型的资源。
为了创建资源的特定视图,必须使用该特定的绑定标志创建资源。 例如,如果资源未使用D3D11_BIND_SHADER_RESOURCE绑定标志创建(表示纹理将作为深度/模板缓冲区绑定到管道),则我们无法为该资源创建一个ID3D11ShaderResourceView。 如果你尝试,你应该得到像下面这样的Direct3D调试错误:
D3D11: ERROR: ID3D11Device::CreateShaderResourceView: A ShaderResourceView cannot be created of a Resource that did not specify the D3D11_BIND_SHADER_RESOURCE BindFlag.
在本章中,我们只会将纹理绑定为着色器资源,以便像素着色器可以对纹理进行采样并使用它们对像素着色。
8.2纹理坐标
Direct3D使用一个纹理坐标系统,该系统由一个水平方向的u轴和一个垂直方向的v轴组成。坐标(u,v)中0≤u,v≤1,标识纹理上的纹理元素。注意,v轴“向下”为正(见图8.2)。另外,请注意标准化的坐标区间[0,1],创造了一个与尺寸无关的范围,如,无论实际纹理尺寸是256×256,512×1024还是2048×2048像素,(0.5,0.5)总是指定中间纹理。同样,(0.25,0.75)表示纹理在水平方向上总宽度的四分之一处,在垂直方向上总高度的四分之三处。目前,纹理坐标总在[0,1]范围内,但是稍后将解释当超出此范围时会发生什么。
图8.2 纹理坐标系统,有时称为纹理空间。
对于每个3D三角形,我们要在要映射到3D三角形的纹理上定义一个相应的三角形(参见图8.3)。假设 p0,p1 p 0 , p 1 和 p2 p 2 是具有相应纹理坐标 q0,q1 q 0 , q 1 和 q2 q 2 的3D三角形的顶点。对于3D三角形上的任意一点(x,y,z),其纹理坐标(u,v)通过在三维三角形上用相同的s,t参数对顶点纹理坐标进行线性插值来求解;那就是,如果
图8.3 左边是三维空间中的一个三角形,右边我们定义了要映射到三角形上的纹理上的二维三角形。
如果 s≥0,t≥0,s+t≤1 s ≥ 0 , t ≥ 0 , s + t ≤ 1 则有:
这样,三角形上的每一个点都有相应的纹理坐标。
因此,我们再次修改顶点结构并添加一对纹理坐标来标识纹理上的一个点。现在每个3D顶点都有相应的2D纹理顶点。因此,由三个顶点定义的每个3D三角形也定义了纹理空间中的二维三角形(即,我们已经为每个三角形关联了2D纹理三角形)。
// Basic 32-byte vertex structure.
struct Basic32
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex;
};
const D3D11_INPUT_ELEMENT_DESC InputLayoutDesc::Basic32[3] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12,
D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24,
D3D11_INPUT_PER_VERTEX_DATA, 0}
};
NOTE:您可以在2D纹理三角形与3D三角形大不相同的情况下创建“畸形”纹理映射。 因此,当2D纹理被映射到3D三角形时,会出现大量的拉伸和变形,使得结果看起来不好。 例如,将锐角三角形映射到直角三角形需要拉伸。 一般来说,纹理失真应该被最小化,除非纹理艺术家需要失真外观。
注意到在图8.3中,我们将整个纹理图像映射到立方体的每个面上。这绝不是必需的。 我们可以只将纹理的一个子集映射到几何图形上。事实上,我们可以在一个大的纹理(这被称为纹理贴图)上放几个不相关的图像,并将其用于几个不同的对象(图8.4)。纹理坐标决定纹理的哪一部分被映射到三角形上。
图8.4 在一个大纹理上存储四个子纹理的纹理图集。 设置每个顶点的纹理坐标,以便将纹理的所需部分映射到几何体。
8.3创建和启用一个纹理
纹理数据通常从存储在磁盘上的图像文件中读取并加载到ID3D11Texture2D对象中(请参阅D3DX11CreateTextureFromFile)。但是,纹理资源不会直接绑定到渲染管道。 相反,您将创建着色器资源视图(ID3D11ShaderResourceView)到纹理,然后将视图绑定到管道。所以需要采取两个步骤:
1.调用D3DX11CreateTextureFromFile从存储在磁盘上的映像文件创建ID3D11Texture2D对象。
2.调用ID3D11Device :: CreateShaderResourceView为纹理创建相应的着色器资源视图。
以下D3DX功能可以同时完成这两个步骤:
HRESULT D3DX11CreateShaderResourceViewFromFile(
ID3D11Device *pDevice,
LPCTSTR pSrcFile,
D3DX11_IMAGE_LOAD_INFO *pLoadInfo,
ID3DX11ThreadPump *pPump,
ID3D11ShaderResourceView **ppShaderResourceView,
HRESULT *pHResult
);
1 . pDevice:指向D3D设备创建纹理的指针。
2 . pSrcFile:要加载的图像的文件名。
3 . pLoadInfo:可选的图像信息; 指定null以使用源图像中的信息。 例如,如果我们在这里指定null,那么源图像尺寸将被用作纹理尺寸; 也会生成完整的mipmap链(第8.4.2节)。这通常是一个很好的默认选择。
4 . pPump:用于产生一个新的线程来加载资源。要在主线程中加载资源,请指定null。在本书中,我们将一直指定null。
5 . ppShaderResourceView:返回指向从文件加载的纹理创建的着色器资源视图的指针。
6 . pHResult:如果为pPump指定了null,则指定null。
此功能可以加载以下任何图像格式:BMP,JPG,PNG,DDS,TIFF,GIF和WMP(请参阅D3DX11_IMAGE_FILE_FORMAT)。
NOTE:有时我们会将纹理及其相应的着色器资源视图互换为可交换的。例如,我们可能会说我们正在将纹理绑定到管线上,即使我们真的在约束它的视图。
例如,要从名为WoodCreate01.dds的图像创建纹理,我们将写入以下内容:
ID3D11ShaderResourceView* mDiffuseMapSRV;
HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,
L"WoodCrate01.dds", 0, 0, &mDiffuseMapSRV, 0 ));
加载纹理后,需要将其设置为一个效果变量,以便它可以在像素着色器中使用。 .fx文件中的2D纹理对象由Texture2D类型表示; 例如,我们在效果文件中声明一个纹理变量,如下所示:
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
如注释,纹理对象放置在常量缓冲区之外。我们可以从我们的C ++应用程序代码中获得一个效果的Texture2D对象(这是一个着色器资源变量)的指针,如下所示:
ID3DX11EffectShaderResourceVariable* DiffuseMap;
fxDiffuseMap = mFX->GetVariableByName("gDiffuseMap")->AsShaderResource();
一旦我们获得了效果的Texture2D对象的指针,我们可以通过C ++接口来更新它,如下所示:
// Set the C++ texture resource view to the effect texture variable.
fxDiffuseMap->SetResource(mDiffuseMapSRV);
与其他效应变量一样,如果我们需要在绘制调用之间改变它们,我们必须调用Apply:
// set crate texture
fxDiffuseMap->SetResource(mCrateMapSRV);
pass->Apply(0, md3dImmediateContext);
DrawCrate();
// set grass texture
fxDiffuseMap->SetResource(mGrassMapSRV);
pass->Apply(0, md3dImmediateContext);
DrawGrass();
// set brick texture
fxDiffuseMap->SetResource(mBrickMapSRV);
pass->Apply(0, md3dImmediateContext);
DrawBricks();
纹理图集可以提高性能,因为它可以通过一次绘制调用来绘制更多的几何图形。例如,假设我们使用了如图8.3所示的纹理地图集,它包含板条箱,草地和砖块纹理。然后通过将每个对象的纹理坐标调整到相应的子纹理,我们可以在一个绘制调用中绘制几何图形(假设每个对象不需要改变其他参数):
// set texture atlas
fxDiffuseMap->SetResource(mAtlasSRV);
pass->Apply(0, md3dImmediateContext);
DrawCrateGrassAndBricks();
由于绘制调用有开销,所以希望用这样的技术来最小化它们。
NOTE:纹理资源实际上可以被任何着色器(顶点,几何或像素着色器)使用。 现在,我们只是在像素着色器中使用它们。 正如我们所提到的,纹理本质上是特殊的数组,所以不难想象数组数据在顶点和几何着色器程序中也可能是有用的。
8.4 滤波器
8.4.1放大倍率
纹理贴图的元素应该被认为是连续图像中的离散颜色样本;不应该被认为是矩形区域。所以问题是:如果我们的纹理坐标(u,v)与texel点之一不一致,会发生什么?这可能发生在以下情况。假设玩家放大场景中的墙壁,使墙壁覆盖整个屏幕。例如,假设显示器分辨率为1024×1024,墙壁的纹理分辨率为256×256。这导致纹理放大 - 我们试图用几个像素来覆盖许多像素。在我们的例子中,每个纹理点之间有四个像素。当顶点纹理坐标在三角形内插时,每个像素将被赋予一对唯一的纹理坐标。因此,将会有纹理坐标与像素点之一不一致的像素。给定纹理上的颜色,我们可以用插值近似纹理像素间的颜色。插补图形硬件有两种支持方法:常量插值和线性插值。一般情况下总是使用线性插值。
图8.5说明了1D中的这些方法:假设我们有256个样本的一维纹理和一个内插纹理坐标u = 0.126484375。 这个归一化的纹理坐标是指0.126484375×256 = 32.38的纹理。 当然,这个值位于我们的两个texel样本之间,所以我们必须使用插值来逼近它。
2D线性插值被称为双线性插值,如图8.6所示。 给定四个纹元之间的一对纹理坐标,我们在u方向上进行两个1D线性插值,然后在v方向上进行一个1D插值。
图8.5 (a)给定纹理点,我们构造一个分段常数函数来逼近纹理点之间的值; 这有时被称为最近邻点采样,因为使用了最近的texel点的值。 (b)给定纹理点,我们构造一个分段线性函数来逼近纹理点之间的值。
图8.6。 这里我们有四个texel点 cij,ci,j+1,ci+1,j c i j , c i , j + 1 , c i + 1 , j 和 ci+1,j+1 c i + 1 , j + 1 。 我们要用插值逼近这四个纹理点之间的c的颜色, 在这个例子中,c位于 cij c i j 的右边0.75个单位和 cij c i j 的0.38个单位。 我们首先在顶部两种颜色之间进行一维线性插值来获得 cT c T 。 同样,我们在底部两种颜色之间进行一维线性插值以得到 cB c B 。 最后,我们在 cT c T 和 cB c B 之间做一维线性插值得到c。
图8.7显示了常数和线性插值之间的差异。 正如你所看到的,常数插值会创建块状图像特性。线性插值更平滑,但仍不会像我们有实际数据(例如,更高分辨率的纹理)而不是通过插值导出的数据一样好。
图8.7 我们放大带有箱子纹理的立方体,使缩放发生。在左边我们使用常量插值,这会导致块状外观; 因为插值函数具有不连续性(图8.5a),这使得变化突然而不平滑。 在右侧,我们使用线性差值,由于插值函数的连续性,这使图像更平滑。
关于这个讨论的一点需要注意的是,在虚拟眼睛可以自由移动和探索的交互式3D程序中,没有真正的放大的方法。从一定距离,纹理看起来很好,但随着眼睛离靠近,就会开始分解。有些游戏限制了虚拟眼睛接近表面以避免过度放大。使用更高分辨率的纹理可以改善该状况。
NOTE:在纹理的情况下,使用常量插值来查找纹理元素之间纹理坐标的纹理值也称为点过滤。并且使用线性插值来查找纹元之间的纹理坐标的纹理值也称为线性滤波。点和线性过滤是Direct3D使用的术语。
8.4.2缩小
缩小与放大相反。缩小中大量纹理像素被映射到少量像素。例如,有一个256×256纹理贴图的墙。看着墙壁的眼睛不停地往后移动,使墙壁变得越来越小,直到屏幕上只覆盖了64×64像素。所以现在我们有256×256像素映射到64×64的屏幕像素。在这种情况下,像素的纹理坐标通常不会与纹理贴图的任何纹理元素重合,所以常数和线性插值滤波器仍然适用于缩小情况。还有更多方法可以完成缩小。直观地说,256×256纹理像素的平均下采样应该被降低到64×64。mipmapping技术提供了一个有效的近似值,但是牺牲了一些额外的内存。在初始化时(或资产创建时),纹理的较小版本是通过对图像进行降采样来创建一个mipmap链(见图8.8)。因此,平均工作是为mipmap大小预先计算的。在运行时,图形硬件将根据程序员指定的mipmap设置执行两种不同的操作:
图8.8 一连串的mipmap; 每个连续的mipmap是以前的mipmap细节级别的每个维度的一半大小,直到1×1。
1.选取并使用最适合纹理的投影屏幕几何分辨率的mipmap级别,根据需要应用常量或线性插值。这就是所谓的mipmap的点过滤,因为它就像常量插值 - 你只需选择最接近的mipmap级别并将其用于纹理化。
2.挑选最接近纹理投影屏幕几何分辨率的两个最接近的mipmap级别(一个会更大,一个会小于屏幕几何分辨率)。接下来,对这两个mipmap级别应用常量或线性过滤,以便为每个纹理颜色生成纹理颜色。最后,在这两个纹理颜色结果之间进行插值。这就是所谓的mipmap的线性滤波,因为它就像线性插值 - 在两个最接近的mipmap级别之间进行线性插值。通过从mipmap链中选择最佳的纹理细节级别,大大减少了缩小量。
8.4.2.1 创建Mipmap
Mipmap级别可以由艺术家直接创建,也可以通过过滤算法创建。
一些像.DDS(DirectDraw Surface格式)这样的图像文件格式可以直接在数据文件中存储mipmap级别;在这种情况下,数据只需要被读取,不需要运行时来计算mipmap级别。 DirectX纹理工具可以生成纹理的mipmap链,并将其导出到.DDS文件。如果图像文件不包含完整的mipmap链,则D3DX11CreateShaderResourceViewFromFile或D3DX11CreateTextureFromFile将使用某种指定的过滤算法(请参阅D3DX11_IMAGE_LOAD_INFO,尤其是SDK文档中的MipFilter数据成员)为您创建mipmap链。因此我们看到mipmapping基本上是自动的。如果源文件还没有包含,D3DX11函数会自动为我们生成一个mipmap链。只要启用了mipmapping,硬件将在运行时选择正确的mipmap级别。
NOTE:有些下采样时一个通用的过滤算法不保留我们想要的细节。例如,在图8.8中,写在箱子上的文字“Direct 3D”在较低的mip级别上变得模糊。如果这不可接受,艺术家总是可以手动创建/调整mip级别以保持重要的细节。
8.4.3各向异性过滤
另一种可以使用的滤波器称为各向异性滤波。 此滤镜有助于减轻多边形的法线矢量与相机外观矢量之间的角度较宽时(例如,当多边形与视图窗口正交时)发生的失真。此滤镜是最昂贵的,但可以付出代价 纠正失真伪影。 图8.9显示了比较各向异性滤波和线性滤波的截图。
图8.9 板条箱的顶面几乎与观察窗正交。 (左)使用线性过滤,箱子的顶部严重模糊。 (右)各向异性过滤在从这个角度渲染箱子的顶面方面做得更好。
8.5采样纹理
我们看到一个Texture2D对象表示效果文件中的纹理。但是,另一个与纹理相关联的对象称为SamplerState对象(或采样器)。采样器对象是我们定义过滤器以用于纹理的地方。 这里有些例子:
// Use linear filtering for minification, magnification, and mipmapping.
SamplerState mySampler0
{
Filter = MIN_MAG_MIP_LINEAR;
};
// Use linear filtering for minification, point filtering for magnification,
// and point filtering for mipmapping.
SamplerState mySampler1
{
Filter = MIN_LINEAR_MAG_MIP_POINT;
};
// Use point filtering for minification, linear filtering for magnification,
// and point filtering for mipmapping.
SamplerState mySampler2
{
Filter = MIN_POINT_MAG_LINEAR_MIP_POINT;
};
// Use anisotropic interpolation for minification, magnification,
// and mipmapping.
SamplerState mySampler3
{
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
};
请注意,对于各向异性过滤,我们必须指定最大的各向异性,这是一个从1到16的数字。较大的值更昂贵,但可以给出更好的结果。您可以从这些示例中找出其他可能的排列,或者可以在SDK文档中查找D3D11_FILTER枚举类型。我们将在本章稍后看到其他属性与采样器相关联,但现在这是我们第一个演示所需要的。
现在,在像素着色器中给出一对像素的纹理坐标,我们实际上使用以下语法对纹理进行采样:
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
SamplerState samAnisotropic
{
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
};
float4 PS(VertexOut pin, uniform int gLightCount) : SV_Target
{
float4 texColor = gDiffuseMap.Sample(samAnisotropic, pin.Tex);
…
正如您所看到的,要对纹理进行采样,我们使用Texture2D :: Sample方法。 我们传递一个SamplerState对象作为第一个参数,然后传入第二个参数的像素(u,v)纹理坐标。 此方法使用SamplerState对象指定的过滤方法,从指定的(u,v)点的纹理贴图中返回内插的颜色。
NOTE:HLSL类型SamplerState镜像接口ID3D11SamplerState。 采样器状态也可以使用ID3DX11EffectSamplerVariable :: SetSampler在应用程序级别进行设置。 请参阅D3D11_SAMPLER_DESC和ID3D11Device :: CreateSamplerState。与渲染状态一样,采样器状态应该在初始化时创建。
8.6纹理和材料
为了将纹理整合到我们的材质/照明系统中,通常使用环境和漫射照明术语来调整纹理颜色,但不能使用镜面光照术语(这通常称为“用延迟添加进行调制”):
// Modulate with late add.
litColor = texColor*(ambient + diffuse) + spec;
这种修改提供了每个像素的环境和漫射材料值,其提供比每个对象材料更精细的分辨率,因为许多纹理元素通常覆盖三角形。也就是说,每个像素都会得到内插的纹理坐标(u,v); 然后使用这些纹理坐标对纹理进行采样以获得对该像素的材料描述有贡献的颜色。
8.7箱子DEMO
我们现在回顾一下将一个箱子纹理添加到一个立方体的关键点(如图8.1所示)。
8.7.1指定纹理坐标
GeometryGenerator :: CreateBox生成框的纹理坐标,以便将整个纹理图像映射到框的每个面上。为了简洁起见,我们只显示前面,后面和上面的顶点定义。还要注意,我们省略了顶点构造函数中法线和切线向量的坐标(纹理坐标用粗体表示)。
void GeometryGenerator::CreateBox(float width, float height, float depth,
MeshData& meshData)
{
Vertex v[24];
float w2 = 0.5f*width;
float h2 = 0.5f*height;
float d2 = 0.5f*depth;
// Fill in the front face vertex data.
v[0] = Vertex(-w2, -h2, -d2, …, 0.0f, 1.0f);
v[1] = Vertex(-w2, +h2, -d2, …, 0.0f, 0.0f);
v[2] = Vertex(+w2, +h2, -d2, …, 1.0f, 0.0f);
v[3] = Vertex(+w2, -h2, -d2, …, 1.0f, 1.0f);
// Fill in the back face vertex data.
v[4] = Vertex(-w2, -h2, +d2, …, 1.0f, 1.0f);
v[5] = Vertex(+w2, -h2, +d2, …, 0.0f, 1.0f);
v[6] = Vertex(+w2, +h2, +d2, …, 0.0f, 0.0f);
v[7] = Vertex(-w2, +h2, +d2, …, 1.0f, 0.0f);
// Fill in the top face vertex data.
v[8] = Vertex(-w2, +h2, -d2, …, 0.0f, 1.0f);
v[9] = Vertex(-w2, +h2, +d2, …, 0.0f, 0.0f);
v[10] = Vertex(+w2, +h2, +d2, …, 1.0f, 0.0f);
v[11] = Vertex(+w2, +h2, -d2, …, 1.0f, 1.0f);
参考图8.3,如果你需要帮助,看看为什么用这种方式指定纹理坐标。
8.7.2创建纹理
我们在初始化时从文件(技术上来说是着色器资源视图到纹理)创建纹理,如下所示:
// CrateApp data members
ID3D11ShaderResourceView* mDiffuseMapSRV;
bool CrateApp::Init()
{
if(!D3DApp::Init())
return false;
// Must init Effects first since InputLayouts depend
// on shader signatures.
Effects::InitAll(md3dDevice);
InputLayouts::InitAll(md3dDevice);
HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,
L"Textures/WoodCrate01.dds", 0, 0, &mDiffuseMapSRV, 0));
BuildGeometryBuffers();
return true;
}
8.7.3设置纹理
纹理数据通常在像素着色器中进行访问。为了使像素着色器能够访问它,我们需要将纹理视图(ID3D11ShaderResourceView)设置为.fx文件中的Texture2D对象。如下:
// Member of BasicEffect.
ID3DX11EffectShaderResourceVariable* DiffuseMap;
// Get pointers to effect file variables.
DiffuseMap = mFX->GetVariableByName("gDiffuseMap")->AsShaderResource();
void BasicEffect::SetDiffuseMap(ID3D11ShaderResourceView* tex)
{
DiffuseMap->SetResource(tex);
}
// [.FX code]
// Effect file texture variable.
Texture2D gDiffuseMap;
8.7.4更新的基本效果
下面是修改后的Basic.fx文件,现在支持纹理(纹理代码已加粗):
//=====================================================================
// Basic.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Basic effect that currently supports transformations, lighting,
// and texturing.
//=====================================================================
#include "LightHelper.fx"
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
float4x4 gTexTransform;
Material gMaterial;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
SamplerState samAnisotropic
{
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
AddressU = WRAP;
AddressV = WRAP;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Output vertex attributes for interpolation across triangle.
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
float4 PS(VertexOut pin, uniform int gLightCount, uniform bool gUseTexure) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// The toEye vector is used in lighting.
float3 toEye = gEyePosW - pin.PosW;
// Cache the distance to the eye from this surface point.
float distToEye = length(toEye);
// Normalize.
toEye /= distToEye;
// Default to multiplicative identity.
float4 texColor = float4(1, 1, 1, 1);
if(gUseTexure)
{
// Sample texture.
texColor = gDiffuseMap.Sample(samAnisotropic, pin.Tex);
}
//
// Lighting.
//
float4 litColor = texColor;
if(gLightCount > 0)
{
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// Sum the light contribution from each light source.
[unroll]
for(int i = 0; i < gLightCount; ++i)
{
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLights[i], pin.NormalW, toEye, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
// Modulate with late add.
litColor = texColor*(ambient + diffuse) + spec;
}
// Common to take alpha from diffuse material and texture.
litColor.a = gMaterial.Diffuse.a * texColor.a;
return litColor;
}
technique11 Light1
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(1, false)));
}
}
technique11 Light2
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(2, false)));
}
}
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(3, false)));
}
}
technique11 Light0Tex
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(0, true)));
}
}
technique11 Light1Tex
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(1, true)));
}
}
technique11 Light2Tex
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(2, true)));
}
}
technique11 Light3Tex
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(3, true)));
}
}
通过使用统一参数gUseTexture,观察Basic.fx具有纹理和不具有纹理的技术。这样,如果我们需要渲染一些不需要纹理的东西,我们选择没有它的技术,因此,不需要花费纹理的代价。同样,我们选择具有所使用光源数量的技术,这样我们就不用支付我们不需要的额外光照计算成本。
我们没有讨论过的一个常量缓冲区变量是gTexTransform。 该变量用于顶点着色器来转换输入纹理坐标:
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
纹理坐标是纹理平面中的二维点。 因此,我们可以像翻译其他任何一点一样翻译,旋转和缩放它们。 在这个演示中,我们使用单位矩阵变换,以便输入纹理坐标保持不变。 但是,正如我们将在§9.9中看到的那样,通过转换纹理坐标可以获得一些特殊的效果。 请注意,要将2D纹理坐标转换为4×4矩阵,我们将其扩展为4维矢量:
vin.Tex ---> float4(vin.Tex, 0.0f, 1.0f)
在乘法完成之后,通过丢弃z和w分量将得到的4D向量投回到2D向量。这就是
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
8.8地址模式
结合常数或线性插值的纹理定义矢量值函数T(u,v)=(r,g,b,a)。 也就是说,给定纹理坐标
(u,v)∈[0,1]2
(
u
,
v
)
∈
[
0
,
1
]
2
,纹理函数T返回一个颜色(r,g,b,a)。 Direct3D允许我们以四种不同的方式(称为地址模式)来扩展此函数的域:wrap, border color, clamp, 和 mirror。
1 . wrap:通过在每个整数交点处重复图像来扩展纹理函数(见图8.10)。
2 . border color:通过将不在[0,1] 2中的每个(u,v)映射到由程序员指定的某种颜色来扩展纹理函数(参见图8.11)。
3 . clamp:将不在
[0,1]2
[
0
,
1
]
2
中的每个(u,v)映射到颜色
T(u0,v0)
T
(
u
0
,
v
0
)
来扩展纹理函数,其中
(u0,v0)
(
u
0
,
v
0
)
是包含(u,v) 在
[0,1]2
[
0
,
1
]
2
中(见图8.12)。
4 .mirror:通过在每个整数交点处映射镜像来扩展纹理函数(见图8.13)。
图8.10 Warp地址模式
图8.11 Border color地址模式
图8.12 Clamp地址模式
图8.13 mirror地址模式
必须指定一个地址模式(warp模式是默认模式),因此,能定义[0,1]范围之外的纹理坐标。
warp地址模式可能是最常用的; 它允许我们在一些表面上重复平铺纹理。这有效地使我们能够在不提供额外数据的情况下增加纹理分辨率(尽管额外的分辨率是重复的)。通过平铺,通常纹理是无缝的。例子中箱子纹理不是无缝的,因为你可以清楚地看到重复。但图8.14显示了一个重复2×3次的无缝砖结构。
采样器对象中指定了地址模式。 以下示例用于创建图8.10-8.13:
SamplerState samTriLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
SamplerState samTriLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = BORDER;
AddressV = BORDER;
// Blue border color
BorderColor = float4(0.0f, 0.0f, 1.0f, 1.0f);
};
SamplerState samTriLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
SamplerState samTriLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = MIRROR;
AddressV = MIRROR;
};
图8.14 砖纹理平铺2×3次。由于纹理是无缝的,重复模式很难注意到。
NOTE:可以独立控制u方向和v方向的地址模式。可以试试这个。
8.9变换纹理
纹理坐标表示纹理平面中的二维点。因此,我们可以像变换其他任何一点一样平移,旋转和缩放它们。以下是一些用于转换纹理的示例:
1 . 砖的贴图沿着墙壁平铺。当前墙壁顶点的纹理坐标在[0,1]的范围内。我们将纹理坐标缩放4倍,将它们缩放到[0,4]的范围,以便纹理将在墙上重复四次。
2 . 我们有云彩纹理在晴朗的蓝天上平铺。通过将纹理坐标变换设置为时间的函数,云彩在天空活动起来。
3 . 纹理旋转可以用于类似粒子的效果,如,我们随着时间的推移旋转火球纹理。
纹理坐标变换就像常规转换一样完成。我们指定一个变换矩阵,并将纹理坐标向量乘以矩阵。 例如:
// Constant buffer variable
float4x4 gTexMtx;
// In shader program
vOut.texC = mul(float4(vIn.texC, 0.0f, 1.0f), gTexMtx);
注意,由于我们正在处理二维纹理坐标,因此我们只关心对前两个坐标进行的转换。例如,如果纹理矩阵转换了z坐标,则不会影响生成的纹理坐标。
8.10纹理的山和水波Demo
在此Demo中,我们添加纹理到地形和水。第一个关键问题是在地形上铺上草地纹理。因为地形网格是一个很大的表面,所以如果我们简单地在它上面延伸一个纹理,那么每个三角形就会包含太少的纹理。换句话说,表面没有足够的纹理分辨率; 纹理会被放大。因此,我们重复地面网格上的草地纹理来获得更高的分辨率。第二个关键问题是将水纹理作为时间的函数在水面网格上滚动。这个使得水更逼真。图8.15显示了Demo的截图。
图8.15 “Land Tex”Demo的屏幕截图
8.10.1网格纹理坐标生成
图8.16显示了xz平面中的一个
m×n
m
×
n
网格和归一化纹理空间域
[0,1]2
[
0
,
1
]
2
中的对应网格。从图中可以清楚地看出,第
ij
i
j
个网格顶点在xz平面上的纹理坐标是第
ij
i
j
个网格顶点在纹理空间中的坐标。第
ij
i
j
个顶点的纹理空间坐标是:
显而易见 Δu=1n−1,Δv=1m−1 Δ u = 1 n − 1 , Δ v = 1 m − 1 。
因此,我们使用下面的代码在GeometryGenerator :: CreateGrid方法中为网格生成纹理坐标:
void GeometryGenerator::CreateGrid(float width, float depth, UINT m, UINT n, MeshData& meshData)
{
UINT vertexCount = m*n;
UINT faceCount = (m-1)*(n-1)*2;
//
// Create the vertices.
//
float halfWidth = 0.5f*width;
float halfDepth = 0.5f*depth;
float dx = width / (n-1);
float dz = depth / (m-1);
float du = 1.0f / (n-1);
float dv = 1.0f / (m-1);
meshData.Vertices.resize(vertexCount);
for(UINT i = 0; i < m; ++i)
{
float z = halfDepth - i*dz;
for(UINT j = 0; j < n; ++j)
{
float x = -halfWidth + j*dx;
meshData.Vertices[i*n+j].Position =
XMFLOAT3(x, 0.0f, z);
meshData.Vertices[i*n+j].Normal =
XMFLOAT3(0.0f, 1.0f, 0.0f);
meshData.Vertices[i*n+j].TangentU =
XMFLOAT3(1.0f, 0.0f, 0.0f);
// Stretch texture over grid.
meshData.Vertices[i*n+j].TexC.x = j*du;
meshData.Vertices[i*n+j].TexC.y = i*dv;
}
}
}
图8.16 网格顶点vij在xz空间中的纹理坐标由uv空间中的第i个网格顶点Tij给出
8.10.2 纹理平铺
要在地面网格上铺上草地纹理。但目前,我们已经计算出纹理坐标位于单位域 [0,1]2 [ 0 , 1 ] 2 ; 所以不会发生拼贴。为了平铺纹理,我们指定了平铺地址模式,并使用纹理变换矩阵将纹理坐标缩放 5倍。因此,纹理坐标被映射到域 [0,5]2 [ 0 , 5 ] 2 ,使得纹理在纹理网格表面上平铺5×5次:
XMMATRIX grassTexScale = XMMatrixScaling(5.0f, 5.0f, 0.0f);
XMStoreFloat4x4(&mGrassTexTransform, grassTexScale);
… Effects::BasicFX->SetTexTransform(XMLoadFloat4x4(&mGrassTexTransform));
…
activeTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mLandIndexCount, 0, 0);
8.10.3纹理动画
为了在水面上滚动水纹理,我们在UpdateScene方法中将纹理平面中的纹理坐标设置为时间的函数。假设每一帧的位移都很小,这就给出了平滑动画的错觉。我们使用平铺地址模式以及无缝纹理,以便我们可以无缝地拼接纹理空间平面周围的纹理坐标。 下面的代码显示了我们如何计算水纹理的偏移矢量,以及如何构建和设置水纹理矩阵:
// Tile water texture.
XMMATRIX wavesScale = XMMatrixScaling(5.0f, 5.0f, 0.0f);
// Translate texture over time.
mWaterTexOffset.y += 0.05f*dt;
mWaterTexOffset.x += 0.1f*dt;
XMMATRIX wavesOffset = XMMatrixTranslation(mWaterTexOffset.x, mWaterTexOffset.y, 0.0f);
// Combine scale and translation.
XMStoreFloat4x4(&mWaterTexTransform, wavesScale*wavesOffset);
… Effects::BasicFX->SetTexTransform(XMLoadFloat4x4(&mWaterTexTransform));
…
activeTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(3*mWaves.TriangleCount(), 0, 0);
8.11压缩纹理格式
随着虚拟世界中纹理数量的增长,GPU内存需求会快速增加(所有这些纹理保留在GPU内存中,以便快速调用它们)。为了帮助缓解内存压力,Direct3D支持压缩纹理格式:BC1,BC2,BC3,BC4,BC5,BC6和BC7:
1 . BC1(DXGI_FORMAT_BC1_UNORM):如果您需要压缩支持三种颜色通道和一个1位(开/关)alpha分量的格式,请使用此格式。
2 . BC2(DXGI_FORMAT_BC2_UNORM):如果您需要压缩支持三种颜色通道的格式,并且仅使用一个4位的alpha分量,则使用此格式。
3 . BC3(DXGI_FORMAT_BC3_UNORM):如果您需要压缩支持三个颜色通道的格式和一个8位alpha分量,请使用此格式。
4 . BC4(DXGI_FORMAT_BC4_UNORM):如果您需要压缩包含一个颜色通道(例如灰度图像)的格式,请使用此格式。
5 . BC5(DXGI_FORMAT_BC5_UNORM):如果您需要压缩支持两个颜色通道的格式,请使用此格式。
6 . BC6(DXGI_FORMAT_BC6_UF16):使用此格式压缩HDR(高动态范围)图像数据。
7 . BC7(DXGI_FORMAT_BC7_UNORM):使用此格式进行高质量的RGBA压缩。 特别是,这种格式大大减少了压缩法线贴图造成的误差。
NOTE:压缩纹理只能用作渲染管线着色器阶段的输入。由于块压缩算法与4×4像素块一起工作,纹理的尺寸必须是4的倍数。
这些格式的优点是可以将它们压缩存储在GPU内存中,然后在需要时由GPU实时解压缩。
如果您有一个包含未压缩的图像数据的文件,则可以使用D3DX11CreateShaderResourceViewFromFile函数的pLoadInfo参数,让Direct3D在加载时将其转换为压缩格式。 例如,参考下面的代码,加载一个BMP文件:
D3DX11_IMAGE_LOAD_INFO loadInfo;
loadInfo.Format = DXGI_FORMAT_BC3_UNORM;
HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,
L"Textures/darkbrick.bmp", &loadInfo, 0, &mDiffuseMapSRV, 0));
// Get the actual 2D texture from the resource view.
ID3D11Texture2D* tex;
mDiffuseMapSRV->GetResource((ID3D11Resource**)&tex);
// Get the description of the 2D texture.
D3D11_TEXTURE2D_DESC texDesc;
tex->GetDesc(&texDesc);
图8.17a显示了调试器中texDesc的使用。我们看到它具有所需的压缩纹理格式。 如果相反,我们为pLoadInfo参数指定了null,则使用源图像的格式(图8.17b),这是未压缩的DXGI_FORMAT_R8G8B8A8_UNORM格式。
或者,您可以使用DDS(Direct Draw Surface)格式,它可以直接存储压缩纹理。 为此,请将图像文件加载到位于SDK目录中的DirectX纹理工具(DxTex.exe):D:\ Microsoft DirectX SDK(2010年6月)\ Utilities \ bin \ x86。 然后进入菜单 - >格式 - >更改表面格式,然后选择DXT1,DXT2,DXT3,DXT4或DXT5。将文件保存为DDS文件。这些格式实际上是Direct3D 9压缩纹理格式,但DXT1与BC1相同,DXT2和DXT3与BC2相同,而DXT4和DXT5与BC3相同。例如,如果我们将文件另存为DXT1并使用D3DX11CreateShaderResourceViewFromFile加载,那么纹理的格式将为DXGI_FORMAT_BC1_UNORM:
HR(D3DX11CreateShaderResourceViewFromFile(md3dDevice,
L"Textures/darkbrickdxt1.dds", 0, 0, &mDiffuseMapSRV, 0));
// Get the actual 2D texture from the resource view.
ID3D11Texture2D* tex;
mDiffuseMapSRV->GetResource((ID3D11Resource**)&tex);
// Get the description of the 2D texture.
D3D11_TEXTURE2D_DESC texDesc;
tex->GetDesc(&texDesc);
图8.17 (a)使用DXGI_FO RMAT_BC3_UNO RM压缩格式创建纹理。 (b)使用DXGI_FORMAT_R8G8B8A8_UNO RM未压缩格式创建纹理
请注意,如果DDS文件使用其中一种压缩格式,则可以为pLoadInfo参数指定null,D3DX11CreateShaderResourceViewFromFile将使用文件指定的压缩格式。
对于BC4和BC5格式,您可以使用NVIDIA纹理工具(http://code.google.com/p/nvidia-texture-tools/)。对于BC6和BC7格式,DirectX SDK有一个名为“BC6HBC7EncoderDecoder11”的示例。该程序可用于将纹理文件转换为BC6或BC7。 该示例包含完整的源代码,以便您可以将其集成到您自己的艺术管道中。有趣的是,如果图形硬件支持计算着色器,则示例使用GPU进行转换; 这比CPU实现提供了更快的转换。
图8.18 纹理是使用DXGI_FO RMAT_BC1_UNO RM格式创建的
您还可以在DirectX纹理工具中生成mipmap级别( Menu-> Format-> Generate Mip Maps),并将它们保存在DDS文件中。 通过这种方式,mipmap级别被预先计算并且与文件一起存储,以便它们不需要在加载时被计算(他们只需要被加载)。
存储在DDS文件中压缩纹理的另一个好处是它们占用的磁盘空间也更少。
8.12总结
1.纹理坐标用于在纹理上定义一个映射到三角网格的三角形。
2.我们可以使用D3DX11CreateShaderResourceViewFromFile函数从存储在磁盘上的图像文件创建纹理。
3.我们可以使用缩小,放大和mipmap滤镜采样器状态来过滤纹理。
4.Direct3D中地址模式定义在[0,1]范围之外的纹理坐标应该如何处理。例如,应该
纹理平铺,镜像,夹紧等?
5.纹理坐标可以像其他点一样缩放,旋转和平移。通过每帧少量增量变换纹理坐标实现纹理动画。
6.通过使用压缩的Direct3D纹理格式BC1,BC2,BC3,BC4,BC5,BC6或BC7,我们可以节省大量的GPU内存。使用DirectX纹理工具生成格式为BC1,BC2和BC3的纹理。对于BC4和BC5,您可以使用NVIDIA纹理工具(http://code.google.com/p/nvidia-texture-tools/)。使用SDK“BC6HBC7EncoderDecoder11”样本生成格式为BC6和BC7的纹理。