概览
在前面的教程中我们介绍了光照,现在我们将在立方体上添加纹理。我们还会介绍常量缓存的概念,以及如何通过最小化带宽的方式利用常量缓存加速处理过程。
这个教程介绍如何在立方体上施加纹理。
这个教程包含了Direct3D 10中的基本概念,以后的教程通过介绍DXUT、网格加载和shader的示例扩展这些概念。
源代码
(SDK root)\Samples\C++\Direct3D10\Tutorials\Tutorial07
纹理映射
纹理映射表示将一张2D图像映射到3D几何体上。我们可以把它想象成包装一个礼物盒,即将一张包装纸覆盖在一个盒子上。要做到这点,我们需要指定几何体表面上的点是如何对应2D图像的。
诀窍是将正确地将纹理对齐到模型的坐标上。对于复杂的模型,很难手动确定纹理的坐标,因此,3D建模工具通常会输出带有纹理坐标信息的模型。因为本示例是一个立方体,所以很容易确定匹配纹理的坐标。纹理坐标是在顶点中定义的,然后会在表面的单个像素间进行插值。
从纹理创建一个Shader Resource
纹理是一张从文件获取的2D图像,用于创建一个shader资源视图(shader-resource view),因此它可以从shader中读取。
1
2
|
hr = D3DX10CreateShaderResourceViewFromFile( g_pd3dDevice, L
"seafloor.dds"
, NULL,
NULL, &g_pTextureRV, NULL );
|
定义坐标
我们必须首先定义立方体每个顶点的纹理坐标才能将图像映射到立方体上。因为图像尺寸不定,因此使用的坐标系统被调整到[0, 1]区间。纹理的左上角对应(0,0),右下角对应(1,1)。
本例中,我们让整个纹理覆盖在立方体的每个面上。你也可以将纹理展开到所有六个面,虽然这样做的话坐标的定义会变得难一些,纹理可能会变形。
首先,我们需要更新结构体使之包含纹理坐标。
1
2
3
4
5
|
struct
SimpleVertex
{
D3DXVECTOR3 Pos;
// Position
D3DXVECTOR2 Tex;
// Texture Coordinate
};
|
然后,更新输入结构使之也包含坐标信息。
1
2
3
4
5
6
|
// Define the input layout
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{ L
"POSITION"
, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },
{ L
"TEXCOORD"
, 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
};
|
因为输入结构发生了变化,对应的顶点着色器也需进行调整。
1
2
3
4
5
|
struct
VS_INPUT
{
float4 Pos : POSITION;
float2 Tex : TEXCOORD;
};
|
最后,就可以在顶点中包含纹理坐标了。第二个参数类型为D3DXVECTOR2,包含了纹理坐标。立方体的每个顶点对应纹理的一个角,这样就创建了一个非常简单的映射,每个顶点的纹理坐标为(0,0)或(0,1)或(1,0)或(1,1)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// Create vertex buffer
SimpleVertex vertices[] =
{
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, -1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, -1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 0.0f ) },
{ D3DXVECTOR3( 1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 1.0f, 1.0f ) },
{ D3DXVECTOR3( -1.0f, 1.0f, 1.0f ), D3DXVECTOR2( 0.0f, 1.0f ) },
};
|
当采样纹理时,我们需要使用材质颜色对纹理颜色进行调制。
将纹理绑定为Shader资源
纹理是一个类似于矩阵和矢量的对象,在它们可以被shader使用前,需要被设置到Effect。这可以通过获取一个指向变量的指针做到,然后将它设置为一个shader资源。
1
|
g_pDiffuseVariable = g_pEffect->GetVariableByName(
"txDiffuse"
)->AsShaderResource();
|
获取了资源指针之后,将它绑定到之前初始化过的2D纹理资源视图中(2D texture resource view)。
1
|
g_pDiffuseVariable->SetResource( g_pTextureRV );
|
现在我们就做好了在shader中使用纹理的准备了。
施加纹理(fx)
要将纹理映射到几何体上,我们需要在像素着色器中调用一个纹理查询方法。这个叫做Sample的方法会执行2D纹理的查询,然后返回一个采样过的颜色。像素着色器调用这个方法并将返回的结果颜色乘以网格的颜色(或材质颜色),最后输出最终的颜色。
- txDiffuse用于储存从传入的纹理,这个纹理绑定在资源视图g_pTextureRV中。
- samLinear在下面介绍,它是用于纹理查询的采样器的详细说明。
- input.Tex是纹理的坐标。
1
2
3
4
5
|
// Pixel Shader
float4 PS( PS_INPUT input) : SV_Target
{
return
txDiffuse.Sample( samLinear, input.Tex ) * vMeshColor;
}
|
变量samLinear是一个描述像素着色器如何对纹理进行采样的结构体。本例中我们使用的线性过滤,纹理包装格式为wrap。这些设置对于简单的纹理是很有用的,而对过滤方式的解释超出了本教程的范畴。
1
2
3
4
5
6
|
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
|
别忘了通过顶点着色器将纹理坐标传递过来,如果你忘了此步,在像素着色器中就会丢失数据。本例中,我们只是将输入的坐标直接输出,硬件会处理余下的工作。
1
2
3
4
5
6
7
8
9
10
11
|
// Vertex Shader
PS_INPUT VS( VS_INPUT input )
{
PS_INPUT output = (PS_INPUT)0;
output.Pos = mul( input.Pos, World );
output.Pos = mul( output.Pos, View );
output.Pos = mul( output.Pos, Projection );
output.Tex = input.Tex;
return
output;
}
|
常量缓存(Constant buffer)
一个应用程序可以使用常量缓存设置shader常量(shader变量)。声明常量缓存的方法类似于C语言的结构体。常量缓存通过将shader常量组合在一起减少了更新时所需的带宽,且与对每个常量单独调用所需的时间是相同的。
更有效率地使用常量缓存的方法是将shader变量基于它们的更新频率创建在常量缓存中。这样做可以让应用程序将更新shader常量所需的带宽减到最小。例如,本教程中将常量分为三组:一个用于每帧都改变的变量,一组用于只在窗口大小改变时才改变的变量,一组只设置一次然后就不再改变。
下面的常量缓存是定义在.fx文件中的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
cbuffer cbNeverChanges
{
matrix View;
};
cbuffer cbChangeOnResize
{
matrix Projection;
};
cbuffer cbChangesEveryFrame
{
matrix World; float4 vMeshColor;
};
|
本教程使用了一个ID3D10Effect对象管理常量缓存的更新。ID3D10Effect接口让许多任务变得更加容易,这些任务包括常量缓存的创建、更新和设置。下面的代码展示了在程序中如何使用常量缓存。
从effect对象中获取常量缓存变量会导致effect对象创建任何所需的常量缓存。
1
2
3
|
//called once after the effect is loaded and the ID3D10Effect instance is created
g_pWorldVariable = g_pEffect->GetVariableByName(
"World"
)->AsMatrix();
g_pMeshColorVariable = g_pEffect->GetVariableByName(
"vMeshColor"
)->AsVector();
|
更新常量缓存变量标志了effect对象在合适的时间更新常量缓存。
1
2
3
|
//called whenever the variables in the constant buffer are changed
g_pWorldVariable->SetMatrix( (
float
* )&g_World );
g_pMeshColorVariable->SetFloatVector( (
float
* )g_vMeshColor );
|