像素着色器是一段执行在图形卡的GPU上的程序,它运行在对每个像素进行光栅化处理时。(不像顶点着色器,Direct3D不会以软件模拟像素着色器的功能。)实际上它替换了固定功能管线的多纹理化阶段(the
multitexturing stage),并赋予我们直接操纵单独的像素和访问每个像素的纹理坐标的能力。这种对像素和纹理坐标的直接访问使我们可以达成各种特效,例如:多纹理化(multitexturing)、每像素光照(per
pixel lighting)、景深(depth of field)、云状物模拟(cloud
simulation)、焰火模拟(fire simulation)、高级阴影技术(sophisticated
shadowing technique)。
图形卡支持的像素着色器的版本可以通过D3DCAPS9结构的PixelShaderVersion成员和D3DPS_VERSION宏进行检查。下列代码片断展示了这点:
// If the
device's supported version is less than version 2.0
if(
caps.PixelShaderVersion < D3DPS_VERSION(2, 0) )
// Then pixel shader version 2.0 is not
supported on this device.
|
多纹理化(Multitexturing)可能是用像素着色器实现的最简单的技巧了。
多纹理化后面的概念有一点和混合(blending)相关,可以将正要被光栅化的像素与之前写入后台缓冲的像素进行混合来达成一种特效。我们延伸这种相同的思想到多纹理化中(multiple
texture)。也就是说,我们一次使用几个纹理,然后定义这些纹理如何被混合在一起,以达到一种特殊效果。多纹理化的一个通常的用法是执行光照。作为在顶点处理阶段使用Direct3D的光照模型的替代,我们使用一种叫做“光照图”(light
map)的特殊纹理贴图(texture map),它编码(encode)表面是如何被光照的。例如,假设我们希望一盏聚光灯(spotlight)照在一个大木箱上,我们要么定义一个D3DLIGHT9结构的聚光灯,要么将代表木箱的纹理贴图与代表聚光灯的光照映射混合在一起,如图18.1所示。
注意:结果图像依赖于纹理被混合的方式。在固定功能管线的多纹理化阶段,混合方程式被纹理渲染状态(texture render
state)控制。用像素着色器,我们能以可编程的方式在代码中写出混合函数的简单表达式。这使我们可以用任何我们想要的方式混合纹理。
混合多个纹理(本例中是两个)来照亮木箱比起Direct3D的光照来有两个好处:
光照是是预先在聚光灯的光照贴图里计算好的。因此,光照不需要在运行时被计算,这节省了处理时间。当然,只有静态对象和静态灯光的光照可以被预先计算。
因为光照图是预先计算好的,我们能够使用比Direct3D的(光照)模型多的多的更加精确的和成熟的光照模型。(更好的光照可以产生更真实的场景。)
备注:多纹理化阶段的典型应用是实现静态对象的完全光照引擎(full lighting engine)。例如,我们可以用一个纹理贴图保存对象的颜色,比如木箱的纹理贴图。然后我们可以用一个散射光照贴图(diffuse
light map)保存散射表面着色(diffuse surface shade),一个单独的镜面光照贴图保存镜面表面着色,一个雾状物贴图(fog
map)保存覆盖在表面的雾状物的总量,还有可以用一个细节贴图(detail map)保存小的、高访问率的表面的细节。当所有这些纹理被组合起来,只需到这些预先计算的纹理中检索,就可以有效的照亮、着色并且增加细节到场景中去。
注意:聚光灯光照贴图在很基础的光照贴图中是一个价值不高(trivial)的例子。一般的的程序通过给定的场景和光源来生成光照贴图。
18.1.1
允许多个纹理
回忆一下,纹理是用IDirect3DDevice9::SetTexture方法设置,而采样器状态(sampler
state)是用IDirect3DDevice9::SetSamplerState方法设置,原型如下:
HRESULT IDirect3DDevice9::SetTexture(
DWORD Stage, // specifies the texture stage
index
IDirect3DBaseTexture9 *pTexture
);
HRESULT IDirect3DDevice9::SetSamplerState(
DWORD Sampler, // specifies the sampler stage
index
D3DSAMPLERSTATETYPE Type,
DWORD Value
);
|
注意:一个特定的采样器阶段索引I关联第i个纹理阶段(texture
stage)。即第i个采样器阶段指定采样器状态是第i集(set)纹理。
纹理/采样器阶段索引标识了我们希望设置的纹理/采样器的纹理/采样器阶段。因此,我们可以允许多个纹理并通过使用不同的阶段索引设置其相应的采样器状态。例如,假设我们要允许三个纹理,我们像这样使用阶段0,1和2:
// Set first
texture and corresponding sampler states.
Device->SetTexture(0, Tex1);
Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// Set second
texture and corresponding sampler states.
Device->SetTexture(1, Tex2);
Device->SetSamplerState(1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(1, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
// Set third
texture and corresponding sampler states.
Device->SetTexture(2, Tex3);
Device->SetSamplerState(2, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(2, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(2, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
|
这段代码使用Tex1,
Tex2和Tex3,并设置每个纹理的过滤模式。
18.1.2
多纹理坐标
对于每个3D三角形,我们应该在纹理上定义一个三角形以映射该3D三角形。我们通过对每个顶点增加纹理坐标完成映射。因此,每三个顶点定义一个三角形,它对应于纹理上的三角形。
现在使用多纹理,每三个顶点定义一个三角形,我们需要在每个被使用的纹理上定义一个相应的三角形。通过给每个顶点增加额外的一套纹理坐标——每个顶点一套,对应于每个使用的纹理。举个例子,如果我们混合三个纹理到一起,那么每个顶点必须有三套纹理坐标以索引到三个使用的纹理。因此,一个包含三个纹理的多纹理化顶点结构看起来可能像这样:
struct
MultiTexVertex
{
MultiTexVertex(float x, float y, float z,
float u0, float v0,
float u1, float v1,
float u2, float v2)
{
_x = x; _y = y; _z = z;
_u0 = u0; _v0 = v0;
_u1 = u1; _v1 = v1;
_u2 = u2; _v2 = v2;
}
float _x, _y, _z;
float _u0, _v0; // Texture coordinates for
texture at stage 0.
float _u1, _v1; // Texture coordinates for
texture at stage 1.
float _u2, _v2; // Texture coordinates for
texture at stage 2.
static const DWORD FVF;
};
const
DWORD MultiTexVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX3;
|
注意,指定自由顶点格式标记D3DFVF_TEX3表明顶点结构包含3套纹理坐标。固定功能管线支持最多8套纹理坐标。如果多于8套,你必须使用顶点声明和可编程顶点管线。
注意:在新版本像素着色器中,我们可以使用一套纹理坐标集来索引多个纹理,并因此消除了对多个纹理坐标的需要。当然这得假设每个纹理阶段使用相同的纹理坐标。如果每个阶段的纹理坐标不同,则我们仍然需要多纹理坐标。
18.2像素着色器输入和输出
有两样东西要输入到像素着色器:颜色和纹理坐标。两样都是以每像素为单位的。
注意:顶点颜色是在图元的面(face
of primitive)间进行插值的。
每个像素的纹理坐标就是简单的 (u , v)
,它指定了纹理的哪个图素被映射到像素上。在输入到像素着色器前,Direct3D根据顶点颜色和顶点纹理坐标,为每个像素计算颜色和纹理坐标。输入到像素着色器的颜色和纹理坐标的数值依赖于顶点着色器输出的颜色和纹理坐标的数值。例如,如果一个顶点着色器输出了两个颜色和三个纹理坐标,那么Direct3D将会为每个像素计算两个颜色和三个纹理坐标并且把它们把它们输入到像素着色器。我们使用带语意的语法(semantic
syntax)映射输入颜色和纹理坐标进我们的着色器程序的变量里。用前面的例子,我们可以这样写:
struct
PS_INPUT
{
vector c0 : COLOR0;
vector c1 : COLOR1;
float2 t0 : TEXCOORD0;
float2 t1 : TEXCOORD1;
float2 t2 : TEXCOORD2;
};
|
对于输出,像素着色器只输出一个计算过的该像素的颜色值:
struct
PS_OUTPUT
{
vector finalPixelColor : COLOR0;
};
|
18.3使用像素着色器的步骤
下面的列表概述了创建和使用像素着色器的必要步骤:
1.
编写并编译像素着色器
2.
创建一个IDirect3DPixelShader9接口来代表基于已编译代码的像素着色器
3.
用IDirect3DDevice9::SetPixelShader方法允许该像素着色器
当然,用完顶点着色器之后我们必须销毁它。
18.3.1
编写并编译像素着色器
我们用与编译顶点着色器一样的方式编译像素着色器。首先,我们必须编写一个像素着色器程序, 我们用HLSL编写我们的着色器。一旦写好着色器代码,我们就可以用D3DXCompileShaderFromFile函数编译该着色器了,这个函数返回一个ID3DXBuffer指针,它包含已编译的着色器代码。
注意:因为我们使用的是像素着色器,所以要记得把编译目标改成像素着色器目标(比如:ps_2_0),而不是顶点着色器目标(比如:vs_2_0)。编译目标通过D3DXCompileShaderFromFile函数的一个参数指定。
18.3.2
创建像素着色器
一旦我们编译了着色器代码,我们就可以获得一个IDirect3DPixelShader的接口指针,它代表一个像素着色器,使用下面的方法:
HRESULT IDirect3DDevice9::CreatePixelShader(
CONST DWORD *pFunction,
IDirect3DPixelShader9** ppShader
);
|
pFunction——已编译着色器代码的指针
ppShader——返回一个IDirect3DPixelShader9接口的指针
例如,假设变量shader是一个包含已编译着色器代码的ID3DXBuffer接口指针。那么要获得IDirect3DPixelShader9接口,我们应该写:
IDirect3DPixelShader9* MultiTexPS = 0;
hr =
Device->CreatePixelShader( (DWORD*)shader->GetBufferPointer(),
&MultiTexPS);
|
注意:重申一遍,D3DXCompileShaderFromFile是一个可以返回已编译着色器代码(shader)的函数。
18.3.3
建立像素着色器
在我们获得一个代表我们的像素着色器的IDirect3DPixelShader9接口的指针之后,我们可以使用下面的方法使用它:
HRESULT IDirect3DDevice9::SetPixelShader(
IDirect3DPixelShader9* pShader
);
|
这个方法只接受一个参数,我们通过它传递一个我们希望使用的指向像素着色器的指针。
Device->SetPixelShader(MultiTexPS);
|
18.3.4
销毁像素着色器
和其它所有Direct3D接口一样,要清除这些接口,我们必须在使用完毕后调用它们的Release方法。
d3d::Release<IDirect3DPixelShader9*>(MultiTexPS);
|