DirectX11编程3 初次接触HLSL 渲染一个方块

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012632851/article/details/82868246

环境:VS2017  语言:C++

 

总起:

红龙书所使用的Effect库已经不建议使用了,所以这边在编译Shader所使用的是X_Jun96大佬手动的方式。

 

附上工程链接:https://github.com/anguangzhihen/Dx11

 

红龙书本身是比较难以研读的,特别是对于初学者而言,它的一般做法是先将理论全部说一遍,然后讲一整个例子,没有由浅入深进行说明,总体而言看起来比较痛苦。不过我会把它读完。

 

红龙书第五章主要是渲染管线的理论知识,关于这个就不做详细叙述了,主要是两个阶段:

  1. 几何阶段,将模型内部坐标转换到齐次剪裁空间,主要通过wvp矩阵进行转换(Unity中为mvp);
  2. 光栅化阶段,通过遍历所有可见模型上的三角形,对其每个像素点进行操作(类似设置法线贴图就在该流程中)。

 

几何阶段对应的程序控制代码的Shader就是顶点着色器,即Vertex Shader(暂不讨论曲面细分着色器和几何着色器);而光栅化阶段是片元着色器,即Pixel Shader。

 

第六章是介绍Dx渲染时的各个函数与数据结构,然后是实践,看起来很多很复杂,实际上比起第四章使用的函数量少的多,所以我们直接从实践出发,看看渲染一个方块究竟要做一些什么操作。

 

渲染一个方块:

首先我们来看一下效果:

 

嗯,好的,非常完美(有同学可能要问:这不是棱锥吗?嗯……可能是方块上面的四个点坍缩成了一个点吧)。

 

首先我们让新建的BoxDemo类继承我们第一篇博文中编写的D3DApp,然后主要重写3个方法:Init、UpdateScene、DrawScene。

 

首先是Init,大部分的操作都集中在此处:

bool BoxApp::Init()
{
	if (!D3DApp::Init())
		return false;

	BuildGeometryBuffers();
	BuildFX();

	return true;
}

创建顶点和索引信息、设置GPU缓存(BuildGeometryBuffers)

在渲染一个模型的时候,我们考虑的第一步就是把这个模型通过顶点建立出来。

 

这边我们除了顶点以外,还需要设置索引信息,什么是索引信息呢。以2D空间举例:

一个方块顶点为:(0, 0) (0, 1) (1, 1) (1, 0)。

则以如果以三角形为图元,需要两个三角形才能渲染该方块,即:0 1 2和1 2 3。(Dx为左手坐标系,顶点顺序顺时针为可见)

 

上面的0 1 2 1 2 3便是索引信息,因为如果要使用顶点来表示索引,便会多出很多多余的内存。

 

首先声明3个缓存成员变量,还有常量缓存使用的数据结构:

ID3D11Buffer* mBoxVB;	// 顶点缓存
ID3D11Buffer* mBoxIB;	// 索引缓存
ID3D11Buffer* mBoxCB;	// 常量缓存
ConstantBuffer mCBuffer;	// 常量数据结构

