Fork me on GitHub

Direct3D11学习:(七)绘图基础——彩色立方体的绘制

转载请注明出处:http://www.cnblogs.com/Ray1024

 

一、概述

在前面的几篇文章中,我们详细介绍了Direct3D渲染所需要的数学基础和渲染管道理论知识。从这篇文章开始,我们就正式开始Direct3D的绘制学习过程了。这篇文章中,主要讲解Direct3D的绘制基础过程,介绍配置渲染管道,定义顶点和像素着色器以及将几何图形提交到渲染管道进行绘制所需的Direct3DAPI接口和方法。

本文通过绘制一个彩色立方体来演示Direct3D的渲染过程,这个例子本身很简单,但是清晰的包含了Direct3D的渲染基本步骤。因为绘制过程中涉及到Direct3D的API接口和方法,我们将在学习彩色立方体的绘制过程中详细介绍这些API接口和方法。

 

二、绘图基础

2.1 创建顶点缓冲

在D3D中,顶点由空间位置和各种附加属性组成。定义顶点结构体如下,由空间位置和颜色组成,我们这个例子中使用的结构体就是它:

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

为了让GPU访问顶点数组,我们必须把它放在顶点缓冲(vertex buffer)中,该容器由ID3D11Buffer接口表示。要创建一个顶点缓冲,我们必须执行以下步骤:

  (1)填写一个D3D11_BUFFER_DESC结构体,描述我们所要创建的缓冲区;

  (2)填写一个D3D11_SUBRESOURCE_DATA结构体,为缓冲区指定初始化数据;

  (3)调用ID3D11Device::CreateBuffer方法来创建缓冲区。

D3D11_BUFFER_DESC结构体的定义如下:

typedef struct D3D11_BUFFER_DESC{
    UINT ByteWidth; 			// 将要创建的顶点缓冲区的大小,单位为字节
    D3D11_USAGE Usage; 			// 一个用于指定缓冲区用途的D3D11_USAGE枚举类型成员
    UINT BindFlags; 			// 对于顶点缓冲区,该参数应设为D3D11_BIND_VERTEX_BUFFER
    UINT CPUAccessFlags; 		// 指定CPU对资源的访问权限
	UINT MiscFlags; 			// 不需要为顶点缓冲区指定任何杂项(miscellaneous)标志值,所以该参数设为0
	UINT StructureByteStride;	// 存储在结构化缓冲中的一个元素的大小,以字节为单位。这个属性只用于结构化缓冲,其他缓冲可以设置为0
} D3D11_BUFFER_DESC;

D3D11_SUBRESOURCE_DATA结构体的定义如下:

typedef struct D3D11_SUBRESOURCE_DATA { 
    const void *pSysMem; 	// 包含初始化数据的系统内存数组的指针
    UINT SysMemPitch; 		// 顶点缓冲区不使用该参数
    UINT SysMemSlicePitch; 	// 顶点缓冲区不使用该参数
} D3D11_SUBRESOURCE_DATA;

下面的代码创建了一个只读的顶点缓冲区,并以中心在原点上的立方体的8个顶点来初始化该缓冲区。之所以说该缓冲区是只读的,是因为当立方体创建后相关的几何数据从不改变——始终保持为一个立方体。另外,我们为每个顶点指定了不同的颜色,颜色类型为D3D11Util.h文件中的类型。

/************************************************************************/
/* 1.创建顶点缓冲                                                       */
/************************************************************************/
Vertex vertices[] =
{
	{ XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White   },
	{ XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black   },
	{ XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red     },
	{ XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green   },
	{ XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue    },
	{ XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow  },
	{ XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan    },
	{ XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }
};
// 准备结构体,描述缓冲区
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
// 准备结构体,为缓冲区指定初始化数据
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
// 创建缓冲区
HR(m_pD3DDevice->CreateBuffer(&vbd, &vinitData, &m_pBoxVB));

 

2.2 创建索引缓冲

由于索引要由GPU访问,所以它们必须放在一个特定的资源容器中,该容器称为索引缓冲(index buffer)。创建索引缓冲的过程与创建顶点缓冲的过程非常相似,只不过索引缓冲存储的是索引而非顶点。所以,这里不再赘述之前讨论过的内容,我们直接给出一个创建索引缓冲区的示例:

/************************************************************************/
/* 2.创建索引缓冲                                                       */
/************************************************************************/
UINT indices[] = {
	// 前表面
	0, 1, 2,
	0, 2, 3,

	// 后表面
	4, 6, 5,
	4, 7, 6,

	// 左表面
	4, 5, 1,
	4, 1, 0,

	// 右表面
	3, 2, 6,
	3, 6, 7,

	// 上表面
	1, 5, 6,
	1, 6, 2,

	// 下表面
	4, 0, 3, 
	4, 3, 7
};
// 准备结构体,描述缓冲区
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
// 准备结构体,为缓冲区指定初始化数据
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
// 创建缓冲区
HR(m_pD3DDevice->CreateBuffer(&ibd, &iinitData, &m_pBoxIB));

 

