从零开始手敲次世代游戏引擎(十五)

上一篇我们在Windows上用OpenGL 4.0版本的接口实现了一个旋转的立方体(当然,实现这个功能实际上并没有使用什么4.0级别的特性。图形API规格和功能级别是两个概念)

到此为止我们应该对于一个基本的画面绘制流程有一定的了解了。虽然我们还没有涉及到诸如贴图光照,以及曲面细分,异步计算等概念,但是作为整个渲染管道的骨架已经基本搭建好了。

接下来让我们一起看看图形API的最前沿,DirectX 12和Vulkan。因为我们要写的是次世代引擎,我们需要考虑使用这些最新的图形API。

我们之所以没有从最新的API开始,因为我们有个学习的过程。图形API不是一步发展到今天这个样子的,我们需要了解这个过程。这不仅能缓和我们的学习曲线,更能让我们看清发展的走向。而能否正确预测这个走向,是评价一个架构好坏的重要基准之一。

DX12我参考的主要资料是【*1】Tutorial: Migrating Your Apps to DirectX* 12 – Part 1

— (题外话开始)–

很意外地,看到了Lv Wenwei的名字。他是蜗牛的技术总监,我2014年刚刚进入SIE的第一个任务,就是到苏州去支持他们移植开发《九阳神功》。虽然那个时候我其实刚进公司,对PS4开发基本一无所知。

吕老师谈吐很儒雅,技术看上去也相当不错。3年前的故事最终是以我把SIE日本的台湾人老师(也是我在SIE的导师)请来救场结束。

— (题外话结束)–

我们开始编码。我是一边看着【*2】Creating a basic Direct3D 12 component 一边升级我的代码的。

由于我们的代码已经比较长了。从本篇起我将不再贴出所有的代码。完整的代码请到GitHub上面去下载。本篇对应的branch为article_15。

首先我们是替换头文件。

// include the basic windows header file
 #include <windows.h>
 #include <windowsx.h>
+#include <stdio.h>
 #include <tchar.h>
 #include <stdint.h>

-#include <d3d11.h>
-#include <d3d11_1.h>
+#include <d3d12.h>
+#include "d3dx12.h"
+#include <DXGI1_4.h>
 #include <d3dcompiler.h>
 #include <DirectXMath.h>
 #include <DirectXPackedVector.h>
 #include <DirectXColors.h>

+#include <wrl/client.h>
+
+#include <string>
+#include <exception>
+

去掉了DX11的头文件,加入DX12的头文件。

从DirectX 10开始,微软导入了DXGI的概念。【*3】

DXGI与DX的关系有点类似OpenGL的Loader与OpenGL的关系;前者是创建绘图的上下文,后者是进行实际的绘图。

d3dx12.h 是一个工具头文件。它并不是DX12的一部分。微软是通过GitHub来提供这个文件的。这个文件主要是为了方便我们使用DX12,让代码看起来简洁一些。我在article_15的branch里面也提供了这个文件的拷贝。

#include语句当中文件名两边是<>还是””的秘密是:如果是系统或者sdk的头文件,就是<>;如果是放在我们项目当中的文件,就是””。这虽然是一些细节,但是不正确使用有的时候是会出现一些奇奇怪怪的问题。

下面的wrl/client.h是WRL的一部分。WRL是Windows Runtime Library的简称。这也是一个辅助性质的库,提供了一些符合design pattern的模板。我们这里主要是使用一个名为ComPtr的模板,用来比较智能地管理COM的Interface。

没错,又是COM。其实前面介绍的OpenGL那种运行时查询并绑定API入口的方式就和COM颇为类似。COM的中心思想就是每个模块都有一个众所周知的接口:IUnknown。这个接口支持一个Qurey方法,来查找其它的接口。这样就可以实现运行时的入口查找和调用。

接下来是一个C++异常陷阱。COM规范规定,所有的COM调用的返回值都是一个HRESULT类型的值,用来报告调用是否成功。我们前面的代码是在每次调用后检查这个返回值,如果失败进行相关log输出之后返回或者中断执行。这种写法的好处是代码可移植性高,缺点是代码里面插入了很多和原本要做的事情无关的代码,简洁性变差。我们这里参考微软官方的例子采用抛c++异常的方式处理这个返回值检查。但是需要注意的是c++异常的可移植性是不太好的。不过这里的代码本来就是平台专用代码,再加上是我们的支线任务,主要是用来打怪升级并探地图的,所以我们就这么用。

然后是全局变量的定义。我们现在因为是在探路,采用最为直观的“平的”代码方式,就是基本上是C的写法,不进行类的封装。等我们确定图形模块的划分之后,这些变量大部分都是要放到类里面去的。而另外一些则作为启动参数允许配置:如分辨率,色深等。

// global declarations
+const uint32_t nFrameCount     = 2;
+const bool     bUseWarpDevice = true;
+D3D12_VIEWPORT                  g_ViewPort = {0.0f, 0.0f,
+                                        static_cast(nScreenWidth),
+                                        static_cast(nScreenHeight)};   // viewport structure
+D3D12_RECT                      g_ScissorRect = {0, 0,
+                
-IDXGISwapChain          *g_pSwapchain = nullptr;              // the pointer to the swap chain interface
+ComPtr<IDXGISwapChain3>         g_pSwapChain = nullptr;             // the pointer to the swap chain interface
-ID3D11Device            *g_pDev       = nullptr;   
          // the pointer to our Direct3D device interface
+ComPtr<ID3D12Device>            g_pDev       = nullptr;             // the pointer to our Direct3D device interface
-ID3D11DeviceContext     *g_pDevcon    = nullptr;              // the pointer to our Direct3D device context
-
-ID3D11RenderTargetView  *g_pRTView    = nullptr;
+ComPtr<ID3D12Resource>          g_pRenderTargets[nFrameCount];      // the pointer to rendering buffer. [descriptor]
+uint32_t    g_nRtvDescriptorSize;
-ID3D11InputLayout       *g_pLayout    = nullptr;              // the pointer to the input layout
-ID3D11VertexShader      *g_pVS        = nullptr;              // the pointer to the vertex shader
-ID3D11PixelShader       *g_pPS        = nullptr;              // the pointer to the pixel shader

+ComPtr<ID3D12CommandAllocator>  g_pCommandAllocator;                // the pointer to command buffer allocator
+ComPtr<ID3D12CommandQueue>      g_pCommandQueue;                    // the pointer to command queue
+ComPtr<ID3D12RootSignature>     g_pRootSignature;                   // a graphics root signature defines what resources are bound to the pipeline
+ComPtr<ID3D12DescriptorHeap>    g_pRtvHeap;                         // an array of descriptors of GPU objects
+ComPtr<ID3D12PipelineState>     g_pPipelineState;                   // an object maintains the state of all currently set shaders
+                                                                    // and certain fixed function state objects
+                                                                    // such as the input assembler, tesselator, rasterizer and output manager
+ComPtr<ID3D12GraphicsCommandList>   g_pCommandList;                 // a list to store GPU commands, which will be submitted to GPU to execute when done
+
+
-ID3D11Buffer            *g_pVBuffer   = nullptr;              // Vertex Buffer
+ComPtr<ID3D12Resource>          g_pVertexBuffer;                         // the pointer to the vertex buffer
+D3D12_VERTEX_BUFFER_VIEW        g_VertexBufferView;                 // a view of the vertex buffer

可以看到最大的变化是DX12将GPU的命令队列暴露了出来,并且不再自动进行CPU与GPU之间的同步。

之前的DX版本对我们来说只有CPU一条时间线,所有API调用看起来是同步的。然而在DX12当中,现在多了一条GPU的时间线,大部分绘图API也从同步变成了“录制”,即,仅仅是在GPU命令队列当中生成一些指令。这个包含了指令的队列,什么时候提交给GPU进行处理,需要我们自己进行控制。所以多了好几个用于同步CPU和GPU的变量:

+// Synchronization objects
+uint32_t            g_nFrameIndex;
+HANDLE              g_hFenceEvent;
+ComPtr<ID3D12Fence> g_pFence;
+uint32_t            g_nFenceValue;

另外一个变化就是在创建和管理方面,不再细分GPU在执行绘图指令的时候会参照的各种资源,比如顶点缓冲区,RenderingTarget等。这些统统交由一个成为Resource的接口去处理。这是因为我们已经很接近显卡驱动了。在那么低的层面,这些东西统统是buffer,没啥太大区别。

在Shader的加载方面,变化不大。但是作为演示,本次我们采用运行时编译的方式。

Layout的概念取消掉了,取而代之的是Pilpeline State Object,用以将GPU的各个功能模块串联起来形成一个渲染流水线。

因为绘图API变成了异步录制执行方式,我们需要确保这些资源在gpu实际完成绘图之前可用。在这个例子当中,我们强制GPU首先完成这些指令的执行,并通过Fence实现和GPU的同步。

// this is the function that loads and prepares the shaders
 void InitPipeline() {
-    // load and compile the two shaders
-    ID3DBlob *VS, *PS;
-
-    D3DReadFileToBlob(L"copy.vso", &VS);
-    D3DReadFileToBlob(L"copy.pso", &PS);
-
-    // encapsulate both shaders into shader objects
-    g_pDev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &g_pVS);
-    g_pDev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &g_pPS);
+    ThrowIfFailed(g_pDev->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&g_pCommandAllocator)));
+
+    // create an empty root signature
+    CD3DX12_ROOT_SIGNATURE_DESC rsd;
+    rsd.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
+
+    ComPtr<ID3DBlob> signature;
+    ComPtr<ID3DBlob> error;
+    ThrowIfFailed(D3D12SerializeRootSignature(&rsd, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
+    ThrowIfFailed(g_pDev->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&g_pRootSignature)));
+
+    // load the shaders
+#if defined(_DEBUG)
+    // Enable better shader debugging with the graphics debugging tools.
+    UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
+#else
+    UINT compileFlags = 0;
+#endif
+    ComPtr<ID3DBlob> vertexShader;
+    ComPtr<ID3DBlob> pixelShader;
+
+    D3DCompileFromFile(
+        GetAssetFullPath(L"copy.vs").c_str(),
+        nullptr,
+        D3D_COMPILE_STANDARD_FILE_INCLUDE,
+        "main",
+        "vs_5_0",
+        compileFlags,
+        0,
+        &vertexShader,
+        &error);
+    if (error) { OutputDebugString((LPCTSTR)error->GetBufferPointer()); error->Release(); throw std::exception(); }
+
+    D3DCompileFromFile(
+        GetAssetFullPath(L"copy.ps").c_str(),
+        nullptr,
+        D3D_COMPILE_STANDARD_FILE_INCLUDE,
+        "main",
+        "ps_5_0",
+        compileFlags,
+        0,
+        &pixelShader,
+        &error);
+    if (error) { OutputDebugString((LPCTSTR)error->GetBufferPointer()); error->Release(); throw std::exception(); }

-    // set the shader objects
-    g_pDevcon->VSSetShader(g_pVS, 0, 0);
-    g_pDevcon->PSSetShader(g_pPS, 0, 0);

     // create the input layout object
-    D3D11_INPUT_ELEMENT_DESC ied[] =
+    D3D12_INPUT_ELEMENT_DESC ied[] =
     {
-        {"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},
+        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
+        {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
     };

-    g_pDev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &g_pLayout);
-    g_pDevcon->IASetInputLayout(g_pLayout);
-
-    VS->Release();
-    PS->Release();
+    // describe and create the graphics pipeline state object (PSO)
+    D3D12_GRAPHICS_PIPELINE_STATE_DESC psod = {};
+    psod.InputLayout    = { ied, _countof(ied) };
+    psod.pRootSignature = g_pRootSignature.Get();
+    psod.VS             = { reinterpret_cast<UINT8*>(vertexShader->GetBufferPointer()), vertexShader->GetBufferSize() };
+    psod.PS             = { reinterpret_cast<UINT8*>(pixelShader->GetBufferPointer()), pixelShader->GetBufferSize() };
+    psod.RasterizerState= CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
+    psod.BlendState     = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
+    psod.DepthStencilState.DepthEnable  = FALSE;
+    psod.DepthStencilState.StencilEnable= FALSE;
+    psod.SampleMask     = UINT_MAX;
+    psod.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
+    psod.NumRenderTargets = 1;
+    psod.RTVFormats[0]  = DXGI_FORMAT_R8G8B8A8_UNORM;
+    psod.SampleDesc.Count = 1;
+    ThrowIfFailed(g_pDev->CreateGraphicsPipelineState(&psod, IID_PPV_ARGS(&g_pPipelineState)));
+
+    ThrowIfFailed(g_pDev->CreateCommandList(0,
+                D3D12_COMMAND_LIST_TYPE_DIRECT,
+                g_pCommandAllocator.Get(),
+                g_pPipelineState.Get(),
+                IID_PPV_ARGS(&g_pCommandList)));
+
+    ThrowIfFailed(g_pCommandList->Close());
 }

 // this is the function that creates the shape to render
@@ -116,31 +257,127 @@ void InitGraphics() {
         {XMFLOAT3(-0.45f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f)}
     };


-    // create the vertex buffer
-    D3D11_BUFFER_DESC bd;
-    ZeroMemory(&bd, sizeof(bd));

-    bd.Usage = D3D11_USAGE_DYNAMIC;                // write access access by CPU and GPU
-    bd.ByteWidth = sizeof(VERTEX) * 3;             // size is the VERTEX struct * 3
-    bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;       // use as a vertex buffer
-    bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;    // allow CPU to write in buffer
-    g_pDev->CreateBuffer(&bd, NULL, &g_pVBuffer);       // create the buffer

+    const UINT vertexBufferSize = sizeof(OurVertices);
+
+    // Note: using upload heaps to transfer static data like vert buffers is not
+    // recommended. Every time the GPU needs it, the upload heap will be marshalled
+    // over. Please read up on Default Heap usage. An upload heap is used here for
+    // code simplicity and because there are very few verts to actually transfer.
+    ThrowIfFailed(g_pDev->CreateCommittedResource(
+        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
+        D3D12_HEAP_FLAG_NONE,
+        &CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
+        D3D12_RESOURCE_STATE_GENERIC_READ,
+        nullptr,
+        IID_PPV_ARGS(&g_pVertexBuffer)));
+
-    // copy the vertices into the buffer
-    D3D11_MAPPED_SUBRESOURCE ms;
-    g_pDevcon->Map(g_pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);    // map the buffer
-    memcpy(ms.pData, OurVertices, sizeof(VERTEX) * 3);                       // copy the data
-    g_pDevcon->Unmap(g_pVBuffer, NULL);                                      // unmap the buffer

+    // copy the vertices into the buffer
+    uint8_t *pVertexDataBegin;
+    CD3DX12_RANGE readRange(0, 0);                  // we do not intend to read this buffer on CPU
+    ThrowIfFailed(g_pVertexBuffer->Map(0, &readRange,
+                reinterpret_cast<void**>(&pVertexDataBegin)));               // map the buffer
+    memcpy(pVertexDataBegin, OurVertices, vertexBufferSize);                 // copy the data
+    g_pVertexBuffer->Unmap(0, nullptr);                                      // unmap the buffer
+
+    // initialize the vertex buffer view
+    g_VertexBufferView.BufferLocation = g_pVertexBuffer->GetGPUVirtualAddress();
+    g_VertexBufferView.StrideInBytes  = sizeof(VERTEX);
+    g_VertexBufferView.SizeInBytes    = vertexBufferSize;
+
+    // create synchronization objects and wait until assets have been uploaded to the GPU
+    ThrowIfFailed(g_pDev->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&g_pFence)));
+    g_nFenceValue = 1;
+
+    // create an event handle to use for frame synchronization
+    g_hFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
+    if (g_hFenceEvent == nullptr)
+    {
+        ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
+    }


+    // wait for the command list to execute; we are reusing the same command
+    // list in our main loop but for now, we just want to wait for setup to
+    // complete before continuing.
+    WaitForPreviousFrame();
+}

所谓Fence,就是内存上一个变量。这个变量GPU和CPU都可以读写,而且通过cache控制的方法避免GPU和CPU之间出现对于这个值的内容不同步的情况。我们知道,在当代计算机系统结构当中,无论是CPU还是GPU都是有很复杂的cache结构,这种结构往往会导致CPU/GPU看到的变量的值与实际内存上保存的值不一致。

https://en.m.wikipedia.org/wiki/Cache_(computing)

而这个Fence,就是一个保证不会出现这种情况的变量。GPU在完成图形渲染任务之后,会更新这个Fence的值。而CPU在检测到这个值被更新之后,就知道GPU已经完成渲染,可以释放/重用相关资源了。

下面是构建整个swapchain。

// this function prepare graphic resources for use
-HRESULT CreateGraphicsResources(HWND hWnd)
+void CreateGraphicsResources(HWND hWnd)
+{
+    if (g_pSwapChain.Get() == nullptr)
+    {
+#if defined(_DEBUG)
+        // Enable the D3D12 debug layer.
+        {
+            ComPtr<ID3D12Debug> debugController;
+            if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
             {
-    HRESULT hr = S_OK;
-    if (g_pSwapchain == nullptr)
+                debugController->EnableDebugLayer();
+            }
+        }
+#endif
+
+        ComPtr<IDXGIFactory4> factory;
+        ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&factory)));
+
+        if (bUseWarpDevice)
+        {
+            ComPtr<IDXGIAdapter> warpAdapter;
+            ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
+
+            ThrowIfFailed(D3D12CreateDevice(
+                warpAdapter.Get(),
+                D3D_FEATURE_LEVEL_11_0,
+                IID_PPV_ARGS(&g_pDev)
+                ));
+        }
+        else
         {
+            ComPtr<IDXGIAdapter1> hardwareAdapter;
+            GetHardwareAdapter(factory.Get(), &hardwareAdapter);
+
+            ThrowIfFailed(D3D12CreateDevice(
+                hardwareAdapter.Get(),
+                D3D_FEATURE_LEVEL_11_0,
+                IID_PPV_ARGS(&g_pDev)
+                ));
+        }
+
+        // Describe and create the command queue.
+        D3D12_COMMAND_QUEUE_DESC queueDesc = {};
+        queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
+        queueDesc.Type  = D3D12_COMMAND_LIST_TYPE_DIRECT;
+
+        ThrowIfFailed(g_pDev->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&g_pCommandQueue)));
+
         // create a struct to hold information about the swap chain
         DXGI_SWAP_CHAIN_DESC scd;

@@ -148,103 +385,109 @@ HRESULT CreateGraphicsResources(HWND hWnd)
         ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));

         // fill the swap chain description struct
-        scd.BufferCount = 1;                                    // one back buffer
-        scd.BufferDesc.Width = SCREEN_WIDTH;
-        scd.BufferDesc.Height = SCREEN_HEIGHT;
+        scd.BufferCount = nFrameCount;                           // back buffer count
+        scd.BufferDesc.Width = nScreenWidth;
+        scd.BufferDesc.Height = nScreenHeight;
         scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;     // use 32-bit color
         scd.BufferDesc.RefreshRate.Numerator = 60;
         scd.BufferDesc.RefreshRate.Denominator = 1;
         scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;      // how swap chain is to be used
+        scd.SwapEffect  = DXGI_SWAP_EFFECT_FLIP_DISCARD;        // DXGI_SWAP_EFFECT_FLIP_DISCARD only supported after Win10
+                                                                // use DXGI_SWAP_EFFECT_DISCARD on platforms early than Win10
         scd.OutputWindow = hWnd;                                // the window to be used
-        scd.SampleDesc.Count = 4;                               // how many multisamples
+        scd.SampleDesc.Count = 1;                               // multi-samples can not be used when in SwapEffect sets to
+                                                                // DXGI_SWAP_EFFECT_FLOP_DISCARD
         scd.Windowed = TRUE;                                    // windowed/full-screen mode
-        scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;     // allow full-screen switching
+        scd.Flags    = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;  // allow full-screen transition

-        const D3D_FEATURE_LEVEL FeatureLevels[] = { D3D_FEATURE_LEVEL_11_1,
-                                                    D3D_FEATURE_LEVEL_11_0,
-                                                    D3D_FEATURE_LEVEL_10_1,
-                                                    D3D_FEATURE_LEVEL_10_0,
-                                                    D3D_FEATURE_LEVEL_9_3,
-                                                    D3D_FEATURE_LEVEL_9_2,
-                                                    D3D_FEATURE_LEVEL_9_1};
-        D3D_FEATURE_LEVEL FeatureLevelSupported;
-
-        HRESULT hr = S_OK;
-
-        // create a device, device context and swap chain using the information in the scd struct
-        hr = D3D11CreateDeviceAndSwapChain(NULL,
-                                      D3D_DRIVER_TYPE_HARDWARE,
-                                      NULL,
-                                      0,
-                                      FeatureLevels,
-                                      _countof(FeatureLevels),
-                                      D3D11_SDK_VERSION,
-                                      &scd,
-                                      &g_pSwapchain,
-                                      &g_pDev,
-                                      &FeatureLevelSupported,
-                                      &g_pDevcon);
-
-        if (hr == E_INVALIDARG) {
-            hr = D3D11CreateDeviceAndSwapChain(NULL,
-                                      D3D_DRIVER_TYPE_HARDWARE,
-                                      NULL,
-                                      0,
-                                      &FeatureLevelSupported,
-                                      1,
-                                      D3D11_SDK_VERSION,
+        ComPtr<IDXGISwapChain> swapChain;
+        ThrowIfFailed(factory->CreateSwapChain(
+                    g_pCommandQueue.Get(),                      // Swap chain needs the queue so that it can force a flush on it
                     &scd,
-                                      &g_pSwapchain,
-                                      &g_pDev,
-                                      NULL,
-                                      &g_pDevcon);
-        }
+                    &swapChain
+                    ));
+
+        ThrowIfFailed(swapChain.As(&g_pSwapChain));

-        if (hr == S_OK) {
+        g_nFrameIndex = g_pSwapChain->GetCurrentBackBufferIndex();
         CreateRenderTarget();
-            SetViewPort();
         InitPipeline();
         InitGraphics();
     }
 }
-    return hr;
-}

因为采用了ComPtr智能指针,不需要手动release了。(会在相关变量被重用或者程序结束的时候自动release)

void DiscardGraphicsResources()
 {
-    SafeRelease(&g_pLayout);
-    SafeRelease(&g_pVS);
-    SafeRelease(&g_pPS);
-    SafeRelease(&g_pVBuffer);
-    SafeRelease(&g_pSwapchain);
-    SafeRelease(&g_pRTView);
-    SafeRelease(&g_pDev);
-    SafeRelease(&g_pDevcon);
+    WaitForPreviousFrame();
+
+    CloseHandle(g_hFenceEvent);
 }

下面是录制绘图指令到command list当中。

+void PopulateCommandList()
 {
+    // command list allocators can only be reset when the associated
+    // command lists have finished execution on the GPU; apps should use
+    // fences to determine GPU execution progress.
+    ThrowIfFailed(g_pCommandAllocator->Reset());
+
+    // however, when ExecuteCommandList() is called on a particular command
+    // list, that command list can then be reset at any time and must be before
+    // re-recording.
+    ThrowIfFailed(g_pCommandList->Reset(g_pCommandAllocator.Get(), g_pPipelineState.Get()));
+
+    // Set necessary state.
+    g_pCommandList->SetGraphicsRootSignature(g_pRootSignature.Get());
+    g_pCommandList->RSSetViewports(1, &g_ViewPort);
+    g_pCommandList->RSSetScissorRects(1, &g_ScissorRect);
+
+    // Indicate that the back buffer will be used as a render target.
+    g_pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
+                g_pRenderTargets[g_nFrameIndex].Get(),
+                D3D12_RESOURCE_STATE_PRESENT,
+                D3D12_RESOURCE_STATE_RENDER_TARGET));
+
+    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(g_pRtvHeap->GetCPUDescriptorHandleForHeapStart(), g_nFrameIndex, g_nRtvDescriptorSize);
+    g_pCommandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
+
     // clear the back buffer to a deep blue
     const FLOAT clearColor[] = {0.0f, 0.2f, 0.4f, 1.0f};
-    g_pDevcon->ClearRenderTargetView(g_pRTView, clearColor);
+    g_pCommandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

     // do 3D rendering on the back buffer here
     {
         // select which vertex buffer to display
-        UINT stride = sizeof(VERTEX);
-        UINT offset = 0;
-        g_pDevcon->IASetVertexBuffers(0, 1, &g_pVBuffer, &stride, &offset);
+        g_pCommandList->IASetVertexBuffers(0, 1, &g_VertexBufferView);

         // select which primtive type we are using
-        g_pDevcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+        g_pCommandList->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

         // draw the vertex buffer to the back buffer
-        g_pDevcon->Draw(3, 0);
+        g_pCommandList->DrawInstanced(3, 1, 0, 0);
+    }
+
+    // Indicate that the back buffer will now be used to present.
+    g_pCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
+                g_pRenderTargets[g_nFrameIndex].Get(),
+                D3D12_RESOURCE_STATE_RENDER_TARGET,
+                D3D12_RESOURCE_STATE_PRESENT));
+
+    ThrowIfFailed(g_pCommandList->Close());
 }

