首页 > 技术文章 > DirectX11笔记3:基本绘图,渲染,绘制一个旋转方形

Windogs 2015-11-17 16:41 原文

上一节的笔记自己写的十分糟糕,那个程序也写的十分糟糕。。。。。。。。。如果真的有人看的话,说声抱歉。

这一节主要是记录一个旋转的正方形的制作过程,先说好:以下所有内容请配合上传了的代码食用。。。。。。。。。。如果真的有人看的话。

 

首先,先大概介绍一下绘制一个图形的基本流程:

一.创建基本的D3D对象:

1.使用D3D11CreateDeviceAndSwapChain创建D3D设备对象与交换链。

2.使用CreateRenderTargetView创建后一个绘制缓冲区。

3.如果需要,创建模板与景深缓冲区。

4.使用RSSetViewports设置视窗。

 

二.编译,链接到effect:

1.组建,加载顶点着色器。

2.初始化着色器的输入布局。

3.组建,加载像素着色器。

 

三.创建绘制图形:

1.创建,定义顶点坐标,创建相应的缓冲区,用已定义的顶点初始化,再将创建好的缓冲区绑到设备输入槽上去。

2.创建顶点索引,创建相应的缓冲区,用以创建的索引初始化。

3.设置好投影空间的位置,中心坐标,z轴坐标(下面再进行详细介绍)。

4.HLSL相关。

 

关于如何创建D3D的一些基本对象就不再叙述了。直接从第二个开始:

首先我们需要一个.fx的文件,关于文件如何组成的之后再说明,我们只要知道现在这个文件给予了程序2个接口:顶点着色器与像素着色器。

关于顶点,我们的图像在程序中都是由一个个三角形组成的,参见书中的各种图形,假设这里我们需要绘制一个方形,那么我们至少需要2个三角形,4个顶点去描述这个方形。

顶点着色器通过我们输入的顶点与它相应的属性去描述各个三角形通过各种坐标变换,最后组成了我们的图像。

像素着色器,图形的颜色是由一个个像素点组成的,像素着色器告诉GPU哪些像素需要着色,需要什么颜色。

(以上是我现在的理解,可能会有许多错误)

代码之前,先对书中内容进行一些讨论:由于D3DX系列的函数在我现在的环境下无法使用,所以书中的代码是无法正常通过编译的,主要是书中使用了D3DX11CompileFromFile去编译fx文件,这对我的学习产生了极大的困扰。

关于这个问题的解决方案,看一下stackoverflow的这个回答。

http://stackoverflow.com/questions/30579016/d3d11-d3dx11createeffectfrommemory-returns-e-noiterface

以及

http://stackoverflow.com/questions/12549472/using-directx-effect11-with-visual-studio-2012

总之,虽然我下载了D3DX的开源代码,并且编译成功了,但是还是没有成功搭建出使用D3DX11CompileFromFile以及之类的函数的环境,所以选择使用其他的函数。

我的代码