2.3 顶点着色器和像素着色器

着色器使用一种称为高级着色语言(HLSL)的脚本语言来编写。在此系列中,我们将根据贯穿本书的每个演示程序所涉及的技术讲解相关的HLSL概念。着色器通常保存在一种称为effect文件(.fx)的纯文本文件中。我们会在下一篇文章中讨论effect文件,而现在我们主要讨论顶点着色器和像素着色器。

顶点着色器(Vertex Shader)和像素着色器(Pixel Shader)是Direct3D渲染中必不可少的最基本的Shader。

(1)顶点着色器

先看一个顶点着色器的代码: 

cbuffer cbPerObject
{
    float4x4 gWorldViewProj; 
};
 
struct VertexIn
{
    float3 PosL  : POSITION;
    float4 Color : COLOR;
};
 
struct VertexOut
{
    float4 PosH  : SV_POSITION;
    float4 Color : COLOR;
};
 
VertexOut VS(VertexIn vin)
{
    VertexOut vout;
     
    // 转换到齐次剪裁空间
    vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
     
    // 将顶点颜色直接传递到像素着色器
    vout.Color = vin.Color;
     
    return vout;
}

VertexIn是一个输入结构,"POSITION”和"COLOR"用来对成员变量进行语义说明,"POSITION"指位置坐标,"COLOR"指顶点的颜色。这些语义说明在创建输入布局时会用,并且必须要一致,这个在本文稍后介绍InputLayout时还会解释。

VertexOut是输出结构,注意PosH成员的语义说明SV_POSITION是固定的,代表系统值,在像素着色器阶段会需要这个值来进行裁剪操作。除了系统值必须固定外,其他的语义值都是可以任意指定的。

顶点着色器是一个称为VS的函数(可以指定任何有效的函数名),其参数为输入顶点结构,输出为相应的输出顶点结构。上面这个顶点着色器的功能就是坐标变换和颜色的简单传递。最常见的顶点着色器即对顶点的位置坐标、纹理坐标、法线等信息进行变换,并返回新的顶点,传递给下一个阶段。一般针对顶点着色器的输入与输出,应该各指定相应的结构来存储相应的顶点信息。

(2)像素着色器

像素着色器是针对逐个像素进行的。在光栅化阶段,一个三角形在其所覆盖的每个像素处使用插值来计算相应顶点的各个属性,然后把插值后的顶点传递给像素着色器。在简单的没有Geometry Shader和Tessellator时,顶点着色器的输出就是像素着色器的输入,像素着色器最终的输出是该像素处对应片段的颜色值。如下即是一个最简单的像素着色器函数:

float4 PS(VertexOut pin) : SV_Target
{
    return pin.Color;
}

该函数参顶点着色器输出结构作为参数,输出为float4类型,注意函数名后面的语义说明:SV_TARGET,它是函数返回值的说明,显然这也是一个系统值,不可更改,它作为相应片段的颜色值传递给下一个阶段。 

(3)常量缓冲

上面的代码中定义了一个称为cbPerObject的cbuffer对象(constant buffer,常量缓冲)。常量缓冲只是一个用于存储各种变量的数据块,这些变量可以由着色器来访问。顶点着色器不能修改常量缓冲中的数据,但是C++程序可以通过effect框架在运行时修改常量缓冲中的内容。它为C++应用程序代码和effect代码提供了一种有效的通信方式。例如,因为每个物体的世界矩阵各不相同,所以每个物体的“WVP”组合矩阵也各不相同;所以,当使用上述顶点着色器绘制多个物体时,我们必须在绘制每个物体前修改gWorldViewProj变量。

通常的建议是根据变量修改的频繁程度创建不同的常量缓冲,对常量缓冲进行分组是为了提高运行效率。当一个常量缓冲区被更新时,它里面的所有变量都会同时更新;所以,根据它们的更新频率进行分组,可以减少不必要的更新操作,提高运行效率。

 

2.4 编译着色器,创建Effect

(1)Effect框架介绍

Effect框架是微软提供的开源代码库,用来把shader代码和相应的渲染状态合理的组织到一起,来实现一个针对性的特效。关于编译Effect及相应的配置IDE的详细步骤,请参见此系列的第一篇文章《Direct3D11学习:(一)开发环境配置》。

一个Effect至少包含一个顶点着色器、一个像素着色器及所需要的全局变量。此外,还包含technique11和pass,我们这里简单介绍一下,下篇文章将详细介绍

  technique11:一个特效可以通过多种不同的方法实现,每个方法可以作为一个technique11。

  pass:每个technique11至少包含一个pass,一个pass至少包含一个顶点着色器和一个像素着色器及其相应渲染状态。一个pass即是对特效的一次渲染。

