DirectX/10.0/Direct3D/3D 模型渲染
本教程将介绍如何使用 HLSL 在 DirectX 11 中渲染 3D 模型。本教程中的代码基于漫射光照教程中的代码。
我们已经在之前的教程中渲染了 3D 模型,但它们是由单个三角形组成的,而且相当无趣。现在已经涵盖了基础知识,我们将继续渲染更复杂的对象。在这种情况下,对象将是一个立方体。在我们开始介绍如何渲染更复杂的模型之前,我们首先讨论模型格式。
有许多工具可供用户创建 3D 模型。Maya 和 3D Studio Max 是两个更受欢迎的 3D 建模程序。还有许多其他功能较少的工具,但仍然可以满足我们的基本需求。
无论您选择使用哪种工具,它们都会将其模型导出为多种不同的格式。我的建议是您创建自己的模型格式并编写解析器将它们的导出格式转换为您自己的格式。这样做的原因是您使用的 3D 建模包可能会随着时间的推移而发生变化,它们的模型格式也会发生变化。此外,您可能使用多个 3D 建模包,因此您将需要处理多种不同的格式。因此,如果您有自己的格式并将它们的格式转换为您自己的格式,那么您的代码将永远不需要更改。您只需要更改解析器程序来将这些格式更改为您自己的格式。此外,大多数 3D 建模包会导出大量仅对该建模程序有用的垃圾,而您在自己的模型格式中不需要这些垃圾。
创建自己的格式最重要的部分是它涵盖了您需要它完成的所有内容,并且它易于您使用。您也可以考虑为不同的对象创建几个不同的格式,因为有些可能包含动画数据,有些可能是静态的,等等。
我将要介绍的模型格式非常基础。它将包含模型中每个顶点的行。每行将与代码中使用的顶点格式匹配,即位置向量 (x, y, z)、纹理坐标 (tu, tv) 和法线向量 (nx, ny, nz)。该格式还将在顶部包含顶点数量,以便您可以读取第一行并构建所需的内存结构,然后再读取数据。该格式还要求每三行构成一个三角形,并且模型格式中的顶点以顺时针顺序呈现。以下是我们将要渲染的立方体的模型文件
Vertex Count: 36 Data: -1.0 1.0 -1.0 0.0 0.0 0.0 0.0 -1.0 1.0 1.0 -1.0 1.0 0.0 0.0 0.0 -1.0 -1.0 -1.0 -1.0 0.0 1.0 0.0 0.0 -1.0 -1.0 -1.0 -1.0 0.0 1.0 0.0 0.0 -1.0 1.0 1.0 -1.0 1.0 0.0 0.0 0.0 -1.0 1.0 -1.0 -1.0 1.0 1.0 0.0 0.0 -1.0 1.0 1.0 -1.0 0.0 0.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 -1.0 -1.0 0.0 1.0 1.0 0.0 0.0 1.0 -1.0 -1.0 0.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 1.0 0.0 0.0 1.0 -1.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 -1.0 1.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0 -1.0 1.0 0.0 1.0 0.0 0.0 1.0 1.0 -1.0 1.0 0.0 1.0 0.0 0.0 1.0 -1.0 1.0 1.0 1.0 0.0 0.0 0.0 1.0 -1.0 -1.0 1.0 1.0 1.0 0.0 0.0 1.0 -1.0 1.0 1.0 0.0 0.0 -1.0 0.0 0.0 -1.0 1.0 -1.0 1.0 0.0 -1.0 0.0 0.0 -1.0 -1.0 1.0 0.0 1.0 -1.0 0.0 0.0 -1.0 -1.0 1.0 0.0 1.0 -1.0 0.0 0.0 -1.0 1.0 -1.0 1.0 0.0 -1.0 0.0 0.0 -1.0 -1.0 -1.0 1.0 1.0 -1.0 0.0 0.0 -1.0 1.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 -1.0 1.0 -1.0 0.0 1.0 0.0 1.0 0.0 -1.0 1.0 -1.0 0.0 1.0 0.0 1.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 -1.0 1.0 1.0 0.0 1.0 0.0 -1.0 -1.0 -1.0 0.0 0.0 0.0 -1.0 0.0 1.0 -1.0 -1.0 1.0 0.0 0.0 -1.0 0.0 -1.0 -1.0 1.0 0.0 1.0 0.0 -1.0 0.0 -1.0 -1.0 1.0 0.0 1.0 0.0 -1.0 0.0 1.0 -1.0 -1.0 1.0 0.0 0.0 -1.0 0.0 1.0 -1.0 1.0 1.0 1.0 0.0 -1.0 0.0
因此,正如您所看到的,有 36 行 x、y、z、tu、tv、nx、ny、nz 数据。每三行组成一个三角形,给我们 12 个三角形,它们将形成一个立方体。该格式非常直接,可以直接读入我们的顶点缓冲区并渲染,无需任何修改。
现在需要注意的一点是,某些 3D 建模程序以不同的顺序导出数据,例如左手或右手坐标系。请记住,默认情况下 DirectX 11 默认情况下是左手坐标系,因此模型数据需要与之匹配。请注意这些差异,并确保您的解析程序能够处理将数据转换为正确的格式/顺序。
在本教程中,我们只需要对 ModelClass 进行一些细微的更改,就可以从我们的文本模型文件渲染 3D 模型。
//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _MODELCLASS_H_ #define _MODELCLASS_H_ ////////////// // INCLUDES // ////////////// #include <d3d11.h> #include <d3dx10math.h>
现在包含 fstream 库来处理从模型文本文件读取数据。
#include <fstream> using namespace std;
/////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "textureclass.h" //////////////////////////////////////////////////////////////////////////////// // Class name: ModelClass //////////////////////////////////////////////////////////////////////////////// class ModelClass { private: struct VertexType { D3DXVECTOR3 position; D3DXVECTOR2 texture; D3DXVECTOR3 normal; };
下一个更改是添加一个新的结构来表示模型格式。它被称为 ModelType。它包含与文件格式相同的位置、纹理和法线向量。
struct ModelType { float x, y, z; float tu, tv; float nx, ny, nz; };
public: ModelClass(); ModelClass(const ModelClass&); ~ModelClass();
Initialize 函数现在将模型文件名的字符字符串作为输入。
bool Initialize(ID3D11Device*, char*, WCHAR*);
void Shutdown(); void Render(ID3D11DeviceContext*); int GetIndexCount(); ID3D11ShaderResourceView* GetTexture(); private: bool InitializeBuffers(ID3D11Device*); void ShutdownBuffers(); void RenderBuffers(ID3D11DeviceContext*); bool LoadTexture(ID3D11Device*, WCHAR*); void ReleaseTexture();
我们还有两个新函数来处理从文本文件加载和卸载模型数据。
bool LoadModel(char*); void ReleaseModel();
private: ID3D11Buffer *m_vertexBuffer, *m_indexBuffer; int m_vertexCount, m_indexCount; TextureClass* m_Texture;
最终的更改是新的私有变量 m_model,它将是新的私有结构 ModelType 的数组。此变量将用于读取模型数据并保存模型数据,然后再将其放入顶点缓冲区。
ModelType* m_model;
}; #endif
//////////////////////////////////////////////////////////////////////////////// // Filename: modelclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "modelclass.h" ModelClass::ModelClass() { m_vertexBuffer = 0; m_indexBuffer = 0; m_Texture = 0;
新的模型结构在类构造函数中设置为 null。
m_model = 0;
} ModelClass::ModelClass(const ModelClass& other) { } ModelClass::~ModelClass() { }
Initialize 函数现在将要加载的模型的文件名作为输入。
bool ModelClass::Initialize(ID3D11Device* device, char* modelFilename, WCHAR* textureFilename)
{ bool result;
在 Initialize 函数中,我们现在首先调用新的 LoadModel 函数。它将从我们提供的文件名加载模型数据到新的 m_model 数组中。填充此模型数组后,我们就可以从它构建顶点和索引缓冲区。由于 InitializeBuffers 现在依赖于此模型数据,因此您必须确保按正确的顺序调用这些函数。
// Load in the model data, result = LoadModel(modelFilename); if(!result) { return false; }
// Initialize the vertex and index buffers. result = InitializeBuffers(device); if(!result) { return false; } // Load the texture for this model. result = LoadTexture(device, textureFilename); if(!result) { return false; } return true; } void ModelClass::Shutdown() { // Release the model texture. ReleaseTexture(); // Shutdown the vertex and index buffers. ShutdownBuffers();
在 Shutdown 函数中,我们在完成之后添加了对 ReleaseModel 函数的调用,以删除 m_model 数组数据。
// Release the model data. ReleaseModel();
return; } void ModelClass::Render(ID3D11DeviceContext* deviceContext) { // Put the vertex and index buffers on the graphics pipeline to prepare them for drawing. RenderBuffers(deviceContext); return; } int ModelClass::GetIndexCount() { return m_indexCount; } ID3D11ShaderResourceView* ModelClass::GetTexture() { return m_Texture->GetTexture(); } bool ModelClass::InitializeBuffers(ID3D11Device* device) { VertexType* vertices; unsigned long* indices; D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc; D3D11_SUBRESOURCE_DATA vertexData, indexData; HRESULT result; int i;
请注意,我们不再在此处手动设置顶点和索引计数。一旦我们到达 ModelClass::LoadModel 函数,您将看到我们将在那时读取顶点和索引计数,而不是在此处。
// 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; }
加载顶点和索引数组已经改变了一些。我们不再手动设置值,而是循环遍历新的 m_model 数组中的所有元素,并将这些数据从那里复制到顶点数组中。索引数组很容易构建,因为我们加载的每个顶点都具有与它加载到的数组中的位置相同的索引号。
// Load the vertex array and index array with data. for(i=0; i<m_vertexCount; i++) { vertices[i].position = D3DXVECTOR3(m_model[i].x, m_model[i].y, m_model[i].z); vertices[i].texture = D3DXVECTOR2(m_model[i].tu, m_model[i].tv); vertices[i].normal = D3DXVECTOR3(m_model[i].nx, m_model[i].ny, m_model[i].nz); indices[i] = i; }
// 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; } // 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; } 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; } 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; } bool ModelClass::LoadTexture(ID3D11Device* device, WCHAR* filename) { bool result; // Create the texture object. m_Texture = new TextureClass; if(!m_Texture) { return false; } // Initialize the texture object. result = m_Texture->Initialize(device, filename); if(!result) { return false; } return true; } void ModelClass::ReleaseTexture() { // Release the texture object. if(m_Texture) { m_Texture->Shutdown(); delete m_Texture; m_Texture = 0; } return; }
这是新的 LoadModel 函数,它负责将模型数据从文本文件加载到 m_model 数组变量中。它打开文本文件并首先读取顶点计数。读取顶点计数后,它创建 ModelType 数组,然后将每行读入数组中。顶点计数和索引计数现在都在此函数中设置。
bool ModelClass::LoadModel(char* filename) { ifstream fin; char input; int i; // Open the model file. fin.open(filename); // If it could not open the file then exit. if(fin.fail()) { return false; } // Read up to the value of vertex count. fin.get(input); while(input != ':') { fin.get(input); } // Read in the vertex count. fin >> m_vertexCount; // Set the number of indices to be the same as the vertex count. m_indexCount = m_vertexCount; // Create the model using the vertex count that was read in. m_model = new ModelType[m_vertexCount]; if(!m_model) { return false; } // Read up to the beginning of the data. fin.get(input); while(input != ':') { fin.get(input); } fin.get(input); fin.get(input); // Read in the vertex data. for(i=0; i<m_vertexCount; i++) { fin >> m_model[i].x >> m_model[i].y >> m_model[i].z; fin >> m_model[i].tu >> m_model[i].tv; fin >> m_model[i].nx >> m_model[i].ny >> m_model[i].nz; } // Close the model file. fin.close(); return true; }
ReleaseModel 函数负责删除模型数据数组。
void ModelClass::ReleaseModel() { if(m_model) { delete [] m_model; m_model = 0; } return; }
GraphicsClass 的标题自上一个教程以来没有改变。
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _GRAPHICSCLASS_H_ #define _GRAPHICSCLASS_H_ /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "d3dclass.h" #include "cameraclass.h" #include "modelclass.h" #include "lightshaderclass.h" #include "lightclass.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(float); private: D3DClass* m_D3D; CameraClass* m_Camera; ModelClass* m_Model; LightShaderClass* m_LightShader; LightClass* m_Light; }; #endif
//////////////////////////////////////////////////////////////////////////////// // Filename: graphicsclass.cpp //////////////////////////////////////////////////////////////////////////////// #include "graphicsclass.h" GraphicsClass::GraphicsClass() { m_D3D = 0; m_Camera = 0; m_Model = 0; m_LightShader = 0; m_Light = 0; } GraphicsClass::GraphicsClass(const GraphicsClass& other) { } GraphicsClass::~GraphicsClass() { } 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; }
模型初始化现在将模型文件的文件名作为输入。在本教程中,我们将使用 cube.txt 文件,因此此模型会加载一个 3D 立方体对象以进行渲染。
// Initialize the model object.
result = m_Model->Initialize(m_D3D->GetDevice(), "../Engine/data/cube.txt", L"../Engine/data/seafloor.dds");
if(!result) { MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK); return false; } // Create the light shader object. m_LightShader = new LightShaderClass; if(!m_LightShader) { return false; } // Initialize the light shader object. result = m_LightShader->Initialize(m_D3D->GetDevice(), hwnd); if(!result) { MessageBox(hwnd, L"Could not initialize the light shader object.", L"Error", MB_OK); return false; } // Create the light object. m_Light = new LightClass; if(!m_Light) { return false; }
在本教程中,我已经将漫射光颜色更改为白色。
// Initialize the light object.
m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
m_Light->SetDirection(0.0f, 0.0f, 1.0f); return true; } void GraphicsClass::Shutdown() { // Release the light object. if(m_Light) { delete m_Light; m_Light = 0; } // Release the light shader object. if(m_LightShader) { m_LightShader->Shutdown(); delete m_LightShader; m_LightShader = 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 D3D object. if(m_D3D) { m_D3D->Shutdown(); delete m_D3D; m_D3D = 0; } return; } bool GraphicsClass::Frame() { bool result; static float rotation = 0.0f; // Update the rotation variable each frame. rotation += (float)D3DX_PI * 0.01f; if(rotation > 360.0f) { rotation -= 360.0f; } // Render the graphics scene. result = Render(rotation); if(!result) { return false; } return true; } bool GraphicsClass::Render(float rotation) { D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix; 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); // Rotate the world matrix by the rotation value so that the triangle will spin. D3DXMatrixRotationY(&worldMatrix, rotation); // 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 light shader. result = m_LightShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture(), m_Light->GetDirection(), m_Light->GetDiffuseColor()); if(!result) { return false; } // Present the rendered scene to the screen. m_D3D->EndScene(); return true; }
通过对 ModelClass 的更改,我们现在可以加载 3D 模型并渲染它们。此处使用的格式仅适用于具有光照的基本静态对象,但是它是了解模型格式工作原理的一个良好起点。
1. 重新编译代码并运行程序。您应该得到一个旋转的立方体,上面有相同的 seafloor.dds 纹理。完成后按 Esc 退出。
2. 找到一个不错的 3D 建模包(希望是免费的),并创建您自己的简单模型并导出它们。开始查看格式。
3. 编写一个简单的解析器程序,它接收模型导出并将其转换为此处使用的格式。将 cube.txt 替换为您的模型,然后运行程序。