`
nanjingjiangbiao_T
  • 浏览: 2594173 次
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

【D3D11游戏编程】学习笔记十四:纹理基础

 
阅读更多

(注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)

光照和材质的加入使得场景的真实感大大增加,但仅仅这些依然不足以表现出真实世界中物体表面的各种细节。毕竟,材质所能够提供的细节等级仅仅停留在顶点级别上。纹理的引入,使得在像素级别上提供细节成为可能,因而可以进一步大大提高物体的真实感。

1. 纹理类型

一般情况下,纹理可以看成是一个二维的数组(这里只讨论二维纹理,一维纹理和三维纹理暂时不讨论),数组中每个元素可以具有不同的数据类型,代表不同的意义。根据元素的类型,纹理可以分为如下几种:

1.1 普通纹理贴图

这种纹理的每个元素代表一个颜色值,可以是3维向量(RGB)或4维向量(RGBA),分别代表颜色值中的红、绿、蓝成分,对于32位颜色值,还有代表透明度的第4个成分。这种纹理是最常见的一种类型,通过把现成的图片贴在物体表面,来增加物体的真实度。

1.2 法线贴图

这种纹理的每个元素代表一个法线,即一个3维向量(位于切线空间Tangent Space:TBN),代表物体表面各处的法线值。这种纹理主要用于法线映射(Normal Mapping)技术中,通过贴图来直接修改物体表面的法线,从而模拟表面的凹凸细节。

1.3 高度图

这种纹理的每个元素代表一个高度值。一个常见的用途即地形渲染,地形渲染中用到的雏形是一个平面的网格,然后通过读取高度图来获取网格不同位置的高度信息(大多数情况下,对读取到的高度值,会再乘以一定的系数来满足所需的高度范围)。另一个用途是Displacement Mapping,配合曲面细分技术,可以修改物体表面的顶点位置,从而实现表面的凹凸细节。通过D3D11新增的Tessellation Stage来实现Displacement Mapping在以后会有专门的例子。

当然,纹理的用途不仅仅这些。我们需要知道的是,提到纹理,不能仅仅认为它就是一个贴在物体表面的图片。不过,在这篇文章中,作为一个纹理的简介,我们仅仅考虑纹理最常见的一个用途,其他高级纹理技术在我们后面学到时会详细介绍。

2. 纹理坐标

对于最常见的二维纹理,纹理坐标用一个二维向量(u,v)表示。在D3D11中,以贴图的左上角为原点,u相对于贴图水平向右,v相对于贴图垂直向下。如下图所示:

该坐标空间我们称之为纹理空间。为了让纹理坐标能够与贴图的尺寸无关,纹理坐标u和v被限制在0到1之间,左上角为(0,0),右上角为(1,0),左下角为(0,1),右下角为(1,1)。这样,我们在给顶点指定纹理坐标时,不需要考虑贴图的大小。无论是一张256*256,还是1024*1024的贴图,纹理坐标都适用。

正如利用顶点坐标的插值来计算一个三角形内部各点的坐标一样,三角形内部的纹理坐标同样需要插值计算,计算方法与顶点一样。例如对于一个三角形的三个顶点坐标p0,p1,p2,对应的纹理坐标为q0,q1,q2。对于三角形内部一点:(x,y,z) = p0 + s(p1 - p0) + t(p2 - p0),那么相应的纹理坐标就是:(u,v) = q0 + s(q1 - q0) + t(q2 - q0)。如下图所示:

3. 纹理寻址模式

尽管纹理坐标被限制在0到1之间,但是D3D依然允许我们为顶点指定该区间之外的坐标值。这时,解析该坐标值依赖于不同的纹理寻址模式。D3D支持如下几种寻址模式:

1. Wrap

Wrap模式下,允许贴图在物体表面进行重复。换句话说,当坐标大于1时,通过去除整数成份而得到的坐标值来读取纹理值,当坐标小于0时,通过加上一个最小的正数,使得坐标值大于0,然后利用该值来获取纹理值。这种方法造成的效果,即坐标超出0到1区间时,纹理在物体表面进行重复。如下图所示:

2. Border

Border模式下,我们可以手动指定一个特定的值,当纹理坐标不在0到1区间内时,即使用该指定值。如下图所示(此时指定值为“蓝色”:

3. Clamp

Clamp模式下,当超出正常坐标范围时,使用正常范围[0,1]内与该坐标最近的那个点的纹理值。举个例子,对于坐标(u,v),如果u>1,v位于[0,1],则使用[1,v]片的纹理值;同样,如果v>1,u位于[0,1],则使用[u,1]处的纹理值;如果u,v都>1,则使用[1,1]处的纹理值。如下图所示:

4. Mirror

Mirror模式下,每当纹理坐标越过一个整数值时,使用的纹理与越过整数值之前的纹理成镜像关系。下图可以直观地说明:

4. 纹理过滤

由于纹理实际上是由多个离散的颜色值组成的,因此,对于一张256*256尺寸的纹理,如果它正好投影在屏幕上256*256尺寸的区域内,那么一个像素对应一个纹理值,这是最理想的情形。但是,当这些颜色值与屏幕上的像素不能够一一对应时,如何计算特定像素处的颜色值?在这种情况下,就需要用到纹理过滤。设想如下两种情形:

1. 当照相机与一张纹理不断靠近时,即使该纹理尺寸远小于屏幕,当距离足够近时,该纹理也有可能投影在整个屏幕上。这时,纹理上的一个元素将覆盖很多个屏幕像素。

2. 当照相机不断远离时,纹理在屏幕上的投影会越来越小,当距离足够远时,有可能带个纹理会投影在一个屏幕像素内。

在这两种情况下,如果计算纹理在屏幕投影范围内对应各个像素的值,即需要纹理过滤。

纹理过滤分为两种:一种为放大,即Magnification; 一种为缩小,即Minification。当一个纹理元素覆盖屏幕上多个像素时,使用的过滤为Magnification,对应于上述第一种情形;当多个纹理元素投影在一个屏幕像素内时,使用的过滤为Minification,对应于上述第二个例子。

4.1 Magnification

考虑一张256*256的纹理,它投影在屏幕上1024*1024范围的空间内。这时,平均一个纹理值覆盖4*4个屏幕上的像素。由于这4*4个像素位于同一个纹理值处,如何决定它们各自的颜色值?D3D提供了如下几种过滤方法:

1. 取最近点(Nearest Point)

这是最快速也是最简单的方法。利用该方法,对于任一像素对应的纹理坐标值,取距离最近的纹理值。比如对于u = 0.126,它对应于纹理上0.126*256 = 32.38的位置,这时最近的位置为32。同理,如果一个像素对应纹理上(80.6,60.2)的位置,则选取(81,60)处的纹理值作为结果。

2. 线性过滤(Linear)

在该方法中,通过与该坐标值最近的4个纹理值进行插值来计算最终颜色值。以一个一维的纹理作为例子,比如对于坐标60.6,首先获取60和61处的两个颜色值C60,C61,然后对它们进行插值来计算最终颜色:C = 0.4*C60 + 0.6*C61。同理,对于二维纹理,需要在u、v方向上进行两次插值,如下图所示例子:

我们要计算c处的纹理值,与c最近的两处分别为Ci,j,Ci,j+1,Ci+1,j,Ci+1,j+1。C在u方向上距离左、右两边的点相对距离分别为0.75和0.25,在v方向上距离上、下两边相对距离分别为0.38和0.62。因此首先在u方向上进行插值计算Ct = 0.25*Ci,j + 0.75*Ci,j+1,Cb = 0.25*Ci+1,j + 0.75*Ci+1,j+1。然后在v方向上对Ct和Cb进行插值,C = 0.62*Ct + 0.38 * Cb。即最终的颜色值。

4.2 Minification

现在来讨论相反的情况。在Minification情况下,多个纹理元素被投影在屏幕上同一个像素位置。比如一个1024*1024的纹理,投影在屏幕上256*256范围的空间内,这样平均每个像素覆盖4*4个纹理值。这种情况下,最流行的过滤方法称为Mipmaping。在该方法中,使用到一个Mipmap链。Mipmap链是原纹理组成的一个数组,数组中第一个纹理为原始纹理,后面的第一个纹理在u、v尺寸上为上一个纹理的一半,依次计算,直接纹理尺寸为1为止。如下图所示:

这样,在运行时,硬件会先把合适的纹理为解决该问题。在Mipmap连中选择合适的纹理同样有两种方法:Point Filtering和Linear Filtering。

1. 使用Point Filtering方法时,硬件会根据多边形在屏幕上投影的尺寸选择最接近的纹理,然后在该纹理上再继续利用Point Filtering或Linear Filtering计算颜色值,作为最终颜色值。

2. 使用Linear Filtering方法时,硬件会选出最接近的两个纹理,然后分别在该两个纹理上利用Point Filtering或Linear Filtering计算颜色值,最终再把两个颜色值进行插值,作为最终结果。

4.3 各向异性过滤(Anisotropic Filtering)

此外还有一种更高级的过滤方法称为各向异性过滤。 这种方法可以更好地解决如下问题,即当物体表面的法线与视线接近90度时的情形。比如水平方向上观察一个立方体的顶部,如下图所示:

左边为使用线性过滤的结果,右边为使用各向异性过滤的结果。这种方法效果最好,当然也最昂贵。

5. 纹理坐标变换

如果位置坐标一样,顶点的纹理坐标一样可以进行各种变换,比如平移、伸缩、旋转等。尽管多大数情况下,我们直接使用初始指定的纹理坐标来提取纹理值,但在一些情况下,通过对纹理坐标进行变换,可以实现一些特殊的效果:

1. 通过使用伸缩变换,可以让纹理沿多边形进行重复。比如一张砖块纹理贴在墙上,顶点纹理坐标位于[0,1]之间的坐标,通过u或v方向上5倍的变换后,将位于[0,5]之间,这样纹理将在墙上被重复5次。可以任意地调整伸缩的实数来实现不同的要求。

2. 通过平移变换,可以让纹理坐标沿特定方向进行平移,从而实现诸如云彩飘动、水流等效果

总之,纹理坐标变换可以在很多场合使用,实现有趣的动画效果。

6. 在D3D11中使用纹理

好了,现在可以来学习D3D11中关于纹理使用的知识了。

在D3D11中,二维纹理对应的接口为ID3D11Texture2D。在前面介绍3D渲染管线时提到过,一个纹理可以在管线的多个阶段使用,且使用前需要先绑定到相应的阶段。此外,真正绑定到管线上的并不是纹理本身,而是相应的视图(View)。在C++程序和Effect中使用纹理的方法如下:

6.1 C++程序

在C++程序中,通过读取图片使用纹理的过程如下:

1. 创建纹理

2. 创建相应阶段的视图

一般情况下,这两步可以通过一个函数实现,该函数原型如下:

 HRESULT  D3DX11CreateShaderResourceViewFromFile(
  __in   ID3D11Device *pDevice,
  __in   LPCTSTR pSrcFile,
  __in   D3DX11_IMAGE_LOAD_INFO *pLoadInfo,
  __in   ID3DX11ThreadPump *pPump,
  __out  ID3D11ShaderResourceView **ppShaderResourceView,
  __out  HRESULT *pHResult
);

pDevice为渲染设备接口指针;

pSrcFile为纹理文件目录;

pLoadInfo负责以特定的格式来读取纹理,如果遵从纹理本身的尺寸、格式等全部信息,则可以设为NULL;

pPump用来另创建一个线程来读取纹理,我们学习过程中只用单个线程,因此这个设为NULL;

ppShaderResourceView即要创建的视图指针的地址;

对于pHResult,如果pPump为NULL,则这个也为NULL。

整个调用如下所示:

	D3DX11CreateShaderResourceViewFromFile(m_d3dDevice,L"Texture/Wood.dds",0,0,&m_texView,0)

调用完该函数,指定的纹理视图就产生了。

此外,为了能够将纹理资源传递到Effect中,需要一个指向相应的Effect全局变量的接口:ID3DX11EffectShaderResourceVariable。创建好该接口后,通过调用将相应的函数:ID3DX11EffectShaderResourceVariable::SetResource(ID3D11ShaderResourceView **ppResource),从而把创建好的纹理视图赋给Effect中的变量。

	m_fxTex = m_fx->GetVariableByName("g_tex")->AsShaderResource();
	m_fxTex->SetResource(m_texView);

6.2 Effect程序

在Effect中,对应于二维纹理资源的类型为Texture2D。注意纹理资源不能与其他全局变量一样声明在cbuffer中!

	//纹理
	Texture2D g_tex;

在Effect中,可以通过SamplerState结构来指定过滤方法。如下:

	SamplerState samTex
	{
		Filter = MIN_MAG_MIP_LINEAR;
	};

该方法即针对Minification、Magnification和Mipmap中纹理的选择全部使用线性过滤方法。此外,还有其他方法可以选择:

MIN_LINEAR_MAG_MIP_POINT:Minification使用线性方法,Magnification和Mipmap选取使用最近点(Point)方法;

MIN_POINT_MAG_LINEAR_MIP_POINT:Minification使用Point方法,Magnification使用线性方法,Mipmap选取使用Point方法;

等等。从中可以看出规律,对于MIN、MAG和MIP,可以分别指定相应的方法(Point或Linear),这样就很容易记住各种参数的命名了。如果需要查看所有Filter参数,可以查看SDK中的D3D11_FILTER部分。

此外,使用各向异性过滤方法如下:

	SamplerState samp
	{
		Filter = ANISOTROPIC; 
		MaxAnisotropy = 4;
	};

此时,除了指定各向异性方法外,还需要指定其最高级别,即1到16。等级越高,效果越好,速度当然也越慢。


当然,纹理的引入,顶点的信息也要发生相应的改变,我们需要加入纹理坐标,这时的顶点结构如下:

//输入顶点信息:位置坐标、法线、纹理坐标
struct VertexIn
{
	float3	pos:	POSITION;
	float3	normal:	NORMAL;
	float2	tex:	TEXCOORD;
};
//输出顶点信息
struct VertexOut
{
	float4posH:SV_POSITION;//投影后坐标
	float3pos:POSITION;//世界变换后坐标
	float3normal:NORMAL;//世界变换后法线
	float2tex:TEXCOORD;//纹理坐标
};

在顶点着色器中也需要添加针对纹理坐标相应的操作,如下(这里未对纹理坐标进行变换,而是直接使用指定的坐标):

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
	vout.posH = mul(float4(vin.pos,1.f),g_worldViewProj);
	vout.pos = mul(float4(vin.pos,1.f),g_world).xyz;
	vout.normal = mul(vin.normal,(float3x3)g_worldInvTranspose);
	vout.tex = vin.tex;

	return vout;
}

在像素着色器中,使用纹理资源的函数为:Texture2D::sample(SamplerState samper, float2 texcoord)。如下:

//设置过滤
SamplerState samTex
{
	Filter = MIN_MAG_MIP_LINEAR;
};
float4 texColor = g_tex.Sample(samTex,pin.tex);

D3D11中纹理的基本使用就这些,下面是这次的示例程序,实现一个基本纹理+光照的效果。

在程序中,通过按‘1’和‘2’来关闭、打开光照(这次例子中使用的是聚光灯)。关闭光照的情况下,物体表面的颜色值只由纹理决定;在打开光照的情况下,纹理颜色和光照计算结果通过如下公式得出:

finalColor = texColor * (ambient + diffuse) + specular

finalColor.a = texColor.a * g_material.diffuse.a

即纹理颜色与环境光和漫反射光的和相乘,再加上全反射光。最终的alpha值通过纹理alpha值与材质的漫反射部分的alpha值相乘。

当然,这只是一种最常用的方法之一,在编写着色器代码,没有哪一种方法是最好的,只要适合自己的需求即可。这也是可编程管线给程序员带来的灵活性的体现。

以下是示例程序的源代码:

纹理使用基础示例代码

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics