こんにちは。
プログラマの中村です。
先日、主流のゲームエンジンの1つであるUnityが、ツールの価格に関する変更を行いました。
国内外で話題になっていたためご存知の方もいるのではないでしょうか。
私たちが利用している様々なサービスは、その規約等が変更される可能性があり、
ゲームエンジンも例外ではありません。
今回はゲームエンジンを使用せず、ゼロからゲームの制作をする場合を想定して、
コンピューターグラフィックス(CG)の初歩である、ポリゴンを画面に表示する方法について紹介していきたいと思います。
ポリゴンとは
ゲームの構成要素である、人やモンスターなどのキャラクター、家や地形などの背景は、
「Maya」や「Blender」といったDCCツールを使って、3Dモデルとして制作します。
この3Dモデルを構成するのがポリゴンとなります。
そもそもポリゴン(Polygon)とは多角形を意味する英単語であり、
その形状によって四角ポリゴンや三角ポリゴンなどと呼ばれます。
また、ゲーム業界ではポリゴンと言えば三角ポリゴンを指すことが多いです。
ポリゴンを画面に表示する
グラフィックAPI
パソコンでポリゴンを表示するには、アプリケーションとしてウインドウを表示し、プログラムがOSに対して描画の命令を送る必要があります。
この描画の命令群はグラフィックAPIと呼ばれ「DirectX」や「OpenGL」、「Vulkan」、「Metal」などが存在します。
Windows向けの開発では、Microsoftが提供しているDirectXを用いる事が多いです。
座標とインデックス
基本的にゲームの3Dモデルは、全て三角形で構成されています。
例えば四角であれば2枚の三角形で、正六面体であれば12枚、球などはその滑らかさによって変わってきます。
CGでは三角形を表示する際は、頂点を座標で表現します。
では四角形を表示する場合、3×2で6頂点の座標が必要になるかというと、実はそうではありません。
当然ながら四角形の頂点は4つですので、必要な座標は4つになります。
全ての座標を持つのは情報量が多く無駄も多いので、同じ頂点では座標を使い回したいです。
そのため頂点インデックスという方法を使います。
頂点インデックスはどの頂点座標を用いるかを表します。
具体的に言えば0:(0, 0)
、1:(0, 1)
、2:(1, 1)
、3:(1, 0)
、という頂点があった場合、
四角形のインデックスは{ 0, 1, 2, 0, 2, 3 }
となります。
グラフィックスプログラミングについて
今回はDirectXを用いて、ポリゴンを描画していきます。
DirectXを用いたプログラミングはローレベルなプログラミングになるため、CGやグラフィックAPIに関するある程度の知識が必要です。
(エンジニアリングにおけるローレベルとは、レベルが低いのではなく、ハードウェアに近いという意味になります。)
私もゼロから全てのプログラムを書ける自信はないので、下記書籍を参考にしながらプログラミングを行いました。
「川野 竜一(2020)、DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで、翔泳社」
実際にいろんな図形を描画してみる
開くと長いですがプログラムの全文は下記になります。
プログラム全文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
#include <Windows.h> #include <iostream> #include <tchar.h> #include <d3d12.h> #include <dxgi1_6.h> #include <vector> #include <DirectXMath.h> #include <d3dcompiler.h> #pragma comment(lib, "d3d12.lib") #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "d3dcompiler.lib") const unsigned int window_width = 960; const unsigned int window_height = 960; const DirectX::XMFLOAT3 Vertices[] = { { -0.6f, -0.6f, 0.0f }, { -0.6f, 0.6f, 0.0f }, { 0.6f, 0.6f, 0.0f }, { 0.6f, -0.6f, 0.0f }, }; const unsigned short Indices[] = { 0, 1, 2, 0, 2, 3 }; const float ClearColor[] = { 0.914f, 0.314f, 0.506f, 1.0f }; LRESULT WindowProcedure(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wp, lp); } ID3D12Device* _device = nullptr; IDXGIFactory6* _dxgiFactory = nullptr; IDXGISwapChain4* _swapChain = nullptr; ID3D12CommandAllocator* _cmdAllocator = nullptr; ID3D12GraphicsCommandList* _cmdList = nullptr; ID3D12CommandQueue* _cmdQueue = nullptr; int main() { WNDCLASSEX w = {}; w.cbSize = sizeof(WNDCLASSEX); w.lpfnWndProc = (WNDPROC)WindowProcedure; w.lpszClassName = _T("Polygon Test"); w.hInstance = GetModuleHandle(nullptr); RegisterClassEx(&w); RECT wrc = { 0, 0, window_width, window_height }; AdjustWindowRect(&wrc, WS_OVERLAPPEDWINDOW, false); HWND hwnd = CreateWindow( w.lpszClassName, _T("Polygon Test"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, wrc.right - wrc.left, wrc.bottom - wrc.top, nullptr, nullptr, w.hInstance, nullptr ); HRESULT result; result = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&_device)); result = CreateDXGIFactory(IID_PPV_ARGS(&_dxgiFactory)); result = _device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&_cmdAllocator)); result = _device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _cmdAllocator, nullptr, IID_PPV_ARGS(&_cmdList)); D3D12_COMMAND_QUEUE_DESC cmdQueueDesc; cmdQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; cmdQueueDesc.NodeMask = 0; cmdQueueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; cmdQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; result = _device->CreateCommandQueue(&cmdQueueDesc, IID_PPV_ARGS(&_cmdQueue)); DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {}; swapChainDesc.Width = window_width; swapChainDesc.Height = window_height; swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; swapChainDesc.Stereo = false; swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0; swapChainDesc.BufferUsage = DXGI_USAGE_BACK_BUFFER; swapChainDesc.BufferCount = 2; swapChainDesc.Scaling = DXGI_SCALING_STRETCH; swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; result = _dxgiFactory->CreateSwapChainForHwnd(_cmdQueue, hwnd, &swapChainDesc, nullptr, nullptr, (IDXGISwapChain1**)&_swapChain); D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {}; heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; heapDesc.NodeMask = 0; heapDesc.NumDescriptors = 2; heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; ID3D12DescriptorHeap* rtvHeaps = nullptr; result = _device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&rtvHeaps)); std::vector<ID3D12Resource*> backBuffers(swapChainDesc.BufferCount); for (int i = 0; i < swapChainDesc.BufferCount; i++) { result = _swapChain->GetBuffer(i, IID_PPV_ARGS(&backBuffers[i])); auto handle = rtvHeaps->GetCPUDescriptorHandleForHeapStart(); handle.ptr += i * _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); _device->CreateRenderTargetView(backBuffers[i], nullptr, handle); } ID3D12Fence* fence = nullptr; UINT64 fenceVal = 0; result = _device->CreateFence(fenceVal, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)); D3D12_HEAP_PROPERTIES heapProp = {}; heapProp.Type = D3D12_HEAP_TYPE_UPLOAD; heapProp.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; heapProp.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; D3D12_RESOURCE_DESC resDesc = {}; resDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; resDesc.Width = sizeof(Vertices); resDesc.Height = 1; resDesc.DepthOrArraySize = 1; resDesc.MipLevels = 1; resDesc.Format = DXGI_FORMAT_UNKNOWN; resDesc.SampleDesc.Count = 1; resDesc.Flags = D3D12_RESOURCE_FLAG_NONE; resDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; ID3D12Resource* vertexBuffer = nullptr; result = _device->CreateCommittedResource(&heapProp, D3D12_HEAP_FLAG_NONE, &resDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertexBuffer)); DirectX::XMFLOAT3* vertMap = nullptr; result = vertexBuffer->Map(0, nullptr, (void**)&vertMap); std::copy(std::begin(Vertices), std::end(Vertices), vertMap); vertexBuffer->Unmap(0, nullptr); D3D12_VERTEX_BUFFER_VIEW vbView = {}; vbView.BufferLocation = vertexBuffer->GetGPUVirtualAddress(); vbView.SizeInBytes = sizeof(Vertices); vbView.StrideInBytes = sizeof(Vertices[0]); ID3D12Resource* idxBuff = nullptr; resDesc.Width = sizeof(Indices); result = _device->CreateCommittedResource(&heapProp, D3D12_HEAP_FLAG_NONE, &resDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&idxBuff)); unsigned short* idxMap = nullptr; idxBuff->Map(0, nullptr, (void**)&idxMap); std::copy(std::begin(Indices), std::end(Indices), idxMap); idxBuff->Unmap(0, nullptr); D3D12_INDEX_BUFFER_VIEW idxView = {}; idxView.BufferLocation = idxBuff->GetGPUVirtualAddress(); idxView.Format = DXGI_FORMAT_R16_UINT; idxView.SizeInBytes = sizeof(Indices); ID3DBlob* vsBlob = nullptr; ID3DBlob* psBlob = nullptr; ID3DBlob* errorBlob = nullptr; result = D3DCompileFromFile(L"VertexShader.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "BasicVS", "vs_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &vsBlob, &errorBlob); result = D3DCompileFromFile(L"PixelShader.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "BasicPS", "ps_5_0", D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, &psBlob, &errorBlob); D3D12_INPUT_ELEMENT_DESC il = {}; il.SemanticName = "POSITION"; il.SemanticIndex = 0; il.Format = DXGI_FORMAT_R32G32B32_FLOAT; il.InputSlot = 0; il.AlignedByteOffset = D3D12_APPEND_ALIGNED_ELEMENT; il.InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; il.InstanceDataStepRate = 0; D3D12_INPUT_ELEMENT_DESC inputLayout[] = { il }; D3D12_GRAPHICS_PIPELINE_STATE_DESC gpipeline = {}; gpipeline.VS.BytecodeLength = vsBlob->GetBufferSize(); gpipeline.VS.pShaderBytecode = vsBlob->GetBufferPointer(); gpipeline.PS.BytecodeLength = psBlob->GetBufferSize(); gpipeline.PS.pShaderBytecode = psBlob->GetBufferPointer(); gpipeline.SampleMask = D3D12_DEFAULT_SAMPLE_MASK; gpipeline.RasterizerState.MultisampleEnable = false; gpipeline.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; gpipeline.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; gpipeline.RasterizerState.DepthClipEnable = true; gpipeline.BlendState.AlphaToCoverageEnable = false; gpipeline.BlendState.IndependentBlendEnable = false; D3D12_RENDER_TARGET_BLEND_DESC renderTargetBlendDesc = {}; renderTargetBlendDesc.BlendEnable = false; renderTargetBlendDesc.LogicOpEnable = false; renderTargetBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; gpipeline.BlendState.RenderTarget[0] = renderTargetBlendDesc; gpipeline.InputLayout.pInputElementDescs = inputLayout; gpipeline.InputLayout.NumElements = _countof(inputLayout); gpipeline.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED; gpipeline.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; gpipeline.NumRenderTargets = 1; gpipeline.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; gpipeline.SampleDesc.Count = 1; gpipeline.SampleDesc.Quality = 0; D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = {}; rootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; ID3DBlob* rootSigBlob = nullptr; result = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &rootSigBlob, &errorBlob); ID3D12RootSignature* rootSignature; result = _device->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&rootSignature)); rootSigBlob->Release(); gpipeline.pRootSignature = rootSignature; ID3D12PipelineState* pipelineState = nullptr; result = _device->CreateGraphicsPipelineState(&gpipeline, IID_PPV_ARGS(&pipelineState)); D3D12_VIEWPORT viewport = {}; viewport.Width = window_width; viewport.Height = window_height; viewport.TopLeftX = 0; viewport.TopLeftY = 0; viewport.MaxDepth = 1.0f; viewport.MinDepth = 0.0f; D3D12_RECT scissorRect = {}; scissorRect.top = 0; scissorRect.left = 0; scissorRect.right = scissorRect.left + window_width; scissorRect.bottom = scissorRect.top + window_height; ShowWindow(hwnd, SW_SHOW); MSG msg = {}; while (true) { if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } if (msg.message == WM_QUIT) { break; } auto bbIdx = _swapChain->GetCurrentBackBufferIndex(); D3D12_RESOURCE_BARRIER barrierDesc = {}; barrierDesc.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; barrierDesc.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; barrierDesc.Transition.pResource = backBuffers[bbIdx]; barrierDesc.Transition.Subresource = 0; barrierDesc.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; barrierDesc.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; _cmdList->ResourceBarrier(1, &barrierDesc); _cmdList->SetPipelineState(pipelineState); auto rtvHandle = rtvHeaps->GetCPUDescriptorHandleForHeapStart(); rtvHandle.ptr += bbIdx * _device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); _cmdList->OMSetRenderTargets(1, &rtvHandle, true, nullptr); _cmdList->ClearRenderTargetView(rtvHandle, ClearColor, 0, nullptr); _cmdList->RSSetViewports(1, &viewport); _cmdList->RSSetScissorRects(1, &scissorRect); _cmdList->SetGraphicsRootSignature(rootSignature); _cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); _cmdList->IASetVertexBuffers(0, 1, &vbView); _cmdList->IASetIndexBuffer(&idxView); _cmdList->DrawIndexedInstanced(6, 1, 0, 0, 0); barrierDesc.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; barrierDesc.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; _cmdList->Close(); ID3D12CommandList* cmdLists[] = { _cmdList }; _cmdQueue->ExecuteCommandLists(1, cmdLists); _cmdQueue->Signal(fence, ++fenceVal); if (fence->GetCompletedValue() != fenceVal) { auto hEvent = CreateEvent(nullptr, false, false, nullptr); fence->SetEventOnCompletion(fenceVal, hEvent); //描画待ち WaitForSingleObject(hEvent, INFINITE); CloseHandle(hEvent); } _cmdAllocator->Reset(); _cmdList->Reset(_cmdAllocator, nullptr); _swapChain->Present(1, 0); } UnregisterClass(w.lpszClassName, w.hInstance); return 0; } |
三角形
座標:
1 2 |
{ -0.600, -0.600, 0.000 }, { 0.000, 0.439, 0.000 }, { 0.600, -0.600, 0.000 } |
頂点インデックス:
1 2 |
{ 0, 1, 2, } |
四角形
座標:
1 2 3 |
{ -0.6f, -0.6f, 0.0f }, { -0.6f, 0.6f, 0.0f }, { 0.6f, 0.6f, 0.0f }, { 0.6f, -0.6f, 0.0f } |
頂点インデックス:
1 2 |
{ 0, 1, 2, 0, 2, 3 } |
八角形
座標:
1 2 3 4 |
{ 0.800, 0.000, 0.000 }, { 0.566, 0.566, 0.000 }, { 0.000, 0.800, 0.000 }, { -0.566, 0.566, 0.000 }, { -0.800, 0.000, 0.000 }, { -0.566, -0.566, 0.000 }, { -0.000, -0.800, 0.000 }, { 0.566, -0.566, 0.000 } |
頂点インデックス:
1 2 |
{ 0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, 0, 6, 7, 0, 7, 8, 0, 8, 1 } |
円(360角形)
座標と頂点インデックスはかなり長くなるため省略します。
さいごに
ただの三角形を画面に表示するだけで、約300行ものプログラムを書く必要があります。
また、ゲームエンジンには3DCGだけでなく、
2DUI、モーション、サウンド、文字の表示、ゲームロジックの組み込みなど、
様々な機能を実装する必要があり、一朝一夕で開発できるものではありません。
私達は様々なプラットフォームの上で生活しています。
ゲームエンジン、DirectXやWindowsなどのシステム、インターネットや電気、水道などのライフライン…。
これらのプラットフォームについて、基礎的な知識を知っておけば、
新しいものを生み出したり、物事の本質を見抜くことができるかもしれませんね。
シンソフィアでは本記事のような、
3Dグラフィックスや、物理などローレベルな開発を行えるエンジニアの採用も行っています。
ご興味のある方は、下記ページをご覧ください。