DirectX11入门笔记---绘制三角形

image-20201217210042177

绘制三角形

写了差不多三百多行代码,才弄出个三角形,真是太难顶了:sweat_smile:

image-20201220192624585

编写着色器

新建Shader筛选项用于存储HLSL文件,新建Triangle.hlsl开始编写着色器

image-20201220192744731

这里推荐安装VS插件,非常好用,提供了HLSL高亮提示等功能

image-20201220192822607

这里跟Unity Shader差不多,编写顶点着色器和片元着色器

//传入顶点着色器的格式
struct VertexIn
{
    float4 pos : POSITION;
    float4 color : COLOR;
};
//传入像素着色器的格式
struct VertexOut
{
    float4 pos : SV_POSITION;
    float4 color : COLOR;
};

//顶点着色器
VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    vOut.pos = vIn.pos;
    vOut.color = vIn.color;
    return vOut;
}

//像素着色器
float4 PS(VertexOut pIn):SV_Target
{
    return pIn.color;
}

编译着色器方法

目前编译与加载着色器的方法有三种,我使用的是在程序运行期间编译着色器代码,并读取生成的字节码,所以要修改Triangle.hlsl的属性,防止一开始就参与编译

参考文章:https://www.cnblogs.com/X-Jun/p/10066282.html

image-20201220193854521

D3DCompileFromFile函数—-可以让我们运行时编译.hlsl文件

HRESULT D3DCompileFromFile(
    LPCWSTR pFileName,                  // [In]要编译的.hlsl文件
    CONST D3D_SHADER_MACRO* pDefines,   // [In_Opt]忽略
    ID3DInclude* pInclude,              // [In_Opt]如何应对#include宏
    LPCSTR pEntrypoint,                 // [In]入口函数名
    LPCSTR pTarget,                     // [In]使用的着色器模型
    UINT Flags1,                        // [In]D3DCOMPILE系列宏
    UINT Flags2,                        // [In]D3DCOMPILE_FLAGS2系列宏
    ID3DBlob** ppCode,                  // [Out]获得着色器的二进制块
    ID3DBlob** ppErrorMsgs);            // [Out]可能会获得错误信息的二进制块
  1. pFileName:要编译hlsl文件的路径
  2. pDefines:高级选项,我们为nullptr
  3. pInclude:其中pInclude用于决定如何处理包含文件。如果设为nullptr,则编译的着色器代码包含#include时会引发编译器报错。如果你需要使用#include,可以传递D3D_COMPILE_STANDARD_FILE_INCLUDE宏,这是一个默认的包含句柄,可以按该着色器代码所处的相对路径去搜索对应的头文件并包含进来。
  4. pEntryPoint:着色器的入口点函数名,因为一个hlsl文件可以包含多个着色器程序,比如一个顶点着色器和像素着色器,所以我们要指定入口
  5. pTarget:指定着色器类型和版本的字符串
  6. Flags1:我们仅使用D3DCOMPILE_DEBUG(获取着色器调试错误信息)和D3DCOMPILE_SKIP_OPTIMIZATION(跳过编译优化阶段以避免不合理状况)
  7. Flags2:高级选项,不使用。
  8. ppcode: 返回一个只想ID3DBlob的指针,存储着编译好的着色器二进制块
  9. ppErrorMsgs:如果在编译过程中发生了错误,它会存储错误字符串

ID3DBlob只是一个普通的内存块,我们常用的有两种方法

  1. GetBufferPointer:返回void*类型指针
  2. GetBufferSize:返回缓冲区字节大小

D3DCompileFromFile

