こんにちは。
プログラマの中村です。
先日、主流のゲームエンジンの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モデルを踊らせるまで、翔泳社」
実際にいろんな図形を描画してみる
開くと長いですがプログラムの全文は下記になります。
プログラム全文
#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;
}
三角形
座標:
{ -0.600, -0.600, 0.000 }, { 0.000, 0.439, 0.000 }, { 0.600, -0.600, 0.000 }
頂点インデックス:
{ 0, 1, 2, }
四角形
座標:
{ -0.6f, -0.6f, 0.0f }, { -0.6f, 0.6f, 0.0f }, { 0.6f, 0.6f, 0.0f },
{ 0.6f, -0.6f, 0.0f }
頂点インデックス:
{ 0, 1, 2, 0, 2, 3 }
八角形
座標:
{ 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 }
頂点インデックス:
{ 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グラフィックスや、物理などローレベルな開発を行えるエンジニアの採用も行っています。
ご興味のある方は、下記ページをご覧ください。