HRESULT DrawFunction::CompileTheShader(void)
{
    HRESULT hr = S_OK;//ret
    ID3DBlob* pVSBlob = nullptr;
    ID3DBlob* pErrorBlob = nullptr;

    //Compiler vertex shader
    //组建加载顶点着色器
    hr = D3DCompileFromFile(L"Squance.fx", nullptr, nullptr, "VS", "vs_5_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &pVSBlob, &pErrorBlob);
    if (FAILED(hr))
    {
#ifdef _DEBUG
        printf("Can not find FX File .\nPlease run this executable from the directory that contains the FX file\n Error HRESULT:%ld\n\n", hr);
        return hr;
#else
        MessageBox( nullptr,
            L"The FX file cannot be compiled.  Please run this executable from the directory that contains the FX file.", L"Error", MB_OK );
        return hr;
#endif
    }
    //Create VERTEX shader
    hr = dev->CreateVertexShader(pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), nullptr, &pVertexShader);
    if (FAILED(hr))
    {
        pVSBlob->Release();
        return hr;
    }
    // Define the input layout
    //定义输入布局
    D3D11_INPUT_ELEMENT_DESC layout[] =
    {
        { "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 },
    };
    UINT numElements = ARRAYSIZE(layout);

    // Create the input layout
    hr = dev->CreateInputLayout(layout, numElements, pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), &pVertexLayout);
    if (FAILED(hr))
        return hr;

    // Set the input layout
    devContext->IASetInputLayout(pVertexLayout);
    // Compile the pixel shader
    //组建加载像素着色器
    ID3DBlob* pPSBlob = nullptr;
    hr = D3DCompileFromFile(L"Squance.fx", nullptr, nullptr, "PS", "ps_5_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &pPSBlob, &pErrorBlob);
    if (FAILED(hr))
    {
#ifdef _DEBUG
        printf("Can not find FX File .\nPlease run this executable from the directory that contains the FX file\n Error HRESULT:%ld\n\n", hr);
        return hr;
#else
        MessageBox(nullptr,
            L"The FX file cannot be compiled.  Please run this executable from the directory that contains the FX file.", L"Error", MB_OK);
        return hr;
#endif
    }
    // Create the pixel shader
    hr = dev->CreatePixelShader(pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), nullptr, &pPixelShader);
    if (FAILED(hr))
        return hr;
    //release resource

    pVSBlob->Release();
    pPSBlob->Release();

    return hr;
}

 

关于输入布局:

对于顶点着色器,我们需要知道输入的顶点包含了哪些结构与元素,这里我就只包含了一个顶点的坐标与颜色(顶点的颜色。。。。好奇怪。。。)

 

 

创建绘制的图像:

上面提到了三角形与图像的关系,还是用方形为例,一个方形需要4个顶点,所以就定义出来

这里我定义了2个方形,因为在绘图的时候会出现背面消影的问题,如果我需要一个旋转的,二面的方形,就需要把它的正反面都定义出来。

struct SimpleVertex    
    {
        DirectX::XMFLOAT3 Pos;
        DirectX::XMFLOAT4 Color;
    };

    SimpleVertex vertices[] =
    {    //正面
        { DirectX::XMFLOAT3(+1.0f, +1.0f, +0.0f), DirectX::XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },//
        { DirectX::XMFLOAT3(+1.0f, -1.0f, +0.0f), DirectX::XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },//
        { DirectX::XMFLOAT3(-1.0f, -1.0f, +0.0f), DirectX::XMFLOAT4(0.0f, 0.5f, 1.0f, 1.0f) },//
        { DirectX::XMFLOAT3(-1.0f, +1.0f, +0.0f), DirectX::XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },//绿
        //反面
        { DirectX::XMFLOAT3(+1.0f, +1.0f, -0.1f), DirectX::XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
        { DirectX::XMFLOAT3(+1.0f, -1.0f, -0.1f), DirectX::XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
        { DirectX::XMFLOAT3(-1.0f, -1.0f, -0.1f), DirectX::XMFLOAT4(0.0f, 0.5f, 1.0f, 1.0f) },
        { DirectX::XMFLOAT3(-1.0f, +1.0f, -0.1f), DirectX::XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }
    };

 

 

定义顶点索引:

顶点索引的原理在书中已经说明了,目的就是为了不对顶点进行复刻,一个顶点只需要定义一次,减少内存与处理器的负担。

    WORD indices[] =
    {
        0, 2, 1,
        0, 3, 2,

        4, 5, 6,
        4, 6, 7
    };

 

这是一个以上面的8个顶点为例的索引,可以看见创建了4个三角形。

然后需要注意一点:背面消隐,具体看书中5.10.2的部分,简而言之,一个三角形只有一面是“有颜色”去显示的,而三角形的正反面是由你定义三角形时的顶点顺序有关的

 

 

投影的视角,坐标:

与其叫投影,我更喜欢将他比喻成相机或眼睛。

想象一下,我们绘制好的图形需要一个摄像机去把它放到我们的屏幕上,需要以下几个参数:

1.摄像机的位置,相对于世界空间,摄像机的摆放在哪个坐标。

2.摄像机的朝向,也就是镜头的方向,镜头向哪里拍摄。

3.垂直方向,在上面2项决定好的情况下,还需要一个固定镜头垂直的方向,来防止镜头随意的旋转,来告诉镜头Z轴。

DirectX::XMVECTOR Eye = DirectX::XMVectorSet(0.0f, 0.0f, 4.0f, 0.0f);;    //眼睛坐标
    DirectX::XMVECTOR Point = DirectX::XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); //看向的位置
    DirectX::XMVECTOR Eye_Up = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);//定义眼睛"向上"的方向
    View_View = DirectX::XMMatrixLookAtLH(Eye, Point, Eye_Up);

    View_Projection = DirectX::XMMatrixPerspectiveFovLH(DirectX::XM_PIDIV2, 800 / (FLOAT)600, 0.01f, 100.0f);

 