ComPtr<ID3DBlob> OpdaGraphics::CompileShader(const WCHAR* hlslFileName, const D3D_SHADER_MACRO* defines, const LPCSTR entryPoint, const LPCSTR shaderModel)
{
    //判断是否处于调试模式
    UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    //D3DCOMPILE_DEBUG用于获取着色器调试信息
    //D3DCOMPILE_SKIP_OPTIMIZATION禁止优化,避免调试期间出现问题
    compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
    //用于获取着色器的二进制块
    ComPtr<ID3DBlob> byteCode = nullptr;
    //用于获取错误信息的二进制块
    ComPtr<ID3DBlob> errorsMsg = nullptr;
    HRESULT hr = S_OK;
    hr = D3DCompileFromFile(hlslFileName, defines, D3D_COMPILE_STANDARD_FILE_INCLUDE, 
        entryPoint, shaderModel, compileFlags, 0, byteCode.GetAddressOf(),errorsMsg.GetAddressOf());

    //输出错误信息
    if (errorsMsg != nullptr)
    {
        OutputDebugStringA((char*)errorsMsg->GetBufferPointer());
    }

    WCHAR strBufferError[300];
    FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        strBufferError, 256, nullptr);
    OutputDebugStringW(strBufferError);

    //返回编译得到的着色器二进制块
    return byteCode;
}

使用例子

    //着色器的二进制块
    ComPtr<ID3DBlob> shaderByteCode;
    //运行编译着色器
    shaderByteCode = CompileShader(L"Triangle.hlsl", nullptr, "VS", "vs_5_0");

HR宏关于dxerr库的替代方案

DX11龙书上面使用的是dxerr库来作为错误原因追踪工具,但是Windows SDK 8.0以上已经没有dxerr库了,这里我用了FormatMessageW函数—获取格式化消息字符串

image-20201220212351653

    WCHAR strBufferError[300];
    FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        strBufferError, 256, nullptr);
    OutputDebugStringW(strBufferError);

参考文章:https://www.cnblogs.com/X-Jun/p/10170535.html

顶点与顶点布局(输入布局)

既然HLSL中已经确定好了我们顶点格式,那么我们C++中也要有与之对应的顶点格式数据结构,这里我新建了一个ShaderStruct.h来存储C++的顶点格式

#pragma once
#include <DirectXMath.h>
using namespace DirectX;

struct VertexPosColor
{
    XMFLOAT4 pos;
    XMFLOAT4 color;
};

定义了顶点结构体之后,我们需要描述顶点结构体的分量结构,让DirectX知道如何使用每个分量。描述信息是以输入布局(ID3D11InputLayout)的形式提供给 Direct3D 的 ,然后输入布局其实是 一 个 D3D11_INPUT_ELEMENT_DESC 数 组 ,我们将 D3D11_INPUT_ELEMENT_DESC 称为输入布局描述

typedef struct D3D11_INPUT_ELEMENT_DESC
{
    LPCSTR SemanticName;        // 语义名,将顶点结构体中的元素映射为顶点着色器参数
    UINT SemanticIndex;         // 语义索引 避免重复语义
    DXGI_FORMAT Format;         // 数据格式
    UINT InputSlot;             // 输入槽索引(0-15)
    UINT AlignedByteOffset;     // 初始位置(字节偏移量) 对于单个输入槽需要用到偏移量
    D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入类型
    UINT InstanceDataStepRate;  // 忽略
} D3D11_INPUT_ELEMENT_DESC;

我们在OpdaGraphics中创建InitEffect方法,专门用来做顶点布局和着色器相关的东西

通过语义、数据类型和起始偏移量,我们就可以建立起C++顶点缓冲区数据和HLSL顶点之间的联系。

