缓冲区、着色器和HLSL
教程 4:缓冲区、着色器和 HLSL
本教程将介绍在 DirectX 11 中编写顶点和像素着色器。还将介绍在 DirectX 11 中使用顶点和索引缓冲区。这些是您需要理解和利用来渲染 3D 图形的最基本概念。
顶点缓冲区
要理解的第一个概念是顶点缓冲区。为了说明这个概念,让我们以球体的 3D 模型为例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lbCNGzrE-1657434825332)(http://www.rastertek.com/pic0163.gif)]
3D 球体模型实际上是由数百个三角形组成:
球体模型中的每个三角形都有三个点,我们称每个点为顶点。因此,为了渲染球体模型,我们需要将形成球体的所有顶点放入一个特殊的数据数组中,我们称之为顶点缓冲区。一旦球体模型的所有点都在顶点缓冲区中,我们就可以将顶点缓冲区发送到 GPU,以便它可以渲染模型。
索引缓冲区
索引缓冲区与顶点缓冲区有关。它们的目的是记录顶点缓冲区中每个顶点的位置。GPU 然后使用索引缓冲区快速找到顶点缓冲区中的特定顶点。索引缓冲区的概念类似于在书中使用索引的概念,它有助于以更高的速度找到您正在寻找的主题。DirectX SDK 文档说,使用索引缓冲区还可以增加将顶点数据缓存到视频内存中更快位置的可能性。因此,出于性能原因,强烈建议也使用这些。
顶点着色器
顶点着色器是主要用于将顶点从顶点缓冲区转换为 3D 空间的小程序。还可以进行其他计算,例如计算每个顶点的法线。顶点着色器程序将由 GPU 为它需要处理的每个顶点调用。例如,一个 5,000 个多边形模型将运行您的顶点着色器程序每帧 15,000 次,以绘制该单个模型。因此,如果您将图形程序锁定为 60 fps,它将每秒调用您的顶点着色器 900,000 次,仅绘制 5,000 个三角形。如您所知,编写高效的顶点着色器很重要。
像素着色器
像素着色器是为我们绘制的多边形着色而编写的小程序。它们由 GPU 为将被绘制到屏幕上的每个可见像素运行。您计划对多边形面进行的着色、纹理、照明和大多数其他效果都由像素着色器程序处理。由于 GPU 将调用像素着色器的次数,因此必须高效地编写像素着色器。
HLSL
HLSL 是我们在 DirectX 11 中用于对这些小型顶点和像素着色器程序进行编码的语言。语法与具有一些预定义类型的 C 语言几乎相同。HLSL 程序文件由全局变量、类型定义、顶点着色器、像素着色器和几何着色器组成。由于这是第一个 HLSL 教程,我们将使用 DirectX 11 做一个非常简单的 HLSL 程序来开始。
更新的框架
本教程的框架已更新。在 GraphicsClass 下,我们添加了三个新类,分别称为 CameraClass、ModelClass 和 ColorShaderClass。CameraClass 将处理我们之前讨论过的视图矩阵。它将处理相机在世界中的位置,并在着色器需要绘制并确定我们从哪里查看场景时将其传递给着色器。ModelClass 将处理我们的 3D 模型的几何形状,在本教程中,为简单起见,3D 模型将只是一个三角形。最后 ColorShaderClass 将负责调用我们的 HLSL 着色器将模型渲染到屏幕上。
我们将首先查看 HLSL 着色器程序来开始教程代码。
color.vs
这些将是我们的第一个着色器程序。着色器是执行模型实际渲染的小程序。这些着色器使用 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 位置向量和红色、绿色、蓝色和不透明度。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 ShaderPixelInputType 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); // mul是矩阵乘法运算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;
}
color.ps
像素着色器在将渲染到屏幕的多边形上绘制每个像素。在这个像素着色器中,它使用 PixelInputType 作为输入,并返回一个 float4 作为输出,代表最终的像素颜色。这个像素着色器程序非常简单,因为我们只是告诉它为像素着色与颜色的输入值相同。请注意,像素着色器从顶点着色器输出中获取其输入。
// Filename: color.ps//
// TYPEDEFS //
//
struct PixelInputType
{float4 position : SV_POSITION;float4 color : COLOR;
};// Pixel Shaderfloat4 ColorPixelShader(PixelInputType input) : SV_TARGET
{return input.color;
}
Modelclass.h
如前所述,ModelClass 负责封装 3D 模型的几何形状。在本教程中,我们将手动设置单个绿色三角形的数据。我们还将为三角形创建一个顶点和索引缓冲区,以便可以渲染它。
// Filename: modelclass.h#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_//
// INCLUDES //
//
#include <d3d11.h>
#include <d3dx10math.h>// Class name: ModelClassclass 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
// Filename: modelclass.cpp#include "modelclass.h"
// 类构造函数将顶点和索引缓冲区指针初始化为空。
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;
}// 从 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 函数是我们处理创建顶点和索引缓冲区的地方。通常您会读入模型并从该数据文件创建缓冲区。对于本教程,我们将手动设置顶点和索引缓冲区中的点,因为它只是一个三角形。
bool ModelClass::InitializeBuffers(ID3D11Device* device)
{VertexType* vertices;unsigned long* indices;D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;D3D11_SUBRESOURCE_DATA vertexData, indexData;HRESULT result;// 首先创建两个临时数组来保存我们稍后将用于填充最终缓冲区的顶点和索引数据。// 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;}// 创建顶点缓冲区和索引缓冲区后,您可以删除顶点和索引数组,因为它们不再需要,因为数据已复制到缓冲区中。// 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;
}
//从 Render 函数调用 RenderBuffers。此函数的目的是将顶点缓冲区和索引缓冲区设置为在 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
我们将使用 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: ColorShaderClassclass ColorShaderClass
{
private://这是将与顶点着色器一起使用的 cBuffer 类型的定义。此 typedef 必须与顶点着色器中的 typedef 完全相同,因为模型数据需要匹配着色器中的 typedef 才能正确渲染。struct MatrixBufferType{D3DXMATRIX world;D3DXMATRIX view;D3DXMATRIX projection;};public:ColorShaderClass();ColorShaderClass(const ColorShaderClass&);~ColorShaderClass();//这里的函数处理着色器的初始化和关闭。渲染函数设置着色器参数,然后使用着色器绘制准备好的模型顶点。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
// Filename: colorshaderclass.cpp#include "colorshaderclass.h"
//像往常一样,类构造函数将类中的所有私有指针初始化为空。
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;
}
//渲染将首先使用 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。您还将看到布局的设置以及顶点缓冲区数据在 GPU 中的图形管道上的外观。布局需要与 modelclass.h 文件中的 VertexType 以及 color.vs 文件中定义的匹配。
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;}//下一步是创建将由着色器处理的顶点数据的布局。由于此着色器使用位置和颜色矢量,我们需要在布局中创建两者并指定两者的大小。语义名称是布局中首先填写的内容,这允许着色器确定布局中该元素的用途。由于我们有两个不同的元素,我们将 POSITION 用于第一个元素,将 COLOR 用于第二个元素。布局的下一个重要部分是格式。对于位置向量,我们使用 DXGI_FORMAT_R32G32B32_FLOAT,对于颜色,我们使用 DXGI_FORMAT_R32G32B32A32_FLOAT。您需要注意的最后一件事是 AlignedByteOffset,它指示数据在缓冲区中的间距。对于这个布局,我们告诉它前 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 stucture 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;//设置好布局描述后,我们可以获取它的大小,然后使用 D3D device创建输入布局。还要释放顶点和像素着色器缓冲区,因为一旦创建布局就不再需要它们。// 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 << compileErrors[i];}// Close the file.fout.close();// Release the error message.errorMessage->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 函数调用期间将它们从那里发送到顶点着色器。
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;
}
Cameraclass.h
我们研究了如何编写 HLSL 着色器,如何设置顶点和索引缓冲区,以及如何调用 HLSL 着色器以使用 ColorShaderClass 绘制这些缓冲区。然而,我们缺少的一件事是绘制它们的视点。为此,我们将需要一个相机类来让 DirectX 11 知道我们从哪里以及如何查看场景。相机类将跟踪相机的位置及其当前旋转。它将使用位置和旋转信息来生成一个视图矩阵,该矩阵将被传递到 HLSL 着色器进行渲染。
// Filename: cameraclass.h#ifndef _CAMERACLASS_H_
#define _CAMERACLASS_H_//
// INCLUDES //
//
#include <d3dx10math.h>// Class name: CameraClassclass 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 函数将用于设置相机对象的位置和旋转。渲染将用于根据相机的位置和旋转创建视图矩阵。最后 GetViewMatrix 将用于从相机对象中检索视图矩阵,以便着色器可以使用它进行渲染。
Cameraclass.cpp
/ //
// 文件名: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 空间中的位置。有了 position、lookAt 和 up 中的正确值,我们就可以使用 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.h
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: GraphicsClassclass 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
Graphicsclass.cpp
/ //
// 文件名: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 函数对它的更改最多。它仍然从清除场景开始,只是它被清除为黑色。之后,它调用相机对象的渲染函数,以根据在 Initialize 函数中设置的相机位置创建视图矩阵。一旦创建了视图矩阵,我们就会从相机类中获取它的副本。我们还从 D3DClass 对象中获取世界和投影矩阵的副本。然后我们调用 ModelClass::Render 函数将绿色三角形模型几何图形放在图形管道上。现在准备好顶点后,我们调用颜色着色器来使用模型信息和用于定位每个顶点的三个矩阵来绘制顶点。绿色三角形现在被绘制到后台缓冲区。
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() 函数中。但是,我是通过适当的框架来做到这一点的,因此接下来的教程只需要对代码进行很少的更改即可制作更复杂的图形。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KICm2Aq3-1657434825337)(http://www.rastertek.com/pic0009.gif)]
做练习
\1. 编译运行教程。确保它在屏幕上绘制一个绿色三角形。退出后按退出键。
\2. 将三角形的颜色更改为红色。
\3. 将三角形改为正方形。
\4. 将相机向后移动 10 个单位。
\5. 更改像素着色器以输出一半明亮的颜色。(巨大的提示:将 ColorPixelShader 中的某些内容乘以 0.5f)
源代码
Visual Studio 2010 项目:dx11tut04.zip
仅来源:dx11src04.zip
仅可执行文件:dx11exe04.zip
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YK5rLWF1-1657434825338)(http://www.rastertek.com/pic1002.gif)]
D->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() 函数中。但是,我是通过适当的框架来做到这一点的,因此接下来的教程只需要对代码进行很少的更改即可制作更复杂的图形。[外链图片转存中...(img-KICm2Aq3-1657434825337)]做练习\1. 编译运行教程。确保它在屏幕上绘制一个绿色三角形。退出后按退出键。\2. 将三角形的颜色更改为红色。\3. 将三角形改为正方形。\4. 将相机向后移动 10 个单位。\5. 更改像素着色器以输出一半明亮的颜色。(巨大的提示:将 ColorPixelShader 中的某些内容乘以 0.5f)源代码Visual Studio 2010 项目:[dx11tut04.zip](http://www.rastertek.com/dx11tut04.zip)仅来源:[dx11src04.zip](http://www.rastertek.com/dx11src04.zip)仅可执行文件:[dx11exe04.zip](http://www.rastertek.com/dx11exe04.zip)[外链图片转存中...(img-YK5rLWF1-1657434825338)][返回教程索引](http://www.rastertek.com/tutindex.html)
缓冲区、着色器和HLSL相关推荐
- (转载)(官方)UE4--图像编程----着色器开发----HLSL 交叉编译器
HLSL 交叉编译器 这个库将 高级着色语言 (HLSL) 着色器源代码编译成高级中间表示法,执行独立于设备的优化,并生成 OpenGL 着色语言 (GLSL) 兼容源代码.这个库在很大程度上基于 M ...
- DirectX 11 Tutorial 4 中文翻译版教程: 缓存区、着色器和HLSL
原英文版地址:http://www.rastertek.com/dx11tut04.html 本教程将介绍如何在Directx11中编写顶点和像素遮影器.它还将介绍如何在Directx11中使用顶点和 ...
- UE4 调试着色器编译过程
调试着色器编译过程 Rolando Caloca 在 April 19, 2016 | 学习编程 Share on Facebook Share on Twitter Share on Google+ ...
- unity Shader Lab(cg hlsl glsl)着色器入门教程 以及 vs2019 支持unity shader语法(更新中2019.9.5)
前言: 如果你对cg glsl hlsl 顶点着色器 片段着色器 表面着色器 固定渲染管线 等等有所疑惑,或是想学会unity的渲染,看这一篇就足够了.另外我博客的shader分类中还有很多shade ...
- 顶点缓冲区与着色器 (The Cherno + LeranOpenGL)笔记
目录 图形渲染管线介绍 (一)顶点缓冲区 (二)VAO/VBO/IBO (三)着色器 (四)索引缓冲区 绘制矩形完整代码 : 图形渲染管线介绍 1.图形渲染管线主要被划分成两个主要部分,第一部分是把物 ...
- HLSL CG 与glsl着色器编译及其原理
导言:公司旧的渲染引擎用的结构无语了,要写一个渲染特效现在Unity 用shaderlab实现调试好,Unity插件导出GLTF格式(shader等已经包含在材质信息中),然后再导到自研引擎(用的OP ...
- 第四章:缓冲区、着色器、GLSL
原文链接: http://www.rastertek.com/gl40tut04.html Tutorial 4: Buffers, Shaders, and GLSL 第四章:缓冲区.着色器.GLS ...
- Unreal Engine 4 使用HLSL自定义着色器(Custom Shaders)教程(下)
本文是<Unreal Engine 4 自定义着色器(Custom Shaders)教程>的下半部分,上半部分请见<Unreal Engine 4 自定义着色器(Custom Sha ...
- WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码
HLSL,High Level Shader Language,高级着色器语言,是 Direct3D 着色器模型所必须的语言.WPF 支持 Direct3D 9,也支持使用 HLSL 来编写着色器.你 ...
最新文章
- Dubbo 整合 Pinpoint 做分布式服务请求跟踪
- SQL SERVER 2008如何卸载干净
- c语言NULL和0区别
- ad10怎么挖铺的铜_一个西北阴阳的诡异经历(五六):镇兽铜虎
- pta最长连续递增子序列C语言,pta 习题集 5-5 最长连续递增子序列 (dp)
- SQLSERVER中SP_WHO2和INPUTBUFFER的用法
- JavaEE程序员必读图书大推荐
- 我就是认真:Linux SWAP 深度解读(必须收藏)
- Linux源码安装PHP7.3.1
- ArcGIS对tiff文件进行重分类
- Vue中Swiper以及vue-awesome-swiper的安装和问题
- 教你Word一键自动生成目录步骤
- 神武3 服务器维护公告,《神武3》手游本周更新后的调整内容抢先知
- 如何识别液晶屏面板的型号及品牌
- firewalld防火墙配置IP伪装和端口转发
- 计算机师徒结对方案,师徒结对计划
- 国内IT公司病种,需要合理协调,共同进步,才能不被嘈乱的世道所唾弃
- android中GridView设置间距
- Web数据挖掘技术综述
- 网络故障和网络诊断工具(2.0)
热门文章
- spring gateway、balance、openFeign关于nacos服务注册服务端context-path的解决方案
- 架构实战项目心得(十):基于spring-ladp的统一用户中心结构设计以及代码结构设计...
- Springboot配置SSL证书后启动提示端口被占用
- g楦和h楦的区别_赛事丨“双驰杯”福建鞋类设计师职业技能竞赛暨全国鞋类设计师技能竞赛 福建分赛区选拔赛理论题库(第二部分)...
- 膜拜大佬!成功收获了蚂蚁、拼多多、字节跳动Offer
- GA、CNN在人流密度检测中的运用
- IDC已是夕阳产业?2016传统IDC商何去何从
- 击中-击不中变换—lhMorpHMT
- 微信公众号批量推送java_微信批量关注公众号、推送消息的方法!
- 安装Windows8.1与Ubuntu14.10双系统遇到的各种“坑”