现在是学习如何使用着色器和着色器资源开发Windows 8的Microsoft DirectX游戏的时候了。我们已经看到如何设置图形设备和资源,甚至可能已经开始修改其管道。所以现在让我们看看像素和顶点着色器。
如果您对着色语言不熟悉,请按顺序进行快速讨论。着色器是在图形管道的特定阶段编译和运行的小型低级程序。他们的专长是非常快速的浮点数学运算。最常见的着色器程序是:
- 顶点着色器——针对场景中的每个顶点执行。这个着色器在调用app提供给它的顶点缓冲区元素上运行,并最小化得到将被光栅化成像素位置的4分量位置矢量。
- 像素着色器——针对渲染目标中的每个像素执行。该着色器接收来自之前着色器阶段的光栅化坐标(在最简单的管线中,这将是顶点着色器),并返回该像素位置的颜色(或其他4分量值),然后写入渲染目标。
这个例子包括非常基本的顶点和像素着色器,只绘制几何图形,以及更复杂的着色器,添加基本的光照计算。
着色器程序是用Microsoft高级着色器语言(HLSL)编写的。HLSL语法看起来很像C,但没有指针。着色器程序必须非常紧凑和高效。如果着色器编译的指令过多,则无法运行,并返回错误。(请注意,允许的指令的确切数量是Direct3D功能级别的一部分。)
在Direct3D中,着色器不会在运行时编译;编译程序的其余部分时编译它们。当您使用Microsoft Visual Studio 2013编译您的应用程序时,HLSL文件将被编译为CSO(.cso)文件,您的应用程序在绘制之前必须加载并放置在GPU内存中。确保在打包时将这些CSO文件包含在您的应用程序中;它们就像网格和纹理一样是资产。
理解HLSL语义
在我们继续之前,花一点时间讨论HLSL语义是很重要的,因为它们通常是新的Direct3D开发人员的混乱点。HLSL语义是识别在应用程序和着色器程序之间传递的值的字符串。虽然它们可以是各种可能的字符串中的任何一种,但最好的做法是使用像POSITION或COLOR这样的字符串来指示用法。您在构建常量缓冲区或输入布局时分配这些语义。您还可以将0到7之间的数字附加到语义中,以便使用单独的寄存器获取相似的值。例如:COLOR0,COLOR1,COLOR2……
前缀为”SV_”的语义是由着色器程序写入的系统值语义;你的游戏本身(运行在CPU上)不能修改它们。通常,这些语义包含来自图形管道中其他着色器阶段的输入或输出的值,或者完全由GPU生成的值。
此外,SV_语义在用于指定着色器阶段的输入或输出时具有不同的行为。例如,SV_POSITION(输出)包含在顶点着色器阶段变换的顶点数据,而SV_POSITION(输入)包含在光栅化阶段由GPU插值的像素位置值。
以下是一些常见的HLSL语义:
- 顶点缓冲区数据的位置(n)。SV_POSITION为像素着色器提供了一个像素位置,不能被你的游戏写入。
- NORMAL(n)为由顶点缓冲区提供的正常数据。
- TEXCOORD(n)提供给着色器的纹理UV坐标数据。
- COLOR(n)提供给着色器的RGBA颜色数据。请注意,它的处理方式与坐标数据相同,包括在光栅化过程中插值;语义只是帮助您确定它是颜色数据。
- SV_Target[n]用于从像素着色器写入目标纹理或其他像素缓冲区。
在我们回顾这个例子时,我们将看到一些HLSL语义的例子。
从常量缓冲区读取
任何着色器都可以从常量缓冲区读取,如果该缓冲区作为资源附加到其舞台上的话。在这个例子中,只有顶点着色器被分配一个常量缓冲区。
常量缓冲区在两个地方声明:在C++代码中,以及在相应的HLSL文件中,它们将访问它。
这是如何在C++代码中声明常量缓冲区结构。
typedef struct _constantBufferStruct{
DirectX::XMFLOAT4X4 world;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
}ConstantBufferStruct;
在C++代码中为常量缓冲区声明结构时,请确保所有数据都沿着16字节的边界正确对齐。最简单的方法是使用DirectXMath类型,如XMFLOAT4或XMFLOAT4X4,如示例代码所示。你也可以通过声明静态声明来防止未对齐的缓冲区:
// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct)%16)==0,"Constant Buffer size must be 16-byte aligned");
如果ConstantBufferStruct不是16字节对齐的,这行代码会在编译时导致错误。 有关常量缓冲区对齐和打包的更多信息,请参见常量变量的打包规则。
现在,这里是如何在顶点着色器HLSL中声明常量缓冲区。
cbuffer ModelViewProjectionConstantBuffer:register(b0){
matrix mWorld;// world matrix for object
matrix View;// view matrix
matrix Projection;// projection matrix
};
所有缓冲区(常量,纹理,采样器或其他)必须定义一个寄存器,以便GPU可以访问它们。每个着色器阶段最多允许15个常量缓冲区,每个缓冲区最多可以容纳4096个常量变量。注册用法声明语法如下所示:
- b#:一个常量缓冲区(cbuffer)的寄存器。
- t#:纹理缓冲区(tbuffer)的寄存器。
- s#:采样器的寄存器。(采样器定义纹理资源中纹理元素的查找行为。)
例如,像素着色器的HLSL可能会使用像这样的声明将纹理和采样器作为输入。
Texture2D simpleTexture:register(t0);
SamplerState simpleSampler:register(s0);
由您决定将常量缓冲区分配给寄存器——当您设置管道时,您将一个常量缓冲区附加到您在HLSL文件中分配给它的同一个插槽中。例如,在上一个主题中,对VSSetConstantBuffers的调用指示第一个参数为”0”。这将告诉Direct3D将常量缓冲区资源附加到寄存器0,该寄存器与HLSL文件中缓冲区的分配与寄存器(b0)相匹配。
从顶点缓冲区读取
顶点缓冲区将场景对象的三角形数据提供给顶点着色器。与常量缓冲区一样,顶点缓冲区结构也是使用类似的包装规则在C++代码中声明的。
typedef struct _vertexPositionColor{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
}VertexPositionColor;
Direct3D 11中没有标准的顶点数据格式,而是使用描述符来定义我们自己的顶点数据布局。数据字段是使用D3D11_INPUT_ELEMENT_DESC结构的数组定义的。在这里,我们展示一个简单的输入布局,描述与前面的struct相同的顶点格式:
D3D11_INPUT_ELEMENT_DESC iaDesc[]={
{
"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,
0,0,D3D11_INPUT_PER_VERTEX_DATA,0
},
{
"COLOR",0,DXGI_FORMAT_R32G32B32_FLOAT,
0,12,D3D11_INPUT_PER_VERTEX_DATA,0
},
};
hr=device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayout
);
如果在修改示例代码时将数据添加到顶点格式,请务必更新输入布局,否则着色器将无法解释它。您可以像这样修改顶点布局:
typedef struct _vertexPositionColorTangent{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT3 tangent;
}VertexPositionColorTangent;
在这种情况下,您可以按如下方式修改输入布局定义。
D3D11_INPUT_ELEMENT_DESC iaDescExtended[]={
{
"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
},
{
"TANGENT",0,DXGI_FORMAT_R32G32B32_FLOAT,
0,12,D3D11_INPUT_PER_VERTEX_DATA,0
},
};
hr=device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayoutExtended
);
每个输入布局元素定义都以一个字符串作为前缀,比如”位置”或”正常”——这是我们前面在本主题中讨论的语义。这就像一个句柄,可以帮助GPU在处理顶点时识别这个元素。为您的顶点元素选择常用,有意义的名称。
就像使用常量缓冲区一样,顶点着色器对于传入的顶点元素也有相应的缓冲区定义。(这就是为什么我们在创建输入布局时提供了对顶点着色器资源的引用——Direct3D使用着色器的输入结构来验证每个顶点数据布局。)请注意输入布局定义和此HLSL缓冲区声明之间的语义是如何匹配的。但是,COLOR附加了一个”0”。如果在布局中只声明了一个COLOR元素,则不需要添加0,但是如果您选择在将来添加更多的颜色元素,最好附加它。
struct VS_INPUT{
float3 vPos:POSITION;
float3 vColor:COLOR0;
};
在着色器之间传递数据
着色器在执行时接受输入类型并从主函数返回输出类型。对于上一节定义的顶点着色器,输入类型是VS_INPUT结构,我们定义了一个匹配的输入布局和C++结构。该结构的数组用于在CreateCube方法中创建顶点缓冲区。
顶点着色器返回一个PS_INPUT结构,该结构必须最低限度地包含4分量(float4)最终顶点位置。该位置值必须具有系统值语义SV_POSITION,以便GPU具有执行下一个绘制步骤所需的数据。请注意,顶点着色器输出和像素着色器输入之间不存在1:1的对应关系;顶点着色器为其给定的每个顶点返回一个结构,但像素着色器为每个像素运行一次。这是因为每个顶点数据首先经过光栅化阶段。这个阶段决定哪个像素”覆盖”你正在绘制的几何图形,计算每个像素的内插的每个顶点数据,然后为每个像素调用像素着色器一次。插值是栅格化输出值时的默认行为,对于输出矢量数据(光矢量,每顶点法线和切线等)的正确处理尤其重要。
struct PS_INPUT{
float4 Position:SV_POSITION; // interpolated vertex position (system value)
float4 Color:COLOR0; // interpolated diffuse color
};
检查顶点着色器
示例顶点着色器非常简单:取顶点(位置和颜色),将位置从模型坐标转换为透视投影坐标,并将其与颜色一起返回到光栅化器。请注意,颜色值与位置数据一起插值,即使顶点着色器没有对颜色值执行任何计算,也会为每个像素提供不同的值。
VS_OUTPUT main(VS_INPUT input){ // main is the default function name
VS_OUTPUT Output;
float4 pos=float4(input.vPos,1.0f);
// Transform the position from object space to homogeneous projection space
pos=mul(pos,mWorld);
pos=mul(pos,View);
pos=mul(pos,Projection);
Output.Position=pos;
// Just pass through the color data
Output.Color=float4(input.vColor,1.0f);
return Output;
}
更复杂的顶点着色器,例如为Phong着色设置对象顶点的着色器,可能看起来更像这样。在这种情况下,我们正在利用向量和法线被内插来逼近光滑表面的事实。
// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer:register(b0){
matrix model;
matrix view;
matrix projection;
};
cbuffer LightConstantBuffer:register(b1){
float4 lightPos;
};
struct VertexShaderInput{
float3 pos:POSITION;
float3 normal:NORMAL;
};
// Per-pixel color data passed through the pixel shader.
struct PixelShaderInput{
float4 position:SV_POSITION;
float3 outVec:POSITION0;
float3 outNormal:NORMAL0;
float3 outLightVec:POSITION1;
};
PixelShaderInput main(VertexShaderInput input){
// Inefficient -- doing this only for instruction. Normally, you would
// premultiply them on the CPU and place them in the cbuffer.
matrix mvMatrix=mul(model,view);
matrix mvpMatrix=mul(mvMatrix,projection);
PixelShaderInput output;
float4 pos=float4(input.pos,1.0f);
float4 normal=float4(input.normal,1.0f);
float4 light=float4(lightPos.xyz,1.0f)
float4 eye=float4(0.0f,0.0f,-2.0f,1.0f);
// Transform the vertex position into projected space.
output.gl_Position=mul(pos,mvpMatrix);
output.outNormal=mul(normal,mvMatrix).xyz;
output.outVec=-(eye-mul(pos,mvMatrix)).xyz;
output.outLightVec=mul(light,mvMatrix).xyz;
return output;
}
检查像素着色器
这个例子中的像素着色器很可能是像素着色器中的绝对最小代码量。它将光栅化期间生成的插值像素颜色数据作为输出返回,并将其写入渲染目标。多无聊!
PS_OUTPUT main(PS_INPUT In){
PS_OUTPUT Output;
Output.RGBColor=In.Color;
return Output;
}
重要的部分是返回值上的SV_TARGET系统值语义。它表示输出将被写入主渲染目标,这是提供给交换链显示的纹理缓冲区。这是像素着色器所必需的——没有来自像素着色器的颜色数据,Direct3D将不会显示任何内容!
用于执行Phong阴影的更复杂像素着色器的示例可能如下所示。由于向量和法线是插值的,所以我们不需要在每个像素的基础上计算它们。但是,由于内插是如何工作的,我们必须重新规范它们;在概念上,我们需要逐渐地将矢量从顶点A的方向”旋转”到顶点B的方向,保持其长度插值而不是跨两个矢量端点之间的直线。
cbuffer MaterialConstantBuffer:register(b2){
float4 lightColor;
float4 Ka;
float4 Kd;
float4 Ks;
float4 shininess;
};
struct PixelShaderInput{
float4 position:SV_POSITION;
float3 outVec:POSITION0;
float3 normal:NORMAL0;
float3 light:POSITION1;
};
float4 main(PixelShaderInput input):SV_TARGET{
float3 L=normalize(input.light);
float3 V=normalize(input.outVec);
float3 R=normalize(reflect(L,input.normal));
float4 diffuse=Ka+(lightColor*Kd*max(dot(input.normal,L),0.0f));
diffuse=saturate(diffuse);
float4 specular=Ks*pow(max(dot(R,V),0.0f),shininess.x-50.0f);
specular=saturate(specular);
float4 finalColor=diffuse+specular;
return finalColor;
}
在另一个例子中,像素着色器采用自己的包含光照和材料信息的常量缓冲区。顶点着色器中的输入布局将被扩展为包括正常数据,并且该顶点着色器的输出预计包括视图坐标系中的顶点,光线和顶点法线的变换矢量。
如果纹理缓冲区和采样器分配了寄存器(分别为t和s),则也可以在像素着色器中访问它们。
Texture2D simpleTexture:register(t0);
SamplerState simpleSampler:register(s0);
struct PixelShaderInput{
float4 pos:SV_POSITION;
float3 norm:NORMAL;
float2 tex:TEXCOORD0;
};
float4 SimplePixelShader(PixelShaderInput input):SV_TARGET{
float3 lightDirection=normalize(float3(1,-1,0));
float4 texelColor=simpleTexture.Sample(simpleSampler,input.tex);
float lightMagnitude=0.8f*saturate(dot(input.norm,-lightDirection))+0.2f;
return texelColor*lightMagnitude;
}
着色器是非常强大的工具,可用于生成过程资源,如阴影贴图或噪点纹理。 事实上,先进的技术要求您更抽象地考虑纹理,而不是视觉元素,而是缓冲区。它们保存诸如高度信息的数据,或者可以在最终像素着色器通道或在该特定帧中采样的其他数据,作为多阶段效果通过的一部分。多重采样是一个强大的工具和许多现代视觉效果的中坚力量。
下一步
希望你在这一点上对DirectX 11感到满意,并准备开始处理你的项目。以下是一些链接,可以帮助您解答有关使用DirectX和C++进行开发的其他问题:
相关话题
使用DirectX设备资源
了解Direct3D 11渲染管道
原文链接:Work with shaders and shader resources
返回目录