在C++程序中,Effect对应的接口为ID3DX11Effect,technique11为ID3D11EffectTechnique。

 

(2)编译着色器

编译在.fx文件中的着色器程序可以创建一个Effect,编译步骤如下:

	// 编译着色器程序
	DWORD shaderFlags = 0;
#if defined( DEBUG ) || defined( _DEBUG )
	shaderFlags |= D3D10_SHADER_DEBUG;
	shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;
#endif

	ID3D10Blob* compiledShader = 0;
	ID3D10Blob* compilationMsgs = 0;
	HRESULT hr = D3DX11CompileFromFile(L"FX/color.fx", 0, 0, 0, "fx_5_0", shaderFlags, 
		0, 0, &compiledShader, &compilationMsgs, 0);

	// compilationMsgs中包含错误或警告信息
	if( compilationMsgs != 0 )
	{
		MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);
		ReleaseCOM(compilationMsgs);
	}

	// 就算没有compilationMsgs,也需要确保没有其他错误
	if(FAILED(hr))
	{
		DXTrace(__FILE__, (DWORD)__LINE__, hr, L"D3DX11CompileFromFile", true);
	}

	// 创建Effect
	HR(D3DX11CreateEffectFromMemory(compiledShader->GetBufferPointer(), compiledShader->GetBufferSize(), 
		0, m_pD3DDevice, &m_pFX));

	// 编译完成释放资源
	ReleaseCOM(compiledShader);

	// 从Effect中获取technique对象
	m_pTech    = m_pFX->GetTechniqueByName("ColorTech");

此外,作为C++程序与Shader程序的衔接,即更新Shader中的全局变量,在C++中定义了各种类型的接口:ID3DX11EffectMatrixVariable、ID3DX11EffectVectorVariable、ID3DX11EffectVariable等,分别对应Shader中的float4x4类型、float4类型及raw数据,即无类型。这些接口通过如下方式获得:

g_fxWorldViewProj = g_effect->GetVariableByName("g_worldViewProj")->AsMatrix();  

这里的“g_worldViewProj”即shader中的全局变量名字。之后就可以在C++程序中通过更改g_fxWorldViewProj接口来更新shader中相应的全局变量了:

g_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));  

 

2.5 创建输入布局

在Direct3D中,顶点由空间位置和各种附加属性组成,Direct3D可以让我们灵活地建立属于我们自己的顶点格式;换句话说,它允许我们定义顶点的分量。

我们使用上面定义的结构体Vertex。有了顶点结构体之后,我们必须设法描述该顶点结构体的分量结构,使Direct3D知道该如何使用每个分量。这一描述信息是以输入布局(ID3D11InputLayout)的形式提供给Direct3D的。输入布局是一个D3D11_INPUT_ELEMENT_DESC数组。D3D11_INPUT_ELEMENT_DESC数组中的每个元素描述了顶点结构体的一个分量。比如,当顶点结构体包含两个分量时,对应的D3D11_INPUT_ELEMENT_DESC数组会包含两个元素。我们将D3D11_INPUT_ELEMENT_DESC称为输入布局描述(input layout description)。D3D11_INPUT_ELEMENT_DESC结构体定义如下:

typedef struct D3D11_INPUT_ELEMENT_DESC { 
    LPCSTR SemanticName;
		//元素相关的字符串。它可以是任何有效的语义名。语义用于将顶点结构体中的元素映射为顶点着色器参数
    UINT SemanticIndex; 
		//附加在语义上的索引值。例如:当顶点结构体包含多组纹理坐标时,我们不是添加一个新的语义名,而是在语义名的后面加上一个索引值。
    DXGI_FORMAT Format; 
		// 一个用于指定元素格式的DXGI_FORMAT枚举类型成员
    UINT InputSlot; 
		// 指定当前元素来自于哪个输入槽(input slot)。
		// Direct3D支持16个输入槽(索引依次为 0到15),通过这些输入槽我们可以向着色器传入顶点数据。
		// 如:当一个顶点由位置和颜色元素组成,既可以使用一个输入槽传送两种元素,也可以将两种元素分开。
		// Direct3D可以将来自于不同输入槽的元素重新组合为顶点。
    UINT AlignedByteOffset 
		// 对于单个输入槽来说,该参数表示从顶点结构体的起始位置到顶点元素的起始位置之间的字节偏移量。
    D3D11_INPUT_CLASSIFICATION InputSlotClass; 
		// 目前指定为D3D11_INPUT_PER_VERTEX_DATA;其他选项用于高级实例技术。
    UINT InstanceDataStepRate; 
		// 目前指定为0;其他值只用于高级实例技术。
} D3D11_INPUT_ELEMENT_DESC;

