Habrには、Unityでの計算シェーダーの使用に関する記事が既に多数ありますが、「クリーンな」Win32 API + DirectX 11での計算シェーダーの使用に関する記事を見つけることは困難です。 ただし、このタスクはそれほど複雑ではなく、より複雑です。
このために使用します:
- Windows 10
- 「C ++でのクラシックアプリケーションの開発」モジュールを備えたVisual Studio 2017 Community Edition
プロジェクトを作成した後、リンカに `d3d11.lib`ライブラリを使用するように指示します。
1秒あたりのフレーム数を計算するには、標準ライブラリを使用します
#include <time.h>
ウィンドウタイトルを通して毎秒のフレーム数を出力します。それには対応する行を形成する必要があります
#include <stdio.h>
エラー処理の詳細は考慮しません。この場合、アプリケーションがデバッグバージョンでクラッシュし、クラッシュ時に表示されることで十分です。
#include <assert.h>
WinAPIのヘッダーファイル:
#define WIN32_LEAN_AND_MEAN #include <tchar.h> #include <Windows.h>
Direct3D 11のヘッダーファイル:
#include <dxgi.h> #include <d3d11.h>
シェーダーをロードするためのリソースID。 代わりに、HLSLコンパイラによって生成されたシェーダーオブジェクトファイルをメモリにロードできます。 リソースファイルの作成については、後で説明します。
#include "resource.h"
シェーダーと呼び出し部分に共通の定数は、別のヘッダーファイルで宣言されます。
#include "SharedConst.h"
後で定義されるWindowsイベントを処理するための関数を宣言します。
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
ウィンドウを作成および破棄するための関数を作成します
int windowWidth, windowHeight; HINSTANCE hInstance; HWND hWnd; void InitWindows() { // hInstance = GetModuleHandle(NULL); windowWidth = 800; windowHeight = 800; WNDCLASS wc; // wc.style = 0; // wc.lpfnWndProc = &WndProc; // wc.cbClsExtra = 0; wc.cbWndExtra = 0; // (), wc.hInstance = hInstance; // wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION); wc.hCursor = LoadCursor(hInstance, IDC_ARROW); // , "" wc.hbrBackground = NULL; // wc.lpszMenuName = NULL; // wc.lpszClassName = _T("WindowClass1"); // ATOM result = RegisterClass(&wc); // , assert(result); // -- , .. DWORD dwStyle = WS_OVERLAPPEDWINDOW; RECT rect; // ( ) rect.left = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2; rect.top = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2; rect.right = rect.left + windowWidth; rect.bottom = rect.top + windowHeight; // . -- AdjustWindowRect(&rect, dwStyle, FALSE); hWnd = CreateWindow( _T("WindowClass1"), _T("WindowName1"), dwStyle, // rect.left, rect.top, // rect.right - rect.left, rect.bottom - rect.top, // // HWND_DESKTOP NULL HWND_DESKTOP, // NULL, // (), hInstance, // NULL); // , assert(hWnd); } void DisposeWindows() { // DestroyWindow(hWnd); // UnregisterClass(_T("WindowClass1"), hInstance); }
次は、ビデオカード(DeviceおよびDeviceContext)および出力バッファーのチェーン(SwapChain)にアクセスするためのインターフェースの初期化です。
IDXGISwapChain *swapChain; ID3D11Device *device; ID3D11DeviceContext *deviceContext; void InitSwapChain() { HRESULT result; DXGI_SWAP_CHAIN_DESC swapChainDesc; // swapChainDesc.BufferDesc.Width = windowWidth; swapChainDesc.BufferDesc.Height = windowHeight; // // .. , swapChainDesc.BufferDesc.RefreshRate.Numerator = 0; swapChainDesc.BufferDesc.RefreshRate.Denominator = 1; // -- 32- RGBA swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; // swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0; // SwapChain swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // "" ( ) swapChainDesc.BufferCount = 1; // swapChainDesc.OutputWindow = hWnd; // swapChainDesc.Windowed = TRUE; // swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; swapChainDesc.Flags = 0; // DirectX 11.0, .. D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_0; // Debug- DirectX #ifndef NDEBUG UINT flags = D3D11_CREATE_DEVICE_DEBUG; #else UINT flags = 0; #endif result = D3D11CreateDeviceAndSwapChain( // - NULL, // D3D_DRIVER_TYPE_HARDWARE, NULL, // . flags, // DirectX &featureLevel, 1, // SDK D3D11_SDK_VERSION, // &swapChainDesc, // , &swapChain, &device, NULL, &deviceContext); // , assert(SUCCEEDED(result)); } void DisposeSwapChain() { deviceContext->Release(); device->Release(); swapChain->Release(); }
シェーダーからレンダリングが実行されるバッファーへのアクセスの初期化:
ID3D11RenderTargetView *renderTargetView; void InitRenderTargetView() { HRESULT result; ID3D11Texture2D *backBuffer; // "" SwapChain result = swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void **)&backBuffer); assert(SUCCEEDED(result)); // result = device->CreateRenderTargetView(backBuffer, NULL, &renderTargetView); assert(SUCCEEDED(result)); // // , , // .. SwapChain, // Release() backBuffer->Release(); // View deviceContext->OMSetRenderTargets(1, &renderTargetView, NULL); // D3D11_VIEWPORT viewport; viewport.TopLeftX = 0; viewport.TopLeftY = 0; viewport.Width = (FLOAT)windowWidth; viewport.Height = (FLOAT)windowHeight; viewport.MinDepth = 0; viewport.MaxDepth = 1; deviceContext->RSSetViewports(1, &viewport); } void DisposeRenderTargetView() { renderTargetView->Release(); }
シェーダーを初期化する前に、シェーダーを作成する必要があります。 Visual Studioはファイル拡張子を認識できるため、拡張子.hlsl
ソースを作成するか、メニューから直接シェーダーを作成できます。 最初の方法を選んだのは、 とにかく、プロパティを通じて、Shader Model 5の使用を設定する必要があります。
同様に、頂点シェーダーとピクセルシェーダーを作成します。
頂点シェーダーでは、座標を2次元ベクトル(ポイントの位置は2次元であるため)から4次元(ビデオカードで受信される)に変換するだけです。
float4 main(float2 input: POSITION): SV_POSITION { return float4(input, 0, 1); }
ピクセルシェーダーでは、白を返します。
float4 main(float4 input: SV_POSITION): SV_TARGET { return float4(1, 1, 1, 1); }
計算シェーダーになりました。 点の相互作用の式を定義します。
採用された質量で1
これは、HLSLでのこの実装の外観です。
#include "SharedConst.h" // , UAV 0 RWBuffer<float2> position: register(u0); // , UAV 1 RWBuffer<float2> velocity: register(u1); // [numthreads(NUMTHREADS, 1, 1)] void main(uint3 id: SV_DispatchThreadID) { float2 acc = float2(0, 0); for (uint i = 0; i < PARTICLE_COUNT; i++) { // float2 diff = position[i] - position[id.x]; // , 0- float len = max(1e-10, length(diff)); float k = 1e-9 * (len - 0.25) / len; acc += k * diff; } position[id.x] += velocity[id.x] + 0.5 * acc; velocity[id.x] += acc; }
SharedConst.h
ファイルがシェーダーに含まれていることに気付くかもしれません。 これは定数を持つヘッダーファイルで、 main.cpp
含まれてmain.cpp
ます。 このファイルの内容は次のとおりです。
#ifndef PARTICLE_COUNT #define PARTICLE_COUNT (1 << 15) #endif #ifndef NUMTHREADS #define NUMTHREADS 64 #endif
1つのグループ内のパーティクルの数とストリームの数を宣言するだけです。 各パーティクルに1つのストリームを割り当てるため、グループPARTICLE_COUNT / NUMTHREADS
数をPARTICLE_COUNT / NUMTHREADS
としてPARTICLE_COUNT / NUMTHREADS
ます。 この数は整数である必要があるため、パーティクルの数をグループ内のフローの数で割る必要があります。
Windowsリソースメカニズムを使用して、コンパイル済みシェーダーバイトコードを読み込みます。 これを行うには、次のファイルを作成します。
resource.h
には、対応するリソースのIDが含まれます。
#pragma once #define IDR_BYTECODE_COMPUTE 101 #define IDR_BYTECODE_VERTEX 102 #define IDR_BYTECODE_PIXEL 103
resource.rc
、次の内容を持つ対応するリソースを生成するためのファイル:
#include "resource.h" IDR_BYTECODE_COMPUTE ShaderObject "compute.cso" IDR_BYTECODE_VERTEX ShaderObject "vertex.cso" IDR_BYTECODE_PIXEL ShaderObject "pixel.cso"
ShaderObject
はリソースのタイプであり、 compute.cso
、 vertex.cso
およびpixel.cso
は、出力ディレクトリ内のコンパイル済みシェーダーオブジェクトファイルの対応する名前です。
ファイルを見つけるには、 resource.rc
プロパティでプロジェクト出力ディレクトリへのパスを指定する必要があります。
Visual Studioは、ファイルをリソースの説明として自動的に認識し、アセンブリに追加しました。これを手動で行う必要はありません。
これでシェーダー初期化コードを書くことができます:
ID3D11ComputeShader *computeShader; ID3D11VertexShader *vertexShader; ID3D11PixelShader *pixelShader; ID3D11InputLayout *inputLayout; void InitShaders() { HRESULT result; HRSRC src; HGLOBAL res; // // // , .. // src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_COMPUTE), _T("ShaderObject")); res = LoadResource(hInstance, src); // result = device->CreateComputeShader( // res, SizeofResource(hInstance, src), // . , .. NULL, // &computeShader); assert(SUCCEEDED(result)); FreeResource(res); // src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_PIXEL), _T("ShaderObject")); res = LoadResource(hInstance, src); result = device->CreatePixelShader(res, SizeofResource(hInstance, src), NULL, &pixelShader); assert(SUCCEEDED(result)); FreeResource(res); // src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_VERTEX), _T("ShaderObject")); res = LoadResource(hInstance, src); result = device->CreateVertexShader(res, SizeofResource(hInstance, src), NULL, &vertexShader); assert(SUCCEEDED(result)); // , // ( ) D3D11_INPUT_ELEMENT_DESC inputDesc; // inputDesc.SemanticName = "POSITION"; // , inputDesc.SemanticIndex = 0; // 32- inputDesc.Format = DXGI_FORMAT_R32G32_FLOAT; // inputDesc.AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; // inputDesc.InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; // inputDesc.InputSlot = 0; // inputDesc.InstanceDataStepRate = 0; result = device->CreateInputLayout( // &inputDesc, 1, // res, SizeofResource(hInstance, src), // &inputLayout); assert(SUCCEEDED(result)); FreeResource(res); } void DisposeShaders() { inputLayout->Release(); computeShader->Release(); vertexShader->Release(); pixelShader->Release(); }
バッファー初期化コード:
ID3D11Buffer *positionBuffer; ID3D11Buffer *velocityBuffer; void InitBuffers() { HRESULT result; float *data = new float[2 * PARTICLE_COUNT]; // , D3D11_SUBRESOURCE_DATA subresource; // subresource.pSysMem = data; // subresource.SysMemPitch = 0; // subresource.SysMemSlicePitch = 0; // D3D11_BUFFER_DESC desc; // desc.ByteWidth = sizeof(float[2 * PARTICLE_COUNT]); // desc.Usage = D3D11_USAGE_DEFAULT; // , desc.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_UNORDERED_ACCESS; // desc.CPUAccessFlags = 0; // desc.MiscFlags = 0; // desc.StructureByteStride = sizeof(float[2]); // for (int i = 0; i < 2 * PARTICLE_COUNT; i++) data[i] = 2.0f * rand() / RAND_MAX - 1.0f; // result = device->CreateBuffer(&desc, &subresource, &positionBuffer); assert(SUCCEEDED(result)); // desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS; // for (int i = 0; i < 2 * PARTICLE_COUNT; i++) data[i] = 0.0f; // result = device->CreateBuffer(&desc, &subresource, &velocityBuffer); assert(SUCCEEDED(result)); // , delete[] data; } void DisposeBuffers() { positionBuffer->Release(); velocityBuffer->Release(); }
そして、計算シェーダーからのバッファーアクセス初期化コード:
ID3D11UnorderedAccessView *positionUAV; ID3D11UnorderedAccessView *velocityUAV; void InitUAV() { HRESULT result; // D3D11_UNORDERED_ACCESS_VIEW_DESC desc; // 32- desc.Format = DXGI_FORMAT_R32G32_FLOAT; // , desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; // desc.Buffer.FirstElement = 0; // desc.Buffer.NumElements = PARTICLE_COUNT; // desc.Buffer.Flags = 0; // result = device->CreateUnorderedAccessView(positionBuffer, &desc, &positionUAV); assert(!result); // result = device->CreateUnorderedAccessView(velocityBuffer, &desc, &velocityUAV); assert(!result); } void DisposeUAV() { positionUAV->Release(); velocityUAV->Release(); }
次に、作成したシェーダーとバッファーを使用するようドライバーに指示します。
void InitBindings() { // // deviceContext->CSSetShader(computeShader, NULL, 0); // deviceContext->VSSetShader(vertexShader, NULL, 0); // deviceContext->PSSetShader(pixelShader, NULL, 0); // deviceContext->CSSetUnorderedAccessViews(1, 1, &velocityUAV, NULL); // deviceContext->IASetInputLayout(inputLayout); // deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST); }
平均フレーム時間を計算するには、次のコードを使用します。
const int FRAME_TIME_COUNT = 128; clock_t frameTime[FRAME_TIME_COUNT]; int currentFrame = 0; float AverageFrameTime() { frameTime[currentFrame] = clock(); int nextFrame = (currentFrame + 1) % FRAME_TIME_COUNT; clock_t delta = frameTime[currentFrame] - frameTime[nextFrame]; currentFrame = nextFrame; return (float)delta / CLOCKS_PER_SEC / FRAME_TIME_COUNT; }
そして、各フレームで-この関数を呼び出します:
void Frame() { float frameTime = AverageFrameTime(); // char buf[256]; sprintf_s(buf, "average framerate: %.1f", 1.0f / frameTime); SetWindowTextA(hWnd, buf); // float clearColor[] = { 0.0f, 0.0f, 0.0f, 0.0f }; deviceContext->ClearRenderTargetView(renderTargetView, clearColor); // 32- UINT stride = sizeof(float[2]); UINT offset = 0; ID3D11Buffer *nullBuffer = NULL; ID3D11UnorderedAccessView *nullUAV = NULL; // deviceContext->IASetVertexBuffers(0, 1, &nullBuffer, &stride, &offset); // deviceContext->CSSetUnorderedAccessViews(0, 1, &positionUAV, NULL); // deviceContext->Dispatch(PARTICLE_COUNT / NUMTHREADS, 1, 1); // deviceContext->CSSetUnorderedAccessViews(0, 1, &nullUAV, NULL); // deviceContext->IASetVertexBuffers(0, 1, &positionBuffer, &stride, &offset); // deviceContext->Draw(PARTICLE_COUNT, 0); // swapChain->Present(0, 0); }
ウィンドウサイズが変更された場合は、レンダリングバッファーのサイズも変更する必要があります。
void ResizeSwapChain() { HRESULT result; RECT rect; // GetClientRect(hWnd, &rect); windowWidth = rect.right - rect.left; windowHeight = rect.bottom - rect.top; // , , // "" DisposeRenderTargetView(); // result = swapChain->ResizeBuffers( // 0, // windowWidth, windowHeight, // DXGI_FORMAT_UNKNOWN, 0); assert(SUCCEEDED(result)); // "" InitRenderTargetView(); }
最後に、メッセージ処理関数を定義できます。
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { switch (Msg) { case WM_CLOSE: PostQuitMessage(0); break; case WM_KEYDOWN: if (wParam == VK_ESCAPE) PostQuitMessage(0); break; case WM_SIZE: ResizeSwapChain(); break; default: return DefWindowProc(hWnd, Msg, wParam, lParam); } return 0; }
そしてmain
機能:
int main() { InitWindows(); InitSwapChain(); InitRenderTargetView(); InitShaders(); InitBuffers(); InitUAV(); InitBindings(); ShowWindow(hWnd, SW_SHOW); bool shouldExit = false; while (!shouldExit) { Frame(); MSG msg; while (!shouldExit && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); if (msg.message == WM_QUIT) shouldExit = true; } } DisposeUAV(); DisposeBuffers(); DisposeShaders(); DisposeRenderTargetView(); DisposeSwapChain(); DisposeWindows(); }
実行中のプログラムのスクリーンショットは、記事のタイトルで見ることができます。