提交上面录制的绘图指令(command list),并执行frame buffer交换,输出画面。

// this is the function used to render a single frame
void RenderFrame()
{
+    // record all the commands we need to render the scene into the command list
+    PopulateCommandList();
+
+    // execute the command list
+    ID3D12CommandList *ppCommandLists[] = { g_pCommandList.Get() };
+    g_pCommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
+
     // swap the back buffer and the front buffer
-    g_pSwapchain->Present(0, 0);
+    ThrowIfFailed(g_pSwapChain->Present(1, 0));
+
+    WaitForPreviousFrame();
 }

绘图指令由Draw升级为了DrawInstanced。后者支持一次drawcall绘制一组相同的物体。这个功能在绘制诸如树木、花草、篱笆、地面、吃瓜群众等群体环境物体时十分有效,可以减少很多CPU-GPU之间的同步成本。在VR当中,同一个物体会出现在两个眼睛的视野当中,也是采用这种方式减少drawcall的。

编译命令行如下(Visual Studio工具链):

D:wenliSourceReposGameEngineFromScratchPlatformWindows>cl /EHsc helloengine_d3d12.cpp user32.lib d3d12.lib dxgi.lib d3dcompiler.lib

这篇代码因为用了很多微软的东西,暂时无法用clang(包括clang-cl)编译。不过反正本来就是windows平台独有的DX12,这没关系。

好了。这样我们就完成了DX11到DX12的升级。下一篇将会加入贴图。

(– EOF –)

参考资料:

  1. Tutorial: Migrating Your Apps to DirectX* 12 – Part 1
  2. Creating a basic Direct3D 12 component
  3. DXGI
  4. DirectXMath Programming Guide

本作品采用知识共享署名 4.0 国际许可协议进行许可。

从零开始手敲次世代游戏引擎(十四)

接上一篇,对代码进行一个简单的说明。

首先我们依然是加入OpenGL的头文件。这里需要特别注意的是Clang缺省是大小写敏感的,所以必须如下书写:

#include <GL/gl.h>

前面提到过,OpenGL是在显卡的驱动程序里面实现的。而显卡的驱动程序因厂商不同而不同。而且,同一个厂商也会有许多不同版本的驱动程序。因此,我们的程序不应该也不可能直接与驱动程序进行链接。因此,OpenGL的API是一种2进制的API,需要在运行的时候动态地去查找这些API所在的位置(也就是这些API在内存上的地址)

我们这次的程序看起来很长,但实际上一大半都是在进行这个工作而已。如果使用Glew等OpenGL Loader(也就是加载工具),那么这些代码都可以省去。但是,在使用方便的工具之前,了解它们实际上都在做些什么,是很有意思的。

/////////////
// DEFINES //
/////////////
#define WGL_DRAW_TO_WINDOW_ARB         0x2001
#define WGL_ACCELERATION_ARB           0x2003
#define WGL_SWAP_METHOD_ARB            0x2007
#define WGL_SUPPORT_OPENGL_ARB         0x2010
#define WGL_DOUBLE_BUFFER_ARB          0x2011
#define WGL_PIXEL_TYPE_ARB             0x2013
#define WGL_COLOR_BITS_ARB             0x2014
#define WGL_DEPTH_BITS_ARB             0x2022
#define WGL_STENCIL_BITS_ARB           0x2023
#define WGL_FULL_ACCELERATION_ARB      0x2027
#define WGL_SWAP_EXCHANGE_ARB          0x2028
#define WGL_TYPE_RGBA_ARB              0x202B
#define WGL_CONTEXT_MAJOR_VERSION_ARB  0x2091
#define WGL_CONTEXT_MINOR_VERSION_ARB  0x2092
#define GL_ARRAY_BUFFER                   0x8892
#define GL_STATIC_DRAW                    0x88E4
#define GL_FRAGMENT_SHADER                0x8B30
#define GL_VERTEX_SHADER                  0x8B31
#define GL_COMPILE_STATUS                 0x8B81
#define GL_LINK_STATUS                    0x8B82
#define GL_INFO_LOG_LENGTH                0x8B84
#define GL_TEXTURE0                       0x84C0
#define GL_BGRA                           0x80E1
#define GL_ELEMENT_ARRAY_BUFFER           0x8893

这是定义了一些识别子。这个定义是OpenGL规范事先规定好的,只是一个规定,并没有什么道理。显卡厂商按照这个规定写驱动,我们也必须按照这个规定写程序,两边才能对接起来。

WGL是微软提供的OpenGL的兼容版本。

下面的是OpenGL的API的类型定义。这些定义,同样的,是OpenGL规范的规定。

