DirectX/10.0/Direct3D/缓冲区着色器 HLSL
本教程将介绍如何在 DirectX 11 中编写顶点和像素着色器。它也将介绍在 DirectX 11 中使用顶点和索引缓冲区。这些是渲染 3D 图形所需要理解和利用的最基本概念。
首先要理解的概念是顶点缓冲区。为了说明这个概念,让我们以 3D 球体模型为例
3D 球体模型实际上是由数百个三角形组成的
球体模型中的每个三角形都有三个点,我们称每个点为顶点。因此,为了渲染球体模型,我们需要将构成球体的所有顶点放到一个称为顶点缓冲区的特殊数据数组中。一旦球体模型的所有点都在顶点缓冲区中,我们就可以将顶点缓冲区发送到 GPU,以便它可以渲染模型。
索引缓冲区与顶点缓冲区相关。它们的目的是记录顶点缓冲区中每个顶点的位置。然后 GPU 使用索引缓冲区快速找到顶点缓冲区中的特定顶点。索引缓冲区类似于使用书籍索引的概念,它有助于以更快的速度找到你正在寻找的主题。DirectX SDK 文档指出,使用索引缓冲区还可以增加将顶点数据缓存到视频内存中更快的区域的可能性。因此,出于性能方面的考虑,强烈建议使用它们。
顶点着色器是主要用于将顶点缓冲区中的顶点转换为 3D 空间的小程序。还可以进行其他计算,例如计算每个顶点的法线。顶点着色器程序将在 GPU 需要处理每个顶点时被调用。例如,一个 5,000 多边形模型将在每一帧运行你的顶点着色器程序 15,000 次,仅仅是为了绘制那个模型。因此,如果你将图形程序锁定在 60 帧每秒,它每秒将调用你的顶点着色器 900,000 次,只绘制 5,000 个三角形。正如你所见,编写高效的顶点着色器非常重要。
像素着色器是专门用于对我们绘制的多边形进行着色的小程序。它们由 GPU 为将要绘制到屏幕上的每个可见像素运行。对你的多边形面进行着色、纹理、照明和大多数其他效果都是由像素着色器程序处理的。由于像素着色器会被 GPU 调用很多次,因此必须编写高效的像素着色器。
HLSL 是我们在 DirectX 11 中用来编写这些小型顶点和像素着色器程序的语言。语法与 C 语言几乎完全相同,只是有一些预定义的类型。HLSL 程序文件由全局变量、类型定义、顶点着色器、像素着色器和几何着色器组成。由于这是第一个 HLSL 教程,我们将使用 DirectX 11 做一个非常简单的 HLSL 程序,以开始学习。
该框架已针对本教程进行了更新。在 GraphicsClass 下,我们添加了三个新类,名为 CameraClass、ModelClass 和 ColorShaderClass。CameraClass 将负责我们之前谈到的视图矩阵。它将处理相机在世界中的位置,并在着色器需要绘制并弄清楚我们从哪里观察场景时将它传递给着色器。ModelClass 将处理我们的 3D 模型的几何形状,在本教程中,3D 模型为了简单起见,将只是一个简单的三角形。最后,ColorShaderClass 将负责渲染模型到屏幕上,并调用我们的 HLSL 着色器。
我们将从查看 HLSL 着色器程序开始本教程代码。
这些将是我们的第一个着色器程序。着色器是进行模型实际渲染的小程序。这些着色器是用 HLSL 编写的,并存储在名为 color.vs 和 color.ps 的源文件中。我暂时将这些文件与 .cpp 和 .h 文件放在了引擎中。该着色器的目的是只绘制彩色三角形,因为在这个第一个 HLSL 教程中,我尽可能地保持简单。以下是顶点着色器的代码
//////////////////////////////////////////////////////////////////////////////// // Filename: color.vs ////////////////////////////////////////////////////////////////////////////////
在着色器程序中,你从全局变量开始。这些全局变量可以从你的 C++ 代码中外部修改。你可以使用多种类型的变量,如 int 或 float,然后从外部设置它们供着色器程序使用。通常,即使只有一个全局变量,你也会将大多数全局变量放到名为“cbuffer”的缓冲区对象类型中。逻辑上组织这些缓冲区对于高效执行着色器以及图形卡存储缓冲区的方式也很重要。在本例中,我将三个矩阵放在同一个缓冲区中,因为我将在每一帧同时更新它们。
///////////// // GLOBALS // ///////////// cbuffer MatrixBuffer { matrix worldMatrix; matrix viewMatrix; matrix projectionMatrix; };
与 C 类似,我们可以创建自己的类型定义。我们将使用 HLSL 提供的不同类型,例如 float4,这使得编程着色器更加容易和可读。在本例中,我们正在创建具有 x、y、z、w 位置向量和红色、绿色、蓝色、alpha 颜色的类型。POSITION、COLOR 和 SV_POSITION 是语义,它们向 GPU 传达了变量的用途。我在这里必须创建两个不同的结构,因为即使结构相同,顶点着色器和像素着色器的语义也不同。POSITION 对顶点着色器有效,SV_POSITION 对像素着色器有效,而 COLOR 对两者都有效。如果你想要多个相同类型的变量,则必须在末尾添加一个数字,例如 COLOR0、COLOR1 等。
////////////// // TYPEDEFS // ////////////// struct VertexInputType { float4 position : POSITION; float4 color : COLOR; }; struct PixelInputType { float4 position : SV_POSITION; float4 color : COLOR; };
当 GPU 处理从发送到它的顶点缓冲区中获取的数据时,将调用顶点着色器。这个名为 ColorVertexShader 的顶点着色器将针对顶点缓冲区中的每个顶点调用。顶点着色器的输入必须与顶点缓冲区中的数据格式以及着色器源文件中定义的类型定义相匹配,在本例中为 VertexInputType。顶点着色器的输出将发送到像素着色器。在本例中,输出类型称为 PixelInputType,它也在上面定义。
考虑到这一点,你会发现顶点着色器创建了一个输出变量,该变量的类型为 PixelInputType。然后它获取输入顶点的位置,并将其乘以世界矩阵、视图矩阵和投影矩阵。这将根据我们的视图将顶点放置在 3D 空间中正确的位置,然后放置到 2D 屏幕上。之后,输出变量将获取输入颜色的副本,然后返回输出,该输出将用作像素着色器的输入。还要注意,我确实设置了输入位置的 W 值为 1.0,否则它将是未定义的,因为我们只读入了位置的 XYZ 向量。
//////////////////////////////////////////////////////////////////////////////// // Vertex Shader //////////////////////////////////////////////////////////////////////////////// PixelInputType ColorVertexShader(VertexInputType input) { PixelInputType output; // Change the position vector to be 4 units for proper matrix calculations. input.position.w = 1.0f; // Calculate the position of the vertex against the world, view, and projection matrices. output.position = mul(input.position, worldMatrix); output.position = mul(output.position, viewMatrix); output.position = mul(output.position, projectionMatrix); // Store the input color for the pixel shader to use. output.color = input.color; return output; }
像素着色器绘制将在屏幕上渲染的多边形上的每个像素。在此像素着色器中,它使用 PixelInputType 作为输入,并返回一个 float4 作为输出,表示最终的像素颜色。此像素着色器程序非常简单,我们只需告诉它将像素颜色设置为与输入值的颜色相同。请注意,像素着色器从顶点着色器的输出获取其输入。
//////////////////////////////////////////////////////////////////////////////// // Filename: color.ps //////////////////////////////////////////////////////////////////////////////// ////////////// // TYPEDEFS // ////////////// struct PixelInputType { float4 position : SV_POSITION; float4 color : COLOR; }; //////////////////////////////////////////////////////////////////////////////// // Pixel Shader //////////////////////////////////////////////////////////////////////////////// float4 ColorPixelShader(PixelInputType input) : SV_TARGET { return input.color; }
Modelclass.h
[edit | edit source]如前所述,ModelClass 负责封装 3D 模型的几何图形。在本教程中,我们将手动设置单个绿色三角形的 data。我们还将为三角形创建顶点和索引缓冲区,以便可以渲染它。
//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _MODELCLASS_H_ #define _MODELCLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3d11.h> #include <d3dx10math.h> //////////////////////////////////////////////////////////////////////////////// // Class name: ModelClass //////////////////////////////////////////////////////////////////////////////// class ModelClass { private:
以下是我们将与 ModelClass 中的顶点缓冲区一起使用的顶点类型的定义。还要注意,此 typedef 必须与稍后在本教程中介绍的 ColorShaderClass 中的布局匹配。
struct VertexType { D3DXVECTOR3 position; D3DXVECTOR4 color; }; public: ModelClass(); ModelClass(const ModelClass&); ~ModelClass();
此处的函数处理模型顶点和索引缓冲区的初始化和关闭。Render 函数将模型几何图形放在显卡上,以准备由颜色着色器绘制。
bool Initialize(ID3D11Device*); void Shutdown(); void Render(ID3D11DeviceContext*); int GetIndexCount(); private: bool InitializeBuffers(ID3D11Device*); void ShutdownBuffers(); void RenderBuffers(ID3D11DeviceContext*);
ModelClass 中的私有变量是顶点和索引缓冲区,以及两个整数用于跟踪每个缓冲区的大小。请注意,所有 DirectX 11 缓冲区通常使用通用 ID3D11Buffer 类型,并且在首次创建时通过缓冲区描述更清楚地识别。
private: ID3D11Buffer *m_vertexBuffer, *m_indexBuffer; int m_vertexCount, m_indexCount; }; #endif
Modelclass.cpp
[edit | edit source]//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "modelclass.h"
类构造函数将顶点和索引缓冲区指针初始化为 null。
ModelClass::ModelClass() { m_vertexBuffer = 0; m_indexBuffer = 0; } ModelClass::ModelClass(const ModelClass& other) { } ModelClass::~ModelClass() { }
Initialize 函数将调用顶点和索引缓冲区的初始化函数。
bool ModelClass::Initialize(ID3D11Device* device) { bool result; // Initialize the vertex and index buffer that hold the geometry for the triangle. result = InitializeBuffers(device); if(!result) { return false; } return true; }
Shutdown 函数将调用顶点和索引缓冲区的关闭函数。
void ModelClass::Shutdown() { // Release the vertex and index buffers. ShutdownBuffers(); return; }
Render 由 GraphicsClass::Render 函数调用。此函数调用 RenderBuffers 将顶点和索引缓冲区放在图形管道上,以便颜色着色器能够渲染它们。
void ModelClass::Render(ID3D11DeviceContext* deviceContext) { // Put the vertex and index buffers on the graphics pipeline to prepare them for drawing. RenderBuffers(deviceContext); return; }
GetIndexCount 返回模型中的索引数量。颜色着色器需要此信息才能绘制此模型。
int ModelClass::GetIndexCount() { return m_indexCount; }
InitializeBuffers 函数是我们在其中处理创建顶点和索引缓冲区的地方。通常,您会读取模型并从该 data 文件创建缓冲区。在本教程中,我们只会手动设置顶点和索引缓冲区中的点,因为它只是一个三角形。
bool ModelClass::InitializeBuffers(ID3D11Device* device) { VertexType* vertices; unsigned long* indices; D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc; D3D11_SUBRESOURCE_DATA vertexData, indexData; HRESULT result;
首先创建两个临时数组来保存顶点和索引 data,我们将在稍后使用这些数据填充最终缓冲区。
// Set the number of vertices in the vertex array. m_vertexCount = 3; // Set the number of indices in the index array. m_indexCount = 3; // Create the vertex array. vertices = new VertexType[m_vertexCount]; if(!vertices) { return false; } // Create the index array. indices = new unsigned long[m_indexCount]; if(!indices) { return false; }
现在用三角形的三个点以及每个点的索引填充顶点和索引数组。请注意,我按绘制它们的顺时针顺序创建点。如果您以逆时针方向执行此操作,它将认为三角形面向相反方向,并且由于背面剔除而不会绘制它。请始终记住,将顶点发送到 GPU 的顺序非常重要。颜色也在这里设置,因为它是顶点描述的一部分。我将颜色设置为绿色。
// Load the vertex array with data. vertices[0].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // Bottom left. vertices[0].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); vertices[1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f); // Top middle. vertices[1].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); vertices[2].position = D3DXVECTOR3(1.0f, -1.0f, 0.0f); // Bottom right. vertices[2].color = D3DXVECTOR4(0.0f, 1.0f, 0.0f, 1.0f); // Load the index array with data. indices[0] = 0; // Bottom left. indices[1] = 1; // Top middle. indices[2] = 2; // Bottom right.
填充顶点数组和索引数组后,我们现在可以使用它们来创建顶点缓冲区和索引缓冲区。创建这两个缓冲区的方式相同。首先填写缓冲区的描述。在描述中,ByteWidth(缓冲区的大小)和 BindFlags(缓冲区的类型)是您需要确保正确填写的内容。填充描述后,您还需要填充一个子资源指针,该指针将指向您之前创建的顶点或索引数组。有了描述和子资源指针,您就可以使用 D3D 设备调用 CreateBuffer,它将返回指向新缓冲区的指针。
// Set up the description of the static vertex buffer. vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount; vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; vertexBufferDesc.CPUAccessFlags = 0; vertexBufferDesc.MiscFlags = 0; vertexBufferDesc.StructureByteStride = 0; // Give the subresource structure a pointer to the vertex data. vertexData.pSysMem = vertices; vertexData.SysMemPitch = 0; vertexData.SysMemSlicePitch = 0; // Now create the vertex buffer. result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer); if(FAILED(result)) { return false; } // Set up the description of the static index buffer. indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; indexBufferDesc.ByteWidth = sizeof(unsigned long) * m_indexCount; indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; indexBufferDesc.CPUAccessFlags = 0; indexBufferDesc.MiscFlags = 0; indexBufferDesc.StructureByteStride = 0; // Give the subresource structure a pointer to the index data. indexData.pSysMem = indices; indexData.SysMemPitch = 0; indexData.SysMemSlicePitch = 0; // Create the index buffer. result = device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer); if(FAILED(result)) { return false; }
创建顶点缓冲区和索引缓冲区后,您可以删除顶点和索引数组,因为它们不再需要,因为 data 已复制到缓冲区中。
// Release the arrays now that the vertex and index buffers have been created and loaded. delete [] vertices; vertices = 0; delete [] indices; indices = 0; return true; }
ShutdownBuffers 函数只是释放了在 InitializeBuffers 函数中创建的顶点缓冲区和索引缓冲区。
void ModelClass::ShutdownBuffers() { // Release the index buffer. if(m_indexBuffer) { m_indexBuffer->Release(); m_indexBuffer = 0; } // Release the vertex buffer. if(m_vertexBuffer) { m_vertexBuffer->Release(); m_vertexBuffer = 0; } return; }
RenderBuffers 由 Render 函数调用。此函数的目的是将顶点缓冲区和索引缓冲区设置为 GPU 中输入汇编器上的活动缓冲区。一旦 GPU 拥有活动顶点缓冲区,它就可以使用着色器来渲染该缓冲区。此函数还定义了这些缓冲区应如何绘制,例如三角形、线、扇形等。在本教程中,我们将顶点缓冲区和索引缓冲区设置为输入汇编器上的活动缓冲区,并告诉 GPU 使用 IASetPrimitiveTopology DirectX 函数以三角形的形式绘制缓冲区。
void ModelClass::RenderBuffers(ID3D11DeviceContext* deviceContext) { unsigned int stride; unsigned int offset; // Set vertex buffer stride and offset. stride = sizeof(VertexType); offset = 0; // Set the vertex buffer to active in the input assembler so it can be rendered. deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset); // Set the index buffer to active in the input assembler so it can be rendered. deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0); // Set the type of primitive that should be rendered from this vertex buffer, in this case triangles. deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); return; }
Colorshaderclass.h
[edit | edit source]ColorShaderClass 是我们将用来调用 HLSL 着色器来绘制 GPU 上的 3D 模型的。
//////////////////////////////////////////////////////////////////////////////// // Filename: colorshaderclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _COLORSHADERCLASS_H_ #define _COLORSHADERCLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3d11.h> #include <d3dx10math.h> #include <d3dx11async.h> #include <fstream> using namespace std; //////////////////////////////////////////////////////////////////////////////// // Class name: ColorShaderClass //////////////////////////////////////////////////////////////////////////////// class ColorShaderClass { private:
以下是将与顶点着色器一起使用的 cBuffer 类型的定义。此 typedef 必须与顶点着色器中的 typedef 完全相同,因为模型 data 需要与着色器中的 typedef 匹配,才能进行正确渲染。
struct MatrixBufferType { D3DXMATRIX world; D3DXMATRIX view; D3DXMATRIX projection; }; public: ColorShaderClass(); ColorShaderClass(const ColorShaderClass&); ~ColorShaderClass();
此处的函数处理着色器的初始化和关闭。render 函数设置着色器参数,然后使用着色器绘制准备好的模型顶点。
bool Initialize(ID3D11Device*, HWND); void Shutdown(); bool Render(ID3D11DeviceContext*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX); private: bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*); void ShutdownShader(); void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*); bool SetShaderParameters(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX); void RenderShader(ID3D11DeviceContext*, int); private: ID3D11VertexShader* m_vertexShader; ID3D11PixelShader* m_pixelShader; ID3D11InputLayout* m_layout; ID3D11Buffer* m_matrixBuffer; }; #endif
Colorshaderclass.cpp
[edit | edit source]//////////////////////////////////////////////////////////////////////////////// // Filename: colorshaderclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "colorshaderclass.h"
与往常一样,类构造函数将类中所有私有指针初始化为 null。
ColorShaderClass::ColorShaderClass() { m_vertexShader = 0; m_pixelShader = 0; m_layout = 0; m_matrixBuffer = 0; } ColorShaderClass::ColorShaderClass(const ColorShaderClass& other) { } ColorShaderClass::~ColorShaderClass() { }
Initialize 函数将调用着色器的初始化函数。我们传入 HLSL 着色器文件的文件名,在本教程中,它们分别命名为 color.vs 和 color.ps。
bool ColorShaderClass::Initialize(ID3D11Device* device, HWND hwnd) { bool result; // Initialize the vertex and pixel shaders. result = InitializeShader(device, hwnd, L"../Engine/color.vs", L"../Engine/color.ps"); if(!result) { return false; } return true; }
Shutdown 函数将调用着色器的关闭。
void ColorShaderClass::Shutdown() { // Shutdown the vertex and pixel shaders as well as the related objects. ShutdownShader(); return; }
Render 将首先使用 SetShaderParameters 函数设置着色器内部的参数。设置完参数后,它将调用 RenderShader 使用 HLSL 着色器绘制绿色三角形。
bool ColorShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix) { bool result; // Set the shader parameters that it will use for rendering. result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix); if(!result) { return false; } // Now render the prepared buffers with the shader. RenderShader(deviceContext, indexCount); return true; }
现在我们将从本教程中最重要的函数之一开始,它被称为 InitializeShader。此函数实际上是加载着色器文件并使其可供 DirectX 和 GPU 使用的函数。您还将看到布局的设置以及顶点缓冲区 data 在 GPU 的图形管道上的外观。布局将需要与 modelclass.h 文件中的 VertexType 以及 color.vs 文件中定义的 VertexType 相匹配。
bool ColorShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename) { HRESULT result; ID3D10Blob* errorMessage; ID3D10Blob* vertexShaderBuffer; ID3D10Blob* pixelShaderBuffer; D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; unsigned int numElements; D3D11_BUFFER_DESC matrixBufferDesc; // Initialize the pointers this function will use to null. errorMessage = 0; vertexShaderBuffer = 0; pixelShaderBuffer = 0;
这里是我们将着色器程序编译成缓冲区的地方。我们提供着色器文件的名称、着色器的名称、着色器版本(DirectX 11 中为 5.0)以及要将着色器编译到的缓冲区。如果它无法编译着色器,它将把错误消息放在 errorMessage 字符串中,我们将其发送到另一个函数以输出错误。如果它仍然失败并且没有 errorMessage 字符串,则意味着它无法找到着色器文件,在这种情况下,我们将弹出一个对话框来说明情况。
// Compile the vertex shader code. result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "ColorVertexShader", "vs_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, &vertexShaderBuffer, &errorMessage, NULL); if(FAILED(result)) { // If the shader failed to compile it should have writen something to the error message. if(errorMessage) { OutputShaderErrorMessage(errorMessage, hwnd, vsFilename); } // If there was nothing in the error message then it simply could not find the shader file itself. else { MessageBox(hwnd, vsFilename, L"Missing Shader File", MB_OK); } return false; } // Compile the pixel shader code. result = D3DX11CompileFromFile(psFilename, NULL, NULL, "ColorPixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, &pixelShaderBuffer, &errorMessage, NULL); if(FAILED(result)) { // If the shader failed to compile it should have writen something to the error message. if(errorMessage) { OutputShaderErrorMessage(errorMessage, hwnd, psFilename); } // If there was nothing in the error message then it simply could not find the file itself. else { MessageBox(hwnd, psFilename, L"Missing Shader File", MB_OK); } return false; }
顶点着色器和像素着色器代码成功编译成缓冲区后,我们将使用这些缓冲区创建着色器对象本身。从现在开始,我们将使用这些指针来与顶点着色器和像素着色器进行交互。
// Create the vertex shader from the buffer. result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader); if(FAILED(result)) { return false; } // Create the pixel shader from the buffer. result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader); if(FAILED(result)) { return false; }
下一步是创建将由着色器处理的顶点 data 的布局。由于此着色器使用位置和颜色向量,因此我们需要在布局中创建两者,指定两者的 size。语义名称是布局中要填写的第一个内容,这允许着色器确定布局的此元素的用途。由于我们有两个不同的元素,因此我们对第一个元素使用 POSITION,对第二个元素使用 COLOR。布局的下一个重要部分是 Format。对于位置向量,我们使用 DXGI_FORMAT_R32G32B32_FLOAT,对于颜色,我们使用 DXGI_FORMAT_R32G32B32A32_FLOAT。您需要注意的最后一件事是 AlignedByteOffset,它指示 data 在缓冲区中的间距。对于此布局,我们告诉它前 12 个字节是位置,接下来的 16 个字节是颜色,AlignedByteOffset 显示每个元素的起点。您可以使用 D3D11_APPEND_ALIGNED_ELEMENT 而不是在 AlignedByteOffset 中放置自己的值,它将为您计算间距。我暂时将其他设置设为默认值,因为它们在本教程中不需要。
// Now setup the layout of the data that goes into the shader. // This setup needs to match the VertexType structure in the ModelClass and in the shader. polygonLayout[0].SemanticName = "POSITION"; polygonLayout[0].SemanticIndex = 0; polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; polygonLayout[0].InputSlot = 0; polygonLayout[0].AlignedByteOffset = 0; polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[0].InstanceDataStepRate = 0; polygonLayout[1].SemanticName = "COLOR"; polygonLayout[1].SemanticIndex = 0; polygonLayout[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; polygonLayout[1].InputSlot = 0; polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[1].InstanceDataStepRate = 0;
设置好布局描述后,我们可以获取它的 size,然后使用 D3D 设备创建输入布局。另外,释放顶点和像素着色器缓冲区,因为它们在创建布局后就不再需要了。
// Get a count of the elements in the layout. numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]); // Create the vertex input layout. result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &m_layout); if(FAILED(result)) { return false; } // Release the vertex shader buffer and pixel shader buffer since they are no longer needed. vertexShaderBuffer->Release(); vertexShaderBuffer = 0; pixelShaderBuffer->Release(); pixelShaderBuffer = 0;
要利用着色器,需要设置的最后一件事是常量缓冲区。正如您在顶点着色器中看到的那样,我们目前只有一个常量缓冲区,因此我们只需要在这里设置一个,以便我们可以与着色器进行交互。缓冲区使用需要设置为动态,因为我们将在每帧更新它。绑定标志指示此缓冲区将是一个常量缓冲区。CPU 访问标志需要与使用匹配,因此它设置为 D3D11_CPU_ACCESS_WRITE。填写描述后,我们可以创建常量缓冲区接口,然后使用该接口访问着色器中的内部变量,使用 SetShaderParameters 函数。
// Setup the description of the dynamic matrix constant buffer that is in the vertex shader. matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType); matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; matrixBufferDesc.MiscFlags = 0; matrixBufferDesc.StructureByteStride = 0; // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class. result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer); if(FAILED(result)) { return false; } return true; }
ShutdownShader 释放了在 InitializeShader 函数中设置的四个接口。
void ColorShaderClass::ShutdownShader() { // Release the matrix constant buffer. if(m_matrixBuffer) { m_matrixBuffer->Release(); m_matrixBuffer = 0; } // Release the layout. if(m_layout) { m_layout->Release(); m_layout = 0; } // Release the pixel shader. if(m_pixelShader) { m_pixelShader->Release(); m_pixelShader = 0; } // Release the vertex shader. if(m_vertexShader) { m_vertexShader->Release(); m_vertexShader = 0; } return; }
OutputShaderErrorMessage 输出编译顶点着色器或像素着色器时产生的错误消息。
void ColorShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename) { char* compileErrors; unsigned long bufferSize, i; ofstream fout; // Get a pointer to the error message text buffer. compileErrors = (char*)(errorMessage->GetBufferPointer()); // Get the length of the message. bufferSize = errorMessage->GetBufferSize(); // Open a file to write the error message to. fout.open("shader-error.txt"); // Write out the error message. for(i=0; i<bufferSize; i++) { fout Release(); errorMessage = 0; // Pop a message up on the screen to notify the user to check the text file for compile errors. MessageBox(hwnd, L"Error compiling shader. Check shader-error.txt for message.", shaderFilename, MB_OK); return; }
SetShaderVariables 函数用于简化设置着色器中的全局变量。该函数中使用的矩阵是在 GraphicsClass 中创建的,然后在 Render 函数调用期间调用该函数,将这些矩阵从 GraphicsClass 发送到顶点着色器。
bool ColorShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix) { HRESULT result; D3D11_MAPPED_SUBRESOURCE mappedResource; MatrixBufferType* dataPtr; unsigned int bufferNumber;
在将矩阵发送到着色器之前,请确保对其进行转置。这是 DirectX 11 的要求。
// Transpose the matrices to prepare them for the shader. D3DXMatrixTranspose(&worldMatrix, &worldMatrix); D3DXMatrixTranspose(&viewMatrix, &viewMatrix); D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix);
锁定 m_matrixBuffer,在其中设置新的矩阵,然后解锁它。
// Lock the constant buffer so it can be written to. result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource); if(FAILED(result)) { return false; } // Get a pointer to the data in the constant buffer. dataPtr = (MatrixBufferType*)mappedResource.pData; // Copy the matrices into the constant buffer. dataPtr->world = worldMatrix; dataPtr->view = viewMatrix; dataPtr->projection = projectionMatrix; // Unlock the constant buffer. deviceContext->Unmap(m_matrixBuffer, 0);
现在在 HLSL 顶点着色器中设置更新后的矩阵缓冲区。
// Set the position of the constant buffer in the vertex shader. bufferNumber = 0; // Finanly set the constant buffer in the vertex shader with the updated values. deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer); return true; }
RenderShader 是 Render 函数中调用的第二个函数。在调用它之前,会先调用 SetShaderParameters,以确保着色器参数设置正确。
此函数的第一步是在输入装配器中将我们的输入布局设置为活动状态。这将让 GPU 了解顶点缓冲区中数据的格式。第二步是设置我们将用于渲染此顶点缓冲区的顶点着色器和像素着色器。设置完着色器后,我们将通过使用 D3D 设备上下文调用 DrawIndexed DirectX 11 函数来渲染三角形。调用此函数后,它将渲染绿色三角形。
void ColorShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount) { // Set the vertex input layout. deviceContext->IASetInputLayout(m_layout); // Set the vertex and pixel shaders that will be used to render this triangle. deviceContext->VSSetShader(m_vertexShader, NULL, 0); deviceContext->PSSetShader(m_pixelShader, NULL, 0); // Render the triangle. deviceContext->DrawIndexed(indexCount, 0, 0); return; }
我们已经研究了如何编写 HLSL 着色器、如何设置顶点和索引缓冲区,以及如何使用 ColorShaderClass 调用 HLSL 着色器来绘制这些缓冲区。但是,我们缺少的是用于绘制这些缓冲区的视点。为此,我们将需要一个摄像机类,以便让 DirectX 11 知道我们从哪里以及如何查看场景。摄像机类将跟踪摄像机的位置及其当前旋转。它将使用位置和旋转信息来生成一个视图矩阵,该矩阵将传递到 HLSL 着色器中以进行渲染。
//////////////////////////////////////////////////////////////////////////////// // Filename: cameraclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _CAMERACLASS_H_ #define _CAMERACLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3dx10math.h> //////////////////////////////////////////////////////////////////////////////// // Class name: CameraClass //////////////////////////////////////////////////////////////////////////////// class CameraClass { public: CameraClass(); CameraClass(const CameraClass&); ~CameraClass(); void SetPosition(float, float, float); void SetRotation(float, float, float); D3DXVECTOR3 GetPosition(); D3DXVECTOR3 GetRotation(); void Render(); void GetViewMatrix(D3DXMATRIX&); private: float m_positionX, m_positionY, m_positionZ; float m_rotationX, m_rotationY, m_rotationZ; D3DXMATRIX m_viewMatrix; }; #endif
CameraClass 标头非常简单,只有四个函数将被使用。SetPosition 和 SetRotation 函数将用于设置摄像机对象的位置和旋转。Render 将用于基于摄像机的位置和旋转创建视图矩阵。最后,GetViewMatrix 将用于从摄像机对象中检索视图矩阵,以便着色器可以使用它来进行渲染。
//////////////////////////////////////////////////////////////////////////////// // Filename: cameraclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "cameraclass.h"
类构造函数将初始化摄像机的位置和旋转,使其位于场景的原点。
CameraClass::CameraClass() { m_positionX = 0.0f; m_positionY = 0.0f; m_positionZ = 0.0f; m_rotationX = 0.0f; m_rotationY = 0.0f; m_rotationZ = 0.0f; } CameraClass::CameraClass(const CameraClass& other) { } CameraClass::~CameraClass() { }
SetPosition 和 SetRotation 函数用于设置摄像机的位置和旋转。
void CameraClass::SetPosition(float x, float y, float z) { m_positionX = x; m_positionY = y; m_positionZ = z; return; } void CameraClass::SetRotation(float x, float y, float z) { m_rotationX = x; m_rotationY = y; m_rotationZ = z; return; }
GetPosition 和 GetRotation 函数返回摄像机的位置和旋转,以便调用函数使用。
D3DXVECTOR3 CameraClass::GetPosition() { return D3DXVECTOR3(m_positionX, m_positionY, m_positionZ); } D3DXVECTOR3 CameraClass::GetRotation() { return D3DXVECTOR3(m_rotationX, m_rotationY, m_rotationZ); }
Render 函数使用摄像机的位置和旋转来构建和更新视图矩阵。我们首先设置用于向上、位置、旋转等的变量。然后,在场景的原点,我们首先根据摄像机的 x、y 和 z 旋转来旋转摄像机。旋转到位后,我们将其平移到 3D 空间中的位置。使用位置、lookAt 和向上中的正确值,我们可以使用 D3DXMatrixLookAtLH 函数来创建视图矩阵,以表示当前的摄像机旋转和平移。
void CameraClass::Render() { D3DXVECTOR3 up, position, lookAt; float yaw, pitch, roll; D3DXMATRIX rotationMatrix; // Setup the vector that points upwards. up.x = 0.0f; up.y = 1.0f; up.z = 0.0f; // Setup the position of the camera in the world. position.x = m_positionX; position.y = m_positionY; position.z = m_positionZ; // Setup where the camera is looking by default. lookAt.x = 0.0f; lookAt.y = 0.0f; lookAt.z = 1.0f; // Set the yaw (Y axis), pitch (X axis), and roll (Z axis) rotations in radians. pitch = m_rotationX * 0.0174532925f; yaw = m_rotationY * 0.0174532925f; roll = m_rotationZ * 0.0174532925f; // Create the rotation matrix from the yaw, pitch, and roll values. D3DXMatrixRotationYawPitchRoll(&rotationMatrix, yaw, pitch, roll); // Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin. D3DXVec3TransformCoord(&lookAt, &lookAt, &rotationMatrix); D3DXVec3TransformCoord(&up, &up, &rotationMatrix); // Translate the rotated camera position to the location of the viewer. lookAt = position + lookAt; // Finally create the view matrix from the three updated vectors. D3DXMatrixLookAtLH(&m_viewMatrix, &position, &lookAt, &up); return; }
调用 Render 函数以创建视图矩阵后,我们可以使用此 GetViewMatrix 函数将更新后的视图矩阵提供给调用函数。视图矩阵将是 HLSL 顶点着色器中使用的三个主要矩阵之一。
void CameraClass::GetViewMatrix(D3DXMATRIX& viewMatrix) { viewMatrix = m_viewMatrix; return; }
GraphicsClass 现在添加了三个新类。CameraClass、ModelClass 和 ColorShaderClass 的标头在这里被添加,以及私有成员变量。请记住,GraphicsClass 是用于渲染场景的主要类,它调用项目所需的全部类对象。
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _GRAPHICSCLASS_H_ #define _GRAPHICSCLASS_H_ /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "d3dclass.h"
#include "cameraclass.h" #include "modelclass.h" #include "colorshaderclass.h"
///////////// // GLOBALS // ///////////// const bool FULL_SCREEN = true; const bool VSYNC_ENABLED = true; const float SCREEN_DEPTH = 1000.0f; const float SCREEN_NEAR = 0.1f; //////////////////////////////////////////////////////////////////////////////// // Class name: GraphicsClass //////////////////////////////////////////////////////////////////////////////// class GraphicsClass { public: GraphicsClass(); GraphicsClass(const GraphicsClass&); ~GraphicsClass(); bool Initialize(int, int, HWND); void Shutdown(); bool Frame(); private: bool Render(); private: D3DClass* m_D3D;
CameraClass* m_Camera; ModelClass* m_Model; ColorShaderClass* m_ColorShader;
}; #endif
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "graphicsclass.h"
对 GraphicsClass 的第一个更改是在类构造函数中将摄像机、模型和颜色着色器对象初始化为 null。
GraphicsClass::GraphicsClass() { m_D3D = 0;
m_Camera = 0; m_Model = 0; m_ColorShader = 0;
}
Initialize 函数也已更新,以创建和初始化三个新对象。
bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd) { bool result; // Create the Direct3D object. m_D3D = new D3DClass; if(!m_D3D) { return false; } // Initialize the Direct3D object. result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR); if(!result) { MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK); return false; }
// Create the camera object. m_Camera = new CameraClass; if(!m_Camera) { return false; } // Set the initial position of the camera. m_Camera->SetPosition(0.0f, 0.0f, -10.0f); // Create the model object. m_Model = new ModelClass; if(!m_Model) { return false; } // Initialize the model object. result = m_Model->Initialize(m_D3D->GetDevice()); if(!result) { MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK); return false; } // Create the color shader object. m_ColorShader = new ColorShaderClass; if(!m_ColorShader) { return false; } // Initialize the color shader object. result = m_ColorShader->Initialize(m_D3D->GetDevice(), hwnd); if(!result) { MessageBox(hwnd, L"Could not initialize the color shader object.", L"Error", MB_OK); return false; }
return true; }
Shutdown 也已更新,以关闭和释放三个新对象。
void GraphicsClass::Shutdown() {
// Release the color shader object. if(m_ColorShader) { m_ColorShader->Shutdown(); delete m_ColorShader; m_ColorShader = 0; } // Release the model object. if(m_Model) { m_Model->Shutdown(); delete m_Model; m_Model = 0; } // Release the camera object. if(m_Camera) { delete m_Camera; m_Camera = 0; }
// Release the Direct3D object. if(m_D3D) { m_D3D->Shutdown(); delete m_D3D; m_D3D = 0; } return; }
Frame 函数与之前的教程相同。
bool GraphicsClass::Frame() { bool result; // Render the graphics scene. result = Render(); if(!result) { return false; } return true; }
正如您所料,Render 函数发生了最大的变化。它仍然从清除场景开始,只不过它被清除为黑色。之后,它调用摄像机对象的 Render 函数,以根据在 Initialize 函数中设置的摄像机位置创建一个视图矩阵。创建完视图矩阵后,我们从摄像机类中获取它的副本。我们还从 D3DClass 对象中获取世界和投影矩阵的副本。然后,我们调用 ModelClass::Render 函数将绿色三角形模型几何体放在图形管道上。现在准备好了顶点后,我们调用颜色着色器,使用模型信息和三个矩阵来定位每个顶点,以绘制顶点。现在绿色三角形被绘制到后缓冲区。这样,场景就完成了,我们调用 EndScene 将其显示到屏幕上。
bool GraphicsClass::Render() {
D3DXMATRIX viewMatrix, projectionMatrix, worldMatrix; bool result; // Clear the buffers to begin the scene. m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f); // Generate the view matrix based on the camera's position. m_Camera->Render(); // Get the world, view, and projection matrices from the camera and d3d objects. m_Camera->GetViewMatrix(viewMatrix); m_D3D->GetWorldMatrix(worldMatrix); m_D3D->GetProjectionMatrix(projectionMatrix); // Put the model vertex and index buffers on the graphics pipeline to prepare them for drawing. m_Model->Render(m_D3D->GetDeviceContext()); // Render the model using the color shader. result = m_ColorShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix); if(!result) { return false; }
// Present the rendered scene to the screen. m_D3D->EndScene(); return true; }
总之,您应该已经了解了顶点和索引缓冲区的基本工作原理。您还应该了解顶点和像素着色器的基础知识,以及如何使用 HLSL 编写它们。最后,您应该了解我们如何将这些新概念合并到我们的框架中,以生成一个渲染到屏幕上的绿色三角形。我还想说,我知道代码很长,只是为了绘制一个三角形,并且它本可以全部放在一个 main() 函数中。但是,我使用了一个合适的框架来实现它,这样一来,在接下来的教程中,只需要对代码进行很少的更改,就可以实现更复杂的图形。
1. 编译并运行教程。确保它在屏幕上绘制一个绿色三角形。绘制完成后,按 Escape 键退出。
2. 将三角形的颜色更改为红色。
3. 将三角形更改为正方形。
4. 将摄像机向后移动 10 个单位。
5. 将像素着色器更改为输出亮度为一半的颜色。(巨大提示:将 ColorPixelShader 中的某个东西乘以 0.5f)