void OpdaGraphics::InitEffect()
{
    //我们先初始化输入布局描述
    D3D11_INPUT_ELEMENT_DESC inputLayout[2] = {
        {"POSITION",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
        {"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,16,D3D11_INPUT_PER_VERTEX_DATA,0}
    };
}

完成输入布局描述之后,可以使用 ID3D11Device::CreateInputLayout 方法获取一个表示输入布局的 ID3D11InputLayout 接口的指针

HRESULT ID3D11Device::CreateInputLayout( 
    const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]输入布局描述
    UINT NumElements,                                   // [In]上述数组元素个数
    const void *pShaderBytecodeWithInputSignature,      // [In]顶点着色器字节码
    SIZE_T BytecodeLength,                              // [In]顶点着色器字节码长度
    ID3D11InputLayout **ppInputLayout);                 // [Out]获取的输入布局

在此之前我们先编译Triangle.hlsl的顶点着色器再创建输入布局

    //着色器的二进制块
    ComPtr<ID3DBlob> shaderByteCode;
    //运行编译着色器
    shaderByteCode = CompileShader(L"Triangle.hlsl", nullptr, "VS", "vs_5_0");

    //创建绑定顶点布局
    DirectxDevice->CreateInputLayout(inputLayout, ARRAYSIZE(inputLayout), shaderByteCode->GetBufferPointer(),
        shaderByteCode->GetBufferSize(), vertexLayout.GetAddressOf());

接下来创建顶点着色器和像素着色器

    //创建顶点着色器
    DirectxDevice->CreateVertexShader(shaderByteCode->GetBufferPointer(), shaderByteCode->GetBufferSize(),
        nullptr, vertexShader.GetAddressOf());

    //创建像素着色器
    shaderByteCode = CompileShader(L"Triangle.hlsl", nullptr, "PS", "ps_5_0");
    DirectxDevice->CreatePixelShader(shaderByteCode->GetBufferPointer(), shaderByteCode->GetBufferSize(),
        nullptr, pixelShader.GetAddressOf());

整个InitEffect函数如下

void OpdaGraphics::InitEffect()
{
    //初始化输入布局描述
    D3D11_INPUT_ELEMENT_DESC inputLayout[2] = {
        {"POSITION",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
        {"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,16,D3D11_INPUT_PER_VERTEX_DATA,0}
    };

    //着色器的二进制块
    ComPtr<ID3DBlob> shaderByteCode;
    //运行编译着色器
    shaderByteCode = CompileShader(L"Triangle.hlsl", nullptr, "VS", "vs_5_0");

    //创建顶点布局
    DirectxDevice->CreateInputLayout(inputLayout, ARRAYSIZE(inputLayout), shaderByteCode->GetBufferPointer(),
        shaderByteCode->GetBufferSize(), vertexLayout.GetAddressOf());

    //创建顶点着色器
    DirectxDevice->CreateVertexShader(shaderByteCode->GetBufferPointer(), shaderByteCode->GetBufferSize(),
        nullptr, vertexShader.GetAddressOf());

    //创建像素着色器
    shaderByteCode = CompileShader(L"Triangle.hlsl", nullptr, "PS", "ps_5_0");
    DirectxDevice->CreatePixelShader(shaderByteCode->GetBufferPointer(), shaderByteCode->GetBufferSize(),
        nullptr, pixelShader.GetAddressOf());

}

顶点缓冲

为了让GPU能访问到顶点数组,我们需要把顶点放到Buffer的容器内,容易用ID3D11Buffer接口表示。

首先让我们定义三角形的顶点属性

    //设置三角形顶点,顶点应按顺时针排布
    VertexPosColor vertices[] =
    {
        { XMFLOAT4(0.0f, 0.5f, 0.5f,1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
        { XMFLOAT4(0.5f, -0.5f, 0.5f,1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
        { XMFLOAT4(-0.5f, -0.5f, 0.5f,1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
    };

首先还是需要先创建顶点缓冲区的描述

typedef struct D3D11_BUFFER_DESC
{
    UINT ByteWidth;             // 数据字节数
    D3D11_USAGE Usage;          // CPU和GPU的读写权限相关
    UINT BindFlags;             // 缓冲区类型的标志
    UINT CPUAccessFlags;        // CPU读写权限的指定
    UINT MiscFlags;             // 忽略
    UINT StructureByteStride;   // 忽略
}     D3D11_BUFFER_DESC;

D3D11_USAGE对应关系

CPU读 CPU写 GPU读 GPU写
D3D11_USAGE_DEFAULT
D3D11_USAGE_IMMUTABLE
D3D11_USAGE_DYNAMIC
D3D11_USAGE_STAGING

由于目前的教程所涉及到的顶点缓冲区在创建后通常是不会修改的,因此将其设为D3D11_USAGE_IMMUTABLE

ZeroMemory的作用是将vertexBufferDesc的内存区域填充为0,可以不使用ZeroMemory,但是就要自己手动去初始化MiscFlags,CPUAccessFlags,StructureByteStride等参数,否则会内存溢出错误

    //创建顶点缓冲区描述
    D3D11_BUFFER_DESC vertexBufferDesc;
    //ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc));
    vertexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
    vertexBufferDesc.ByteWidth = sizeof(vertices);
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vertexBufferDesc.CPUAccessFlags = 0;
    vertexBufferDesc.MiscFlags = 0;
    vertexBufferDesc.StructureByteStride = 0;

还需要使用D3D11_SUBRESOURCE_DATA结构体来指定要用来初始化的数据

typedef struct D3D11_SUBRESOURCE_DATA
{
    const void *pSysMem;        // 用于初始化的数据
    UINT SysMemPitch;           // 忽略
    UINT SysMemSlicePitch;      // 忽略
}     D3D11_SUBRESOURCE_DATA;
    //指定要初始化的数据
    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = vertices;

最后就可以创建顶点缓冲区了

HRESULT ID3D11Device::CreateBuffer( 
    const D3D11_BUFFER_DESC *pDesc,     // [In]顶点缓冲区描述
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In]子资源数据
    ID3D11Buffer **ppBuffer);           // [Out] 获取缓冲区
    //顶点缓冲区
    DirectxDevice->CreateBuffer(&vertexBufferDesc, &InitData, vertexBuffer.GetAddressOf());

绑定至渲染管线

首先将顶点缓冲绑定到设备的输入槽,这样才能将顶点送入管线

void ID3D11DeviceContext::IASetVertexBuffers( 
    UINT StartSlot,     // [In]输入槽索引
    UINT NumBuffers,    // [In]缓冲区数目
    ID3D11Buffer *const *ppVertexBuffers,   // [In]指向顶点缓冲区数组的指针
    const UINT *pStrides,   // [In]一个数组,规定了对所有缓冲区每次读取的字节数分别是多少
    const UINT *pOffsets);  // [In]一个数组,规定了对所有缓冲区的初始字节偏移量
//输入装配阶段的顶点缓冲区设置
    UINT stride = sizeof(VertexPosColor);        //跨越字节数
    UINT offset = 0;        //起始偏移量

    DirectxDeviceContext->IASetVertexBuffers(0, 1, vertexBuffer.GetAddressOf(), &stride, &offset);

接下来就是设置图元类型,设置输入布局

    // 设置图元类型,设定输入布局
    DirectxDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    DirectxDeviceContext->IASetInputLayout(vertexLayout.Get());

将着色器绑定到管线

    // 将着色器绑定到渲染管线
    DirectxDeviceContext->VSSetShader(vertexShader.Get(), nullptr, 0);
    DirectxDeviceContext->PSSetShader(pixelShader.Get(), nullptr, 0);

最后利用ID3D11DeviceContex::Draw方法—-根据已经绑定的顶点缓冲区进行绘制

void ID3D11DeviceContext::Draw( 
    UINT VertexCount,           // [In]需要绘制的顶点数目
    UINT StartVertexLocation);  // [In]起始顶点索引
void OpdaGraphics::RenderScene()
{
    XMVECTORF32 color = { 0.0f,0.0f,0.0f,1.0f };
    DirectxDeviceContext->ClearRenderTargetView(renderTargetView, reinterpret_cast<float*>(&color));
    DirectxDeviceContext->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    //绘制三角形
    DirectxDeviceContext->Draw(3, 0);
    dxgiSwapChain->Present(0, 0);
}

总结

绘制三角形的步骤可以分为:

  1. 编写着色器HLSL文件,包含顶点着色器,像素着色器

  2. 运行时编译着色器文件,获得着色器的二进制块,创建顶点布局

  3. 创建顶点着色器,像素着色器

  4. 填充顶点缓冲区的描述,指定初始化数据,创建顶点缓冲

  5. 最后将所有创建的东西都绑定到渲染管线上

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2021 Opda
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信