上面的代码中,我将镜头设置在(0,0,4)的位置,看向原点(0,0,0),镜头的垂直方向是(0,1,0)就是Y轴。

 

 

HLSL语言:

使用时,需要设置FX文件的属性为不参与生成,否则会报一个没有main入口的错误。

着色器的编写需要HLSL这种语言,好在他与C++极其相似。听说以前的着色器语言是用汇编写的。。。这真是太糟糕了。

对于语法什么的也没啥好讲的,主要是1个功能:

常量缓冲

C++代码中有:

//结构定义
    struct ConstantBuffer        //用于描述视角坐标
    {
        DirectX::XMMATRIX mWorld;
        DirectX::XMMATRIX mView;
        DirectX::XMMATRIX mProjection;
    };

//创建
    bd.Usage = D3D11_USAGE_DEFAULT;
    bd.ByteWidth = sizeof(ConstantBuffer);
    bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    bd.CPUAccessFlags = 0;
    hr = dev->CreateBuffer(&bd, nullptr, &pConstantBuffer);
    if (FAILED(hr))
    {
#ifdef _DEBUG
        printf("Can not create the constant buffer\n Error HRESULT:%ld\n\n", hr);
#endif
        return hr;
    }

 

fx文件中有:

cbuffer ConstantBuffer : register(b0)
{
    matrix World;
    matrix View;
    matrix Projection;
}

VS_OUTPUT VS(float4 Pos : POSITION, float4 Color : COLOR)
{
    VS_OUTPUT output = (VS_OUTPUT)0;
    output.Pos = mul(Pos, World);
    output.Pos = mul(output.Pos, View);
    output.Pos = mul(output.Pos, Projection);
    output.Color = Color;
    return output;
}

 

可以看作着色器与程序共享了一块内存,运行时,着色器不能改变常量缓冲,我们通过C++代码改变常量缓冲的内容,加以该变着色器的显示。

 

最后,也是最重要的,数学变换原理:

1.局部坐标,世界坐标相互转换:

上面的的内容说了,我们在世界坐标的某点有一个摄像机,然而,对于程序而言,绘图就是绘制一个个像素点,坐标分析的步骤需要我们去完成:

在世界坐标内有一个坐标点,那么在我们一相机为原点的坐标系内它的坐标是什么呢?

部分代码:

    View_World = DirectX::XMMatrixIdentity();//XMMatrixIdentity构建单位矩阵

    // Initialize the view matrix
    DirectX::XMVECTOR Eye = DirectX::XMVectorSet(0.0f, 0.0f, 4.0f, 0.0f);;    //眼睛坐标
    DirectX::XMVECTOR Point = DirectX::XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); //看向的位置
    DirectX::XMVECTOR Eye_Up = DirectX::XMVectorSet(0.0f, 1.0f, 100.0f, 0.0f);//定义眼睛"向上"的方向
    View_View = DirectX::XMMatrixLookAtLH(Eye, Point, Eye_Up);        //XMMatrixLookAtLH函数返回的是世界->视图变换矩阵

 