初始化的内容:
void BoxApp::BuildGeometryBuffers()
{
	// 顶点
	Vertex vertices[] =
	{
		{ XMFLOAT3(-1.0f, 0.0f, -1.0f), XMFLOAT4((const float*)&Colors::Red) },
		{ XMFLOAT3(-1.0f, 0.0f, +1.0f), XMFLOAT4((const float*)&Colors::Green) },
		{ XMFLOAT3(+1.0f, 0.0f, +1.0f), XMFLOAT4((const float*)&Colors::Blue) },
		{ XMFLOAT3(+1.0f, 0.0f, -1.0f), XMFLOAT4((const float*)&Colors::Yellow) },
		{ XMFLOAT3(0.0f, 1.41f, 0.0f), XMFLOAT4((const float*)&Colors::Black) },
	};

	// 顶点缓存的描述
	D3D11_BUFFER_DESC vbd;
	ZeroMemory(&vbd, sizeof(vbd));
	vbd.Usage = D3D11_USAGE_DEFAULT;
	vbd.ByteWidth = sizeof vertices;
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = 0;

	// 将顶点设置到描述中
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory(&InitData, sizeof(InitData));
	InitData.pSysMem = vertices;

	// 创建缓存
	HR(md3dDevice->CreateBuffer(&vbd, &InitData, &mBoxVB));

	// 绑定到渲染管线上
	UINT stride = sizeof(Vertex);
	UINT offset = 0;
	md3dImmediateContext->IASetVertexBuffers(0, 1, &mBoxVB, &stride, &offset);


	// 创建索引
	UINT indices[] = {
		// 底面
		0, 2, 1,
		0, 3, 2,	
		// 4个三角面
		0, 1, 4,	
		1, 2, 4,
		2, 3, 4,
		3, 0, 4,
	};

	// 索引缓存的描述
	D3D11_BUFFER_DESC ibd;
	ZeroMemory(&ibd, sizeof(ibd));
	ibd.Usage = D3D11_USAGE_DEFAULT;
	ibd.ByteWidth = sizeof indices;
	ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
	ibd.CPUAccessFlags = 0;

	// 将索引设置到描述中
	InitData.pSysMem = indices;
	HR(md3dDevice->CreateBuffer(&ibd, &InitData, &mBoxIB));

	// 绑定到渲染管线上
	md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);	// 设定图元类型
	md3dImmediateContext->IASetIndexBuffer(mBoxIB, DXGI_FORMAT_R32_UINT, 0);

	
	// 设置常量缓存区
	D3D11_BUFFER_DESC cbd;
	ZeroMemory(&cbd, sizeof(cbd));
	cbd.Usage = D3D11_USAGE_DEFAULT;
	cbd.ByteWidth = sizeof(ConstantBuffer);
	cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cbd.CPUAccessFlags = 0;
	HR(md3dDevice->CreateBuffer(&cbd, nullptr, &mBoxCB));
	md3dImmediateContext->VSSetConstantBuffers(0, 1, &mBoxCB);
}

可以看到顶点缓存、索引缓存和常量缓存都用到了D3D11_BUFFER_DESC进行描述。其中顶点缓存、索引缓存使用了 D3D11_SUBRESOURCE_DATA在描述中直接指定数据,而常量缓存到时候会在UpdateScene时动态指定。

 

以下是D3D11_BUFFER_DESC重要的几个参数介绍:

Usage,指定该缓存会被怎样使用:

  1. D3D11_USAGE_DEAFULT,GPU可读可写,当使用mapping API(例如ID3D11DeviceContext::Map)时,CPU不可读不可写。否则CPU可使用ID3D11DeviceContext::UpdateSubresource进行更新缓存信息;
  2. D3D11_USAGE_IMMUTABLE,指定该内容在创建后便不会被更改,GPU只读,CPU不可读不可写;
  3. D3D11_USAGE_DYNAMIC,指定内容CPU可写,而GPU可读,应该尽量避免该用法,因为数据会从内存传送到显存中,效率比较低;
  4. D3D11_USAGE_STAGING,指定内容需要CPU可读,CPU可以使用ID3D11DeviceContext::CopyResource和ID3D11DeviceContext::CopySubresourceRegion从显存传输到内存中进行读取。

 

BindFlags,指定当前缓存的类型:

  1. D3D11_BIND_VERTEX_BUFFER,顶点缓存;
  2. D3D11_BIND_INDEX_BUFFER,索引缓存;
  3. D3D11_BIND_CONSTANT_BUFFER,常量缓存。

 

CPUAccessFlags,CPU访问标记:

  1. 指定为0,CPU不可读不可写;
  2. D3D11_CPU_ACCESS_WRITE,CPU可写;
  3. D3D11_CPU_ACCESS_READ,CPU可读。

 

