DX11梯台&旋转运动
目录
题目&要求
-
1.绘制一个梯台
-
2.使用键盘和鼠标来控制该梯台的旋转
存在问题
- 虽然对xjun师兄的整体代码流程有了基本的了解,但是还存在一些问题
-
- 1、可能对CPU、GPU以及缓冲区、内存读取资源等等原理不太了解。
- 2、看xjun师兄原代码,对整体流程以及代码都差不多能说出来。但是自己重新写出来,现在还不能够。
学习情况&整体思路
-
对图形渲染代码的初始化已经有的初步的认识。
-
下面来说一下梯台在代码中实现的整体流程以及思路分析。
-
**1、**在Main函数中调用GameApp::Init()函数,同时GameApp和D3DApp的构造函数一起调用,
-
- 对D3DApp内的数据进行初始化。
-
**2、**GameApp::Init()函数中调用D3DApp::Init()函数,然后调用里面InitMainWindow()函数,进行创建窗口。
-
- 再调用InitDirect3D()函数创建D3D设备 、D3D设备上下文和交换链。(这里面的创建流程就不细说了)
-
**3、**接下来回到GameApp::Init()中调用InitEffect()函数(着色器或特效相关的初始化),里面创建了顶点着色器, 同时利用到HLSL编译。创建创建并绑定顶点布局。创建像素着色器,一样利用到HLSL编译。其中HR宏是对错误的追踪。
-
**4、**再调用InitResource()函数,
-
- 1、创建顶点坐标数组。
-
- 2、创建顶点缓冲区。
-
- 3、 索引数组,并且创建索引缓冲区,再是输入装配阶段的索引缓冲区设置。
-
- 4、创建常量缓冲区,初始化常量缓冲区的值。
-
- 5、渲染管线各个阶段绑定好所需资源:输入装配阶段的顶点缓冲区设置、设置图元类型,设定输入布局、将着色器绑定到渲染管线、将更新好的常量缓冲区绑定到顶点着色器。
-
- 6、到初始化鼠标。
-
- 7、回到GameApp::Run()函数中,信息循环。
-
5、:GameApp::UpdateScene(float dt)对鼠标状态的处理和利用,同时更新常量缓冲区,让立方体转起来。GameApp::DrawScene()绘制窗口背景,深度/模板视图,绘制图。
-
对以上流程代码部分实现分析(其他的在前面渲染管线讲完了)。
一、索引缓冲区
-
- 看过我前面那一章的,有没有发现方法二实现,是不是重复的顶点特别多,这样的绘制方法会占用大量的内存空间,那怎么办?,有问题就肯定有解决的方法。
- 接下来会讲另外一种绘制方法,可以只提供立方体的8个顶点数据,然后用一个索引数组来指代使用哪些顶点,按怎样的顺序绘制。
- 那就是使用索引缓冲区进行替代指定顺序绘制,可以有效减少顶点缓冲区的占用空间,避免提供大量重复的顶点数据。
- 首先是数据,没有数据怎么行。
// ******************
// 设置立方体顶点
// 5________ 6
// /| /|
// /_|_____/ |
// 1|4|_ _ 2|_|7
// | / | /
// |/______|/
// 0 3
VertexPosColor vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
};
......
// ******************
// 索引数组
// 使用索引缓冲区进行替代指定顺序绘制,可以有效减少顶点缓冲区的占用空间,避免提供大量重复的顶点数据
DWORD indices[] = {
// 正面
0, 1, 2,
2, 3, 0,
// 左面
4, 5, 1,
1, 0, 4,
// 顶面
1, 5, 6,
6, 2, 1,
// 背面
7, 6, 5,
5, 4, 7,
// 右面
3, 2, 6,
6, 7, 3,
// 底面
4, 0, 3,
3, 7, 4
};
-
万物即可三角形,每一个面是不是两个三角形 0、1、2 和2、3、0,你再参考一下上面的正方体的顶点位置,是正面吧。如果我没有用索引数组,是不是我上面设置立方体顶点那里需要36个顶点,像下面索引数组那样排布,数据量不是一般的大了。上一章方法二18个顶点就已经…
-
然后是创建,和之前的顶点缓冲区的创建一样
// 设置索引缓冲区描述
D3D11_BUFFER_DESC ibd;
ZeroMemory(&ibd, sizeof(ibd));//ZeroMemory其作用是用0来填充一块内存区域,将结构中所有字节置0,初始化重置,没有用的参数必须要设为零
ibd.Usage = D3D11_USAGE_IMMUTABLE;//CPU和GPU的读写权限相关
ibd.ByteWidth = sizeof indices;//数据字节数
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;//缓冲区类型的标志
ibd.CPUAccessFlags = 0;//CPU读写权限的指定
// 新建索引缓冲区
//使用D3D11_SUBRESOURCE_DATA结构体来指定要用来初始化的数据
//InitData是在上面顶点那里设置的对象
InitData.pSysMem = indices;//初始化数据
//最后通过ID3D11Device::CreateBuffer来创建一个索引缓冲区
//([In]索引缓冲区描述,[In]子资源数据,[Out] 获取缓冲区)
HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));
-
设备那里创建了,那么设备上下文也设置一下了 之间关联(m_pIndexBuffer)
-
在装配的时候你需要指定每个索引所占的字节数:
DXGI_FORMAT 字节数 索引范围 DXGI_FORMAT_R8_UINT 1 0-255 DXGI_FORMAT_R16_UINT 2 0-65535 DXGI_FORMAT_R32_UINT 4 0-2147483647
// 输入装配阶段的索引缓冲区设置
// ([In]索引缓冲区,[In]数据格式,[In]字节偏移量)
// DXGI_FORMAT_R32_UINT 4(字节) 0-2147483647(索引范围)
m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
二、常量缓冲区
-
- 发现了没有,这里的正方体会旋转。怎么实现的呢?
-
那就是用到常量缓冲区了
-
首先是在GameApp.h中,也就是c++应用层这边定义一下旋转用的矩阵
-
struct ConstantBuffer { DirectX::XMMATRIX world; //世界坐标 DirectX::XMMATRIX view; //观察 DirectX::XMMATRIX proj; //投射,射影 };
-
单单在c++这边怎么可以,也要供着色器代码使用,那就在HLSL中声明一下
-
//cbuffer 用于声明一个常量缓冲区 //register(b0) 指的是该常量缓冲区位于寄存器索引为0的缓冲区 cbuffer ConstantBuffer : register(b0) { matrix g_World; // matrix可以用float4x4替代。不加row_major的情况下,矩阵默认为列主矩阵, matrix g_View; // 可以在前面添加row_major表示行主矩阵 matrix g_Proj; // 该教程往后将使用默认的列主矩阵,但需要在C++代码端预先将矩阵进行转置。 }
-
正方体旋转,是不是需要频繁的更新。好,来看一下怎样描述
-
// ****************** // 设置常量缓冲区描述 // D3D11_BUFFER_DESC cbd; ZeroMemory(&cbd, sizeof(cbd)); cbd.Usage = D3D11_USAGE_DYNAMIC;//常量缓冲区大多数需要频繁更新,因此后续都将主要使用DYNAMIC更新 cbd.ByteWidth = sizeof(ConstantBuffer); //上面创建的结构体的大小 cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 新建常量缓冲区,不用初始数据,又没有顶点等等什么的 HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffer.GetAddressOf()));
-
不过,常量缓冲区ConstantBuffer里面的值还是要初始化一下的
-
// 初始化常量缓冲区的值 // 如果你不熟悉这些矩阵,可以先忽略,待读完第四章后再回头尝试修改 m_CBuffer.world = XMMatrixIdentity(); // 单位矩阵的转置是它本身(Identity 本体) m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH( XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f), XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f) )); //观察矩阵 :XMMatrixLookAtLH(摄影机坐标,摄影机焦点坐标,摄影机上朝向坐标) // XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f) 设置每个分量并获取一个矩阵 m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f)); //XMMatrixTranspose 转置 //透视投影矩阵:XMMatrixPerspectiveFovLH(中心垂直弧度,宽高比,近平面距离,远平面距离) // float D3DApp::AspectRatio()const{ // return static_cast<float>(m_ClientWidth) / m_ClientHeight;}
-
现在就可以将常量缓冲区绑定到顶点着色器了吧,不急不急,我不是说要频繁更新吗?来看代码实现
-
1、更新数据,你看你想怎么更新
-
void GameApp::UpdateScene(float dt){ //让立方体同时绕X轴和Y轴旋转,修改世界矩阵即可 static float phi = 0.0f, theta = 0.0f; phi += 0.0001f, theta += 0.00015f; //旋转速度 //XMMatrixRotationY XMMatrixRotationX XMMatrixRotationZ 按什么轴旋转 //XMMatrixTranspose 转置:顺时针旋转 不加上就相反旋转 m_CBuffer.world = XMMatrixTranspose(XMMatrixRotationY(phi) * XMMatrixRotationY(theta));
-
2、这里更新了,那常量缓冲区怎么知道?
-
D3D11_MAP枚举值类型的成员如下:
D3D11_MAP成员 含义 D3D11_MAP_READ 映射到内存的资源用于读取。该资源在创建的时候必须绑定了 D3D11_CPU_ACCESS_READ标签 D3D11_MAP_WRITE 映射到内存的资源用于写入。该资源在创建的时候必须绑定了 D3D11_CPU_ACCESS_WRITE标签 D3D11_MAP_READ_WRITE 映射到内存的资源用于读写。该资源在创建的时候必须绑定了 D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE标签 D3D11_MAP_WRITE_DISCARD 映射到内存的资源用于写入,之前的资源数据将会被抛弃。该 资源在创建的时候必须绑定了D3D11_CPU_ACCESS_WRITE和 D3D11_USAGE_DYNAMIC标签 D3D11_MAP_WRITE_NO_OVERWRITE 映射到内存的资源用于写入,但不能复写已经存在的资源。 该枚举值只能用于顶点/索引缓冲区。该资源在创建的时候需要 有D3D11_CPU_ACCESS_WRITE标签,在Direct3D 11不能用于 设置了D3D11_BIND_CONSTANT_BUFFER标签的资源,但在 11.1后可以。具体可以查阅MSDN文档 -
// 更新常量缓冲区,让立方体转起来 D3D11_MAPPED_SUBRESOURCE mappedData; //map获取指向缓冲区中数据的指针并拒绝GPU对该缓冲区的访问 //([In]包含ID3D11Resource接口的资源对象, // [In]缓冲区资源填0, // [In]D3D11_MAP枚举值,指定读写相关操作, // [In]填0,CPU需要等待GPU使用完毕当前缓冲区, // [Out]获取到的已经映射到缓冲区的内存) HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData)); //&mappedData和下面的联系 //安全内存复制 // errno_t memcpy_s( // void* dest, //目的内存地址 // size_t destMaxLen, //目的内存最大长度 // const void* src, //源内存地址 // size_t count); //要拷贝的长度 //映射出来的内存我们可以通过memcpy_s函数来更新 //将数据从系统内存复制到常量缓冲区,即是上面更改的m_CBuffer的内容复制给已经映射到缓冲区的内存 //这样才实现了更改,然后执行这次常量缓冲区的内容 memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer)); //完成一个常量缓冲区时,我们应该在释放内存之前取消映射它(当然这里不是指针) //让指向资源的指针无效并重新启用GPU对该资源的访问权限 //([In]包含ID3D11Resource接口的资源对象,[In]缓冲区资源填0) m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
-
万事俱备了,现在就可以将常量缓冲区绑定到顶点着色器了
-
// 将更新好的常量缓冲区绑定到顶点着色器 //([In]放入缓冲区的起始索引,例如上面指定了b0,则这里应为0,[In]设置的缓冲区数目,[In]用于设置的缓冲区数组) m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());
三,HLSL代码实现
//Cube.hlsli
//cbuffer 用于声明一个常量缓冲区
//register(b0) 指的是该常量缓冲区位于寄存器索引为0的缓冲区
cbuffer ConstantBuffer : register(b0)
{
matrix g_World; // matrix可以用float4x4替代。不加row_major的情况下,矩阵默认为列主矩阵,
matrix g_View; // 可以在前面添加row_major表示行主矩阵
matrix g_Proj; // 该教程往后将使用默认的列主矩阵,但需要在C++代码端预先将矩阵进行转置。
}
struct VertexIn
{
float3 posL : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
// Cube_VS.hlsl
#include "Cube.hlsli"
VertexOut VS(VertexIn vIn)
{
// mul 才是矩阵乘法, 运算符*要求操作对象为行列数相等的两个矩阵,结果为Cij = Aij * Bij
vOut.posH = mul(float4(vIn.posL, 1.0f), gWorld); //外面修改的不就是世界矩阵吗,世界转
vOut.posH = mul(vOut.posH, gView);
vOut.posH = mul(vOut.posH, gProj);
vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
return vOut;
}
//变换矩阵是通过与顶点坐标(或向量)进行矩阵乘法来实现对顶点的变换,变换得到的顶点坐标(或向量)为在原坐标系下对应的坐标。
// Cube_PS.hlsl
#include "Cube.hlsli"
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
四、鼠键实现
1、这个我还是非常非常推荐看X_jun师兄的,我写的话,可能就完全复制粘贴了…
作业相关代码&代码分析
- 这里就简单这次作业的代码分析一下
- 一、顶点设置
// ******************
// 设置梯台顶点
// 5________ 6
// /| /\
// /_|_____/ \
// 1|4|_ _ 2\_ _\7
// | / \ /
// |/______ _\/
// 0 3
VertexPosColor vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },//0
{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },//1
{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },//2
{ XMFLOAT3(2.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },//3
{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },//4
{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },//5
{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },//6
{ XMFLOAT3(2.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }//7
};
- 二、索引数组
// 索引数组
// 使用索引缓冲区进行替代指定顺序绘制,可以有效减少顶点缓冲区的占用空间,避免提供大量重复的顶点数据
DWORD indices[] = {
// 正面
0, 1, 2,
2, 3, 0,
// 左面
4, 5, 1,
1, 0, 4,
// 顶面
1, 5, 6,
6, 2, 1,
// 背面
7, 6, 5,
5, 4, 7,
// 右面
3, 2, 6,
6, 7, 3,
// 底面
4, 0, 3,
3, 7, 4
};
// 设置索引缓冲区描述
D3D11_BUFFER_DESC ibd;
ZeroMemory(&ibd, sizeof(ibd));
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof indices;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
// 新建索引缓冲区
InitData.pSysMem = indices;//初始化数据
HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));
// 输入装配阶段的索引缓冲区设置
// ([In]索引缓冲区,[In]数据格式,[In]字节偏移量)
// DXGI_FORMAT_R32_UINT 4(字节) 0-2147483647(索引范围)
m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
- 三、让立方体旋转&代码分析
//让立方体同时绕X轴和Y轴旋转,修改世界矩阵即可
static float phi = 0.0f, theta = 0.0f;
phi += 0.0001f, theta += 0.00015f;
m_CBuffer.world = XMMatrixTranspose(XMMatrixRotationX(phi) * XMMatrixRotationY(theta));
// 更新常量缓冲区,让立方体转起来
D3D11_MAPPED_SUBRESOURCE mappedData;
//map获取指向缓冲区中数据的指针并拒绝GPU对该缓冲区的访问
//([In]包含ID3D11Resource接口的资源对象,[In]缓冲区资源填0,[In]D3D11_MAP枚举值,指定读写相关操作
// [In]填0,CPU需要等待GPU使用完毕当前缓冲区,[Out]获取到的已经映射到缓冲区的内存)
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
//映射出来的内存我们可以通过memcpy_s函数来更新
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
//让指向资源的指针无效并重新启用GPU对该资源的访问权限
//([In]包含ID3D11Resource接口的资源对象,[In]缓冲区资源填0)
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
- 四、旋转立方体实现原理
//cbuffer 用于声明一个常量缓冲区
//register(b0) 指的是该常量缓冲区位于寄存器索引为0的缓冲区
cbuffer ConstantBuffer : register(b0)
{
matrix g_World; // matrix可以用float4x4替代。不加row_major的情况下,矩阵默认为列主矩阵,
matrix g_View; // 可以在前面添加row_major表示行主矩阵
matrix g_Proj; // 该教程往后将使用默认的列主矩阵,但需要在C++代码端预先将矩阵进行转置。
}
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = mul(float4(vIn.posL, 1.0f), g_World); // mul 才是矩阵乘法, 运算符*要求操作对象为
vOut.posH = mul(vOut.posH, g_View); // 行列数相等的两个矩阵,结果为
vOut.posH = mul(vOut.posH, g_Proj); // Cij = Aij * Bij
vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
return vOut;
}
- 五、鼠标,键盘状态
static float cubePhi = 0.0f, cubeTheta = 0.0f;
// 获取鼠标状态
Mouse::State mouseState = m_pMouse->GetState();//获取当前帧下鼠标的运动状态
Mouse::State lastMouseState = m_MouseTracker.GetLastState();//返回lastState绝对坐标x或相对偏移量
// 获取键盘状态
Keyboard::State keyState = m_pKeyboard->GetState();
Keyboard::State lastKeyState = m_KeyboardTracker.GetLastState();
// 更新鼠标按钮状态跟踪器,仅当鼠标按住的情况下才进行移动
m_MouseTracker.Update(mouseState);
m_KeyboardTracker.Update(keyState);
if (mouseState.leftButton == true && m_MouseTracker.leftButton == m_MouseTracker.HELD)
{
cubeTheta -= (mouseState.x - lastMouseState.x) * 0.01f;//获取两帧之间的鼠标x相对位移量
cubePhi -= (mouseState.y - lastMouseState.y) * 0.01f;//获取两帧之间的鼠标y相对位移量
}
if (keyState.IsKeyDown(Keyboard::W))
cubePhi += dt * 2;
if (keyState.IsKeyDown(Keyboard::S))
cubePhi -= dt * 2;
if (keyState.IsKeyDown(Keyboard::A))
cubeTheta += dt * 2;
if (keyState.IsKeyDown(Keyboard::D))
cubeTheta -= dt * 2;
思考&小结
- 还是这么说吧,对图形渲染这学习呢,从刚刚开始的完全看不懂到现在已经慢慢了解,可以来说是一个很大的进步了,我也是好久没有这种感觉了,什么感觉呢?是那种对一样东西从刚刚开始去接触,一头雾水,完全看不见往什么地方走,想放弃。到后面慢慢去学,然后豁然开朗的感觉。果然呀,船到桥头自然直!
- 现在呢我还是按xjun师兄的博客去学习,一个一个的去看,不会的或者博客中没有解释的就上百度去搜索,找到答案,然后记录在代码上面。对于代码呢,我也是去看每一个代码,去找代码的实现原理,然后注释,便于以后回头去看,也同样让自己能学的更好。可能有时候,有些比较复杂或者困难的代码,不得不去暂时放弃一下,这应该往后会渐渐理解的。
- 当然我还是有一个问题的,对于师兄给的作业呢?应该师兄是想要我们去了解如何实现吧,如果单单去做出来,直接利用xjun师兄的代码框架,那应该就很简单了(应该吧)。
- 对于下周的计划呢,像我在大组作业中的一样,对图形渲染的学习,还是看xjun师兄的博客,第二段那样去学习,同时学习c++,加强一下。
- 不过我现在不是很知道其他人学习是什么情况,可能别人都是非常厉害,感觉我是不是慢了。嗯…对未知的恐惧,慢慢有点担心了。算了,还是看一步走一步吧,努力过就行,没有遗憾就好。