代码中定义了一个单位矩阵作为原世界坐标,在(0,0,4)这点定义了相机,然后看一个API:XMMatrixLookAtLH,它的作用是产生一个变换矩阵,用于将世界坐标转换为局部坐标,也就是一相机为原点的坐标。描述一下它的实现原理:

将固定好的相机方向单位化,计算出从局部坐标转换成世界坐标的矩阵,然后求这个矩阵的逆矩阵,就是从世界坐标转换为局部坐标的转换矩阵。

我们可以直接在书中3.4.5节找到直接求逆的公式,完整的用代码表示就是:

XMMATRIX XM_CALLCONV XMMatrixLookAtLH
(
    FXMVECTOR Eye
    FXMVECTOR At
    FXMVECTOR Up
)

zaxis = normal(At - Eye)
xaxis = normal(cross(Up, zaxis))
yaxis = cross(zaxis, xaxis)
   
对应矩阵:
xaxis.x yaxis.x zaxis.x
0 xaxis.y yaxis.y zaxis.y 0 xaxis.z yaxis.z zaxis.z 0 -dot(xaxis, eye) -dot(yaxis, eye) -dot(zaxis, eye) 1

 

这里normal指求单位向量,cross指向量叉乘,dot指向量点乘

 

2.投影空间

现在,我们有了一个自己的相机坐标,与相对于相机坐标的通过世界——》局部矩阵转换后的点。

那么,我们需要一个投影方式,将他push到我们的屏幕上。

投影空间可以看作一个4棱锥的投射。在书中,我们需要以下4个变量表示这个锥形:

1.垂直视角域

2.纵横比

3.近剪裁面

4.远剪裁面

示例:我们现在以自己的眼睛为例,眼睛看向的方向为z轴,头的向上的方向为y轴,肩膀的方向为x轴。

很明显,眼睛向上的视角是有限的,你当然不可能看到额头上有什么,这个视角,就是垂直视角域。那么,水平视角域怎么处理呢?它使用了一个纵横比,就是你水平可以看见的长度的极限除以你垂直可以看见长度 的极限。至于远,近裁剪面,定义了你的视野最远与最近可以看见的东西。

代码:

View_Projection = DirectX::XMMatrixPerspectiveFovLH(DirectX::XM_PIDIV2, 800 / (FLOAT)600, 0.01f, 100.0f);

 

这里,我定义了垂直视角为90度,这里指的是总视角0到180度的,横纵比为800/600,最近显示0.01,最远显示100。

好,看一下我定义的边长为2的正方形:

        { DirectX::XMFLOAT3(+1.0f, +1.0f, +0.0f), DirectX::XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },//
        { DirectX::XMFLOAT3(+1.0f, -1.0f, +0.0f), DirectX::XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },//
        { DirectX::XMFLOAT3(-1.0f, -1.0f, +0.0f), DirectX::XMFLOAT4(0.0f, 0.5f, 1.0f, 1.0f) },//
        { DirectX::XMFLOAT3(-1.0f, +1.0f, +0.0f), DirectX::XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },//绿

 

以及摄像机的位置:

    DirectX::XMVECTOR Eye = DirectX::XMVectorSet(0.0f, 0.0f, 4.0f, 0.0f);;    //眼睛坐标
    DirectX::XMVECTOR Point = DirectX::XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f); //看向的位置
    DirectX::XMVECTOR Eye_Up = DirectX::XMVectorSet(0.0f, 1.0f, 100.0f, 0.0f);//定义眼睛"向上"的方向

 

摄像机在(0,0,4)看向(0,0,0)

那么可以计算出这个正方形的高度占显示总高度的大小,为4分之1,就是说,我们的如果程序窗口的高度为600像素(假设D3D占满整个屏幕),那么显示的时候这个正方形边长为150像素。

 

 

 

 

 

 

 

其它:

我还是没搞懂HLSL中mul的详细作用,以及投影详细的数学推导,什么时候看懂了再写。

***工程代码***

 

推荐阅读