以下介绍D3D11_SUBRESOURCE_DATA的重要参数:

pSysMem,内容指针,如果缓存指定能存n个顶点,则这边给的数据必须大于或等于n。

 

描述创建完了之后使用ID3D11Device::CreateBuffer创建缓存。

 

之后使用以下三个方法分别将缓存绑定到渲染管线上:

1. ID3D11DeviceContext:: IASetVertexBuffers 绑定顶点缓存;

2. ID3D11DeviceContext:: IASetIndexBuffer 绑定索引缓存;

3. ID3D11DeviceContext:: VSSetConstantBuffers 绑定常量缓存。

 

在索引缓存绑定到渲染管线上之前,需要设定图元类型:

ID3D11DeviceContext:: IASetPrimitiveTopology

  1. D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,点;
  2. D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP,线条,当前点与上一个点相连;
  3. D3D11_PRIMITIVE_TOPOLOGY_LINELIST,线,每两个点为一条线;
  4. D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,三角,当前点与上两个点组成一个三角;
  5. D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST,三角,每三个点组成一个三角,常用。

 

初始化着色器(BuildFX)

接下来初始化Shader,创建Shader的CreateShaderFromFile函数直接使用大佬的了,也不做过多的介绍了。

 

首先声明成员变量:

ID3D11InputLayout* mVertexLayout;	// 顶点布局
ID3D11VertexShader* mVertexShader;	// 顶点Shader
ID3D11PixelShader* mPixelShader;	// 片元Shader

接下来初始化:


void BoxApp::BuildFX()
{
	ID3DBlob* blob;

	// 创建顶点着色器
	HR(D3DUtil::CreateShaderFromFile(L"HLSL\\Box_VS.vso", L"HLSL\\Box_VS.hlsl", "VS", "vs_5_0", &blob));
	HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &mVertexShader));

	// 创建顶点布局
	D3D11_INPUT_ELEMENT_DESC vertexLayout[2] = {
		{ "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 }
	};
	HR(md3dDevice->CreateInputLayout(vertexLayout, ARRAYSIZE(vertexLayout), blob->GetBufferPointer(), blob->GetBufferSize(), &mVertexLayout));
	md3dImmediateContext->IASetInputLayout(mVertexLayout);// 设定输入布局
	ReleaseCOM(blob);

	// 创建像素着色器
	HR(D3DUtil::CreateShaderFromFile(L"HLSL\\Box_PS.pso", L"HLSL\\Box_PS.hlsl", "PS", "ps_5_0", &blob));
	HR(md3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, &mPixelShader));
	ReleaseCOM(blob);

	md3dImmediateContext->VSSetShader(mVertexShader, nullptr, 0);
	md3dImmediateContext->PSSetShader(mPixelShader, nullptr, 0);
}

使用CreateShaderFromFile后获得ID3DBlob,相当于Shader的二进制数据,然后使用ID3D11Device:: CreateVertexShader创建Shader,最后将Shader通过ID3D11DeviceContext:: VSSetShader/ PSSetShader设置到渲染管线中。

 

重点来说一下顶点布局ID3D11InputLayout

首先有两个数据结构:

// 顶点Shader的传入参数
struct Vertex
{
	DirectX::XMFLOAT3 Pos;
	DirectX::XMFLOAT4 Color;
};

// 常量缓存
struct ConstantBuffer
{
	DirectX::XMMATRIX wvp;
};

 

接着看一下Shader的三个文件:

// Box.fx
cbuffer cbPerObject : register(b0)
{
    row_major matrix wvp;	// 默认列主矩阵
};

struct VertexIn
{
	float3 PosL : POSITION;
	float4 Color : COLOR;
};

struct VertexOut
{
	float4 PosH : SV_POSITION;
	float4 Color : COLOR;
};

// Box_VS.hlsl顶点着色器
#include "Box.fx"