//////////////
// TYPEDEFS //
//////////////
typedef BOOL (WINAPI   * PFNWGLCHOOSEPIXELFORMATARBPROC) (HDC hdc, const int *piAttribIList, const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
typedef HGLRC (WINAPI  * PFNWGLCREATECONTEXTATTRIBSARBPROC) (HDC hDC, HGLRC hShareContext, const int *attribList);
typedef BOOL (WINAPI   * PFNWGLSWAPINTERVALEXTPROC) (int interval);
typedef void (APIENTRY * PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRY * PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (APIENTRY * PFNGLBINDVERTEXARRAYPROC) (GLuint array);
typedef void (APIENTRY * PFNGLBUFFERDATAPROC) (GLenum target, ptrdiff_t size, const GLvoid *data, GLenum usage);
typedef void (APIENTRY * PFNGLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint(APIENTRY * PFNGLCREATEPROGRAMPROC) (void);
typedef GLuint(APIENTRY * PFNGLCREATESHADERPROC) (GLenum type);
typedef void (APIENTRY * PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
typedef void (APIENTRY * PFNGLDELETEPROGRAMPROC) (GLuint program);
typedef void (APIENTRY * PFNGLDELETESHADERPROC) (GLuint shader);
typedef void (APIENTRY * PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays);
typedef void (APIENTRY * PFNGLDETACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRY * PFNGLENABLEVERTEXATTRIBARRAYPROC) (GLuint index);
typedef void (APIENTRY * PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (APIENTRY * PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays);
typedef GLint(APIENTRY * PFNGLGETATTRIBLOCATIONPROC) (GLuint program, const char *name);
typedef void (APIENTRY * PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei bufSize, GLsizei *length, char *infoLog);
typedef void (APIENTRY * PFNGLGETPROGRAMIVPROC) (GLuint program, GLenum pname, GLint *params);
typedef void (APIENTRY * PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog);
typedef void (APIENTRY * PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
typedef void (APIENTRY * PFNGLLINKPROGRAMPROC) (GLuint program);
typedef void (APIENTRY * PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length);
typedef void (APIENTRY * PFNGLUSEPROGRAMPROC) (GLuint program);
typedef void (APIENTRY * PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
typedef void (APIENTRY * PFNGLBINDATTRIBLOCATIONPROC) (GLuint program, GLuint index, const char *name);
typedef GLint(APIENTRY * PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const char *name);
typedef void (APIENTRY * PFNGLUNIFORMMATRIX4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
typedef void (APIENTRY * PFNGLACTIVETEXTUREPROC) (GLenum texture);
typedef void (APIENTRY * PFNGLUNIFORM1IPROC) (GLint location, GLint v0);
typedef void (APIENTRY * PFNGLGENERATEMIPMAPPROC) (GLenum target);
typedef void (APIENTRY * PFNGLDISABLEVERTEXATTRIBARRAYPROC) (GLuint index);
typedef void (APIENTRY * PFNGLUNIFORM3FVPROC) (GLint location, GLsizei count, const GLfloat *value);
typedef void (APIENTRY * PFNGLUNIFORM4FVPROC) (GLint location, GLsizei count, const GLfloat *value);

然后我们用这些函数指针类型定义我们自己的函数指针,用来存储OpenGL API的调用(跳转)地址:

PFNGLATTACHSHADERPROC glAttachShader;
PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
PFNGLBUFFERDATAPROC glBufferData;
PFNGLCOMPILESHADERPROC glCompileShader;
PFNGLCREATEPROGRAMPROC glCreateProgram;
PFNGLCREATESHADERPROC glCreateShader;
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLDELETEPROGRAMPROC glDeleteProgram;
PFNGLDELETESHADERPROC glDeleteShader;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
PFNGLDETACHSHADERPROC glDetachShader;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
PFNGLGETPROGRAMIVPROC glGetProgramiv;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
PFNGLGETSHADERIVPROC glGetShaderiv;
PFNGLLINKPROGRAMPROC glLinkProgram;
PFNGLSHADERSOURCEPROC glShaderSource;
PFNGLUSEPROGRAMPROC glUseProgram;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv;
PFNGLACTIVETEXTUREPROC glActiveTexture;
PFNGLUNIFORM1IPROC glUniform1i;
PFNGLGENERATEMIPMAPPROC glGenerateMipmap;
PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray;
PFNGLUNIFORM3FVPROC glUniform3fv;
PFNGLUNIFORM4FVPROC glUniform4fv;

PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;
PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;

OpenGL的API分为核心API,扩展API,以及平台API。核心API是那些成熟的,已经正式纳入某个版本的OpenGL规范的API。扩展API是那些厂商拓展的,还没有正式纳入某个版本的OpenGL的API。平台API则是那些和平台密切相关的,比如生成上下文,改变分辨率,改变刷新率的API。上面以wgl开头的,为平台API。

随着显卡的发展和OpenGL的版本升级,这些API都是在动态改变的。[*1]

typedef struct VertexType
{
	VectorType position;
	VectorType color;
} VertexType;

这是定义了一个顶点所包含的数据(正式名称称为属性)的个数和类型。这个在前面的文章当中就已经用到过了。顶点数据结构对于渲染的性能指标有着很大的影响。因为在GPU工作的时候,它会将这些数据挨个读入到内部寄存器,然后交给内部的Shader执行器去执行我们写的Shader代码。这个数据结构越复杂,占用的内部寄存器就越多,那么可以并行执行的Shader就会减少,从而导致整个渲染时间增加。

Epic在使用UE4制作Paragon的时候,就遇到了这个问题。因为从别的角度来看,顶点数据结构以及Shader的通用程度也是十分重要的。UE4广泛使用基于物理的渲染,所以它的顶点数据结构是比较复杂的,包括很多参数。同时,Shader也是在执行期进行绑定的。也就是说,在UE4当中,并不是像我们现在这样,将顶点的数据结构和Shader一一对应起来。这就造成了在执行的时候,读入寄存器的大量顶点数据当中的相当一部分属性,对于大部分的Shader来说,其实是用不到的。这就白白的浪费了GPU的寄存器,降低了并行执行的数量,拉长了渲染时间。

因此,Epic在Paragon项目当中,采用了一些特别的手法,将不需要的顶点属性剔除,不读入GPU寄存器。但是实现这个是有前提条件的,比如AMD的GCN架构,因为它在执行VS Shader之前,有一个被称为是LS的阶段,专门用来处理数据加载的,那么就可以通过改变这个LS来实现这样的目的。

const char VS_SHADER_SOURCE_FILE[] = "color.vs";
const char PS_SHADER_SOURCE_FILE[] = "color.ps";

这里定义了VS Shader和PS Shader的程序文件名。在我们之前的DX的例子里,我们是在离线情况下先将Shader编译成二进制,再在运行时读入到GPU当中去的。这里我们演示了另外一种方法,直接读取Shader的源代码,然后在运行时内进行编译,之后再设置到GPU当中去。这种方法实质上允许我们在执行的时候动态生成Shader代码给自己使用。就类似Java/Javascript的JIT编译。我们现在有很多网游是支持“热更新”的,这种“热更新”往往就是采用了这种手段。当然,不仅仅是Shader,还包括游戏本身的逻辑,UI等(比如Lua语言)。

但是这种方法从安全角度来看是有巨大的隐患的。它意味着程序可以执行任何代码,包括未知的代码。因此,在一些成熟的平台,比如PS4平台上,这种功能是被禁止掉的。也就是说,只能使用离线编译好的代码。

float g_worldMatrix[16];
float g_viewMatrix[16];
float g_projectionMatrix[16];

这个就是所谓的MVP矩阵,是实现3D空间到2D空间的投影,摄像机语言,以及动画的关键。之前的文章我们渲染的都是静态的图像;而这里我们加入了这个MVP矩阵,我们就可以渲染动画了。

接下来都是一些辅助性函数(子过程),我们先跳过它们,看我们的主函数(WinMain)

首先我们可以看到的一个大的改变是我们创建了两次Window。

// fill in the struct with the needed information
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    wc.lpfnWndProc = DefWindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.lpszClassName = _T("Temporary");

    // register the window class
    RegisterClassEx(&wc);

    // create the temporary window for OpenGL extension setup.
    hWnd = CreateWindowEx(WS_EX_APPWINDOW,
                          _T("Temporary"),    // name of the window class
                          _T("Temporary"),   // title of the window
                          WS_OVERLAPPEDWINDOW,    // window style
                          0,    // x-position of the window
                          0,    // y-position of the window
                          640,    // width of the window
                          480,    // height of the window
                          NULL,    // we have no parent window, NULL
                          NULL,    // we aren't using menus, NULL
                          hInstance,    // application handle
                          NULL);    // used with multiple windows, NULL

                                    // Don't show the window.
    ShowWindow(hWnd, SW_HIDE);

    InitializeExtensions(hWnd);

    DestroyWindow(hWnd);
    hWnd = NULL;

    // clear out the window class for use
    ZeroMemory(&wc, sizeof(WNDCLASSEX));

    // fill in the struct with the needed information
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.lpszClassName = _T("Hello, Engine!");

    // register the window class
    RegisterClassEx(&wc);

    // create the window and use the result as the handle
    hWnd = CreateWindowEx(WS_EX_APPWINDOW,
        _T("Hello, Engine!"),    // name of the window class
        _T("Hello, Engine!"),   // title of the window
        WS_OVERLAPPEDWINDOW,    // window style
        300,    // x-position of the window
        300,    // y-position of the window
        960,    // width of the window
        540,    // height of the window
        NULL,    // we have no parent window, NULL
        NULL,    // we aren't using menus, NULL
        hInstance,    // application handle
        NULL);    // used with multiple windows, NULL

    InitializeOpenGL(hWnd, 960, 540, SCREEN_DEPTH, SCREEN_NEAR, true);

这是因为在Windows当中,获取OpenGL API入口的API,它本身也是一个OpenGL扩展函数。OpenGL的API必须在创建了一个Draw Context(DC)的情况下,才可以使用。因此我们先创建了一个纯粹以加载DC为目的的临时窗口,将OpenGL API的入口地址全部加载到我们事先定义好的函数指针之中。

这里需要注意的有几个点:

  1. 注册的windows class (wc)的wc.style,必须包含CS_OWNDC。这个标志强迫Windows为使用该windows class的每一个窗口创建一个单独的DC。如果没有这个标志,那么DC会在所有使用这个windows class的窗口之间共用,那么这是一个很危险的事情。(也就是某个窗口改变了frame buffer的格式会导致其它同一个windows class的窗口的frame buffer的格式也改变)
  2. 由于在调用wgl系的函数之前,要先确定frame buffer的pixel format,也就是frame buffer的格式,我们在第一次创建窗口之后,InitializeExtensions里面,指定了缺省的pixel format。(因为这个时候我们OpenGL API的函数入口都没有找到,没有办法知道显卡所支持的pixel format)然而不幸的是,这个SetPixelFormat对于每一个窗口,只能执行一次。因此,在我们完成InitializeExtensions之后,获得了OpenGL API的入口,想要将Frame buffer指定为我们喜欢的格式的时候,我们必须关闭当前这个(临时)窗口,重新创建一个窗口(也就是重新创建DC)
  3. 相同的window class的窗口是共用一个消息处理函数(windowproc)的。
    About Window Classes
    从而,如果我们在创建两次窗口的时候,使用了同一个window class,而且第二次创建紧接着第一次窗口的销毁之后(就如同我们这个例子)的话,那么我们在第二次创建窗口之后,很可能会收到第一次创建的窗口被销毁时发过来的WM_DESTROY消息。那么会导致我们第二个窗口立即被关闭。(如果我们像这个例子这样处理了WM_DESTROY的话)

接下来我们读入Shader程序,并对其进行编译和绑定(指设定GPU相关的寄存器,使其指向我们的程序在内存当中的地址,以及绑定Shader程序(在这个例子里是VS Shader)输入参数的格式。(在这个例子里就是我们VertexType的两个属性: position, color)

     // Bind the shader input variables.
        glBindAttribLocation(g_shaderProgram, 0, "inputPosition");
        glBindAttribLocation(g_shaderProgram, 1, "inputColor");

这其实就是告诉GPU,怎么去看我们输入的顶点数据。(否则,从GPU来看输入就是一块内存区域,一连串的byte,不知道每个顶点的数据从哪里开始,从哪里结束,各是什么意思)

然后是初始化我们的顶点数据和索引数据。索引数据并不是必须的。比如我们前一篇就没有用到索引数据。但是在一个实际的游戏当中,物体都是比较复杂的,每个顶点被多个三角面所用。所以,相对于带有许多属性的顶点数据来说,索引数据要轻量得多。与其让顶点数据不断重复出现(因为每3个顶点才能组成一个三角形,对于相邻的三角形,它们有1-2个顶点是共用的,这1-2个顶点就会重复出现),不如让索引重复出现。(顶点-索引机制就是先告诉GPU所有不同的顶点的数据,然后告诉GPU把哪些点连起来形成一个面,很像我们小时候玩的按数字连点画图游戏)

所以对于正方体来说,它一共有8个不同的顶点;6个面,每个面用一条对角线切成两个三角形的话,一共是12个三角形;每个三角形需要3个索引来描述,一共也就是36个索引。

     VertexType vertices[] = {
   // Position:  x       y      z  Color:r    g      b
            {{  1.0f,  1.0f,  1.0f }, { 1.0f, 0.0f, 0.0f }},
            {{  1.0f,  1.0f, -1.0f }, { 0.0f, 1.0f, 0.0f }},
            {{ -1.0f,  1.0f, -1.0f }, { 0.0f, 0.0f, 1.0f }},
            {{ -1.0f,  1.0f,  1.0f }, { 1.0f, 1.0f, 0.0f }},
            {{  1.0f, -1.0f,  1.0f }, { 1.0f, 0.0f, 1.0f }},
            {{  1.0f, -1.0f, -1.0f }, { 0.0f, 1.0f, 1.0f }},
            {{ -1.0f, -1.0f, -1.0f }, { 0.5f, 1.0f, 0.5f }},
            {{ -1.0f, -1.0f,  1.0f }, { 1.0f, 0.5f, 1.0f }},
        };
        uint16_t indices[] = { 1, 2, 3, 3, 2, 6, 6, 7, 3, 3, 0, 1, 0, 3, 7, 7, 6, 4, 4, 6, 5, 0, 7, 4, 1, 0, 4, 1, 4, 5, 2, 1, 5, 2, 5, 6 };

索引的排列有一定技巧。在当代的GPU当中,为了高速化,内部有各种各样的Cache,也就是缓存。GPU是按照索引顺序依次对顶点的坐标进行变换(一般就是乘以我们给它的MVP矩阵,把空间的点先按照我们的指示进行移动旋转,然后投射到屏幕坐标(2D)当中)。但是就如我们看到的,索引有重复。对于重复的索引所代表的顶点进行重复的计算是没有意义的。事实上,GPU会在一定程度上Cache之前计算的结果。如果后面的索引在这个Cache里面找到了,则直接利用前面计算的结果,而不重新计算。

然而,现实游戏当中的顶点是千万-亿级别的。GPU不可能有那么大的Cache全部记住。事实上能够记住的只有最近的几个而已。因此在排列这些索引的时候就很有讲究了。尽量让重复的排得近一点。这个里面有算法的,这里就先不展开,放在后面的文章讨论。

另外需要注意的是,为了减轻GPU的负担,大多数的表面都是有正反的。对于反面朝向我们的三角形,如果没有特别指定,GPU会直接将它丢弃掉。GPU判断这个正反是通过3个顶点在投射到屏幕空间之后,按照索引的顺序去看,是逆时针顺序还是顺时针顺序判断的。因此,在我们创建索引的时候,需要将空间几何体首先展平(也就是制作贴图的时候的UV展开),然后根据在那个平面上的逆时针方向编制每个三角形的索引。

(图片来自网络)

好了,接下来就是计算绑定MVP了。MVP的特点是每帧更新一次(当然也可以不变),在单帧的绘制过程当中,它是一个常数。所以,MVP在GPU当中存放的地方,也叫Constant Buffer。这个Constant,就是指在一帧当中,它们是常数(同样的还有光照数据等)

 // Update world matrix to rotate the model
    rotateAngle += PI / 120;
    float rotationMatrixY[16];
    float rotationMatrixZ[16];
    MatrixRotationY(rotationMatrixY, rotateAngle);
    MatrixRotationZ(rotationMatrixZ, rotateAngle);
    MatrixMultiply(g_worldMatrix, rotationMatrixZ, rotationMatrixY);

    // Generate the view matrix based on the camera's position.
    CalculateCameraPosition();

    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
    glUseProgram(g_shaderProgram);
    SetShaderParameters(g_worldMatrix, g_viewMatrix, g_projectionMatrix);

我们这里使用了一个局部的静态变量,rotateAngle,来存储一个角度因变量(自变量是时间)。然后我们根据这个角度因变量计算出Y轴和Z轴的旋转矩阵。这两个矩阵相乘,就是我们的M矩阵。它实现了我们模型的动画。注意OpenGL是右手坐标系,这个和DX是不一样的。

同时我们根据镜头的当前姿态,计算出View矩阵。这个矩阵决定了我们观察的位置。

而P矩阵是我们提前根据视口的状态以及摄像机的FOV计算好的。在摄像机FOV不变化,屏幕分辨率不变化,视口不变化的情况下,它是不变化的。

在SetShaderParameter当中,我们查找到已经位于内存(而且是GPU可见的内存)当中的Shader的MVP变量的地址(也就是占位符的地址),把这些计算的结果覆盖上去。

最后我们调用绘图指令,命令GPU开始一帧的绘制:

     // Render the vertex buffer using the index buffer.
        glDrawElements(GL_TRIANGLES, g_indexCount, GL_UNSIGNED_SHORT, 0);

所以我们可以看到,其实一个标准的(基本)绘制流程就是CPU先设置好绘图的上下文(frame buffer,显卡的状态等),然后决定画面当中绘制哪些物体,将这些物体的顶点以及索引数据调入内存的一片区域,将这个区域暴露给GPU并绑定在GPU的特定寄存器上;计算每个物体的MVP,将结果更新到绘制该物体的Shader当中;签发绘制指令,让GPU完成物体的绘制工作。

最后我们再来看一下Shader。OpenGL的Shader是用一种称为GLSL的语言书写的。基本语法等依然是来自于C/C++语言。与DX的HLSL相比,大致上有以下区别:

  1. 没有特殊寄存器绑定的语言扩展。但是有特殊变量,起到寄存器绑定的作用。特殊变量以”gl_”开头;
  2. 输入输出变量都是作为全局变量进行声明,而非作为函数的参数声明;
  3. 类型名称不同。HLSL当中的数据类型名称以基本形的扩展形式出现,如float3;而GLSL当中则以vec3这种扩展的形式出现。
////////////////////////////////////////////////////////////////////////////////
// Filename: color.vs
////////////////////////////////////////////////////////////////////////////////

#version 400

/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec3 inputColor;

//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec3 color;

///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
	// Calculate the position of the vertex against the world, view, and projection matrices.
	gl_Position = worldMatrix * vec4(inputPosition, 1.0f);
	gl_Position = viewMatrix * gl_Position;
	gl_Position = projectionMatrix * gl_Position;

	// Store the input color for the pixel shader to use.
	color = inputColor;
}

这个shader实现的就是把顶点的坐标乘以MVP矩阵,使其投射到2维视口坐标当中;另外原样拷贝输出色彩属性。

////////////////////////////////////////////////////////////////////////////////
// Filename: color.ps
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 color;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
	outputColor = vec4(color, 1.0f);
}

这个PS Shader实现的也是色彩的原样输出。因为我们这个例子当中frame buffer的格式是RGBA,而顶点色只有RGB三个分量,所以添加了一个A通道,值为1.0,意思是完全不透明。

(– EOF –)

参考引用:

  1. Load OpenGL Functions
  2. ARB assembly language

本作品采用知识共享署名 4.0 国际许可协议进行许可。

从零开始手敲次世代游戏引擎(十三)

上一篇我们在Linux系统上用OpenGL绘制了一个基本的空间矩形。本篇我们看看在Windows平台上如何使用OpenGL,并且看一下高版本(4.0)的OpenGL的使用方法。

注意无论是DX还是OpenGL,都需要硬件(GPU)的支持。不同版本的显卡能够支持的图形API版本也是不一样的。因此可能存在部分机器无法运行接下来的代码的情况。特别是在远程登录的情况下,或者在X forwarding情况下,不通过一些特别的技巧,一般是无法正确运行需要GPU加速的应用的。

另外纠正前面的文章的一个错误。vmware workstation player的免费版,是无法手动打开GPU加速的。

OpenGL API是由显卡的驱动程序实现的。所以我们的程序实际上并不会链接到这些API,而是在运行的时候去查找这些API的入口地址。这个用于查找API入口地址的API,一般就是gl.h和libGL(在windows当中为opengl32.lib)所提供的内容,但也是因系统不同而不同的。

由于OpenGL和DX是并行的关系,所以我们选择helloengine_win.c作为我们的起点,而不是helloengine_d*d.cpp。拷贝helloengine_win.c到helloengine_opengl.cpp,开始我们的编辑。

(本文大部分代码参考 rastertek.com/gl40tut03 编写。本文的目的是探查在Windows上使用OpenGL的方法。本文的代码并不会直接成为我们引擎的正式代码。)[*1]

#include <windows.h>
 #include <windowsx.h>
 #include <tchar.h>
+#include <GL/gl.h>
+#include <fstream>
+
+#include "math.h"
+
+using namespace std;
+
+/////////////
+// DEFINES //
+/////////////
+#define WGL_DRAW_TO_WINDOW_ARB         0x2001
+#define WGL_ACCELERATION_ARB           0x2003
+#define WGL_SWAP_METHOD_ARB            0x2007
+#define WGL_SUPPORT_OPENGL_ARB         0x2010
+#define WGL_DOUBLE_BUFFER_ARB          0x2011
+#define WGL_PIXEL_TYPE_ARB             0x2013
+#define WGL_COLOR_BITS_ARB             0x2014
+#define WGL_DEPTH_BITS_ARB             0x2022
+#define WGL_STENCIL_BITS_ARB           0x2023
+#define WGL_FULL_ACCELERATION_ARB      0x2027
+#define WGL_SWAP_EXCHANGE_ARB          0x2028
+#define WGL_TYPE_RGBA_ARB              0x202B
+#define WGL_CONTEXT_MAJOR_VERSION_ARB  0x2091
+#define WGL_CONTEXT_MINOR_VERSION_ARB  0x2092
+#define GL_ARRAY_BUFFER                   0x8892
+#define GL_STATIC_DRAW                    0x88E4
+#define GL_FRAGMENT_SHADER                0x8B30
+#define GL_VERTEX_SHADER                  0x8B31
+#define GL_COMPILE_STATUS                 0x8B81
+#define GL_LINK_STATUS                    0x8B82
+#define GL_INFO_LOG_LENGTH                0x8B84
+#define GL_TEXTURE0                       0x84C0
+#define GL_BGRA                           0x80E1
+#define GL_ELEMENT_ARRAY_BUFFER           0x8893
+
+//////////////
+// TYPEDEFS //
+//////////////
+typedef BOOL (WINAPI   * PFNWGLCHOOSEPIXELFORMATARBPROC) (HDC hdc, const int *piAttribIList, const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
+typedef HGLRC (WINAPI  * PFNWGLCREATECONTEXTATTRIBSARBPROC) (HDC hDC, HGLRC hShareContext, const int *attribList);
+typedef BOOL (WINAPI   * PFNWGLSWAPINTERVALEXTPROC) (int interval);
+typedef void (APIENTRY * PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
+typedef void (APIENTRY * PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
+typedef void (APIENTRY * PFNGLBINDVERTEXARRAYPROC) (GLuint array);
+typedef void (APIENTRY * PFNGLBUFFERDATAPROC) (GLenum target, ptrdiff_t size, const GLvoid *data, GLenum usage);
+typedef void (APIENTRY * PFNGLCOMPILESHADERPROC) (GLuint shader);
+typedef GLuint(APIENTRY * PFNGLCREATEPROGRAMPROC) (void);
+typedef GLuint(APIENTRY * PFNGLCREATESHADERPROC) (GLenum type);
+typedef void (APIENTRY * PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
+typedef void (APIENTRY * PFNGLDELETEPROGRAMPROC) (GLuint program);
+typedef void (APIENTRY * PFNGLDELETESHADERPROC) (GLuint shader);
+typedef void (APIENTRY * PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays);
+typedef void (APIENTRY * PFNGLDETACHSHADERPROC) (GLuint program, GLuint shader);
+typedef void (APIENTRY * PFNGLENABLEVERTEXATTRIBARRAYPROC) (GLuint index);
+typedef void (APIENTRY * PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
+typedef void (APIENTRY * PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays);
+typedef GLint(APIENTRY * PFNGLGETATTRIBLOCATIONPROC) (GLuint program, const char *name);
+typedef void (APIENTRY * PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei bufSize, GLsizei *length, char *infoLog);+typedef void (APIENTRY * PFNGLGETPROGRAMIVPROC) (GLuint program, GLenum pname, GLint *params);
+typedef void (APIENTRY * PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog);
+typedef void (APIENTRY * PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
+typedef void (APIENTRY * PFNGLLINKPROGRAMPROC) (GLuint program);
+typedef void (APIENTRY * PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length);
+typedef void (APIENTRY * PFNGLUSEPROGRAMPROC) (GLuint program);
+typedef void (APIENTRY * PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
+typedef void (APIENTRY * PFNGLBINDATTRIBLOCATIONPROC) (GLuint program, GLuint index, const char *name);
+typedef GLint(APIENTRY * PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const char *name);
+typedef void (APIENTRY * PFNGLUNIFORMMATRIX4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRY * PFNGLACTIVETEXTUREPROC) (GLenum texture);
+typedef void (APIENTRY * PFNGLUNIFORM1IPROC) (GLint location, GLint v0);
+typedef void (APIENTRY * PFNGLGENERATEMIPMAPPROC) (GLenum target);
+typedef void (APIENTRY * PFNGLDISABLEVERTEXATTRIBARRAYPROC) (GLuint index);
+typedef void (APIENTRY * PFNGLUNIFORM3FVPROC) (GLint location, GLsizei count, const GLfloat *value);
+typedef void (APIENTRY * PFNGLUNIFORM4FVPROC) (GLint location, GLsizei count, const GLfloat *value);
+
+PFNGLATTACHSHADERPROC glAttachShader;
+PFNGLBINDBUFFERPROC glBindBuffer;
+PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
+PFNGLBUFFERDATAPROC glBufferData;
+PFNGLCOMPILESHADERPROC glCompileShader;
+PFNGLCREATEPROGRAMPROC glCreateProgram;
+PFNGLCREATESHADERPROC glCreateShader;
+PFNGLDELETEBUFFERSPROC glDeleteBuffers;
+PFNGLDELETEPROGRAMPROC glDeleteProgram;
+PFNGLDELETESHADERPROC glDeleteShader;
+PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
+PFNGLDETACHSHADERPROC glDetachShader;
+PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
+PFNGLGENBUFFERSPROC glGenBuffers;
+PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
+PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation;
+PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
+PFNGLGETPROGRAMIVPROC glGetProgramiv;
+PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
+PFNGLGETSHADERIVPROC glGetShaderiv;
+PFNGLLINKPROGRAMPROC glLinkProgram;
+PFNGLSHADERSOURCEPROC glShaderSource;
+PFNGLUSEPROGRAMPROC glUseProgram;
+PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
+PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation;
+PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
+PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv;
+PFNGLACTIVETEXTUREPROC glActiveTexture;
+PFNGLUNIFORM1IPROC glUniform1i;
+PFNGLGENERATEMIPMAPPROC glGenerateMipmap;
+PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray;
+PFNGLUNIFORM3FVPROC glUniform3fv;
+PFNGLUNIFORM4FVPROC glUniform4fv;
+
+PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
+PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;
+PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT;
+
+typedef struct VertexType
+{
+       VectorType position;
+       VectorType color;
+} VertexType;
+
+HDC     g_deviceContext = 0;
+HGLRC   g_renderingContext = 0;
+char    g_videoCardDescription[128];
+
+const bool VSYNC_ENABLED = true;
+const float SCREEN_DEPTH = 1000.0f;
+const float SCREEN_NEAR = 0.1f;
+
+int     g_vertexCount, g_indexCount;
+unsigned int g_vertexArrayId, g_vertexBufferId, g_indexBufferId;
+
+unsigned int g_vertexShader;
+unsigned int g_fragmentShader;
+unsigned int g_shaderProgram;
+
+const char VS_SHADER_SOURCE_FILE[] = "color.vs";
+const char PS_SHADER_SOURCE_FILE[] = "color.ps";
+
+float g_positionX = 0, g_positionY = 0, g_positionZ = -10;
+float g_rotationX = 0, g_rotationY = 0, g_rotationZ = 0;
+float g_worldMatrix[16];
+float g_viewMatrix[16];
+float g_projectionMatrix[16];
+
+bool InitializeOpenGL(HWND hwnd, int screenWidth, int screenHeight, float screenDepth, float screenNear, bool vsync)
+{
+        int attributeListInt[19];
+        int pixelFormat[1];
+        unsigned int formatCount;
+        int result;
+        PIXELFORMATDESCRIPTOR pixelFormatDescriptor;
+        int attributeList[5];
+        float fieldOfView, screenAspect;
+        char *vendorString, *rendererString;
+
+
+        // Get the device context for this window.
+        g_deviceContext = GetDC(hwnd);
+        if(!g_deviceContext)
+        {
+                return false;
+        }
+
+        // Support for OpenGL rendering.
+        attributeListInt[0] = WGL_SUPPORT_OPENGL_ARB;
+        attributeListInt[1] = TRUE;
+
+        // Support for rendering to a window.
+        attributeListInt[2] = WGL_DRAW_TO_WINDOW_ARB;
+        attributeListInt[3] = TRUE;
+
+        // Support for hardware acceleration.
+        attributeListInt[4] = WGL_ACCELERATION_ARB;
+        attributeListInt[5] = WGL_FULL_ACCELERATION_ARB;
+
+        // Support for 24bit color.
+        attributeListInt[6] = WGL_COLOR_BITS_ARB;
+        attributeListInt[7] = 24;
+
+        // Support for 24 bit depth buffer.
+        attributeListInt[8] = WGL_DEPTH_BITS_ARB;
+        attributeListInt[9] = 24;
+
+        // Support for double buffer.
+        attributeListInt[10] = WGL_DOUBLE_BUFFER_ARB;
+        attributeListInt[11] = TRUE;
+
+        // Support for swapping front and back buffer.
+        attributeListInt[12] = WGL_SWAP_METHOD_ARB;
+        attributeListInt[13] = WGL_SWAP_EXCHANGE_ARB;
+
+        // Support for the RGBA pixel type.
+        attributeListInt[14] = WGL_PIXEL_TYPE_ARB;
+        attributeListInt[15] = WGL_TYPE_RGBA_ARB;
+
+        // Support for a 8 bit stencil buffer.
+        attributeListInt[16] = WGL_STENCIL_BITS_ARB;
+        attributeListInt[17] = 8;
+
+        // Null terminate the attribute list.
+        attributeListInt[18] = 0;
+
+
+        // Query for a pixel format that fits the attributes we want.
+        result = wglChoosePixelFormatARB(g_deviceContext, attributeListInt, NULL, 1, pixelFormat, &formatCount);
+        if(result != 1)
+        {
+                return false;
+        }
+
+        // If the video card/display can handle our desired pixel format then we set it as the current one.
+        result = SetPixelFormat(g_deviceContext, pixelFormat[0], &pixelFormatDescriptor);
+        if(result != 1)
+        {
+                return false;
+        }
+
+        // Set the 4.0 version of OpenGL in the attribute list.
+        attributeList[0] = WGL_CONTEXT_MAJOR_VERSION_ARB;
+        attributeList[1] = 4;
+        attributeList[2] = WGL_CONTEXT_MINOR_VERSION_ARB;
+        attributeList[3] = 0;
+
+        // Null terminate the attribute list.
+        attributeList[4] = 0;
+
+        // Create a OpenGL 4.0 rendering context.
+        g_renderingContext = wglCreateContextAttribsARB(g_deviceContext, 0, attributeList);
+        if(g_renderingContext == NULL)
+        {
+                return false;
+        }
+
+        // Set the rendering context to active.
+        result = wglMakeCurrent(g_deviceContext, g_renderingContext);
+        if(result != 1)
+        {
+                return false;
+        }
+
+        // Set the depth buffer to be entirely cleared to 1.0 values.
+        glClearDepth(1.0f);
+
+        // Enable depth testing.
+        glEnable(GL_DEPTH_TEST);
+
+        // Set the polygon winding to front facing for the left handed system.
+        glFrontFace(GL_CW);
+
+        // Enable back face culling.
+        glEnable(GL_CULL_FACE);
+        glCullFace(GL_BACK);
+
+               // Initialize the world/model matrix to the identity matrix.
+               BuildIdentityMatrix(g_worldMatrix);
+
+               // Set the field of view and screen aspect ratio.
+               fieldOfView = PI / 4.0f;
+               screenAspect = (float)screenWidth / (float)screenHeight;
+
+               // Build the perspective projection matrix.
+               BuildPerspectiveFovLHMatrix(g_projectionMatrix, fieldOfView, screenAspect, screenNear, screenDepth);
+
+        // Get the name of the video card.
+        vendorString = (char*)glGetString(GL_VENDOR);
+        rendererString = (char*)glGetString(GL_RENDERER);
+        // Store the video card name in a class member variable so it can be retrieved later.
+        strcpy_s(g_videoCardDescription, vendorString);
+        strcat_s(g_videoCardDescription, " - ");
+        strcat_s(g_videoCardDescription, rendererString);
+
+        // Turn on or off the vertical sync depending on the input bool value.
+        if(vsync)
+        {
+                result = wglSwapIntervalEXT(1);
+        }
+        else
+        {
+                result = wglSwapIntervalEXT(0);
+        }
+
+        // Check if vsync was set correctly.
+        if(result != 1)
+        {
+                return false;
+        }
+
+        return true;
+}
+
+bool LoadExtensionList()
+{
+        // Load the OpenGL extensions that this application will be using.
+        wglChoosePixelFormatARB = (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress("wglChoosePixelFormatARB");
+        if(!wglChoosePixelFormatARB)
+        {
+                return false;
+        }
+
+        wglCreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB");
+        if(!wglCreateContextAttribsARB)
+        {
+                return false;
+        }
+
+        wglSwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)wglGetProcAddress("wglSwapIntervalEXT");
+        if(!wglSwapIntervalEXT)
+        {
+                return false;
+        }
+
+        glAttachShader = (PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader");
+        if(!glAttachShader)
+        {
+                return false;
+        }
+
+        glBindBuffer = (PFNGLBINDBUFFERPROC)wglGetProcAddress("glBindBuffer");
+        if(!glBindBuffer)
+        {
+                return false;
+        }
+
+        glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)wglGetProcAddress("glBindVertexArray");
+        if(!glBindVertexArray)
+        {
+                return false;
+        }
+
+        glBufferData = (PFNGLBUFFERDATAPROC)wglGetProcAddress("glBufferData");
+        if(!glBufferData)
+        {
+                return false;
+        }
+
+        glCompileShader = (PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader");
+        if(!glCompileShader)
+        {
+                return false;
+        }
+
+        glCreateProgram = (PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram");
+        if(!glCreateProgram)
+        {
+                return false;
+        }
+
+        glCreateShader = (PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader");
+        if(!glCreateShader)
+        {
+                return false;
+        }
+
+        glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)wglGetProcAddress("glDeleteBuffers");
+        if(!glDeleteBuffers)
+        {
+                return false;
+        }
+
+        glDeleteProgram = (PFNGLDELETEPROGRAMPROC)wglGetProcAddress("glDeleteProgram");
+        if(!glDeleteProgram)
+        {
+                return false;
+        }
+
+        glDeleteShader = (PFNGLDELETESHADERPROC)wglGetProcAddress("glDeleteShader");
+        if(!glDeleteShader)
+        {
+                return false;
+        }
+
+        glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC)wglGetProcAddress("glDeleteVertexArrays");
+        if(!glDeleteVertexArrays)
+        {
+                return false;
+        }
+
+        glDetachShader = (PFNGLDETACHSHADERPROC)wglGetProcAddress("glDetachShader");
+        if(!glDetachShader)
+        {
+                return false;
+        }
+
+        glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glEnableVertexAttribArray");
+        if(!glEnableVertexAttribArray)
+        {
+                return false;
+        }
+
+        glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers");
+        if(!glGenBuffers)
+        {
+                return false;
+        }
+
+        glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)wglGetProcAddress("glGenVertexArrays");
+        if(!glGenVertexArrays)
+        {
+                return false;
+        }
+
+        glGetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC)wglGetProcAddress("glGetAttribLocation");
+        if(!glGetAttribLocation)
+        {
+                return false;
+        }
+
+        glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)wglGetProcAddress("glGetProgramInfoLog");
+        if(!glGetProgramInfoLog)
+        {
+                return false;
+        }
+
+        glGetProgramiv = (PFNGLGETPROGRAMIVPROC)wglGetProcAddress("glGetProgramiv");
+        if(!glGetProgramiv)
+        {
+                return false;
+        }
+
+        glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)wglGetProcAddress("glGetShaderInfoLog");
+        if(!glGetShaderInfoLog)
+        {
+                return false;
+        }
+
+        glGetShaderiv = (PFNGLGETSHADERIVPROC)wglGetProcAddress("glGetShaderiv");
+        if(!glGetShaderiv)
+        {
+                return false;
+        }
+
+        glLinkProgram = (PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram");
+        if(!glLinkProgram)
+        {
+                return false;
+        }
+
+        glShaderSource = (PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource");
+        if(!glShaderSource)
+        {
+                return false;
+        }
+
+        glUseProgram = (PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram");
+        if(!glUseProgram)
+        {
+                return false;
+        }
+
+        glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)wglGetProcAddress("glVertexAttribPointer");
+        if(!glVertexAttribPointer)
+        {
+                return false;
+        }
+
+        glBindAttribLocation = (PFNGLBINDATTRIBLOCATIONPROC)wglGetProcAddress("glBindAttribLocation");
+        if(!glBindAttribLocation)
+        {
+                return false;
+        }
+
+        glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation");
+        if(!glGetUniformLocation)
+        {
+                return false;
+        }
+
+        glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)wglGetProcAddress("glUniformMatrix4fv");
+        if(!glUniformMatrix4fv)
+        {
+                return false;
+        }
+
+        glActiveTexture = (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture");
+        if(!glActiveTexture)
+        {
+                return false;
+        }
+
+        glUniform1i = (PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i");
+        if(!glUniform1i)
+        {
+                return false;
+        }
+
+        glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC)wglGetProcAddress("glGenerateMipmap");
+        if(!glGenerateMipmap)
+        {
+                return false;
+        }
+
+        glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glDisableVertexAttribArray");
+        if(!glDisableVertexAttribArray)
+        {
+                return false;
+        }
+
+        glUniform3fv = (PFNGLUNIFORM3FVPROC)wglGetProcAddress("glUniform3fv");
+        if(!glUniform3fv)
+        {
+                return false;
+        }
+
+        glUniform4fv = (PFNGLUNIFORM4FVPROC)wglGetProcAddress("glUniform4fv");
+        if(!glUniform4fv)
+        {
+                return false;
+        }
+
+        return true;
+}
+
+void FinalizeOpenGL(HWND hwnd)
+{
+        // Release the rendering context.
+        if(g_renderingContext)
+        {
+                wglMakeCurrent(NULL, NULL);
+                wglDeleteContext(g_renderingContext);
+                g_renderingContext = 0;
+        }
+
+        // Release the device context.
+        if(g_deviceContext)
+        {
+                ReleaseDC(hwnd, g_deviceContext);
+                g_deviceContext = 0;
+        }
+}
+
+void GetVideoCardInfo(char* cardName)
+{
+        strcpy_s(cardName, 128, g_videoCardDescription);
+        return;
+}
+
+bool InitializeExtensions(HWND hwnd)
+{
+        HDC deviceContext;
+        PIXELFORMATDESCRIPTOR pixelFormat;
+        int error;
+        HGLRC renderContext;
+        bool result;
+
+
+        // Get the device context for this window.
+        deviceContext = GetDC(hwnd);
+        if(!deviceContext)
+        {
+                return false;
+        }
+
+        // Set a temporary default pixel format.
+        error = SetPixelFormat(deviceContext, 1, &pixelFormat);
+        if(error != 1)
+        {
+                return false;
+        }
+
+        // Create a temporary rendering context.
+        renderContext = wglCreateContext(deviceContext);
+        if(!renderContext)
+        {
+                return false;
+        }
+
+        // Set the temporary rendering context as the current rendering context for this window.
+        error = wglMakeCurrent(deviceContext, renderContext);
+        if(error != 1)
+        {
+                return false;
+        }
+
+        // Initialize the OpenGL extensions needed for this application.  Note that a temporary rendering context was needed to do so.
+        result = LoadExtensionList();
+        if(!result)
+        {
+                return false;
+        }
+
+        // Release the temporary rendering context now that the extensions have been loaded.
+        wglMakeCurrent(NULL, NULL);
+        wglDeleteContext(renderContext);
+        renderContext = NULL;
+
+        // Release the device context for this window.
+        ReleaseDC(hwnd, deviceContext);
+        deviceContext = 0;
+
+        return true;
+}
+
+void OutputShaderErrorMessage(HWND hwnd, unsigned int shaderId, const char* shaderFilename)
+{
+        int logSize, i;
+        char* infoLog;
+        ofstream fout;
+        wchar_t newString[128];
+        unsigned int error;
+        size_t convertedChars;
+
+
+        // Get the size of the string containing the information log for the failed shader compilation message.
+        glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &logSize);
+
+        // Increment the size by one to handle also the null terminator.
+        logSize++;
+
+        // Create a char buffer to hold the info log.
+        infoLog = new char[logSize];
+        if(!infoLog)
+        {
+                return;
+        }
+
+        // Now retrieve the info log.
+        glGetShaderInfoLog(shaderId, logSize, NULL, infoLog);
+
+        // Open a file to write the error message to.
+        fout.open("shader-error.txt");
+
+        // Write out the error message.
+        for(i=0; i<logSize; i++)
+        {
+                fout << infoLog[i];
+        }
+
+        // Close the file.
+        fout.close();
+
+        // Convert the shader filename to a wide character string.
+        error = mbstowcs_s(&convertedChars, newString, 128, shaderFilename, 128);
+        if(error != 0)
+        {
+                return;
+        }
+
+        // Pop a message up on the screen to notify the user to check the text file for compile errors.
+        MessageBoxW(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", newString, MB_OK);
+
+        return;
+}
+
+void OutputLinkerErrorMessage(HWND hwnd, unsigned int programId)
+{
+        int logSize, i;
+        char* infoLog;
+        ofstream fout;
+
+
+        // Get the size of the string containing the information log for the failed shader compilation message.
+        glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &logSize);
+
+        // Increment the size by one to handle also the null terminator.
+        logSize++;
+
+        // Create a char buffer to hold the info log.
+        infoLog = new char[logSize];
+        if(!infoLog)
+        {
+                return;
+        }
+
+        // Now retrieve the info log.
+        glGetProgramInfoLog(programId, logSize, NULL, infoLog);
+
+        // Open a file to write the error message to.
+        fout.open("linker-error.txt");
+
+        // Write out the error message.
+        for(i=0; i<logSize; i++)
+        {
+                fout << infoLog[i];
+        }
+
+        // Close the file.
+        fout.close();
+
+        // Pop a message up on the screen to notify the user to check the text file for linker errors.
+        MessageBox(hwnd, _T("Error compiling linker.  Check linker-error.txt for message."), _T("Linker Error"), MB_OK);
+}
+
+char* LoadShaderSourceFile(const char* filename)
+{
+        ifstream fin;
+        int fileSize;
+        char input;
+        char* buffer;
+
+
+        // Open the shader source file.
+        fin.open(filename);
+
+        // If it could not open the file then exit.
+        if(fin.fail())
+        {
+                return 0;
+        }
+
+        // Initialize the size of the file.
+        fileSize = 0;
+
+        // Read the first element of the file.
+        fin.get(input);
+
+        // Count the number of elements in the text file.
+        while(!fin.eof())
+        {
+                fileSize++;
+                fin.get(input);
+        }
+
+        // Close the file for now.
+        fin.close();
+
+        // Initialize the buffer to read the shader source file into.
+        buffer = new char[fileSize+1];
+        if(!buffer)
+        {
+                return 0;
+        }
+
+        // Open the shader source file again.
+        fin.open(filename);
+
+        // Read the shader text file into the buffer as a block.
+        fin.read(buffer, fileSize);
+
+        // Close the file.
+        fin.close();
+
+        // Null terminate the buffer.
+        buffer[fileSize] = '';
+
+        return buffer;
+}
+
+bool InitializeShader(HWND hwnd, const char* vsFilename, const char* fsFilename)
+{
+        const char* vertexShaderBuffer;
+        const char* fragmentShaderBuffer;
+        int status;
+
+        // Load the vertex shader source file into a text buffer.
+        vertexShaderBuffer = LoadShaderSourceFile(vsFilename);
+        if(!vertexShaderBuffer)
+        {
+                return false;
+        }
+
+        // Load the fragment shader source file into a text buffer.
+        fragmentShaderBuffer = LoadShaderSourceFile(fsFilename);
+        if(!fragmentShaderBuffer)
+        {
+                return false;
+        }
+
+        // Create a vertex and fragment shader object.
+        g_vertexShader = glCreateShader(GL_VERTEX_SHADER);
+        g_fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
+
+        // Copy the shader source code strings into the vertex and fragment shader objects.
+        glShaderSource(g_vertexShader, 1, &vertexShaderBuffer, NULL);
+        glShaderSource(g_fragmentShader, 1, &fragmentShaderBuffer, NULL);
+
+        // Release the vertex and fragment shader buffers.
+        delete [] vertexShaderBuffer;
+        vertexShaderBuffer = 0;
+
+        delete [] fragmentShaderBuffer;
+        fragmentShaderBuffer = 0;
+
+        // Compile the shaders.
+        glCompileShader(g_vertexShader);
+        glCompileShader(g_fragmentShader);
+
+        // Check to see if the vertex shader compiled successfully.
+        glGetShaderiv(g_vertexShader, GL_COMPILE_STATUS, &status);
+        if(status != 1)
+        {
+                // If it did not compile then write the syntax error message out to a text file for review.
+                OutputShaderErrorMessage(hwnd, g_vertexShader, vsFilename);
+                return false;
+        }
+
+        // Check to see if the fragment shader compiled successfully.
+        glGetShaderiv(g_fragmentShader, GL_COMPILE_STATUS, &status);
+        if(status != 1)
+        {
+                // If it did not compile then write the syntax error message out to a text file for review.
+                OutputShaderErrorMessage(hwnd, g_fragmentShader, fsFilename);
+                return false;
+        }
+
+        // Create a shader program object.
+        g_shaderProgram = glCreateProgram();
+
+        // Attach the vertex and fragment shader to the program object.
+        glAttachShader(g_shaderProgram, g_vertexShader);
+        glAttachShader(g_shaderProgram, g_fragmentShader);
+
+        // Bind the shader input variables.
+        glBindAttribLocation(g_shaderProgram, 0, "inputPosition");
+        glBindAttribLocation(g_shaderProgram, 1, "inputColor");
+
+        // Link the shader program.
+        glLinkProgram(g_shaderProgram);
+
+        // Check the status of the link.
+        glGetProgramiv(g_shaderProgram, GL_LINK_STATUS, &status);
+        if(status != 1)
+        {
+                // If it did not link then write the syntax error message out to a text file for review.
+                OutputLinkerErrorMessage(hwnd, g_shaderProgram);
+                return false;
+        }
+
+        return true;
+}
+
+void ShutdownShader()
+{
+        // Detach the vertex and fragment shaders from the program.
+        glDetachShader(g_shaderProgram, g_vertexShader);
+        glDetachShader(g_shaderProgram, g_fragmentShader);
+
+        // Delete the vertex and fragment shaders.
+        glDeleteShader(g_vertexShader);
+        glDeleteShader(g_fragmentShader);
+
+        // Delete the shader program.
+        glDeleteProgram(g_shaderProgram);
+}
+
+bool SetShaderParameters(float* worldMatrix, float* viewMatrix, float* projectionMatrix)
+{
+        unsigned int location;
+
+        // Set the world matrix in the vertex shader.
+        location = glGetUniformLocation(g_shaderProgram, "worldMatrix");
+        if(location == -1)
+        {
+                return false;
+        }
+        glUniformMatrix4fv(location, 1, false, worldMatrix);
+
+        // Set the view matrix in the vertex shader.
+        location = glGetUniformLocation(g_shaderProgram, "viewMatrix");
+        if(location == -1)
+        {
+                return false;
+        }
+        glUniformMatrix4fv(location, 1, false, viewMatrix);
+
+        // Set the projection matrix in the vertex shader.
+        location = glGetUniformLocation(g_shaderProgram, "projectionMatrix");
+        if(location == -1)
+        {
+                return false;
+        }
+        glUniformMatrix4fv(location, 1, false, projectionMatrix);
+
+        return true;
+}
+
+bool InitializeBuffers()
+{
+        VertexType vertices[] = {
+                       {{  1.0f,  1.0f,  1.0f }, { 1.0f, 0.0f, 0.0f }},
+                       {{  1.0f,  1.0f, -1.0f }, { 0.0f, 1.0f, 0.0f }},
+                       {{ -1.0f,  1.0f, -1.0f }, { 0.0f, 0.0f, 1.0f }},
+                       {{ -1.0f,  1.0f,  1.0f }, { 1.0f, 1.0f, 0.0f }},
+                       {{  1.0f, -1.0f,  1.0f }, { 1.0f, 0.0f, 1.0f }},
+                       {{  1.0f, -1.0f, -1.0f }, { 0.0f, 1.0f, 1.0f }},
+                       {{ -1.0f, -1.0f, -1.0f }, { 0.5f, 1.0f, 0.5f }},
+                       {{ -1.0f, -1.0f,  1.0f }, { 1.0f, 0.5f, 1.0f }},
+               };
+        uint16_t indices[] = { 1, 2, 3, 3, 2, 6, 6, 7, 3, 3, 0, 1, 0, 3, 7, 7, 6, 4, 4, 6, 5, 0, 7, 4, 1, 0, 4, 1, 4, 5, 2, 1, 5, 2, 5, 6 };
+
+        // Set the number of vertices in the vertex array.
+        g_vertexCount = sizeof(vertices) / sizeof(VertexType);
+
+        // Set the number of indices in the index array.
+        g_indexCount = sizeof(indices) / sizeof(uint16_t);
+
+        // Allocate an OpenGL vertex array object.
+        glGenVertexArrays(1, &g_vertexArrayId);
+
+        // Bind the vertex array object to store all the buffers and vertex attributes we create here.
+        glBindVertexArray(g_vertexArrayId);
+
+        // Generate an ID for the vertex buffer.
+        glGenBuffers(1, &g_vertexBufferId);
+
+        // Bind the vertex buffer and load the vertex (position and color) data into the vertex buffer.
+        glBindBuffer(GL_ARRAY_BUFFER, g_vertexBufferId);
+        glBufferData(GL_ARRAY_BUFFER, g_vertexCount * sizeof(VertexType), vertices, GL_STATIC_DRAW);
+
+        // Enable the two vertex array attributes.
+        glEnableVertexAttribArray(0);  // Vertex position.
+        glEnableVertexAttribArray(1);  // Vertex color.
+
+        // Specify the location and format of the position portion of the vertex buffer.
+        glBindBuffer(GL_ARRAY_BUFFER, g_vertexBufferId);
+        glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(VertexType), 0);
+
+        // Specify the location and format of the color portion of the vertex buffer.
+        glBindBuffer(GL_ARRAY_BUFFER, g_vertexBufferId);
+        glVertexAttribPointer(1, 3, GL_FLOAT, false, sizeof(VertexType), (char*)NULL + (3 * sizeof(float)));
+
+        // Generate an ID for the index buffer.
+        glGenBuffers(1, &g_indexBufferId);
+
+        // Bind the index buffer and load the index data into it.
+        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_indexBufferId);
+        glBufferData(GL_ELEMENT_ARRAY_BUFFER, g_indexCount* sizeof(uint16_t), indices, GL_STATIC_DRAW);
+
+        return true;
+}
+
+void ShutdownBuffers()
+{
+        // Disable the two vertex array attributes.
+        glDisableVertexAttribArray(0);
+        glDisableVertexAttribArray(1);
+
+        // Release the vertex buffer.
+        glBindBuffer(GL_ARRAY_BUFFER, 0);
+        glDeleteBuffers(1, &g_vertexBufferId);
+
+        // Release the index buffer.
+        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+        glDeleteBuffers(1, &g_indexBufferId);
+
+        // Release the vertex array object.
+        glBindVertexArray(0);
+        glDeleteVertexArrays(1, &g_vertexArrayId);
+
+        return;
+}
+
+void RenderBuffers()
+{
+        // Bind the vertex array object that stored all the information about the vertex and index buffers.
+        glBindVertexArray(g_vertexArrayId);
+
+        // Render the vertex buffer using the index buffer.
+        glDrawElements(GL_TRIANGLES, g_indexCount, GL_UNSIGNED_SHORT, 0);
+
+        return;
+}
+
+void CalculateCameraPosition()
+{
+    VectorType up, position, lookAt;
+    float yaw, pitch, roll;
+    float rotationMatrix[9];
+
+
+    // Setup the vector that points upwards.
+    up.x = 0.0f;
+    up.y = 1.0f;
+    up.z = 0.0f;
+
+    // Setup the position of the camera in the world.
+    position.x = g_positionX;
+    position.y = g_positionY;
+    position.z = g_positionZ;
+
+    // Setup where the camera is looking by default.
+    lookAt.x = 0.0f;
+    lookAt.y = 0.0f;
+    lookAt.z = 1.0f;
+
+    // Set the yaw (Y axis), pitch (X axis), and roll (Z axis) rotations in radians.
+    pitch = g_rotationX * 0.0174532925f;
+    yaw   = g_rotationY * 0.0174532925f;
+    roll  = g_rotationZ * 0.0174532925f;
+
+    // Create the rotation matrix from the yaw, pitch, and roll values.
+    MatrixRotationYawPitchRoll(rotationMatrix, yaw, pitch, roll);
+
+    // Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin.
+    TransformCoord(lookAt, rotationMatrix);
+    TransformCoord(up, rotationMatrix);
+
+    // Translate the rotated camera position to the location of the viewer.
+    lookAt.x = position.x + lookAt.x;
+    lookAt.y = position.y + lookAt.y;
+    lookAt.z = position.z + lookAt.z;
+
+    // Finally create the view matrix from the three updated vectors.
+    BuildViewMatrix(position, lookAt, up, g_viewMatrix);
+}
+
+void Draw()
+{
+       static float rotateAngle = 0.0f;
+
+    // Set the color to clear the screen to.
+    glClearColor(0.2f, 0.3f, 0.4f, 1.0f);
+    // Clear the screen and depth buffer.
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+       // Update world matrix to rotate the model
+       rotateAngle += PI / 120;
+       float rotationMatrixY[16];
+       float rotationMatrixZ[16];
+       MatrixRotationY(rotationMatrixY, rotateAngle);
+       MatrixRotationZ(rotationMatrixZ, rotateAngle);
+       MatrixMultiply(g_worldMatrix, rotationMatrixZ, rotationMatrixY);
+
+    // Generate the view matrix based on the camera's position.
+       CalculateCameraPosition();
+
+    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
+       glUseProgram(g_shaderProgram);
+    SetShaderParameters(g_worldMatrix, g_viewMatrix, g_projectionMatrix);
+
+    // Render the model using the color shader.
+    RenderBuffers();
+
+    // Present the back buffer to the screen since rendering is complete.
+    SwapBuffers(g_deviceContext);
+}

 // the WindowProc function prototype
 LRESULT CALLBACK WindowProc(HWND hWnd,
@@ -25,32 +1049,75 @@ int WINAPI WinMain(HINSTANCE hInstance,

        // fill in the struct with the needed information
        wc.cbSize = sizeof(WNDCLASSEX);
-    wc.style = CS_HREDRAW | CS_VREDRAW;
+       wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
+    wc.lpfnWndProc = DefWindowProc;
+    wc.hInstance = hInstance;
+    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
+    wc.lpszClassName = _T("Temporary");
+
+    // register the window class
+    RegisterClassEx(&wc);
+
+    // create the temporary window for OpenGL extension setup.
+    hWnd = CreateWindowEx(WS_EX_APPWINDOW,
+                          _T("Temporary"),    // name of the window class
+                          _T("Temporary"),   // title of the window
+                          WS_OVERLAPPEDWINDOW,    // window style
+                          0,    // x-position of the window
+                          0,    // y-position of the window
+                          640,    // width of the window
+                          480,    // height of the window
+                          NULL,    // we have no parent window, NULL
+                          NULL,    // we aren't using menus, NULL
+                          hInstance,    // application handle
+                          NULL);    // used with multiple windows, NULL
+
+                                                                       // Don't show the window.
+       ShowWindow(hWnd, SW_HIDE);
+
+    InitializeExtensions(hWnd);
+
+       DestroyWindow(hWnd);
+       hWnd = NULL;
+
+       // clear out the window class for use
+       ZeroMemory(&wc, sizeof(WNDCLASSEX));
+
+       // fill in the struct with the needed information
+       wc.cbSize = sizeof(WNDCLASSEX);
+       wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
        wc.lpfnWndProc = WindowProc;
        wc.hInstance = hInstance;
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
-    wc.lpszClassName = _T("WindowClass1");
+       wc.lpszClassName = _T("Hello, Engine!");

        // register the window class
        RegisterClassEx(&wc);

        // create the window and use the result as the handle
-    hWnd = CreateWindowEx(0,
-                          _T("WindowClass1"),    // name of the window class
+       hWnd = CreateWindowEx(WS_EX_APPWINDOW,
+               _T("Hello, Engine!"),    // name of the window class
                _T("Hello, Engine!"),   // title of the window
                WS_OVERLAPPEDWINDOW,    // window style
                300,    // x-position of the window
                300,    // y-position of the window
-                          500,    // width of the window
-                          400,    // height of the window
+               960,    // width of the window
+               540,    // height of the window
                NULL,    // we have no parent window, NULL
                NULL,    // we aren't using menus, NULL
                hInstance,    // application handle
                NULL);    // used with multiple windows, NULL

+    InitializeOpenGL(hWnd, 960, 540, SCREEN_DEPTH, SCREEN_NEAR, true);
+
        // display the window on the screen
        ShowWindow(hWnd, nCmdShow);
+       SetForegroundWindow(hWnd);
+
+    InitializeShader(hWnd, VS_SHADER_SOURCE_FILE, PS_SHADER_SOURCE_FILE);
+    InitializeBuffers();

     // enter the main loop:

@@ -67,6 +1134,10 @@ int WINAPI WinMain(HINSTANCE hInstance,
         DispatchMessage(&msg);
     }

+    ShutdownBuffers();
+    ShutdownShader();
+    FinalizeOpenGL(hWnd);
+
     // return this part of the WM_QUIT message to Windows
     return msg.wParam;
 }
@@ -79,14 +1150,8 @@ LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPara
     {
     case WM_PAINT:
         {
-               PAINTSTRUCT ps;
-               HDC hdc = BeginPaint(hWnd, &ps);
-               RECT rec = { 20, 20, 60, 80 };
-               HBRUSH brush = (HBRUSH) GetStockObject(BLACK_BRUSH);
-
-               FillRect(hdc, &rec, brush);
-
-               EndPaint(hWnd, &ps);
+          Draw();
+                 return 0;
         } break;
         // this message is read when the window is closed
     case WM_DESTROY:

另外新增一个数学头文件(math.h),用以实现一些基本的线性代数计算:

#include <math.h>

#ifndef PI
#define PI 3.14159265358979323846f
#endif

#ifndef TWO_PI
#define TWO_PI 3.14159265358979323846f * 2.0f
#endif

typedef struct VectorType
{
	union {
		struct { float x, y, z; };
		struct { float r, g, b; };
	};
} VectorType;

void MatrixRotationYawPitchRoll(float* matrix, float yaw, float pitch, float roll)
{
	float cYaw, cPitch, cRoll, sYaw, sPitch, sRoll;


	// Get the cosine and sin of the yaw, pitch, and roll.
	cYaw = cosf(yaw);
	cPitch = cosf(pitch);
	cRoll = cosf(roll);

	sYaw = sinf(yaw);
	sPitch = sinf(pitch);
	sRoll = sinf(roll);

	// Calculate the yaw, pitch, roll rotation matrix.
	matrix[0] = (cRoll * cYaw) + (sRoll * sPitch * sYaw);
	matrix[1] = (sRoll * cPitch);
	matrix[2] = (cRoll * -sYaw) + (sRoll * sPitch * cYaw);
	
	matrix[3] = (-sRoll * cYaw) + (cRoll * sPitch * sYaw);
	matrix[4] = (cRoll * cPitch);
	matrix[5] = (sRoll * sYaw) + (cRoll * sPitch * cYaw);
	
	matrix[6] = (cPitch * sYaw);
	matrix[7] = -sPitch;
	matrix[8] = (cPitch * cYaw);

	return;
}

void TransformCoord(VectorType& vector, float* matrix)
{
	float x, y, z;


	// Transform the vector by the 3x3 matrix.
	x = (vector.x * matrix[0]) + (vector.y * matrix[3]) + (vector.z * matrix[6]);
	y = (vector.x * matrix[1]) + (vector.y * matrix[4]) + (vector.z * matrix[7]);
	z = (vector.x * matrix[2]) + (vector.y * matrix[5]) + (vector.z * matrix[8]);

	// Store the result in the reference.
	vector.x = x;
	vector.y = y;
	vector.z = z;

	return;
}

void BuildViewMatrix(VectorType position, VectorType lookAt, VectorType up, float* result)
{
	VectorType zAxis, xAxis, yAxis;
	float length, result1, result2, result3;


	// zAxis = normal(lookAt - position)
	zAxis.x = lookAt.x - position.x;
	zAxis.y = lookAt.y - position.y;
	zAxis.z = lookAt.z - position.z;
	length = sqrt((zAxis.x * zAxis.x) + (zAxis.y * zAxis.y) + (zAxis.z * zAxis.z));
	zAxis.x = zAxis.x / length;
	zAxis.y = zAxis.y / length;
	zAxis.z = zAxis.z / length;

	// xAxis = normal(cross(up, zAxis))
	xAxis.x = (up.y * zAxis.z) - (up.z * zAxis.y);
	xAxis.y = (up.z * zAxis.x) - (up.x * zAxis.z);
	xAxis.z = (up.x * zAxis.y) - (up.y * zAxis.x);
	length = sqrt((xAxis.x * xAxis.x) + (xAxis.y * xAxis.y) + (xAxis.z * xAxis.z));
	xAxis.x = xAxis.x / length;
	xAxis.y = xAxis.y / length;
	xAxis.z = xAxis.z / length;

	// yAxis = cross(zAxis, xAxis)
	yAxis.x = (zAxis.y * xAxis.z) - (zAxis.z * xAxis.y);
	yAxis.y = (zAxis.z * xAxis.x) - (zAxis.x * xAxis.z);
	yAxis.z = (zAxis.x * xAxis.y) - (zAxis.y * xAxis.x);

	// -dot(xAxis, position)
	result1 = ((xAxis.x * position.x) + (xAxis.y * position.y) + (xAxis.z * position.z)) * -1.0f;

	// -dot(yaxis, eye)
	result2 = ((yAxis.x * position.x) + (yAxis.y * position.y) + (yAxis.z * position.z)) * -1.0f;

	// -dot(zaxis, eye)
	result3 = ((zAxis.x * position.x) + (zAxis.y * position.y) + (zAxis.z * position.z)) * -1.0f;

	// Set the computed values in the view matrix.
	result[0]  = xAxis.x;
	result[1]  = yAxis.x;
	result[2]  = zAxis.x;
	result[3]  = 0.0f;

	result[4]  = xAxis.y;
	result[5]  = yAxis.y;
	result[6]  = zAxis.y;
	result[7]  = 0.0f;

	result[8]  = xAxis.z;
	result[9]  = yAxis.z;
	result[10] = zAxis.z;
	result[11] = 0.0f;

	result[12] = result1;
	result[13] = result2;
	result[14] = result3;
	result[15] = 1.0f;
}

void BuildIdentityMatrix(float* matrix)
{
	matrix[0] = 1.0f;
	matrix[1] = 0.0f;
	matrix[2] = 0.0f;
	matrix[3] = 0.0f;

	matrix[4] = 0.0f;
	matrix[5] = 1.0f;
	matrix[6] = 0.0f;
	matrix[7] = 0.0f;

	matrix[8] = 0.0f;
	matrix[9] = 0.0f;
	matrix[10] = 1.0f;
	matrix[11] = 0.0f;

	matrix[12] = 0.0f;
	matrix[13] = 0.0f;
	matrix[14] = 0.0f;
	matrix[15] = 1.0f;

	return;
}


void BuildPerspectiveFovLHMatrix(float* matrix, float fieldOfView, float screenAspect, float screenNear, float screenDepth)
{
	matrix[0] = 1.0f / (screenAspect * tan(fieldOfView * 0.5f));
	matrix[1] = 0.0f;
	matrix[2] = 0.0f;
	matrix[3] = 0.0f;

	matrix[4] = 0.0f;
	matrix[5] = 1.0f / tan(fieldOfView * 0.5f);
	matrix[6] = 0.0f;
	matrix[7] = 0.0f;

	matrix[8] = 0.0f;
	matrix[9] = 0.0f;
	matrix[10] = screenDepth / (screenDepth - screenNear);
	matrix[11] = 1.0f;

	matrix[12] = 0.0f;
	matrix[13] = 0.0f;
	matrix[14] = (-screenNear * screenDepth) / (screenDepth - screenNear);
	matrix[15] = 0.0f;

	return;
}


void MatrixRotationY(float* matrix, float angle)
{
	matrix[0] = cosf(angle);
	matrix[1] = 0.0f;
	matrix[2] = -sinf(angle);
	matrix[3] = 0.0f;

	matrix[4] = 0.0f;
	matrix[5] = 1.0f;
	matrix[6] = 0.0f;
	matrix[7] = 0.0f;

	matrix[8] = sinf(angle);
	matrix[9] = 0.0f;
	matrix[10] = cosf(angle);
	matrix[11] = 0.0f;

	matrix[12] = 0.0f;
	matrix[13] = 0.0f;
	matrix[14] = 0.0f;
	matrix[15] = 1.0f;

	return;
}


void MatrixTranslation(float* matrix, float x, float y, float z)
{
	matrix[0] = 1.0f;
	matrix[1] = 0.0f;
	matrix[2] = 0.0f;
	matrix[3] = 0.0f;

	matrix[4] = 0.0f;
	matrix[5] = 1.0f;
	matrix[6] = 0.0f;
	matrix[7] = 0.0f;

	matrix[8] = 0.0f;
	matrix[9] = 0.0f;
	matrix[10] = 1.0f;
	matrix[11] = 0.0f;

	matrix[12] = x;
	matrix[13] = y;
	matrix[14] = z;
	matrix[15] = 1.0f;

	return;
}


void MatrixRotationZ(float* matrix, float angle)
{
	matrix[0] = cosf(angle);
	matrix[1] = -sinf(angle);
	matrix[2] = 0.0f;
	matrix[3] = 0.0f;

	matrix[4] = sinf(angle);
	matrix[5] = cosf(angle);
	matrix[6] = 0.0f;
	matrix[7] = 0.0f;

	matrix[8] = 0.0f;
	matrix[9] = 0.0f;
	matrix[10] = 1.0f;
	matrix[11] = 0.0f;

	matrix[12] = 0.0f;
	matrix[13] = 0.0f;
	matrix[14] = 0.0f;
	matrix[15] = 1.0f;

	return;
}


void MatrixMultiply(float* result, float* matrix1, float* matrix2)
{
	result[0] = (matrix1[0] * matrix2[0]) + (matrix1[1] * matrix2[4]) + (matrix1[2] * matrix2[8]) + (matrix1[3] * matrix2[12]);
	result[1] = (matrix1[0] * matrix2[1]) + (matrix1[1] * matrix2[5]) + (matrix1[2] * matrix2[9]) + (matrix1[3] * matrix2[13]);
	result[2] = (matrix1[0] * matrix2[2]) + (matrix1[1] * matrix2[6]) + (matrix1[2] * matrix2[10]) + (matrix1[3] * matrix2[14]);
	result[3] = (matrix1[0] * matrix2[3]) + (matrix1[1] * matrix2[7]) + (matrix1[2] * matrix2[11]) + (matrix1[3] * matrix2[15]);

	result[4] = (matrix1[4] * matrix2[0]) + (matrix1[5] * matrix2[4]) + (matrix1[6] * matrix2[8]) + (matrix1[7] * matrix2[12]);
	result[5] = (matrix1[4] * matrix2[1]) + (matrix1[5] * matrix2[5]) + (matrix1[6] * matrix2[9]) + (matrix1[7] * matrix2[13]);
	result[6] = (matrix1[4] * matrix2[2]) + (matrix1[5] * matrix2[6]) + (matrix1[6] * matrix2[10]) + (matrix1[7] * matrix2[14]);
	result[7] = (matrix1[4] * matrix2[3]) + (matrix1[5] * matrix2[7]) + (matrix1[6] * matrix2[11]) + (matrix1[7] * matrix2[15]);

	result[8] = (matrix1[8] * matrix2[0]) + (matrix1[9] * matrix2[4]) + (matrix1[10] * matrix2[8]) + (matrix1[11] * matrix2[12]);
	result[9] = (matrix1[8] * matrix2[1]) + (matrix1[9] * matrix2[5]) + (matrix1[10] * matrix2[9]) + (matrix1[11] * matrix2[13]);
	result[10] = (matrix1[8] * matrix2[2]) + (matrix1[9] * matrix2[6]) + (matrix1[10] * matrix2[10]) + (matrix1[11] * matrix2[14]);
	result[11] = (matrix1[8] * matrix2[3]) + (matrix1[9] * matrix2[7]) + (matrix1[10] * matrix2[11]) + (matrix1[11] * matrix2[15]);

	result[12] = (matrix1[12] * matrix2[0]) + (matrix1[13] * matrix2[4]) + (matrix1[14] * matrix2[8]) + (matrix1[15] * matrix2[12]);
	result[13] = (matrix1[12] * matrix2[1]) + (matrix1[13] * matrix2[5]) + (matrix1[14] * matrix2[9]) + (matrix1[15] * matrix2[13]);
	result[14] = (matrix1[12] * matrix2[2]) + (matrix1[13] * matrix2[6]) + (matrix1[14] * matrix2[10]) + (matrix1[15] * matrix2[14]);
	result[15] = (matrix1[12] * matrix2[3]) + (matrix1[13] * matrix2[7]) + (matrix1[14] * matrix2[11]) + (matrix1[15] * matrix2[15]);

	return;
}

编译方法(使用Visual Studio编译工具包):

D:wenliSourceReposGameEngineFromScratchPlatformWindows>cl /EHsc /Z7 opengl32.lib user32.lib gdi32.lib helloengine_opengl.cpp

编译方法(使用Clang-cl):

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang-cl /EHsc -o helloengine_opengl helloengine_opengl.cpp user32.lib gdi32.lib opengl32.lib

编译方法(使用Clang):

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang -o helloengine_opengl helloengine_opengl.cpp -luser32 -lgdi32 -lopengl32
helloengine_opengl-99755a.o : warning LNK4217: 本地定义的符号 ___std_terminate 在函数 "int `public: virtual __thiscall std::basic_filebuf<char,struct std::char_traits<char> >::~basic_filebuf<char,struct std::char_traits<char> >(void)'::`1'::dtor$8" (?dtor$8@?0???1?$basic_filebuf@DU?$char_traits@D@std@@@std@@UAE@XZ@4HA) 中导入
helloengine_opengl-99755a.o : warning LNK4217: 本地定义的符号 __CxxThrowException@8 在函数 "class std::codecvt<char,char,struct _Mbstatet> const & __cdecl std::use_facet<class std::codecvt<char,char,struct _Mbstatet> >(class std::locale const &)" (??$use_facet@V?$codecvt@DDU_Mbstatet@@@std@@@std@@YAABV?$codecvt@DDU_Mbstatet@@@0@ABVlocale@0@@Z) 中导入

会出两个warning,这是因为我们目前这个代码是Windows平台专用的,在C++的异常模式方面,有一些Clang的小小兼容问题(用Clang-cl /EHsc可以解决这个问题),但是可以无视。

Shader程序(需要放在和源代码一个目录):

color.vs

////////////////////////////////////////////////////////////////////////////////
// Filename: color.vs
////////////////////////////////////////////////////////////////////////////////

#version 400

/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 inputPosition;
in vec3 inputColor;

//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec3 color;

///////////////////////
// UNIFORM VARIABLES //
///////////////////////
uniform mat4 worldMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
	// Calculate the position of the vertex against the world, view, and projection matrices.
	gl_Position = worldMatrix * vec4(inputPosition, 1.0f);
	gl_Position = viewMatrix * gl_Position;
	gl_Position = projectionMatrix * gl_Position;

	// Store the input color for the pixel shader to use.
	color = inputColor;
}

color.ps

////////////////////////////////////////////////////////////////////////////////
// Filename: color.ps
////////////////////////////////////////////////////////////////////////////////
#version 400


/////////////////////
// INPUT VARIABLES //
/////////////////////
in vec3 color;


//////////////////////
// OUTPUT VARIABLES //
//////////////////////
out vec4 outputColor;


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
void main(void)
{
	outputColor = vec4(color, 1.0f);
}

最后的运行效果如下图:

截图工具的关系,动画的颜色比较少,出现明显的色阶:

篇幅关系,代码的说明在后面的文章进行。

从零开始手敲次世代游戏引擎(十二)

上一篇我们在Windows环境下面用D3D绘制了一个在三维空间当中的三角形。

本篇我们在Linux环境当中用OpenGL来绘制一个在三维空间当中的矩形。

本文所用的代码存储在GitHub:article_12这个分支当中。

netwarm007/GameEngineFromScratch

与之前一样,为了便于之后比较差异,考虑图形模块的具体设计,我们尽量留用前面的代码。

首先进入Platform/Linux目录,复制helloengine_xcb.c到helloengine_opengl.cpp。然后作如下变更:

 #include <stdlib.h>
 #include <string.h>
 
+#include <X11/Xlib.h>
+#include <X11/Xlib-xcb.h>
 #include <xcb/xcb.h>
 
+#include <GL/gl.h> 
+#include <GL/glx.h> 
+#include <GL/glu.h>
+
+#define GLX_CONTEXT_MAJOR_VERSION_ARB       0x2091
+#define GLX_CONTEXT_MINOR_VERSION_ARB       0x2092
+typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
+
+// Helper to check for extension string presence.  Adapted from:
+//   http://www.opengl.org/resources/features/OGLextensions/
+static bool isExtensionSupported(const char *extList, const char *extension)
+{
+  const char *start;
+  const char *where, *terminator;
+  
+  /* Extension names should not have spaces. */
+  where = strchr(extension, ' ');
+  if (where || *extension == '')
+    return false;
+
+  /* It takes a bit of care to be fool-proof about parsing the
+     OpenGL extensions string. Don't be fooled by sub-strings,
+     etc. */
+  for (start=extList;;) {
+    where = strstr(start, extension);
+
+    if (!where)
+      break;
+
+    terminator = where + strlen(extension);
+
+    if ( where == start || *(where - 1) == ' ' )
+      if ( *terminator == ' ' || *terminator == '' )
+        return true;
+
+    start = terminator;
+  }
+
+  return false;
+}
+
+static bool ctxErrorOccurred = false;
+static int ctxErrorHandler(Display *dpy, XErrorEvent *ev)
+{
+    ctxErrorOccurred = true;
+    return 0;
+}
+
+void DrawAQuad() {
+    glClearColor(1.0, 1.0, 1.0, 1.0); 
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
+
+    glMatrixMode(GL_PROJECTION); 
+    glLoadIdentity(); 
+    glOrtho(-1., 1., -1., 1., 1., 20.); 
+
+    glMatrixMode(GL_MODELVIEW); 
+    glLoadIdentity(); 
+    gluLookAt(0., 0., 10., 0., 0., 0., 0., 1., 0.); 
+
+    glBegin(GL_QUADS); 
+    glColor3f(1., 0., 0.); 
+    glVertex3f(-.75, -.75, 0.); 
+    glColor3f(0., 1., 0.); 
+    glVertex3f( .75, -.75, 0.); 
+    glColor3f(0., 0., 1.); 
+    glVertex3f( .75, .75, 0.); 
+    glColor3f(1., 1., 0.); 
+    glVertex3f(-.75, .75, 0.); 
+    glEnd(); 
+} 
+
 int main(void) {
     xcb_connection_t    *pConn;
     xcb_screen_t        *pScreen;
@@ -11,41 +85,145 @@ int main(void) {
     xcb_gcontext_t      foreground;
     xcb_gcontext_t      background;
     xcb_generic_event_t *pEvent;
+    xcb_colormap_t colormap;
     uint32_t        mask = 0;
-       uint32_t                values[2];
+    uint32_t        values[3];
     uint8_t         isQuit = 0;
 
-       char title[] = "Hello, Engine!";
+    char title[] = "Hello, Engine![OpenGL]";
     char title_icon[] = "Hello, Engine! (iconified)";
 
+    Display *display;
+    int default_screen;
+    GLXContext context;
+    GLXFBConfig *fb_configs;
+    GLXFBConfig fb_config;
+    int num_fb_configs = 0;
+    XVisualInfo *vi;
+    GLXDrawable drawable;
+    GLXWindow glxwindow;
+    glXCreateContextAttribsARBProc glXCreateContextAttribsARB;
+    const char *glxExts;
+
+    // Get a matching FB config
+    static int visual_attribs[] =
+    {
+      GLX_X_RENDERABLE    , True,
+      GLX_DRAWABLE_TYPE   , GLX_WINDOW_BIT,
+      GLX_RENDER_TYPE     , GLX_RGBA_BIT,
+      GLX_X_VISUAL_TYPE   , GLX_TRUE_COLOR,
+      GLX_RED_SIZE        , 8,
+      GLX_GREEN_SIZE      , 8,
+      GLX_BLUE_SIZE       , 8,
+      GLX_ALPHA_SIZE      , 8,
+      GLX_DEPTH_SIZE      , 24,
+      GLX_STENCIL_SIZE    , 8,
+      GLX_DOUBLEBUFFER    , True,
+      //GLX_SAMPLE_BUFFERS  , 1,
+      //GLX_SAMPLES         , 4,
+      None
+    };
+
+    int glx_major, glx_minor;
+
+    /* Open Xlib Display */ 
+    display = XOpenDisplay(NULL);
+    if(!display)
+    {
+        fprintf(stderr, "Can't open displayn");
+        return -1;
+    }
+
+    // FBConfigs were added in GLX version 1.3.
+    if (!glXQueryVersion(display, &glx_major, &glx_minor) || 
+       ((glx_major == 1) && (glx_minor < 3)) || (glx_major < 1))
+    {
+        fprintf(stderr, "Invalid GLX versionn");
+        return -1;
+    }
+
+    default_screen = DefaultScreen(display);
+
+    /* Query framebuffer configurations */
+    fb_configs = glXChooseFBConfig(display, default_screen, visual_attribs, &num_fb_configs);
+    if(!fb_configs || num_fb_configs == 0)
+    {
+        fprintf(stderr, "glXGetFBConfigs failedn");
+        return -1;
+    }
+
+    /* Pick the FB config/visual with the most samples per pixel */
+    {
+        int best_fbc = -1, worst_fbc = -1, best_num_samp = -1, worst_num_samp = 999;
+
+        for (int i=0; i<num_fb_configs; ++i)
+        {
+            XVisualInfo *vi = glXGetVisualFromFBConfig(display, fb_configs[i]);
+            if (vi)
+            {
+                int samp_buf, samples;
+                glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLE_BUFFERS, &samp_buf);
+                glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLES, &samples);
+      
+                printf( "  Matching fbconfig %d, visual ID 0x%lx: SAMPLE_BUFFERS = %d,"
+                        " SAMPLES = %dn", 
+                        i, vi -> visualid, samp_buf, samples);
+
+                if (best_fbc < 0 || (samp_buf && samples > best_num_samp))
+                    best_fbc = i, best_num_samp = samples;
+                if (worst_fbc < 0 || !samp_buf || samples < worst_num_samp)
+                    worst_fbc = i, worst_num_samp = samples;
+            }
+            XFree( vi );
+        }
+
+        fb_config = fb_configs[best_fbc];
+    }
+
+    /* Get a visual */
+    vi = glXGetVisualFromFBConfig(display, fb_config);
+    printf("Chosen visual ID = 0x%lxn", vi->visualid);
+
     /* establish connection to X server */
-       pConn = xcb_connect(0, 0);
+    pConn = XGetXCBConnection(display);
+    if(!pConn)
+    {
+        XCloseDisplay(display);
+        fprintf(stderr, "Can't get xcb connection from displayn");
+        return -1;
+    }
 
-       /* get the first screen */
-       pScreen = xcb_setup_roots_iterator(xcb_get_setup(pConn)).data;
+    /* Acquire event queue ownership */
+    XSetEventQueueOwner(display, XCBOwnsEventQueue);
+
+    /* Find XCB screen */
+    xcb_screen_iterator_t screen_iter = 
+        xcb_setup_roots_iterator(xcb_get_setup(pConn));
+    for(int screen_num = vi->screen;
+        screen_iter.rem && screen_num > 0;
+        --screen_num, xcb_screen_next(&screen_iter));
+    pScreen = screen_iter.data;
 
     /* get the root window */
     window = pScreen->root;
 
-       /* create black (foreground) graphic context */
-       foreground = xcb_generate_id(pConn);
-       mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
-       values[0] = pScreen->black_pixel;
-       values[1] = 0;
-       xcb_create_gc(pConn, foreground, window, mask, values);
+    /* Create XID's for colormap */
+    colormap = xcb_generate_id(pConn);
 
-       /* create which (background) graphic context */
-       background = xcb_generate_id(pConn);
-       mask = XCB_GC_BACKGROUND | XCB_GC_GRAPHICS_EXPOSURES;
-       values[0] = pScreen->white_pixel;
-       values[1] = 0;
-       xcb_create_gc(pConn, background, window, mask, values);
+    xcb_create_colormap(
+        pConn,
+        XCB_COLORMAP_ALLOC_NONE,
+        colormap,
+        window,
+        vi->visualid 
+        );
 
     /* create window */
     window = xcb_generate_id(pConn);
-       mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
-       values[0] = pScreen->white_pixel;
-       values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
+    mask = XCB_CW_EVENT_MASK  | XCB_CW_COLORMAP;
+    values[0] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
+    values[1] = colormap;
+    values[2] = 0;
     xcb_create_window (pConn,                   /* connection */
                        XCB_COPY_FROM_PARENT,    /* depth */
                        window,                  /* window ID */
@@ -54,9 +232,11 @@ int main(void) {
                        640, 480,                /* width, height */
                        10,                      /* boarder width */
                        XCB_WINDOW_CLASS_INPUT_OUTPUT, /* class */
-                                          pScreen->root_visual,        /* visual */
+                       vi->visualid,            /* visual */
                        mask, values);           /* masks */
 
+    XFree(vi);
+
     /* set the title of the window */
     xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
                 XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
@@ -72,13 +252,120 @@ int main(void) {
 
     xcb_flush(pConn);
 

+    /* Get the default screen's GLX extension list */
+    glxExts = glXQueryExtensionsString(display, default_screen);
+
+    /* NOTE: It is not necessary to create or make current to a context before
+       calling glXGetProcAddressARB */
+    glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
+           glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );
+
+    /* Create OpenGL context */
+    ctxErrorOccurred = false;
+    int (*oldHandler)(Display*, XErrorEvent*) =
+        XSetErrorHandler(&ctxErrorHandler);
+
+    if (!isExtensionSupported(glxExts, "GLX_ARB_create_context") ||
+       !glXCreateContextAttribsARB )
+    {
+        printf( "glXCreateContextAttribsARB() not found"
+            " ... using old-style GLX contextn" );
+        context = glXCreateNewContext(display, fb_config, GLX_RGBA_TYPE, 0, True);
+        if(!context)
+        {
+            fprintf(stderr, "glXCreateNewContext failedn");
+            return -1;
+        }
+    }
+    else
+    {
+        int context_attribs[] =
+          {
+            GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
+            GLX_CONTEXT_MINOR_VERSION_ARB, 0,
+            None
+          };
+
+        printf( "Creating contextn" );
+        context = glXCreateContextAttribsARB(display, fb_config, 0,
+                                          True, context_attribs );
+
+        XSync(display, False);
+        if (!ctxErrorOccurred && context)
+          printf( "Created GL 3.0 contextn" );
+        else
+        {
+          /* GLX_CONTEXT_MAJOR_VERSION_ARB = 1 */
+          context_attribs[1] = 1;
+          /* GLX_CONTEXT_MINOR_VERSION_ARB = 0 */
+          context_attribs[3] = 0;
+
+          ctxErrorOccurred = false;
+
+          printf( "Failed to create GL 3.0 context"
+                  " ... using old-style GLX contextn" );
+          context = glXCreateContextAttribsARB(display, fb_config, 0, 
+                                            True, context_attribs );
+        }
+    }
+
+    XSync(display, False);
+
+    XSetErrorHandler(oldHandler);
+
+    if (ctxErrorOccurred || !context)
+    {
+        printf( "Failed to create an OpenGL contextn" );
+        return -1;
+    }
+
+    /* Verifying that context is a direct context */
+    if (!glXIsDirect (display, context))
+    {
+        printf( "Indirect GLX rendering context obtainedn" );
+    }
+    else
+    {
+        printf( "Direct GLX rendering context obtainedn" );
+    }
+
+    /* Create GLX Window */
+    glxwindow = 
+            glXCreateWindow(
+                display,
+                fb_config,
+                window,
+                0
+                );
+
+    if(!window)
+    {
+        xcb_destroy_window(pConn, window);
+        glXDestroyContext(display, context);
+
+        fprintf(stderr, "glXDestroyContext failedn");
+        return -1;
+    }
+
+    drawable = glxwindow;
+
+    /* make OpenGL context current */
+    if(!glXMakeContextCurrent(display, drawable, drawable, context))
+    {
+        xcb_destroy_window(pConn, window);
+        glXDestroyContext(display, context);
+
+        fprintf(stderr, "glXMakeContextCurrent failedn");
+        return -1;
+    }
+
+
-       while((pEvent = xcb_wait_for_event(pConn)) && !isQuit) {
+    while(!isQuit && (pEvent = xcb_wait_for_event(pConn))) {
         switch(pEvent->response_type & ~0x80) {
         case XCB_EXPOSE:
             {       
-                       xcb_rectangle_t rect = { 20, 20, 60, 80 };
-                       xcb_poly_fill_rectangle(pConn, window, foreground, 1, &rect);
-                       xcb_flush(pConn);
+                DrawAQuad();
+                glXSwapBuffers(display, drawable);
             }
             break;
         case XCB_KEY_PRESS:
@@ -88,6 +375,8 @@ int main(void) {
         free(pEvent);
     }
 
+
+    /* Cleanup */
     xcb_disconnect(pConn);
 
     return 0;

几个要点:

首先,在X(当前版本:11)环境当中,就如我们之前所说的,它是被设计为C-S架构,而显卡是被XServer所隐蔽的,所以如果遵从这个架构,我们是不能直接访问显卡,而需要通过一个被称为GLX的X扩展库,将3D绘图指令以X协议扩展的方式发给X Server,然后X Server再发送给显卡。

但是这样的架构对于软实时系统的游戏来说,其实是过于复杂。所以在2008年开始,X导入了DRI架构,也就是对于本地渲染的情况,可以将OpenGL指令直接发给显卡驱动,而不需要经过X。后来又出现了DRI2等。具体细节请参考下面的链接。

GLX – Wikipedia

但是,GLX这个库在书写的时候还没有XCB,所以它是牢牢绑定Xlib的。而XCB和Xlib是一种替代关系。

所以,在基于XCB的GLX出来之前,我们不得不同时使用XCB和Xlib。用XCB来创建和管理基本的X窗口,而用Xlib + GLX来创建OpenGL相关的图形资源。这就是我们在代码里加了很多头文件的原因。

在代码当中,我们首先给出了我们想要的FrameBuffer(就是用来保存渲染结果并最终生成显示图像的内存上的一片区域)的格式:

    // Get a matching FB config
    static int visual_attribs[] =
    {
      GLX_X_RENDERABLE    , True,
      GLX_DRAWABLE_TYPE   , GLX_WINDOW_BIT,
      GLX_RENDER_TYPE     , GLX_RGBA_BIT,
      GLX_X_VISUAL_TYPE   , GLX_TRUE_COLOR,
      GLX_RED_SIZE        , 8,
      GLX_GREEN_SIZE      , 8,
      GLX_BLUE_SIZE       , 8,
      GLX_ALPHA_SIZE      , 8,
      GLX_DEPTH_SIZE      , 24,
      GLX_STENCIL_SIZE    , 8,
      GLX_DOUBLEBUFFER    , True,
      //GLX_SAMPLE_BUFFERS  , 1,
      //GLX_SAMPLES         , 4,
      None
    };

接下来的代码是罗列出缺省显示器所支持的符合上述条件的所有FrameBuffer格式,然后选择一个最好的(采样数最多的):

    /* Query framebuffer configurations */
    fb_configs = glXChooseFBConfig(display, default_screen, visual_attribs, &num_fb_configs);
    if(!fb_configs || num_fb_configs == 0)
    {
        fprintf(stderr, "glXGetFBConfigs failedn");
        return -1;
    }

    /* Pick the FB config/visual with the most samples per pixel */
    {
        int best_fbc = -1, worst_fbc = -1, best_num_samp = -1, worst_num_samp = 999;

        for (int i=0; i<num_fb_configs; ++i)
        {
            XVisualInfo *vi = glXGetVisualFromFBConfig(display, fb_configs[i]);
            if (vi)
            {
                int samp_buf, samples;
                glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLE_BUFFERS, &samp_buf);
                glXGetFBConfigAttrib(display, fb_configs[i], GLX_SAMPLES, &samples);

                printf( "  Matching fbconfig %d, visual ID 0x%lx: SAMPLE_BUFFERS = %d,"
                        " SAMPLES = %dn",
                        i, vi -> visualid, samp_buf, samples);

                if (best_fbc < 0 || (samp_buf && samples > best_num_samp))
                    best_fbc = i, best_num_samp = samples;
                if (worst_fbc < 0 || !samp_buf || samples < worst_num_samp)
                    worst_fbc = i, worst_num_samp = samples;
            }
            XFree( vi );
        }

        fb_config = fb_configs[best_fbc];
    }

因为上面都是通过Xlib进行的操作,但是我们要使用XCB来创建窗口并管理窗口,所以接下来做了一个同步,让XCB和Xlib都指向同一块屏幕(FrameBuffer)

    /* establish connection to X server */
    pConn = XGetXCBConnection(display);
    if(!pConn)
    {
        XCloseDisplay(display);
        fprintf(stderr, "Can't get xcb connection from displayn");
        return -1;
    }

    /* Acquire event queue ownership */
    XSetEventQueueOwner(display, XCBOwnsEventQueue);

    /* Find XCB screen */
    xcb_screen_iterator_t screen_iter =
        xcb_setup_roots_iterator(xcb_get_setup(pConn));
    for(int screen_num = vi->screen;
        screen_iter.rem && screen_num > 0;
        --screen_num, xcb_screen_next(&screen_iter));
    pScreen = screen_iter.data;

然后我们通过XCB创建窗体,这里和(九)是基本完全一样的。

再通过Xlib+GLX来创建这个窗体当中的OpenGL绘图上下文(Context)。这里取代的是(九)当中的foreground和background。这里的代码看起来稍微有些复杂,因为在OpenGL 3.0之前(不含)的版本与之后的版本的创建方法是不一样的。当然我们可以按照低版本创建,但是版本越低,能够使用的OpenGL功能就越少。所以我们的代码进行了一些版本的探查,并根据探查结果选择最好的创建方式:

    /* Get the default screen's GLX extension list */
    glxExts = glXQueryExtensionsString(display, default_screen);

    /* NOTE: It is not necessary to create or make current to a context before
       calling glXGetProcAddressARB */
    glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
           glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );

    /* Create OpenGL context */
    ctxErrorOccurred = false;
    int (*oldHandler)(Display*, XErrorEvent*) =
        XSetErrorHandler(&ctxErrorHandler);

    if (!isExtensionSupported(glxExts, "GLX_ARB_create_context") ||
       !glXCreateContextAttribsARB )
    {
        printf( "glXCreateContextAttribsARB() not found"
            " ... using old-style GLX contextn" );
        context = glXCreateNewContext(display, fb_config, GLX_RGBA_TYPE, 0, True);
        if(!context)
        {
            fprintf(stderr, "glXCreateNewContext failedn");
            return -1;
        }
    }
    else
    {
        int context_attribs[] =
          {
            GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
            GLX_CONTEXT_MINOR_VERSION_ARB, 0,
            None
          };

        printf( "Creating contextn" );
        context = glXCreateContextAttribsARB(display, fb_config, 0,
                                          True, context_attribs );

        XSync(display, False);
        if (!ctxErrorOccurred && context)
          printf( "Created GL 3.0 contextn" );
        else
        {
          /* GLX_CONTEXT_MAJOR_VERSION_ARB = 1 */
          context_attribs[1] = 1;
          /* GLX_CONTEXT_MINOR_VERSION_ARB = 0 */
          context_attribs[3] = 0;

          ctxErrorOccurred = false;

          printf( "Failed to create GL 3.0 context"
                  " ... using old-style GLX contextn" );
          context = glXCreateContextAttribsARB(display, fb_config, 0,
                                            True, context_attribs );
        }
    }

    XSync(display, False);

    XSetErrorHandler(oldHandler);

    if (ctxErrorOccurred || !context)
    {
        printf( "Failed to create an OpenGL contextn" );
        return -1;
    }

然后为了让GLX能够使用我们通过XCB创建出来的窗口,我们对窗口进行了一次转换,让它也绑定到GLX的对象当中:

    /* Create GLX Window */
    glxwindow =
            glXCreateWindow(
                display,
                fb_config,
                window,
                0
                );

    if(!window)
    {
        xcb_destroy_window(pConn, window);
        glXDestroyContext(display, context);

        fprintf(stderr, "glXDestroyContext failedn");
        return -1;
    }

通知OpenGL(显卡)画布的位置:

    drawable = glxwindow;

    /* make OpenGL context current */
    if(!glXMakeContextCurrent(display, drawable, drawable, context))
    {
        xcb_destroy_window(pConn, window);
        glXDestroyContext(display, context);

        fprintf(stderr, "glXMakeContextCurrent failedn");
        return -1;
    }

之后用XCB处理窗体消息队列,并在XCB_EXPOSE消息处理流程当中,使用OpenGL函数完成绘图。

    while(!isQuit && (pEvent = xcb_wait_for_event(pConn))) {
        switch(pEvent->response_type & ~0x80) {
        case XCB_EXPOSE:
            {
                DrawAQuad();
                glXSwapBuffers(display, drawable);
            }
            break;
        case XCB_KEY_PRESS:
            isQuit = 1;
            break;
        }
        free(pEvent);
    }

这个程序的编译命令行如下:

[tim@localhost Linux]$ clang -lxcb -lX11 -lX11-xcb -lGL -lGLU -o helloengine_opengl helloengine_opengl.cpp

需要事先用apt或者yum安装libGL-dev,libGLU-dev, libX11-dev,libX11-xcb-dev,libxcb-dev。注意在不同的发行版本当中包的名字会稍有不同。

如果需要调试,则需要增加一个“-g”选项。然后使用gdb进行调试。

运行结果如下:

对比前一篇的Direct 3D,我们可以看到我们并没有提供任何的Shader程序。实际绘图的指令也仅仅是如下数行,比Direct 3D的一连串API调用要简洁明了许多。这就是我们之前提到过的,OpenGL是一种比较高层的封装,它让我们集中在要绘制的内容本身的同时,也隐藏了很多实际的处理。对于CAD、科学仿真等领域来说十分好用,但是对于更为复杂的应用来说,特别是游戏这种需要做深度优化的图形运用来讲,就显得有些封装过头了(当然,我们这里使用的是最简单的固定管道的OpenGL。OpenGL高版本也是支持GPU编程的,这个在后续介绍):

void DrawAQuad() {
    glClearColor(1.0, 1.0, 1.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1., 1., -1., 1., 1., 20.);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0., 0., 10., 0., 0., 0., 0., 1., 0.);

    glBegin(GL_QUADS);
    glColor3f(1., 0., 0.);
    glVertex3f(-.75, -.75, 0.);
    glColor3f(0., 1., 0.);
    glVertex3f( .75, -.75, 0.);
    glColor3f(0., 0., 1.);
    glVertex3f( .75, .75, 0.);
    glColor3f(1., 1., 0.);
    glVertex3f(-.75, .75, 0.);
    glEnd();
}

从零开始手敲次世代游戏引擎(十一)

上一篇我们用Direct 2D绘制了一个平面图形。接下来我们用Direct 3D绘制一个3D图形。

首先我们还是通过复制的方法重用我们的代码。拷贝helloengine_d2d.cpp到helloengine_d3d.cpp。然后作如下修改(本文使用的是D3D 11接口,因为直接上D3D 12的话学习难度坡度太大):

@@ -2,13 +2,39 @@
 #include <windows.h>
 #include <windowsx.h>
 #include <tchar.h>
+#include <stdint.h>

-#include <d2d1.h>
+#include <d3d11.h>
+#include <d3d11_1.h>
+#include <d3dcompiler.h>
+#include <DirectXMath.h>
+#include <DirectXPackedVector.h>
+#include <DirectXColors.h>

-ID2D1Factory                   *pFactory = nullptr;
-ID2D1HwndRenderTarget  *pRenderTarget = nullptr;
-ID2D1SolidColorBrush   *pLightSlateGrayBrush = nullptr;
-ID2D1SolidColorBrush   *pCornflowerBlueBrush = nullptr;
+using namespace DirectX;
+using namespace DirectX::PackedVector;
+
+const uint32_t SCREEN_WIDTH  =  960;
+const uint32_t SCREEN_HEIGHT =  480;
+
+// global declarations
+IDXGISwapChain          *g_pSwapchain = nullptr;              // the pointer to the swap chain interface
+ID3D11Device            *g_pDev       = nullptr;              // the pointer to our Direct3D device interface
+ID3D11DeviceContext     *g_pDevcon    = nullptr;              // the pointer to our Direct3D device context
+
+ID3D11RenderTargetView  *g_pRTView    = nullptr;
+
+ID3D11InputLayout       *g_pLayout    = nullptr;              // the pointer to the input layout
+ID3D11VertexShader      *g_pVS        = nullptr;              // the pointer to the vertex shader
+ID3D11PixelShader       *g_pPS        = nullptr;              // the pointer to the pixel shader
+
+ID3D11Buffer            *g_pVBuffer   = nullptr;              // Vertex Buffer
+
+// vertex buffer structure
+struct VERTEX {
+        XMFLOAT3    Position;
+        XMFLOAT4    Color;
+};

 template<class T>
 inline void SafeRelease(T **ppInterfaceToRelease)
@@ -21,32 +47,164 @@ inline void SafeRelease(T **ppInterfaceToRelease)
     }
 }

+void CreateRenderTarget() {
+    HRESULT hr;
+    ID3D11Texture2D *pBackBuffer;
+
+    // Get a pointer to the back buffer
+    g_pSwapchain->GetBuffer( 0, __uuidof( ID3D11Texture2D ),
+                                 ( LPVOID* )&pBackBuffer );
+
+    // Create a render-target view
+    g_pDev->CreateRenderTargetView( pBackBuffer, NULL,
+                                          &g_pRTView );
+    pBackBuffer->Release();
+
+    // Bind the view
+    g_pDevcon->OMSetRenderTargets( 1, &g_pRTView, NULL );
+}
+
+void SetViewPort() {
+    D3D11_VIEWPORT viewport;
+    ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));
+
+    viewport.TopLeftX = 0;
+    viewport.TopLeftY = 0;
+    viewport.Width = SCREEN_WIDTH;
+    viewport.Height = SCREEN_HEIGHT;
+
+    g_pDevcon->RSSetViewports(1, &viewport);
+}
+
+// this is the function that loads and prepares the shaders
+void InitPipeline() {
+    // load and compile the two shaders
+    ID3DBlob *VS, *PS;
+
+    D3DReadFileToBlob(L"copy.vso", &VS);
+    D3DReadFileToBlob(L"copy.pso", &PS);
+
+    // encapsulate both shaders into shader objects
+    g_pDev->CreateVertexShader(VS->GetBufferPointer(), VS->GetBufferSize(), NULL, &g_pVS);
+    g_pDev->CreatePixelShader(PS->GetBufferPointer(), PS->GetBufferSize(), NULL, &g_pPS);
+
+    // set the shader objects
+    g_pDevcon->VSSetShader(g_pVS, 0, 0);
+    g_pDevcon->PSSetShader(g_pPS, 0, 0);
+
+    // create the input layout object
+    D3D11_INPUT_ELEMENT_DESC ied[] =
     {
+        {"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},
+    };


+    g_pDev->CreateInputLayout(ied, 2, VS->GetBufferPointer(), VS->GetBufferSize(), &g_pLayout);
+    g_pDevcon->IASetInputLayout(g_pLayout);

+    VS->Release();
+    PS->Release();
+}

+// this is the function that creates the shape to render
+void InitGraphics() {
+    // create a triangle using the VERTEX struct
+    VERTEX OurVertices[] =
+    {
+        {XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f)},
+        {XMFLOAT3(0.45f, -0.5, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f)},
+        {XMFLOAT3(-0.45f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f)}
+    };

+    // create the vertex buffer
+    D3D11_BUFFER_DESC bd;
+    ZeroMemory(&bd, sizeof(bd));

+    bd.Usage = D3D11_USAGE_DYNAMIC;                // write access access by CPU and GPU
+    bd.ByteWidth = sizeof(VERTEX) * 3;             // size is the VERTEX struct * 3
+    bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;       // use as a vertex buffer
+    bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;    // allow CPU to write in buffer
+
+    g_pDev->CreateBuffer(&bd, NULL, &g_pVBuffer);       // create the buffer
+
+    // copy the vertices into the buffer
+    D3D11_MAPPED_SUBRESOURCE ms;
+    g_pDevcon->Map(g_pVBuffer, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);    // map the buffer
+    memcpy(ms.pData, OurVertices, sizeof(VERTEX) * 3);                       // copy the data
+    g_pDevcon->Unmap(g_pVBuffer, NULL);                                      // unmap the buffer
+}
+
+// this function prepare graphic resources for use
HRESULT CreateGraphicsResources(HWND hWnd)
{
    HRESULT hr = S_OK;
-    if (pRenderTarget == nullptr)
-    {
-        RECT rc;
-        GetClientRect(hWnd, &rc);
-        D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,
-                        rc.bottom - rc.top);
-        hr = pFactory->CreateHwndRenderTarget(
-            D2D1::RenderTargetProperties(),
-            D2D1::HwndRenderTargetProperties(hWnd, size),
-            &pRenderTarget);
-        if (SUCCEEDED(hr))
-        {
-            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);
-        }

-        if (SUCCEEDED(hr))
-        {
-            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);
+    if (g_pSwapchain == nullptr)
+    {
+        // create a struct to hold information about the swap chain
+        DXGI_SWAP_CHAIN_DESC scd;
+
+        // clear out the struct for use
+        ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
+
+        // fill the swap chain description struct
+        scd.BufferCount = 1;                                    // one back buffer
+        scd.BufferDesc.Width = SCREEN_WIDTH;
+        scd.BufferDesc.Height = SCREEN_HEIGHT;
+        scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;     // use 32-bit color
+        scd.BufferDesc.RefreshRate.Numerator = 60;
+        scd.BufferDesc.RefreshRate.Denominator = 1;
+        scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;      // how swap chain is to be used
+        scd.OutputWindow = hWnd;                                // the window to be used
+        scd.SampleDesc.Count = 4;                               // how many multisamples
+        scd.Windowed = TRUE;                                    // windowed/full-screen mode
+        scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;     // allow full-screen switching
+
+        const D3D_FEATURE_LEVEL FeatureLevels[] = { D3D_FEATURE_LEVEL_11_1,
+                                                    D3D_FEATURE_LEVEL_11_0,
+                                                    D3D_FEATURE_LEVEL_10_1,
+                                                    D3D_FEATURE_LEVEL_10_0,
+                                                    D3D_FEATURE_LEVEL_9_3,
+                                                    D3D_FEATURE_LEVEL_9_2,
+                                                    D3D_FEATURE_LEVEL_9_1};
+        D3D_FEATURE_LEVEL FeatureLevelSupported;
+
+        HRESULT hr = S_OK;
+
+        // create a device, device context and swap chain using the information in the scd struct
+        hr = D3D11CreateDeviceAndSwapChain(NULL,
+                                      D3D_DRIVER_TYPE_HARDWARE,
+                                      NULL,
+                                      0,
+                                      FeatureLevels,
+                                      _countof(FeatureLevels),
+                                      D3D11_SDK_VERSION,
+                                      &scd,
+                                      &g_pSwapchain,
+                                      &g_pDev,
+                                      &FeatureLevelSupported,
+                                      &g_pDevcon);
+
+        if (hr == E_INVALIDARG) {
+            hr = D3D11CreateDeviceAndSwapChain(NULL,
+                                      D3D_DRIVER_TYPE_HARDWARE,
+                                      NULL,
+                                      0,
+                                      &FeatureLevelSupported,
+                                      1,
+                                      D3D11_SDK_VERSION,
+                                      &scd,
+                                      &g_pSwapchain,
+                                      &g_pDev,
+                                      NULL,
+                                      &g_pDevcon);
+        }
+
+        if (hr == S_OK) {
+            CreateRenderTarget();
+            SetViewPort();
+            InitPipeline();
+            InitGraphics();
         }
     }
     return hr;
@@ -54,11 +212,40 @@ HRESULT CreateGraphicsResources(HWND hWnd)

 void DiscardGraphicsResources()
 {
-    SafeRelease(&pRenderTarget);
-    SafeRelease(&pLightSlateGrayBrush);
-    SafeRelease(&pCornflowerBlueBrush);
+    SafeRelease(&g_pLayout);
+    SafeRelease(&g_pVS);
+    SafeRelease(&g_pPS);
+    SafeRelease(&g_pVBuffer);
+    SafeRelease(&g_pSwapchain);
+    SafeRelease(&g_pRTView);
+    SafeRelease(&g_pDev);
+    SafeRelease(&g_pDevcon);
 }

+// this is the function used to render a single frame
+void RenderFrame()
+{
+    // clear the back buffer to a deep blue
+    const FLOAT clearColor[] = {0.0f, 0.2f, 0.4f, 1.0f};
+    g_pDevcon->ClearRenderTargetView(g_pRTView, clearColor);
+
+    // do 3D rendering on the back buffer here
+    {
+        // select which vertex buffer to display
+        UINT stride = sizeof(VERTEX);
+        UINT offset = 0;
+        g_pDevcon->IASetVertexBuffers(0, 1, &g_pVBuffer, &stride, &offset);
+
+        // select which primtive type we are using
+        g_pDevcon->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+        // draw the vertex buffer to the back buffer
+        g_pDevcon->Draw(3, 0);
+    }
+
+    // swap the back buffer and the front buffer
+    g_pSwapchain->Present(0, 0);
+}

 // the WindowProc function prototype
 LRESULT CALLBACK WindowProc(HWND hWnd,
@@ -77,9 +264,6 @@ int WINAPI WinMain(HINSTANCE hInstance,
     // this struct holds information for the window class
     WNDCLASSEX wc;

-    // initialize COM
-    if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;
-
     // clear out the window class for use
     ZeroMemory(&wc, sizeof(WNDCLASSEX));

@@ -97,17 +281,17 @@ int WINAPI WinMain(HINSTANCE hInstance,

     // create the window and use the result as the handle
     hWnd = CreateWindowEx(0,
-                          _T("WindowClass1"),    // name of the window class
-                          _T("Hello, Engine![Direct 2D]"),   // title of the window
-                          WS_OVERLAPPEDWINDOW,    // window style
-                          100,    // x-position of the window
-                          100,    // y-position of the window
-                          960,    // width of the window
-                          540,    // height of the window
-                          NULL,    // we have no parent window, NULL
-                          NULL,    // we aren't using menus, NULL
-                          hInstance,    // application handle
-                          NULL);    // used with multiple windows, NULL
+                          _T("WindowClass1"),                   // name of the window class
+                          _T("Hello, Engine![Direct 3D]"),      // title of the window
+                          WS_OVERLAPPEDWINDOW,                  // window style
+                          100,                                  // x-position of the window
+                          100,                                  // y-position of the window
+                          SCREEN_WIDTH,                         // width of the window
+                          SCREEN_HEIGHT,                        // height of the window
+                          NULL,                                 // we have no parent window, NULL
+                          NULL,                                 // we aren't using menus, NULL
+                          hInstance,                            // application handle
+                          NULL);                                // used with multiple windows, NULL

     // display the window on the screen
     ShowWindow(hWnd, nCmdShow);
@@ -127,9 +311,6 @@ int WINAPI WinMain(HINSTANCE hInstance,
         DispatchMessage(&msg);
     }

-    // uninitialize COM
-    CoUninitialize();
-
     // return this part of the WM_QUIT message to Windows
     return msg.wParam;
 }
@@ -144,108 +325,27 @@ LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPara
     switch(message)
     {
        case WM_CREATE:
-               if (FAILED(D2D1CreateFactory(
-                                       D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
-               {
-                       result = -1; // Fail CreateWindowEx.
-               }
                wasHandled = true;
-        result = 1;
         break;

        case WM_PAINT:
-           {
-                       HRESULT hr = CreateGraphicsResources(hWnd);
-                       if (SUCCEEDED(hr))
-                       {
-                               PAINTSTRUCT ps;
-                               BeginPaint(hWnd, &ps);
-
-                               // start build GPU draw command
-                               pRenderTarget->BeginDraw();
-
-                               // clear the background with white color
-                               pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
-
-                // retrieve the size of drawing area
-                D2D1_SIZE_F rtSize = pRenderTarget->GetSize();
-
-                // draw a grid background.
-                int width = static_cast<int>(rtSize.width);
-                int height = static_cast<int>(rtSize.height);
-
-                for (int x = 0; x < width; x += 10)
-                {
-                    pRenderTarget->DrawLine(
-                        D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
-                        D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
-                        pLightSlateGrayBrush,
-                        0.5f
-                        );
-                }
-
-                for (int y = 0; y < height; y += 10)
-                {
-                    pRenderTarget->DrawLine(
-                        D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
-                        D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
-                        pLightSlateGrayBrush,
-                        0.5f
-                        );
-                }
-
-                // draw two rectangles
-                D2D1_RECT_F rectangle1 = D2D1::RectF(
-                     rtSize.width/2 - 50.0f,
-                     rtSize.height/2 - 50.0f,
-                     rtSize.width/2 + 50.0f,
-                     rtSize.height/2 + 50.0f
-                     );
-
-                 D2D1_RECT_F rectangle2 = D2D1::RectF(
-                     rtSize.width/2 - 100.0f,
-                     rtSize.height/2 - 100.0f,
-                     rtSize.width/2 + 100.0f,
-                     rtSize.height/2 + 100.0f
-                     );
-
-                // draw a filled rectangle
-                pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);
-
-                // draw a outline only rectangle
-                pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);
-
-                               // end GPU draw command building
-                               hr = pRenderTarget->EndDraw();
-                               if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
-                               {
-                                       DiscardGraphicsResources();
-                               }
-
-                               EndPaint(hWnd, &ps);
-                       }
-           }
+               result = CreateGraphicsResources(hWnd);
+               RenderFrame();
                wasHandled = true;
         break;

        case WM_SIZE:
-               if (pRenderTarget != nullptr)
+               if (g_pSwapchain != nullptr)
                {
-                       RECT rc;
-                       GetClientRect(hWnd, &rc);
-
-                       D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
-
-                       pRenderTarget->Resize(size);
+                   DiscardGraphicsResources();
                }
                wasHandled = true;
         break;

        case WM_DESTROY:
                DiscardGraphicsResources();
-               if (pFactory) {pFactory->Release(); pFactory=nullptr; }
                PostQuitMessage(0);
-        result = 1;
                wasHandled = true;
         break;

简单解释一下:

(关于D3D编程的系统教程在这里:Getting Started with Direct3D

基本窗体创建没有任何变化。因为不是COM,不需要创建Factory。(需要考证。从结果上来说是这样,但是这里不需要COM相关的代码的原因应该是D3D11的库当中已经进行了相关的处理。因为D3D12明显是COM)所以WM_CREATE里面基本不做任何事情。

新加了如下几个函数(子过程):

         CreateRenderTarget();
            SetViewPort();
            InitPipeline();
            InitGraphics();

第一个依然是我们熟悉的,创建RenderTarget,也就是画布。

第二个是设置视口。也就是设置渲染结果在画布当中的映射。我们目前是将整个画布都分配给了一个视口。在实际的游戏开发当中,会有多人分屏游玩的模式。这个时候就需要把一张画布分割成好几个视口。另外一个典型的运用就是VR。VR需要绘制左眼和右眼两幅图像,因此也需要将画布分割为两个视口。

第三个是初始化渲染管道。渲染管道就是GPU的工作流水线。使用GPU进行3D渲染的时候,最一般的会有顶点变换,像素化和像素填色这3个阶段。在这个过程当中,顶点变换和填色是可以编程的(而像素化是硬件固定功能,不可编程)。在这个初始化函数里面,我们可以看到我们从磁盘读取了两个GPU用的程序,一个叫“copy.vso”,一个叫”copy.pso”。它们分别对应着GPU的顶点变换阶段(Vertex Shading)和像素填色阶段(Pixel Shading)。这两个程序是我们编写的,使用的语言是HLSL(这是一种类似C语言的,微软推出的GPU编程语言)。具体内容在下面说明。

第四个则是传入实际要绘制的模型的顶点信息了。我们这里绘制的是一个三角形,因此有3个顶点。注意在D3D当中,使用的坐标系为左手坐标系,就是x轴向右,y轴向上,z轴指向屏幕里面。(这点很特别,今后写别的图形API的时候就有比较)

Coordinate Systems (Direct3D 9)

我们的顶点结构是这样的:

// vertex buffer structure
struct VERTEX {
        XMFLOAT3    Position;
        XMFLOAT4    Color;
};

因此,我们是这样初始化顶点的:

 // create a triangle using the VERTEX struct
    VERTEX OurVertices[] =
    {
        {XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f)},
        {XMFLOAT3(0.45f, -0.5, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f)},
        {XMFLOAT3(-0.45f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f)}
    };

坐标系的原点在视口的中心,因为我们只有一个视口,所以就是屏幕中心。视口在各个坐标轴的缺省的范围是[-1, 1],因此0.5差不多正好是在画布长宽各1/4的地方。第二个部分是颜色。分别对应R(红色通道)G(绿色通道)B(蓝色通道)和A(透明通道)。这个顺序是在代码的layout部分指定的。范围是[0, 1]。所以我们可以看到

  1. 第一个顶点是上方中央,红色
  2. 第二个顶点是下方右侧,绿色
  3. 第三个顶点是下方左侧,蓝色

然后我们来看我们所写的GPU程序(Shader,称为着色器)

copy.vs

#include "cbuffer.h"
#include "vsoutput.hs"

v2p main(a2v input) {
	v2p output;
	output.position = float4(input.position, 1.0);
	output.color = input.color;

	return output;
}

看到这个程序基本就是将输入原样输出。其中输入是来自我们的应用程序(就是我们上面定义的VERTEX),而输出是输出给流水线的下一个步骤。在我们这个例子里面,就是像素着色器:

copy.ps

#include "vsoutput.hs"

float4 main(v2p input) : SV_TARGET
{
    return input.color;
}

而像素着色器也只是原样输出输入的颜色。

注意到我们用到了两个头文件。一个是定义应用程序传给Vertex Shader的数据结构,一个是定义Vertex Shader输出给Pixel Shader的数据结构。内容如下:

cbuffer.h

struct a2v {
	float3 position : POSITION;
	float4 color	: COLOR;
};

vsoutput.hs

struct v2p {
	float4 position : SV_POSITION;
	float4 color	: COLOR;
};

我们可以看到,这里面有一些奇怪的,标准C/C++不支持的东西。就是冒号后面的那些东西。这些东西是用来将变量和GPU的寄存器进行绑定的。因为GPU并不是全部可编程的,整个处理流水线当中混杂着可编程的环节和不可编程的环节。因此,当我们的输出要提供给不可编程的环节使用的时候(比如Vertex Shader的输出当中的position会被像素化模块用来插值计算三角形内部的点的坐标;比如Pixel Shader输出的color会被GPU的显示输出模块用来输出画面),就需要将这些变量绑定到一些事先定义好的寄存器当中去。

具体细节,请参考HLSL教程:

HLSL (Windows)

以及GPU渲染管道的说明:

en.m.wikipedia.org/wiki

在Windows当中,编译Shader(D3D规格)的方法如下:

fxc /T vs_5_0 /Zi /Fo copy.vso copy.vs
fxc /T ps_5_0 /Zi /Fo copy.pso copy.ps

如果找不到fxc.exe,那么应该是没有安装DirectX相关的开发包。重新运行Visual Studio选择安装就可以了。

代码编译的方法如下(调试版)(编译出现大量DirectXMath相关的错误的话,继续看下面):

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang-cl -I./DirectXMath/Inc -c -Z7 -o helloengine_d3d.obj helloengine_d3d.cpp
D:wenliSourceReposGameEngineFromScratchPlatformWindows>link -debug user32.lib d3d11.lib d3dcompiler.lib helloengine_d3d.obj

Release版:

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang -I./DirectXMath/Inc -l user32.lib -l d3d11.lib -l d3dcompiler.lib -o helloengine_d3d.exe helloengine_d3d.cpp

Direct 3D工作的方式与Direct 2D不同,不是采用COM的方式,(需要考证。从结果上来说确实不需要链接ole32.lib。但是有可能是D3D11的库里面包括了这一部分)而是直接调用显卡的驱动。所以我们去掉COM相关的初始化代码,链接的时候也去掉ole32.lib。

注意我们这里加入了一个新的头文件目录:./DirectXMath。这是因为目前随Visual Studio安装的(正确来说应该是随Windows SDK安装的)DirectXMath库似乎版本还是比较老的,不支持clang的编译(就是说用了clang所不支持的特性)。所以我们需要从github上面下载一个最新的版本:

D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule add https://github.com/Microsoft/DirectXMath.git DirectXMath
D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule init DirectXMath
D:wenliSourceReposGameEngineFromScratchPlatformWindows>git submodule update DirectXMath

然后重新编译应该就好了。

运行的效果如下:

三角形内部出现了漂亮的过渡色。这是因为我们程序给GPU的只有3个顶点,对于其它的点,GPU是根据它到3个顶点的距离(准确说是重心坐标,就是从被计算的点向3个顶点作辅助线,从而整个3角形被划分为3个小三角形(如果点在三角形边缘的时候会出现面积为0的塌缩三角形),每个小三角性的面积除以原本的大三角形的面积,得到3个[0,1]之间的值,根据这个值加权平均3个顶点的颜色)来进行插值运算的。这种插值运算不仅仅发生在position这个参数当中,也发生在color这个参数当中。所以最终就形成了这么一种结果。

好了。我们已经跨入了3D的殿堂了。保存我们的代码,准备下一个部分。

参考引用:

  1. DirectX 11 Tutorials
  2. Direct3D Tutorials

从零开始手敲次世代游戏引擎(十)

上一篇文章我们分别使用GDI(Windows)和XCB(Linux)在窗体当中绘制了一个矩形。

然而,这些矩形其实是由CPU绘制的,而不是GPU。因此这种绘制方式是很慢的。

本篇开始我们使用GPU完成图形的绘制。首先让我们看一下Windows平台特有的Direct X。

Direct X在早期其实分为几个模块:专门绘制2D图形的DirectDraw(现在改名为Direct2D),专门绘制3D图形的Direct3D,专门用于多媒体播放的DirectShow,等等。

我们首先来看看使用Direct2D来进行绘制的代码是什么样子的。

因为D2D只是提供一种用GPU绘制图形的方式,创建窗口等操作还是和以前一样的,也就是说我们可以重用之前写的helloengine_win.c的大部分代码。另外,为了将来可以很方便的在CPU绘制和GPU绘制之间切换比较,我们应该保留helloengine_win.c。

所以,首先将helloengine_win.c复制一份,命名为helloengine_d2d.cpp。(改为.cpp后缀的原因是我们将要使用的d2d的头文件需要按照C++方式编译)

然后如下修改这个文件(左侧有”+“号的行为新增加的行,”-“的行为删除的行):

(本文代码参考MSDN Direct 2D教程编写)

@@ -3,6 +3,63 @@
 #include <windowsx.h>
 #include <tchar.h>

+#include <d2d1.h>
+
+ID2D1Factory           *pFactory = nullptr;
+ID2D1HwndRenderTarget  *pRenderTarget = nullptr;
+ID2D1SolidColorBrush   *pLightSlateGrayBrush = nullptr;
+ID2D1SolidColorBrush   *pCornflowerBlueBrush = nullptr;
+
+template<class T>
+inline void SafeRelease(T **ppInterfaceToRelease)
+{
+    if (*ppInterfaceToRelease != nullptr)
+    {
+        (*ppInterfaceToRelease)->Release();
+
+        (*ppInterfaceToRelease) = nullptr;
+    }
+}
+
+HRESULT CreateGraphicsResources(HWND hWnd)
+{
+    HRESULT hr = S_OK;
+    if (pRenderTarget == nullptr)
+    {
+        RECT rc;
+        GetClientRect(hWnd, &rc);
+
+        D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,
+                        rc.bottom - rc.top);
+
+        hr = pFactory->CreateHwndRenderTarget(
+            D2D1::RenderTargetProperties(),
+            D2D1::HwndRenderTargetProperties(hWnd, size),
+            &pRenderTarget);
+
+        if (SUCCEEDED(hr))
+        {
+            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);
+
+        }
+
+        if (SUCCEEDED(hr))
+        {
+            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);
+
+        }
+    }
+    return hr;
+}
+
+void DiscardGraphicsResources()
+{
+    SafeRelease(&pRenderTarget);
+    SafeRelease(&pLightSlateGrayBrush);
+    SafeRelease(&pCornflowerBlueBrush);
+}
+
+
 // the WindowProc function prototype
 LRESULT CALLBACK WindowProc(HWND hWnd,
                          UINT message,
@@ -20,6 +77,9 @@ int WINAPI WinMain(HINSTANCE hInstance,
     // this struct holds information for the window class
     WNDCLASSEX wc;

+    // initialize COM
+    if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;
+
     // clear out the window class for use
     ZeroMemory(&wc, sizeof(WNDCLASSEX));

@@ -28,7 +88,7 @@ int WINAPI WinMain(HINSTANCE hInstance,
     wc.style = CS_HREDRAW | CS_VREDRAW;
     wc.lpfnWndProc = WindowProc;
     wc.hInstance = hInstance;
-    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
     wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
     wc.lpszClassName = _T("WindowClass1");

@@ -38,12 +98,12 @@ int WINAPI WinMain(HINSTANCE hInstance,
     // create the window and use the result as the handle
     hWnd = CreateWindowEx(0,
                           _T("WindowClass1"),    // name of the window class
-                          _T("Hello, Engine!"),   // title of the window
+                          _T("Hello, Engine![Direct 2D]"),   // title of the window
                           WS_OVERLAPPEDWINDOW,    // window style
-                          300,    // x-position of the window
-                          300,    // y-position of the window
-                          500,    // width of the window
-                          400,    // height of the window
+                          100,    // x-position of the window
+                          100,    // y-position of the window
+                          960,    // width of the window
+                          540,    // height of the window
                           NULL,    // we have no parent window, NULL
                           NULL,    // we aren't using menus, NULL
                           hInstance,    // application handle
@@ -58,7 +118,7 @@ int WINAPI WinMain(HINSTANCE hInstance,
     MSG msg;

     // wait for the next message in the queue, store the result in 'msg'
-    while(GetMessage(&msg, NULL, 0, 0))
+    while(GetMessage(&msg, nullptr, 0, 0))
     {
         // translate keystroke messages into the right format
         TranslateMessage(&msg);
@@ -67,6 +127,9 @@ int WINAPI WinMain(HINSTANCE hInstance,
         DispatchMessage(&msg);
     }

+    // uninitialize COM
+    CoUninitialize();
+
     // return this part of the WM_QUIT message to Windows
     return msg.wParam;
 }
@@ -74,30 +137,126 @@ int WINAPI WinMain(HINSTANCE hInstance,
 // this is the main message handler for the program
 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 {
+    LRESULT result = 0;
+    bool wasHandled = false;
+
     // sort through and find what code to run for the message given
     switch(message)
     {
+       case WM_CREATE:
+               if (FAILED(D2D1CreateFactory(
+                                       D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
+               {
+                       result = -1; // Fail CreateWindowEx.
+                       return result;
+               }
+               wasHandled = true;
+        result = 0;
+        break;
+
        case WM_PAINT:
            {
-               PAINTSTRUCT ps;
-               HDC hdc = BeginPaint(hWnd, &ps);
-               RECT rec = { 20, 20, 60, 80 };
-               HBRUSH brush = (HBRUSH) GetStockObject(BLACK_BRUSH);
-
-               FillRect(hdc, &rec, brush);
-
-               EndPaint(hWnd, &ps);
-           } break;
-        // this message is read when the window is closed
-        case WM_DESTROY:
-            {
-                // close the application entirely
-                PostQuitMessage(0);
-                return 0;
-            } break;
+                       HRESULT hr = CreateGraphicsResources(hWnd);
+                       if (SUCCEEDED(hr))
+                       {
+                               PAINTSTRUCT ps;
+                               BeginPaint(hWnd, &ps);
+
+                               // start build GPU draw command
+                               pRenderTarget->BeginDraw();
+
+                               // clear the background with white color
+                               pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
+
+                // retrieve the size of drawing area
+                D2D1_SIZE_F rtSize = pRenderTarget->GetSize();
+
+                // draw a grid background.
+                int width = static_cast<int>(rtSize.width);
+                int height = static_cast<int>(rtSize.height);
+
+                for (int x = 0; x < width; x += 10)
+                {
+                    pRenderTarget->DrawLine(
+                        D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
+                        D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
+                        pLightSlateGrayBrush,
+                        0.5f
+                        );
+                }
+
+                for (int y = 0; y < height; y += 10)
+                {
+                    pRenderTarget->DrawLine(
+                        D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
+                        D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
+                        pLightSlateGrayBrush,
+                        0.5f
+                        );
+                }
+
+                // draw two rectangles
+                D2D1_RECT_F rectangle1 = D2D1::RectF(
+                     rtSize.width/2 - 50.0f,
+                     rtSize.height/2 - 50.0f,
+                     rtSize.width/2 + 50.0f,
+                     rtSize.height/2 + 50.0f
+                     );
+
+                 D2D1_RECT_F rectangle2 = D2D1::RectF(
+                     rtSize.width/2 - 100.0f,
+                     rtSize.height/2 - 100.0f,
+                     rtSize.width/2 + 100.0f,
+                     rtSize.height/2 + 100.0f
+                     );
+
+                // draw a filled rectangle
+                pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);
+
+                // draw a outline only rectangle
+                pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);
+
+                               // end GPU draw command building
+                               hr = pRenderTarget->EndDraw();
+                               if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
+                               {
+                                       DiscardGraphicsResources();
+                               }
+
+                               EndPaint(hWnd, &ps);
+                       }
+           }
+               wasHandled = true;
+        break;
+
+       case WM_SIZE:
+               if (pRenderTarget != nullptr)
+               {
+                       RECT rc;
+                       GetClientRect(hWnd, &rc);
+
+                       D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
+
+                       pRenderTarget->Resize(size);
+               }
+               wasHandled = true;
+        break;
+
+       case WM_DESTROY:
+               DiscardGraphicsResources();
+               if (pFactory) {pFactory->Release(); pFactory=nullptr; }
+               PostQuitMessage(0);
+        result = 0;
+               wasHandled = true;
+        break;
+
+    case WM_DISPLAYCHANGE:
+        InvalidateRect(hWnd, nullptr, false);
+        wasHandled = true;
+        break;
     }

     // Handle any messages the switch statement didn't
-    return DefWindowProc (hWnd, message, wParam, lParam);
+    if (!wasHandled) { result = DefWindowProc (hWnd, message, wParam, lParam); }
+    return result;
 }

简单说明一下:

首先,包含了d2d1.h。这是一个Direct2D的Wrapper,也就是一个对Direct2D进行了简单的包装的头文件。

然后,定义了如下4个全局变量:

ID2D1Factory           *pFactory = nullptr;
ID2D1HwndRenderTarget  *pRenderTarget = nullptr;
ID2D1SolidColorBrush   *pLightSlateGrayBrush = nullptr;
ID2D1SolidColorBrush   *pCornflowerBlueBrush = nullptr;

第一个是程序设计模式(Design Pattern)当中的所谓建造工厂的一个接口。第二个到第四个与我们应该已经比较眼熟了,一个是渲染对象,就是画布,后面是两支画笔。不过这里的都是COM的接口。也就是说,这些对象实际上是存在于COM当中的,我们的程序只是拥有一个指向它们的接口。

D2D的库是以一种被称为”COM组件“的方式提供的。粗略地来说类似于Java当中的Reflection,不仅仅是一个动态库,而且这个库所提供的API也是可以动态查询的,而不是事先通过头文件进行申明。

“COM”自身是一个很复杂的概念,是微软当时为了让Office能够在办公软件当中胜出,所开发出来的一种技术。在Office当中使用的基于”COM”的技术主要有”OLE”和”DDE“。前者是嵌入式对象,就是我们可以把一个视频、或者一个别的什么原本Office当中不支持的文件,放到Office文档当中。然后这个东西就会显示为一个图标,或者是一个静态的图片(snapshot),双击这个图标或者这个静态的图片就会启动能够支持这个格式的软件对其进行播放或者编辑。这种播放或者编辑有两种形式:一种是in place,就是直接在Office文档当中进行,一直是standalone,就是在Office之外进行。in place的时候,其实是在后台启动能够支持这种格式的一个程序,但是隐藏其窗口。然后把Office当中的一小块客户区域(就是之前我们用过的Rect所定义的一个矩形区域)传递给这个后台程序,让其负责处理这块区域的绘制和用户输入。也就是说,在Office程序的WM_PAINT事件的处理当中,将Office窗口的整个客户区域分割为由自己绘制的部分和由OLE绘制的部分,由OLE绘制的部分通过COM技术传递给后台应用进行绘制。比如我们嵌入的OLE对象是一个视频,那么当你在Office文档内播放这个视频的时候,实际上后台会启动Windows Media Player,只不过它的界面是隐藏的。Windows Media Player对视频进行解码播放,只不过和平常不一样的是,最后画面不是画在Windows Media Player自己的窗体上,而是画在Office文档当中的一块矩形区域当中。

最常见的应用就是在PPT里面放一个视频,或者放一个Excel表格,Word文档什么的。这个其实就是用的”OLE”技术。

而”DDE“大部分和”OLE”类似,所不同的是这个对象是单独存放在磁盘上,而不是嵌入到Office文档当中进行保存的。我们将一个Excel拖入到PPT的时候,Office会问我们是作为嵌入式对象,还是链接。嵌入式对象就是”OLE”,而链接就是”DDE”。“DDE” 的特点是你可以随时在外部编辑那个文件,而改变会自动反映到使用“DDE”链接进的那个文档当中。也就是说,如果你用“链接”的方式把一个Excel放入PPT,那么后面如果你修改了那个Excel,PPT里面的那个Excel对象的数据也会跟着变。

除了这种应用,Windows服务,DirectX 3D当中所用的filter,.NET技术,IE Browser所用的插件,Office所用的插件,等等,都是基于”COM”技术。”COM“技术还有后继的”COM+”技术以及在多个电脑上分布式处理的”DCOM“(在Windows Server当中我们可以由一台服务器部署管理其它服务器,就是靠着“DCOM”) 技术。

–(题外话开始) —

笔者刚刚参加工作的时候,所在的项目组是负责一台名为”Morpheus“的台式机的开发(正式商品名”VAIO Type X”)

vaio.sony.co.jp/Product

这台机器可以支持7个电视频道同时24小时x7天无缝录像。当时一套的售价(含显示器)是大约100万日元,按那个时候的汇率大概是7-8万RMB。(东京只有7个免费电视频道)

我当时进入公司的时候这个项目的开发已经接近一半。也就是硬件基本设计定型了而软件才刚刚开始。这个时候公司突然决定要去参加一个VAIO的市场活动,需要展示这台巨无霸机器。然而如果只是展示硬件颇为无趣,所以想要展示录像功能,虽然录像功能并没有做好。

所以,需要快速地开发一种替代模式来进行展示。当时的别的型号的VAIO也是可以录像的,只不过每台只能录制一个频道。所以为了实现7个频道的同时录制,就需要7台电脑同时工作一个礼拜。但是如果只是将7台电脑打开放在那里,录制的节目是连续的,并不会按照电子节目单(EPG)进行分割。而如果找7个人去手动按开始结束,在国内可能可行,在日本这个开销就大了。因为要3班倒,需要21个人。

笔者采用DCOM解决了这个问题。就是写个程序去按照点开始和结束,然后导入第8台机器,下载分析EPG并通过DCOM去控制那7台电脑上面的程序。

–(题外话结束) —

不过这个“COM” 技术虽然很NB,但并不是微软原创的技术。这种技术实际上是一种名为”CORBA(Welcome To CORBA Web Site!)”的技术的微软版本而已。

+
+template<class T>
+inline void SafeRelease(T **ppInterfaceToRelease)
+{
+    if (*ppInterfaceToRelease != nullptr)
+    {
+        (*ppInterfaceToRelease)->Release();
+
+        (*ppInterfaceToRelease) = nullptr;
+    }
+}
+

这是我们写的第一个使用了C++模板机制的函数。模板也称为泛型,具体就不展开了,有兴趣的可以去看C++的书。

+HRESULT CreateGraphicsResources(HWND hWnd)
+{
+    HRESULT hr = S_OK;
+    if (pRenderTarget == nullptr)
+    {
+        RECT rc;
+        GetClientRect(hWnd, &rc);
+
+        D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left,
+                        rc.bottom - rc.top);
+
+        hr = pFactory->CreateHwndRenderTarget(
+            D2D1::RenderTargetProperties(),
+            D2D1::HwndRenderTargetProperties(hWnd, size),
+            &pRenderTarget);
+
+        if (SUCCEEDED(hr))
+        {
+            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightSlateGray), &pLightSlateGrayBrush);
+
+        }
+
+        if (SUCCEEDED(hr))
+        {
+            hr = pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &pCornflowerBlueBrush);
+
+        }
+    }
+    return hr;
+}

这个函数的作用是创建绘图所需要的画布、画笔。使用GPU绘图的时候,这个部分其实是有很多工作需要做的。然而这些D2D都为我们封装好了,所以我们可以简简单单地,以一种非常接近于GDI的方式去调用。(但同时是使我们少了很多控制力,这个就是之前所说的新的DX12所要解决的问题)

+void DiscardGraphicsResources()
+{
+    SafeRelease(&pRenderTarget);
+    SafeRelease(&pLightSlateGrayBrush);
+    SafeRelease(&pCornflowerBlueBrush);
+}
+

这个是用来释放画布、画笔所对应的GPU资源的。使用了我们上面定义的泛型函数。在我们这个例子里面,需要释放这些资源的主要有两种情况:

  1. 窗口的大小发生了改变
  2. 窗口被销毁(程序结束)
+    // initialize COM
+    if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE))) return -1;
+

初始化COM。所有使用COM的程序,都需要在程序的开头做这么个调用,因为COM和普通动态库不同,它其实是一个比较独立的东西,有自己的一套机制。(其实动态库在使用之前也是需要加载的。只不过很多只在Windows上面写程序的程序员,因为微软深度傻瓜封装的关系,不太知道)

第一个参数是固定为nullptr(就是0。因为C++是强类型语言,0是一个整数,而空指针应该是指针类型,所以C++ 11里面定义了这个nullptr,用来取代之前的0,来满足类型匹配的要求)。

第二个参数由两部分组成。

COINIT_APARTMENTTHREADED

这个是告诉COM以一种所谓STA的方式运行。很粗略的来说可以认为COM组件是以一种与我们程序同步的方式运行。如果想知道细节请参考COM相关资料,比如下面这篇官方文档:

COINIT enumeration

简单来说,就如我上面解释“OLE”的时候介绍的,其实这个时候在我们的窗体之外,D2D COM会创建一个隐藏的窗体,然后监视着我们的窗体的消息队列。同时,所有的绘制都重定向到我们的窗体,而不是它自己的窗体。

COINIT_DISABLE_OLE1DDE

这个是关闭一些已经过时的COM功能,减少不必要的开销。

既然我们在应用程序初始化的时候初始化了COM组件,那么我们就需要在应用程序结束的地方结束它:

+    // uninitialize COM
+    CoUninitialize();
+

然后我们需要在窗口创建的过程当中创建Factory工厂。因为只有有了工厂我们才能创建画布、画笔。(在前面GDI或者XCB的代码当中,因为这些对象都是我们程序内部创建的,所以我们并不需要工厂。但是,现在我们是使用D2D,对象是在游离在我们程序本体之外的一个COM组件里面创建的。对于这些对象我们所知甚少,所以就要通过工厂创建。打个比喻,“外包”)。WM_CREATE是在我们调用CreateWindowEx()这个系统API的时候,系统回调我们的消息处理函数所发送给我们的消息。

+       case WM_CREATE:
+               if (FAILED(D2D1CreateFactory(
+                                       D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
+               {
+                       result = -1; // Fail CreateWindowEx.
+                       return result;
+               }
+               wasHandled = true;
+        result = 0;
+        break;
+

然后改动最大的部分,WM_PAINT消息处理部分:

+                       HRESULT hr = CreateGraphicsResources(hWnd);
+                       if (SUCCEEDED(hr))
+                       {
+                               PAINTSTRUCT ps;
+                               BeginPaint(hWnd, &ps);
+
+                               // start build GPU draw command
+                               pRenderTarget->BeginDraw();
+
+                               // clear the background with white color
+                               pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
+
+                // retrieve the size of drawing area
+                D2D1_SIZE_F rtSize = pRenderTarget->GetSize();
+
+                // draw a grid background.
+                int width = static_cast<int>(rtSize.width);
+                int height = static_cast<int>(rtSize.height);
+
+                for (int x = 0; x < width; x += 10)
+                {
+                    pRenderTarget->DrawLine(
+                        D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
+                        D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
+                        pLightSlateGrayBrush,
+                        0.5f
+                        );
+                }
+
+                for (int y = 0; y < height; y += 10)
+                {
+                    pRenderTarget->DrawLine(
+                        D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
+                        D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
+                        pLightSlateGrayBrush,
+                        0.5f
+                        );
+                }
+
+                // draw two rectangles
+                D2D1_RECT_F rectangle1 = D2D1::RectF(
+                     rtSize.width/2 - 50.0f,
+                     rtSize.height/2 - 50.0f,
+                     rtSize.width/2 + 50.0f,
+                     rtSize.height/2 + 50.0f
+                     );
+
+                 D2D1_RECT_F rectangle2 = D2D1::RectF(
+                     rtSize.width/2 - 100.0f,
+                     rtSize.height/2 - 100.0f,
+                     rtSize.width/2 + 100.0f,
+                     rtSize.height/2 + 100.0f
+                     );
+
+                // draw a filled rectangle
+                pRenderTarget->FillRectangle(&rectangle1, pLightSlateGrayBrush);
+
+                // draw a outline only rectangle
+                pRenderTarget->DrawRectangle(&rectangle2, pCornflowerBlueBrush);
+
+                               // end GPU draw command building
+                               hr = pRenderTarget->EndDraw();
+                               if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
+                               {
+                                       DiscardGraphicsResources();
+                               }
+
+                               EndPaint(hWnd, &ps);
+                       }
+           }
+               wasHandled = true;
+        break;

这部分咋看改动很多,其实和GDI绘制是十分类似的。所不同的是所有绘制指令我们都通过pRenderTarget这个接口调用。pRenderTarget是D2D COM组件所提供给我们的一个接口,那么也就是说实际的GPU绘图指令是在D2D COM组件当中完成的,而我们只是将命令和参数传(外包)给D2D。事实上,我们这些调用只是生成一些D2D消息放在我们窗体的消息队列当中,然后D2D看到这些消息就会进行处理,命令GPU进行绘制。

+       case WM_SIZE:
+               if (pRenderTarget != nullptr)
+               {
+                       RECT rc;
+                       GetClientRect(hWnd, &rc);
+
+                       D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
+
+                       pRenderTarget->Resize(size);
+               }
+               wasHandled = true;
+        break;

这个是处理窗口尺寸变化的。当窗口尺寸变化的时候,我们需要通知GPU调整画布的大小(实际上会导致抛弃所有之前的绘图资源,重新建立一套新的画布、画笔)

+    case WM_DISPLAYCHANGE:
+        InvalidateRect(hWnd, nullptr, false);
+        wasHandled = true;
+        break;

InvalidateRect是通知系统窗口的客户区域(Client Rect)需要进行重新绘制。而WM_DISPLAYCHANGE是指显示器分辨率发生变化。

+    if (!wasHandled) { result = DefWindowProc (hWnd, message, wParam, lParam); }
+    return result;

这部分是捡漏。Windows的消息队列当中的消息很多,包括上面所说的COM相关消息。我们的代码里之进行了一部分消息的定制化处理。对于我们没有处理的消息,在这里调用系统缺省的处理方式进行处理。这个步骤很重要,否则窗口都不会创建成功。

好了。我们已经完成了整个代码的更改。接下来是编译它。因为我们用到了COM,所以我们追加需要链接old32.lib这个库;我们用到了D2D1,所以我们需要追加链接d2d1.lib这个库。我们现在没有用到GDI,所以不需要gid32.lib这个库了。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang -l user32.lib -l ole32.lib -l d2d1.lib -o helloengine_d2d.exe helloengine_d2d.cpp

执行helloengine_d2d.exe,我们看到了下面这个结果:

好了,我们完成了人生中第一次与GPU的亲密接触。

保存我们的程序。

Windows下面的调试方法

现在我们的程序已经变得比较复杂了。因此很可能会有各种bug。解决bug的方式是调试。

虽然我们使用了Clang工具进行编译,但是我们依旧可以使用Visual Studio进行调试。方法如下:

首先,我们需要将编译分为两个步骤,先使用clang进行obj的生成(也就是编译)。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>clang-cl -c -Z7 -o helloengine_d2d.obj helloengine_d2d.cpp

注意我们这里实际上使用的是clang-cl这个工具。这个工具是clang的一个兼容性版本,可以识别Visual Studio提供的cl.exe编译器的选项

llvm.org/devmtg/2014-04

然后我们使用Visual Studo的链接器进行链接

D:wenliSourceReposGameEngineFromScratchPlatformWindows>link -debug user32.lib ole32.lib d2d1.lib helloengine_d2d.obj

这样我们就可以看到目录当中生成了.pdb文件。这个文件就是Visual Studio的调试符号库。

我们可以使用下面的命令启动Visual Studio的调试窗口:

D:wenliSourceReposGameEngineFromScratchPlatformWindows>devenv /debug helloengine_d2d.exe

接下来就和常规的Visual Studio调试没有任何区别了。

参考引用:

  1. Direct2D (Windows)

从零开始手敲次世代游戏引擎(九)

上一篇我们在Linux上面通过XCB创建了一个基本的窗口。

既然画布已经准备好,接下来就应该是准备画笔进行作画的阶段了。让我们在Windows和Linux这两个版本的窗口上各自绘制一个矩形。

首先我们来看Windows版本。依然回到我们的PlatformWindows目录。上一次我们是使用我们自己编译的Clang工具链进行编译的。这次我们用Visual Studio自带的cl.exe进行编译:

D:wenliSourceReposGameEngineFromScratchPlatformWindows>cl -l user32.lib helloengine_win.c
用于 x86  Microsoft (R) C/C++ 优化编译器 19.11.25506 
版权所有(C) Microsoft Corporation。保留所有权利。

cl: 命令行 warning D9002 :忽略未知选项“-l
helloengine_win.c
Microsoft (R) Incremental Linker Version 14.11.25506.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:helloengine_win.exe
user32.lib
helloengine_win.obj

可以看到,编译正常,没有任何问题。这是因为我们的代码是严格按照C/C++标准写的,Clang这方面的检查比Visual C++要严格很多,因此在Clang上面能够编译的代码,在Visual C++上面一般也是没有任何问题可以编译的。(当然,严格来说,Clang可以兼容gcc的一些gnu扩展。这些扩展是Visual C++所不能接受的。因此,并不是说Clang可以通过的代码VC++就一定能通过,这里面还有一个条件就是不要使用任何编译器特定的扩展)

好了,下面给我们的helloengine_win.c添加几行绘图指令(下面是通过git diff显示的文件变化情况。左侧有一个+号的行是本次新加的)

D:wenliSourceReposGameEngineFromScratchPlatformWindows>git diff helloengine_win.c
diff --git a/Platform/Windows/helloengine_win.c b/Platform/Windows/helloengine_win.c
index 041462a..b996344 100644
--- a/Platform/Windows/helloengine_win.c
+++ b/Platform/Windows/helloengine_win.c
@@ -77,6 +77,17 @@ LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lPara
     // sort through and find what code to run for the message given
     switch(message)
     {
+       case WM_PAINT:
+           {
+               PAINTSTRUCT ps;
+               HDC hdc = BeginPaint(hWnd, &ps);
+               RECT rec = { 20, 20, 60, 80 };
+               HBRUSH brush = (HBRUSH) GetStockObject(BLACK_BRUSH);
+
+               FillRect(hdc, &rec, brush);
+
+               EndPaint(hWnd, &ps);
+           } break;
         // this message is read when the window is closed
         case WM_DESTROY:
             {

然后编译这个程序。因为我们使用了GetStockObject这个函数,这个函数属于GDI接口(就是我前面提到过的,因为爆慢而最后导致不得不推出Direct X的接口),我们需要在编译的时候加上gdi32.lib这个库。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>cl user32.lib gdi32.lib helloengine_win.c

运行生成的程序,看到如下画面:

很好,如果我们再在右边画个方块,中间画个圆圈,并且加入一点碰撞计算的代码和键盘操作代码的话,那么一个Ping-pong游戏(乒乓游戏)就完成了。那就到达了二十几岁乔布斯走过的地方:)

不过不要忘了我们目前是在图形支线任务里面。所以让我们暂时不要去管碰撞计算,也不要管键盘操作。让我们Save我们的代码,然后切换到Linux平台。

D:wenliSourceReposGameEngineFromScratchPlatformWindows>git add helloengine_win.c

D:wenliSourceReposGameEngineFromScratchPlatformWindows>git commit -m "in the middle of article_9"
[master 7f05a0e] in the middle of article_9
 1 file changed, 11 insertions(+)

首先,让我们如下修改我们的helloengine_xcb.c:

diff --git a/Platform/Linux/helloengine_xcb.c b/Platform/Linux/helloengine_xcb.c
index 62cb467..7e44b48 100644
--- a/Platform/Linux/helloengine_xcb.c
+++ b/Platform/Linux/helloengine_xcb.c
@@ -75,6 +75,11 @@ int main(void) {
        while((pEvent = xcb_wait_for_event(pConn)) && !isQuit) {
                switch(pEvent->response_type & ~0x80) {
                case XCB_EXPOSE:
+                   {
+                       xcb_rectangle_t rect = { 20, 20, 60, 80 };
+                       xcb_poly_fill_rectangle(pConn, window, foreground, 1, &r
+                       xcb_flush(pConn);
+                   }
                        break;
                case XCB_KEY_PRESS:
                        isQuit = 1;
@@ -83,6 +88,8 @@ int main(void) {
                free(pEvent);
        }

+       xcb_disconnect(pConn);
+
        return 0;

最后的xcb_disconnect是上次漏掉的,补上。任何事情要善始善终,Connect了就一定要Disconnect。

注意这里有一个小小的Trick。因为我们的文件后缀名为.c,所以Clang会将我们的代码作为C代码来编译。在C代码当中,变量的定义必须出现在每个block的开始,而不能夹杂在语句之间。但是,有些变量我们只有在函数当中某个地方才会使用,放在函数体的头上也怪怪的。比如这个rect。

所以,我们在这里使用了{},将这部分声明为一个语句块,也就是block。这样我们就可以在这里面申明变量了。

当然,还有一个简便的方法。你可以把文件后缀改为.cpp。因为所有合法的.c都是合法的.cpp,而C++允许在语句中间申明变量。

不过我依然不鼓励这么做。就像我前面说的,要脱离Visual Studio。舒服的环境只会使人模棱两可。我们首先应该精确地掌握各种概念,在这个基础之上,选择舒服方便的方法,才是正确的。

好了,编译这个代码:

tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Linux$ clang -lxcb -o helloengine_xcb helloengine_xcb.c

执行它,看到如下画面:

好了,我们在Linux上面也实现了同样的效果。保存我们的代码,准备迈出下一步。

tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Linux$ git status
# On branch article_8
# Your branch is ahead of 'origin/article_8' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   helloengine_xcb.c
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       helloengine_xcb
no changes added to commit (use "git add" and/or "git commit -a")
tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Linux$ git add helloengine_xcb.c
tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Linux$ git commit -m "end of article_9"
[article_8 c502b89] end of article_9
 1 file changed, 7 insertions(+)

参考引用:

  1. Basic Graphics Programming With The XCB Library
  2. tutorial
  3. XCB – Wikipedia

从零开始手敲次世代游戏引擎(八)

上一篇我们在Windows上面创建了一个最基本的窗体。本篇我们将在Linux上面创建一个窗体。

如果说Windows的窗体是深深植入系统的,那么Linux的窗体系统则更像一个外挂。在Linux当中,图形界面是交给一个叫做Xserver的服务进行绘制管理,而客户端程序实际上并不直接绘制画面,而是生成一系列画面绘制指令,通过一种事先定义好的协议(X协议),发送给Xserver。Xserver根据这些绘图指令进行实际的绘图。打个小小的比方,X协议就好比HTML,而Xserver就好比浏览器。

也就是说,Linux的图形界面部分,其实是client-server结构,具有天生的分布式能力。事实上,你可以在你的安卓手机上安装一个Xserver,然后让远程的服务器将绘图指令发送到你的手机上,这样桌面就是由你的手机的硬件绘制的。

这和VNC技术以及RDP技术(Windows远程桌面技术)有着本质的不同。VNC/RDP技术的图形绘制是发生在远程机器的,而本地机器只是播放远程机器传来的实时视频流。

XCB则是X协议的C绑定。通过XCB,我们可以方便地生成遵循X协议指令,最终将这些指令发给Xserver完成绘制。

接下来就让我们通过XCB创建一个Linux的窗口。在我们的项目里新建一个Platform/Linux目录,在其中创建一个helloengine_xcb.c,然后输入如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <xcb/xcb.h>

int main(void) {
        xcb_connection_t        *pConn;
        xcb_screen_t            *pScreen;
        xcb_window_t            window;
        xcb_gcontext_t          foreground;
        xcb_gcontext_t          background;
        xcb_generic_event_t     *pEvent;
        uint32_t                mask = 0;
        uint32_t                values[2];
        uint8_t                 isQuit = 0;

        char title[] = "Hello, Engine!";
        char title_icon[] = "Hello, Engine! (iconified)";

        /* establish connection to X server */
        pConn = xcb_connect(0, 0);

        /* get the first screen */
        pScreen = xcb_setup_roots_iterator(xcb_get_setup(pConn)).data;

        /* get the root window */
        window = pScreen->root;

        /* create black (foreground) graphic context */
        foreground = xcb_generate_id(pConn);
        mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
        values[0] = pScreen->black_pixel;
        values[1] = 0;
        xcb_create_gc(pConn, foreground, window, mask, values);

        /* create white (background) graphic context */
        background = xcb_generate_id(pConn);
        mask = XCB_GC_BACKGROUND | XCB_GC_GRAPHICS_EXPOSURES;
        values[0] = pScreen->white_pixel;
        values[1] = 0;
        xcb_create_gc(pConn, background, window, mask, values);

        /* create window */
        window = xcb_generate_id(pConn);
        mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
        values[0] = pScreen->white_pixel;
        values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS;
        xcb_create_window (pConn,                                       /* connection */
                                           XCB_COPY_FROM_PARENT,        /* depth */
                                           window,                                      /* window ID */
                                           pScreen->root,                       /* parent window */
                                           20, 20,                                      /* x, y */
                                           640, 480,                            /* width, height */
                                           10,                                          /* boarder width */
                                           XCB_WINDOW_CLASS_INPUT_OUTPUT, /* class */
                                           pScreen->root_visual,        /* visual */
                                           mask, values);                       /* masks */

        /* set the title of the window */
        xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
                            XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8,
                            strlen(title), title);

        /* set the title of the window icon */
        xcb_change_property(pConn, XCB_PROP_MODE_REPLACE, window,
                            XCB_ATOM_WM_ICON_NAME, XCB_ATOM_STRING, 8,
                            strlen(title_icon), title_icon);

        /* map the window on the screen */
        xcb_map_window(pConn, window);

        xcb_flush(pConn);

        while((pEvent = xcb_wait_for_event(pConn)) && !isQuit) {
                switch(pEvent->response_type & ~0x80) {
                case XCB_EXPOSE:
                        break;
                case XCB_KEY_PRESS:
                        isQuit = 1;
                        break;
                }
                free(pEvent);
        }

        return 0;
}

用clang编译这个代码。注意需要链接xcb库。如果clang抱怨找不到这个库,请用yum或者apt安装。在CentOS里这个包叫xcb-devel,在Ubuntu里应该是叫libxcb-dev(未亲自验证)

tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Linux$ clang -lxcb -o helloengine_xcb helloengine_xcb.c

编译通过之后执行这个代码,就可以看到如图所示的窗口了。注意,同样的,需要在桌面环境当中执行。

从零开始手敲次世代游戏引擎(七)

那么首先让我们从最熟悉的Windows开始我们的图形编程支线任务。
想象一下在现实生活当中,如果我们想要画画,需要准备一些什么东西。在计算机领域,这个过程也是很相似的。因为当计算机从CUI(就是文字界面)转向GUI的时候,拓荒GUI的那些先驱们,也是参考着生活进行设计和编程的。
显然,我们首先需要有一块画布。在Windows系统当中,就是需要创建一个窗体。相信看这篇文章的很多人都写过Windows应用,而且是GUI应用。用Visual Studio生成一个windows应用,会自动生成创建这个窗体的代码。甚至,如果使用UWP,那么窗口对我们来说更像一张网页。然而这不是我想要介绍的方法。
我想要介绍的是使用windows系统API,也就是俗称win32 API(64位windows的API仍然称为win32 API)的方式创建窗口。之所以这么做,是为了最小限度的依赖Visual Studio以及相关的库,也是为了便于与其它系统的类似API作个比较。
好了,让我们开始。首先依然是做个记录点:

[tim@iZ625ivhudwZ GameEngineFromScratch]$ git checkout -b article_7
Switched to a new branch 'article_7'

然后新建一个Platform目录,用于存放和平台相关的代码。

[tim@iZ625ivhudwZ GameEngineFromScratch]$ mkdir Platform
[tim@iZ625ivhudwZ GameEngineFromScratch]$ cd Platform

在Platform下面新建Windows目录,用于存放Windows平台相关代码:

[tim@iZ625ivhudwZ Platform]$ mkdir Windows 
[tim@iZ625ivhudwZ Platform]$ cd Windows

新建helloworld_win.c,敲入如下代码:

// include the basic windows header file
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>

// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd,
                         UINT message,
                         WPARAM wParam,
                         LPARAM lParam);

// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPTSTR lpCmdLine,
                   int nCmdShow)
{
    // the handle for the window, filled by a function
    HWND hWnd;
    // this struct holds information for the window class
    WNDCLASSEX wc;

    // clear out the window class for use
    ZeroMemory(&wc, sizeof(WNDCLASSEX));

    // fill in the struct with the needed information
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.lpszClassName = _T("WindowClass1");

    // register the window class
    RegisterClassEx(&wc);

    // create the window and use the result as the handle
    hWnd = CreateWindowEx(0,
                          _T("WindowClass1"),    // name of the window class
                          _T("Hello, Engine!"),   // title of the window
                          WS_OVERLAPPEDWINDOW,    // window style
                          300,    // x-position of the window
                          300,    // y-position of the window
                          500,    // width of the window
                          400,    // height of the window
                          NULL,    // we have no parent window, NULL
                          NULL,    // we aren't using menus, NULL
                          hInstance,    // application handle
                          NULL);    // used with multiple windows, NULL

    // display the window on the screen
    ShowWindow(hWnd, nCmdShow);

    // enter the main loop:

    // this struct holds Windows event messages
    MSG msg;

    // wait for the next message in the queue, store the result in 'msg'
    while(GetMessage(&msg, NULL, 0, 0))
    {
        // translate keystroke messages into the right format
        TranslateMessage(&msg);

        // send the message to the WindowProc function
        DispatchMessage(&msg);
    }

    // return this part of the WM_QUIT message to Windows
    return msg.wParam;
}

// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // sort through and find what code to run for the message given
    switch(message)
    {
        // this message is read when the window is closed
        case WM_DESTROY:
            {
                // close the application entirely
                PostQuitMessage(0);
                return 0;
            } break;
    }

    // Handle any messages the switch statement didn't
    return DefWindowProc (hWnd, message, wParam, lParam);
}

好了,让我们来用Clang对它进行编译。注意这是一个Windows程序,所以你需要在Windows里面编译它。并且,因为我们用到了一些Windows平台的接口,需要链接相关的库:

C:UsersTim.AzureADSourceReposGameEngineFromScratchPlatformWindows>clang -l user32.lib -o helloengine_win.exe helloengine_win.c

编译成功的话,会出现一个helloengine_win.exe

C:UsersTim.AzureADSourceReposGameEngineFromScratchPlatformWindows>dir
 驱动器 C 中的卷是 OS
 卷的序列号是 38A2-CBDD

 C:UsersTim.AzureADSourceReposGameEngineFromScratchPlatformWindows 的目录

2017/08/21  08:20    <DIR>          .
2017/08/21  08:20    <DIR>          ..
2017/08/21  08:10             3,163 helloengine_win.c
2017/08/21  08:21            73,216 helloengine_win.exe
               2 个文件         76,379 字节
               2 个目录 885,621,440,512 可用字节

执行它,我们就可以看到我们的窗体啦!

接下来再玩些别的酷酷的东西。在Linux上面交叉编译它(把代码Commit/Push到Github上面,然后在Linux上面把代码再Pull下来。不会的网上搜一下Github/Git教程。)

[tim@iZ625ivhudwZ GameEngineFromScratch]$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 5 (delta 1), reused 5 (delta 1), pack-reused 0
Unpacking objects: 100% (5/5), done.
From github.com:netwarm007/GameEngineFromScratch
   264a4aa..5587a7d  article_7  -> origin/article_7
Updating 264a4aa..5587a7d
Fast-forward
 Platform/Windows/{helloworld_win.c => helloengine_win.c} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
 rename Platform/Windows/{helloworld_win.c => helloengine_win.c} (97%)
[tim@iZ625ivhudwZ GameEngineFromScratch]$ cd Platform/Windows/
[tim@iZ625ivhudwZ Windows]$ ls
helloengine_win.c
[tim@iZ625ivhudwZ Windows]$ docker run -it --rm -v $(pwd):/usr/src tim03/mingw64
docker@691b5f941825:/$ cd /usr/src
docker@691b5f941825:/usr/src$ ls
helloengine_win.c
docker@691b5f941825:/usr/src$ x86_64-w64-mingw32-gcc -o helloengine_win.exe helloengine_win.c
docker@691b5f941825:/usr/src$ ls
helloengine_win.c  helloengine_win.exe
docker@691b5f941825:/usr/src$ exit
exit

这个程序,当然,这样直接是没有办法在Linux上执行的,因为它是一个Windows程序。但是,我们可以使用wine来执行它。wine是一个在Linux上面模拟Windows系统接口的执行环境,可以用了执行Windows程序。当时开发wine的主要目的,就是在Linux上面跑只有Windows版本的游戏。

不过需要注意的是,要执行图形界面程序,我们必须在Linux的桌面环境下(也就是不能在TTY环境下)。切换到桌面环境(不会的请自己在网上搜索Linux基本教程),打开终端仿真窗口,执行下面的命令(如果没有安装wine,请用yum或者apt命令安装):

tim@iZuf6iup3mphicesefdwajZ:~/src/GameEngineFromScratch/Platform/Windows$ wine helloengine_win.exe

从零开始手敲次世代游戏引擎(六)

现在我们已经有了一个跨平台的基本应用框架。接下来我们可以开始图形方面的编码了。
参照我们编写应用模块的方式,我们可以很快地添加图形模块的骨架。
让我们在Framework/Common下面添加两个文件:
GraphicsManager.hpp:

#pragma once 
#include "IRuntimeModule.hpp" 
namespace My { 
 class GraphicsManager : implements IRuntimeModule
 { 
 public:
                virtual ~GraphicsManager() {} 
 }; 
}

GraphicsManager.cpp:

#include "GraphicsManager.hpp"

注意我们并没有重载那些纯虚函数。在我们真正开始实例化这个类之前,这是没有关系的。
因为我们添加了文件,需要编辑Framework/Common下面的CMakeLists.txt,将新文件加入编译:

add_library(Common 
BaseApplication.cpp 
GraphicsManager.cpp 
main.cpp
)

好了,我们可以重新编译我们的代码了。

到目前为止,我们已经写了好几个文件,好几个类了。但是,从实现得功能上来讲,和(一)当中的hello engine,基本上没有啥差别。那个甚至还有个输出,现在我们这个程序连输出都没有。
所以,架构是有overhead,就是有额外的开销的。架构往往在给程序带来一种内部的结构,使概念上比较独立的部分解耦成单独的模块,从而更为方便可以进行部分的扩张和替换的同时,也带来了很多条条框框和额外的开销。某些本来从事物的流程上来说是线性的处理,因为模块的划分,需要分割到不同模块进行处理,从而使得代码不那么一目了然。
在我们码农的日常当中,处于初级阶段的时候主要的精力可能放在一些语言特性和系统接口的认知方面;而到了中级阶段的时候可能最多的时间是花在了选择实现方法上面;到了高级阶段则就是常常面临着平衡问题。之前通过各种渠道所学的,自己所坚信的,都随着人生阅历的丰富变得不那么绝对,任何事情也都不再是黑白分明,有的只是灰多少的问题。
但是平衡这种东西就是属于只可意会不可言传的范畴。这东西似乎是逝去的时间和自我反省的混合物,除了极个别的所谓天才,大多数人还是无法顿悟的。特别是对于我们所不熟悉的领域,在深入了解它之前就想进行架构设计,那只有一种方法:拍脑袋。这样的架构最后基本上是肯定还不如没有架构的。
很多计算机领域的书籍都是按照架构设计-详细设计-实现这样完美地组织的。这是因为他们早就把《血缘》玩成了“无双”,然后才来做的梳理。这并没有什么不好,但绝不是拓荒的真实故事。
因此,在我们进入图形模块的详细设计和编码之前,很有必要暂时接个支线任务。先忘记架构,抛开我们刚刚写的架子,用最为直观的方式,先探一下图形编程的路。

— (图形支线任务开始)–

在写作本文的时间点,图形API用得比较多的是微软的Direct X系列,OpenGL系列,和苹果的Metal系列。手机上用的OpenGL ES是OpenGL的一个子集。而Vulkan是最近刚刚兴起的一个图形API,被称为OpenGL的后继。Vulkan被称为OpenGL的后继是因为,Vulkan是由OpenGL的标准组织策定的。这个API本身和OpenGL并不是兼容的。

Direct X系列是微软专利产品。其实早期微软推出Direct X是迫于无奈的。早期的游戏,如《仙剑一》,都是基于DOS的。这种情况在Windows推出很多年之后,仍然是这样。大家坚守微软已经不更新的DOS,而不转移阵地到微软大力推荐的Windows的主要原因是当时的Windows只有一个图形API,叫GDI,那个是非常非常慢的一个图形API。如果用来做游戏,帧率简直惨不忍睹。

所以,为了让大家都上Windows这条贼船,用劣币驱逐良币的玩法彻底搞死比Windows推出得更早的IBM OS/2 Warp,以及苹果的Mac OS,微软急急忙忙地推出了Direct X。早期的Direct X其实就是在微软幸幸苦苦封装的GDI上面打个洞,让应用程序能够直接使用比较底层的API,从而提高效率。(说穿了就是架构Overhead了,然后搭了个梯子完全绕开架构)

但这种乱暴的方式其实依然没有能够收买多少游戏开发商的心。因为这种乱暴的手段带来性能提升的同时,也带来了蓝屏。

— (题外话开始) —

知乎上最近很多人在讨论鹅厂的问题。其实,这是资本积累阶段常见的现象,早在2,30年前微软就玩过的东西。但是这并不妨碍微软今天成为大家心目中的偶像。所以鹅厂将来一片光明,做大了撒点钱洗洗就好,一年洗不干净就洗十年,总能洗干净的。

— (题外话结束)–

这种局面的根本性改变,是在Win95之后,也就是在Windows一举占据家用电脑大半江山之后。凭借着舍我其谁的市场占有率,Direct X反过来变成一种对于显卡厂商的强制标准。当然显卡厂商也并不是有多么憋屈,听话就不愁卖,大家Happy。

— (题外话开始) —

所以不要听近年的企业吹嘘什么互联网思想,共享经济。说穿了就是不择手段垄断市场,然后怎么都能赚钱,就是这么一会儿事情。只不过对于传统的制造业服务业来说,法规成熟,不允许倾销;但到了互联网这里,人家服务业,免费提供服务,不算倾销。绑架用户也不叫绑架,叫扫码登陆,免费用券。

— (题外话结束)–

Direct X发展到今天,由于商业利益链条的形成,倒确确实实是推动图形硬件发展的先锋力量了。微软在这方面做了大量的工作,普及了先端科技,培养了大量的人才。

近代GPU方面一个较大的变化就是GPGPU的出现。在DX9时代,图形渲染管道是固定的。也就是说,除了通过调整一些参数有限地改变GPU的行为之外,是没有办法对GPU的行为进行自定义编程的。

然而从DX9时代之后,出现了GPGPU。GPU变得和CPU一样,可以跑我们码农写的程序了。甚至,一些GPU开始不务正业了,并不是用来绘图,而是用来计算,算天气,跑阿尔法狗。

而OpenGL是来自于工业界,AutoCAD什么的。其最大的特点除了支持多平台之外,就是封装得比较彻底,或者说API比较傻瓜。傻瓜到网页也可以用它来画图,这个叫WebGL。如果你还没有体验过的话,可以到下面这个地方去逛一逛。

get.webgl.org/

然而这还是一个平衡问题。高度的封装固然对于CAD软件的开发来说是个好事,但是对于游戏这种软实时系统来说,就显得过于臃肿。OpenGL当中有非常复杂的状态管理和预测机制,它时时刻刻都在试图推测应用程序的意图,来弥补过于简单抽象的API造成的信息不足的问题。就好比我写这个专栏,很多人觉得写得太细太罗嗦,并不需要交代那么多基础的事情。这是因为这些人他们自己有一套完整的知识体系,你只需要给他们一个方向,然后他们自行可以脑补很多。

这对于我来说当然是很省力的好事。然而,在软件开发当中,这也意味着更少的控制力。我们无法知道OpenGL到底是怎么去做这件事情,什么时候开始做,大概需要多久。我们甚至不能确保结果是完全符合我们期待的。

同样的情况,也存在在DX12之前(不含)的DX API当中。

而主机平台,就如我之前的文章所介绍的,从历史上就是厂商把集成块焊接好,然后写个最基本的系统,把硬件接口暴露出来,剩下的就是游戏开发商自己好自为之了。所以主机历来是一个十分高效的平台,因为它没有那么多封装,架构,额外的开销。

随着PC游戏的飞速发展,DX/OpenGL面临了再一次的挑战。之前为了减少开发者负担所做的种种努力,在今天看来相当一部分反而成为了束缚性能的“并不需要的东西”。DX 12/Vulkan/Metal就是在这样的环境下诞生出来的新一代图形API。这些API更接近主机所提供的图形API。

所以,在接下来的文章当中,我们将逐个体验这些图形API,用这些API开发一些最基本的图形程序,然后抽象出他们的共性。基于这些共性,我们就可以进行我们渲染模块的详细设计了。