(注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)
这一节讨论有关纹理映射的进阶内容:Cube Mapping。
1. 简介
单从名字上,就大概可以看出点端倪了,翻译成中文为立方体映射,因此肯定跟立方体有关系。确实,Cube Mapping就是使用六张正方形的图片来进行纹理映射的。这六张图片分别对应了一个立方体中的六个面。由于这个立方体是轴对齐的,因此每个面可以用坐标系中的六个轴方向来惟一的表示:正X面,负X面,正Y面,负Y面,正Z面,负Z面。我们把对应这六个面的六张纹理图,称之为“Cube Map”。 如下就是一张从立方体展开的Cube Map:
2. 纹理映射方法
通常情况下,纹理映射是通过顶点上的纹理坐标u,v来进行的。对于一个二维纹理来说,u,v就惟一确定了texel的位置。但对于一张Cube Map,仅仅给出u,v坐标是无法确定像素位置的,因为它有六张纹理图。因此,这时我们需要一种新的映射技术,以一对一地确定纹理中对应的texel。
实际上,在Cube Mapping中,映射是通过一个三维的向量实现的。该三维向量可以设想为以立方体的中心为起点,指向立方体外面。该向量与立方体的交点处对应的texel就是映射结果对应的texel。显然,一个三维向量与立方体有且仅有一个交点,因此这种方法可以用来实现立方体映射。
以下是在二维平面上进行的图示:v代表映射使用的向量,正方形代表cube map,图中给出的交点处对应的texel即我们要的结果。
下面我们从数学的角度来导出映射过程:
第一步,给出一个3D向量[x,y,z],首先找出值最大的那一维。以[-3.2, 5.1, -8.4]为例子, 这时最大维为Z,最大维用来把cube map从六张图定位到一张图上。-8.4为负数,对应了立方体的负Z面,因此接下来我们关注的焦点将是立方体负Z面对应的那张纹理;
第二步,把向量中另外两维分别除以最大维,得到一个二维向量,(3.2/8.4, -5.1/8.4)。很容易知道,这个二维向量中的数值范围位于[-1, 1]之间。
第三步,把上阶段中得到的二维向量转换到[0, 1]之间。很简单,把位于[-1, 1]之间的数转换到[0, 1]之间的方法为: (x + 1) / 2。对于上面的例子, 为3.2/8.4 * 2 + 0.5 = 0.31, -5.1 / 8.4 * 2 + 0.5 = 0.51。因此得到二维向量(0.31, 0.51)。这个二维向量就是我们用来在负Z轴上获得texel的纹理坐标。
小结一下,Cube Mapping中通过3D向量进行纹理映射的过程分为三步:
1. 根据最大维惟一地确定一张纹理图
2. 使用其他两维作为二维向量,并分别除以最大维
3. 把二维向量中的值转换到[0, 1]之间,得到二维纹理坐标,以通过常规方法对二维纹理进行映射。
3. D3D11中Cube Map的使用
在HLSL中,用专门用来表示cube map的类型,即TextureCube,纹理采样方法与常规二维纹理完全一样。 如下所示:
TextureCube g_cubeMap;
SamplerState samTexture
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
给定一个3维向量,使用cube map来获取纹理值的方法如下:
g_cubeMap.Sample(samTexture,dir);
看得出来,与常规二维纹理基本一样,区别仅仅在于第二个参数从二维纹理坐标变成了三维。
在C++程序中,用来存放cube map的类型依然是ID3D11Texture2D。
在之前的所有程序中,我们用它来存放单张二维纹理图。实际上,这个接口类型的功能十分强大,除了可用来存放单张纹理外,还可以存放纹理数组。纹理数组,即多张纹理图片。不仅仅是纹理数组,每张纹理的带个mip链都可以存放在其中。对我们来说,只需要通过ID3D11Texture2D来表示。如下图示清楚地表示了使用该类型表示多张纹理、及其mip链时,各个纹理图的分布:
从左到右分别表示各个不同的纹理,垂直方向上分别对应各个纹理的mip链。
有关ID3D11Texture2D的详细内容,在后面会有专门文章进入讲解,这里暂时不关注这些细节。而是把注意力集中在cube map的表示上。
此外,使用d3d自带的.dds格式的图片,可以集中存放cube map中的六张纹理图。读取图片获得纹理视图的方法与之前的二维纹理完全一样:
D3DX11CreateShaderResourceViewFromFile(m_d3dDevice,L"textures/snowcube1024.dds",0,0,&m_SkyBoxSRV,0)
同样,通过这个视图:m_SkyBoxSRV,可以直接用来给HLSL中的Texture2D赋值。
4. 使用Cube Mapping实现天空盒效果(SkyBox)
为了给人以身临其境的感觉,游戏中所有的场景都会使用天空盒技术,用以模拟无穷远处的天空、山峦等景象。天空盒的实现有以下几个关键点:
1. 天空理论上应该位于无限远处,场景中任何物品都位于天空盒前部,而不会被其遮住;
2. 当在场景中移动时,场景中的物体会相对角色移动,但天空盒与自身相对位置保持静止不变;
针对1, 可以通过让天空盒的变换后的z坐标位于可视范围内的最大值。这样场景中任何物品都能被正确渲染,并能遮挡远处的天空。因而符合实际情况;
针对2, 为了实现让天空盒相对自己静止的效果,有两种方法:一是针对任一时刻自己的世界坐标,让天空盒具有相同的平移,这样自己将永远位于天空盒中心,从而相对静止;另一种方法是在渲染天空盒时,不对天空盒进行世界变换,并且让其视角矩阵中平移部分变为[0, 0, 0],这样天空盒可以保持在原点,由于视角空间中相机永远位于原点,因此自己相对天空盒位置总是保持相对静止。
下面来介绍一下天空盒实现的具体步骤:
1. 天空盒的几何体表示
在现实生活中,天空给我们的感觉是一个半球形。因此这里我们使用一个圆球,作为天空盒的几何体表示;
2. 天空盒纹理图
为了表示无限远处的天空、山峦等现象,我们需要在该圆球表面贴上包含空间任一角度的纹理图。
Cube map由于包含六张表示立方体各个面的贴图,而整个贴图把立方体内部整个空间完整地包围起来,因此可以用它来模拟周围的环境。这处情况下的应用,我们称之为环境映射(Environment Mapping)。这个想法其实很容易理解,考虑以下情形:把摄像机调整为90度视角,宽、高比设为1,放于某一点处。然后向周边上、下、左、右、前、后六个方向上分别拍照。这种情况下摄像机所能拍摄到的部分将囊括整个场景,而这六张拍摄的图片所组成的Cube Map即可作为表示相机位置处的周边场景的环境图(Environment Map)。
3. 天空纹理映射
有了天空盒的几何表示以及环境图,现在的问题就是如何映射该环境图了。在上面介绍Cube Mapping时我们说过,Cube Mapping使用3D向量来实现纹理映射。这里Environment Mapping作为一种特殊的Cube Mapping,当然也可以使用这种映射方法。
请看如下图示,该图很好地解释了天空盒纹理映射的方法:
由图可知,对于圆球上的任一点,可以使用从球中心出发、经过该点的3D向量来对环境图进行映射,这也符合我们现实当中观察物体的情形。
4. 程序实现
天空盒的实现,核心在于shader部分。在C++程序中,我们要做的与之前的程序完全一样:创建圆球及其对应的顶点、索引缓冲区,创建天空纹理视图等等。因此这里只给出了Shader部分的关键点的实现:
首先是顶点着色器中的输入顶点结构,由于我们这里实现天空盒时不进行光照计算,而只进行纹理映射,因此顶点结构只包含顶点位置。没有纹理坐标,因为这里通过自己计算得到的3D向量来进行映射。如下:
struct VertexIn
{
float3 posL: POSITION;
};
对于输出顶点,投影空间的位置永远是必须的。此外,为了在像素着色器中计算3D向量以用于纹理映射,我们还需要保留模型空间的顶点坐标。
struct VertexOut
{
float4 posH: SV_POSITION;
float3 posL: POSITION;
};
现在是最关键的地方:在前面提到过,天空盒的一个关键点是它总是位于无穷远,即场景中任何物体的后面。换句话说,它永远位于可视距离的最远处。由于不同场景的大小是不固定的,因此通过给圆球指定特定的半径不是一种好办法。因为这种情况下我们在使用天空盒时总是需要指定其合适的半径。这里,我们使用了一种更为巧妙的方法,位于顶点着色器中,代码如下:
VertexOut VS(VertexIn vin)
{
VertexOut vout;
vout.posL = vin.posL;
vout.posH = mul(float4(vin.posL,1.f), g_worldViewProj).xyww;
return vout;
}
关键是第三行对顶点进行的变换。一般情况下,我们需要的是对顶点进行“世界、视角、投影变换”后的坐标,即xyzw成分。但这里,我们并没有接受z分量,而是使用了w分量作为投影变换后的z分量。乍看下很是奇怪,但这种方法确实很实用。我们知道,对于[x, y, z, w]形表示的3D坐标,其对应到实际3D空间中表示为[x/w, y/w, z/w]。因此,这里把z分量变为w后,该点真实坐标为[x/w, y/w, 1],即x,y坐标保持不变,z分量始终为1. 还要知道,由于现在顶点投影空间,可视范围(z值)被变换到[0, 1]之间,1即表示最大可视距离。因此,天空盒几何体上的任一点,不论其世界空间中距离摄像机的远近,在经过投影变换后,其始终保证位于“无穷远处”(最大可视距离处),从而达到了我们的要求。
在像素着色器中,我们要做的仅仅是进行纹理映射获得天空任一点对应的颜色值:
float4 PS(VertexOut pin): SV_TARGET
{
return g_cubeMap.Sample(samTexture,pin.posL);
}
这里,我们直接使用圆球世界空间下的顶点作为3D向量,因为向量的起始点位于原点。
最后还有2点要注意:
1. 由于在清屏时我们把默认深度值设为1.0, 而此时天空盒上所有顶点的深度值全为1,此时需要相应地设置渲染状态,令深度测试函数为LESS_EQUAL,以保证天空盒全部通过深度测试。
2. 由于我们现在是位于圆球内部,此时观察到的圆球内表面的三角形顶点顺序为逆时针,因此还需要将渲染状态设置为:逆时针为正面。
相应的设置如下:
RasterizerState CounterClockFrontRS
{
FrontCounterClockwise = true;
};
DepthStencilState LessEqualDSS
{
DepthFunc = LESS_EQUAL;
};
并在technique中开启这些设置:
technique11 SkyBoxTech
{
pass P0
{
SetVertexShader( CompileShader(vs_5_0, VS()) );
SetPixelShader( CompileShader(ps_5_0, PS()) );
SetDepthStencilState(LessEqualDSS, 0);
SetRasterizerState(CounterClockFrontRS);
}
}
5. 本节示例程序
好啦,整个天空盒的实现细节就这些,这里提供一个非常简单的示例程序,以演示天空盒的效果。为了简单明了地突出天空盒的实现,场景中除了天空之外没有任何其他物体。以下是一张截图:
点击这里获取源代码