VertexOut VS(VertexIn pIn)
{
	VertexOut pOut;
	pOut.PosH = mul(float4(pIn.PosL, 1.0f), wvp);

	pOut.Color = pIn.Color;
	return pOut;
}

// Box_PS.hlsl片元着色器
#include "Box.fx"

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

从数据结构出发,C++中的常量缓存ConstantBuffer对应的是cbPerObject,而Vertex对应的是VertexIn,VertexOut是顶点着色器传给片元着色器,所以在C++中并不需要(注意这边C++的数据结构一定要和Shader的数据结构完全对应,顺序都要一致,并且传的时候不能有null。不要问我为什么知道,我的眼中常含泪水)。

 

常量缓存我们在上一节中已经指定好了。

 

而顶点数据,我们在上一节中使用Vertex创建了每一个顶点,并设置到了缓存中,但并没有告诉Shader怎么用,这边设定顶点布局便是告诉Shader怎么使用输入的顶点数据。

 

D3D11_INPUT_ELEMENT_DESC,顶点每一个数据的描述(这边POSITION和COLOR便分别对应Vertex中的Pos和Color):

  1. SemanticName,名称,如果指定COLOR,便对应Shader数据成员变量后缀的COLOR;
  2. SemanticIndex,在相同的名称有多个时指定第几个,其中POSITION等同于POSITION0;
  3. Format,指定数据的类型;
  4. InputSlot,指定槽位,Dx支持16个槽位;
  5. AlignedByteOffset,针对槽位的数据偏移;
  6. InputSlotClass,暂时只指定D3D11_INPUT_PER_VERTEX_DATA;
  7. InstanceDataStepRate,暂时只指定为0。

 

描述创建完之后,使用ID3D11Device::CreateInputLayout创建顶点布局,接着使用ID3D11DeviceContext::IASetInputLayout设置顶点布局。

 

至此终于将准备工作完成了,接下来是每帧的更新和渲染。

 

更新场景

void BoxApp::UpdateScene(float dt)
{
	float x = mRadius * sinf(mPhi) * cosf(mTheta);
	float z = mRadius * sinf(mPhi) * sinf(mTheta);
	float y = mRadius * cosf(mPhi);

	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);
	DirectX::XMStoreFloat4x4(&mView, V);

	// Unity Matrix4x4 列优先填充  Unity Shader float4x4 行优先填充
	DirectX::XMMATRIX world = DirectX::XMLoadFloat4x4(&mWorld);	// 主行矩阵
	DirectX::XMMATRIX view = DirectX::XMLoadFloat4x4(&mView);
	DirectX::XMMATRIX proj = DirectX::XMLoadFloat4x4(&mProj);

	mCBuffer.wvp = world * view * proj;

	// 更新WVP矩阵数据
	md3dImmediateContext->UpdateSubresource(mBoxCB, 0, nullptr, &mCBuffer, 0, 0);
}

常量中的数据是wvp矩阵,所以在渲染之前需要先将该矩阵计算好传给Shader,这边是固定了物体,旋转视角来观察不同角度的物体(因为没有参照物,所以看起来像是在旋转物体),因此需要每帧更新View矩阵来重置相机的位置,如果是旋转物体则需要更新World矩阵(坐标、旋转、缩放信息都包含在其中),而投影矩阵一般不用更新,除非窗口大小发生了变化。

 

通过UpdateSubresource更新完wvp常量后,开始渲染。

 

更新场景

void BoxApp::DrawScene()
{
	md3dImmediateContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&Colors::LightSteelBlue));
	md3dImmediateContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	// 绘制Box的36的顶点(12个三角)
	//md3dImmediateContext->DrawIndexed(36, 0, 0);
	md3dImmediateContext->DrawIndexed(18, 0, 0);

	HR(mSwapChain->Present(0, 0));
}

重置RT后,直接调用DrawIndexed进行渲染,好了,没了。

猜你喜欢

转载自blog.csdn.net/u012632851/article/details/82868246