接下来我们就可以创建输入布局了,下面是代码:

/************************************************************************/
/* 5.创建输入布局                                                       */
/************************************************************************/
// 顶点输入布局描述
D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
	{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
	{"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};

// 从technique对象中获取pass信息
D3DX11_PASS_DESC passDesc;
m_pTech->GetPassByIndex(0)->GetDesc(&passDesc);

// 创建顶点输入布局
HR(m_pD3DDevice->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, 
	passDesc.IAInputSignatureSize, &m_pInputLayout));

上面的代码中使用了上一节介绍的Effect框架,通过它获取相应technique11中相应的pass的描述信息。

 

2.6 更新矩阵变换

在这个例子中,用鼠标点击拖动改变摄像机的视角和远近,在UpdateScene函数中计算变换矩阵,并更新到shader中去。这里就不详细介绍了,有兴趣的朋友请看源码。

void D3D11BoxDemoApp::UpdateScene(float dt)
{
	/************************************************************************/
	/* 6.更新每帧的矩阵变换                                                 */
	/************************************************************************/

	// 视角变换矩阵

	// 将球面坐标转换为笛卡尔坐标
	float x = m_radius*sinf(m_phi)*cosf(m_theta);
	float z = m_radius*sinf(m_phi)*sinf(m_theta);
	float y = m_radius*cosf(m_phi);

	XMVECTOR pos    = XMVectorSet(x, y, z, 1.0f);
	XMVECTOR target = XMVectorZero();
	XMVECTOR up     = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

	XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
	XMStoreFloat4x4(&m_view, V);

	// 把三个变换相乘,合成一个
	XMMATRIX world = XMLoadFloat4x4(&m_world);
	XMMATRIX view  = XMLoadFloat4x4(&m_view);
	XMMATRIX proj  = XMLoadFloat4x4(&m_proj);
	XMMATRIX worldViewProj = world*view*proj;

	// 通过C++程序更新Shader相应的变量
	m_pFXWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
}

 

2.7 绘制场景

到这里我们就可以开始绘制操作了。

绘制场景的步骤为:

  a.清屏,清空深度/模版缓冲区;

  b.指定输入布局、图元拓扑类型、定点缓冲、索引缓冲、渲染状态等等;

  c.从technique获取pass并逐个渲染;

  d.显示。

具体代码如下:

void D3D11BoxDemoApp::DrawScene()
{
	/************************************************************************/
	/* 7.场景绘制                                                           */
	/************************************************************************/

	// 清屏
	m_pD3DImmediateContext->ClearRenderTargetView(m_pRenderTargetView, reinterpret_cast<const float*>(&Colors::LightSteelBlue));
	m_pD3DImmediateContext->ClearDepthStencilView(m_pDepthStencilView, D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);

	// 指定输入布局、图元拓扑类型、顶点缓冲、索引缓冲、渲染状态
	m_pD3DImmediateContext->IASetInputLayout(m_pInputLayout);
	m_pD3DImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	UINT stride = sizeof(Vertex);
	UINT offset = 0;
	m_pD3DImmediateContext->IASetVertexBuffers(0, 1, &m_pBoxVB, &stride, &offset);
	m_pD3DImmediateContext->IASetIndexBuffer(m_pBoxIB, DXGI_FORMAT_R32_UINT, 0);
	// 是否使用线框模式
	//m_pD3DImmediateContext->RSSetState(m_pWireframeRS);

	// 从technique获取pass并逐个渲染
	D3DX11_TECHNIQUE_DESC techDesc;
	m_pTech->GetDesc( &techDesc );
	for(UINT p = 0; p < techDesc.Passes; ++p)
	{
		m_pTech->GetPassByIndex(p)->Apply(0, m_pD3DImmediateContext);
		m_pD3DImmediateContext->DrawIndexed(36, 0, 0);// 立方体有36个索引
	}

	// 显示
	HR(m_pSwapChain->Present(0, 0));
}

 

到这里为止,我们的彩色立方体程序就完成了。这是运行效果图:

需要源码的朋友点此下载,源码为4_D3DBoxDemo。

 

注意:上面代码中使用了一个文件MathHelper.h中的内容,这个文件中定义了我们常用的数学工具(函数或宏),此文件也放在Common文件夹中。

 

三、结语

我们通过绘制一个彩色立方体程序演示了Direct3D的渲染过程,到这里我们自己可以根据绘制各种几何形状与着色或线框模式了。

这篇文章中我们简单使用了Effect框架,下篇文章我们将详细介绍它。

 

posted @ 2016-11-30 09:39  江湖码客Mark  阅读(3857)  评论(0编辑  收藏  举报