Schreiben eines Systems paarweise interagierender Partikel in C ++ mit DirectX 11


Es gibt bereits viele Artikel über Habr über die Verwendung von Computational Shadern mit Unity. Es ist jedoch schwierig, einen Artikel über die Verwendung des Computational Shaders auf der "sauberen" Win32 API + DirectX 11 zu finden. Diese Aufgabe ist jedoch nicht viel komplizierter und detaillierter - unter dem Schnitt.


Dazu verwenden wir:


  • Windows 10
  • Visual Studio 2017 Community Edition mit dem Modul "Entwicklung klassischer Anwendungen in C ++"
    Nach dem Erstellen des Projekts werden wir den Linker anweisen, die Bibliothek `d3d11.lib` zu verwenden.


Header-Dateien

Um die Anzahl der Bilder pro Sekunde zu berechnen, verwenden wir die Standardbibliothek


#include <time.h> 

Wir geben die Anzahl der Bilder pro Sekunde über den Fenstertitel aus, für den wir die entsprechende Zeile bilden müssen


 #include <stdio.h> 

Wir werden die Fehlerbehandlung nicht im Detail betrachten. In unserem Fall reicht es aus, dass die Anwendung in der Debug-Version abstürzt und zum Zeitpunkt des Absturzes Folgendes anzeigt:


 #include <assert.h> 

Header-Dateien für WinAPI:


 #define WIN32_LEAN_AND_MEAN #include <tchar.h> #include <Windows.h> 

Header-Dateien für Direct3D 11:


 #include <dxgi.h> #include <d3d11.h> 

Ressourcen-IDs zum Laden eines Shaders. Stattdessen können Sie die vom HLSL-Compiler generierte Shader-Objektdatei in den Speicher laden. Das Erstellen einer Ressourcendatei wird später beschrieben.


 #include "resource.h" 

Die dem Shader und dem aufrufenden Teil gemeinsamen Konstanten werden in einer separaten Header-Datei deklariert.


 #include "SharedConst.h" 

Wir deklarieren eine Funktion zur Verarbeitung von Windows-Ereignissen, die später definiert wird:


 LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); 

Wir werden Funktionen zum Erstellen und Zerstören eines Fensters schreiben


 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); } 

Als nächstes folgt die Initialisierung der Schnittstelle für den Zugriff auf die Grafikkarte (Device und DeviceContext) und die Kette der Ausgabepuffer (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(); } 

Initialisierung des Zugriffs von Shadern auf den Puffer, für den das Rendern durchgeführt wird:


 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(); } 

Bevor Sie die Shader initialisieren, müssen Sie sie erstellen. Visual Studio kann die Dateierweiterung erkennen, sodass wir einfach eine Quelle mit der Erweiterung .hlsl erstellen oder direkt über das Menü einen Shader erstellen können. Ich habe die erste Methode gewählt, weil Auf jeden Fall müssen Sie über die Eigenschaften die Verwendung von Shader Model 5 festlegen.




Erstellen Sie auf ähnliche Weise Scheitelpunkt- und Pixel-Shader.


Im Vertex-Shader konvertieren wir einfach die Koordinaten von einem zweidimensionalen Vektor (da die Positionen der Punkte, die wir haben, zweidimensional sind) in vierdimensional (von der Grafikkarte empfangen):


 float4 main(float2 input: POSITION): SV_POSITION { return float4(input, 0, 1); } 

Im Pixel-Shader geben wir Weiß zurück:


 float4 main(float4 input: SV_POSITION): SV_TARGET { return float4(1, 1, 1, 1); } 

Jetzt ein Computational Shader. Wir definieren diese Formel für die Wechselwirkungen von Punkten:


 vecFij=109 vecrij frac lvert vecrij rvert0,25 lvert vecrij rvert


Mit der angenommenen Masse 1


So wird die Implementierung auf HLSL aussehen:


 #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; } 

Möglicherweise stellen Sie fest, dass die Datei SharedConst.h im Shader enthalten ist. Dies ist die Header-Datei mit Konstanten, die in main.cpp . Hier ist der Inhalt dieser Datei:


 #ifndef PARTICLE_COUNT #define PARTICLE_COUNT (1 << 15) #endif #ifndef NUMTHREADS #define NUMTHREADS 64 #endif 

Geben Sie einfach die Anzahl der Partikel und die Anzahl der Ströme in einer Gruppe an. Wir werden jedem Partikel einen Stream zuweisen, also werden wir PARTICLE_COUNT / NUMTHREADS Anzahl der Gruppen als PARTICLE_COUNT / NUMTHREADS . Diese Anzahl muss eine ganze Zahl sein, daher ist es erforderlich, dass die Anzahl der Partikel durch die Anzahl der Flüsse in der Gruppe geteilt wird.


Wir werden den kompilierten Shader-Bytecode mithilfe des Windows-Ressourcenmechanismus laden. Erstellen Sie dazu folgende Dateien:


resource.h , die die ID der entsprechenden Ressource enthält:


 #pragma once #define IDR_BYTECODE_COMPUTE 101 #define IDR_BYTECODE_VERTEX 102 #define IDR_BYTECODE_PIXEL 103 

Und resource.rc , eine Datei zum Generieren der entsprechenden Ressource mit folgendem Inhalt:


 #include "resource.h" IDR_BYTECODE_COMPUTE ShaderObject "compute.cso" IDR_BYTECODE_VERTEX ShaderObject "vertex.cso" IDR_BYTECODE_PIXEL ShaderObject "pixel.cso" 

Wobei ShaderObject der Ressourcentyp ist und compute.cso , vertex.cso und pixel.cso die entsprechenden Namen der kompilierten Shader-Objektdateien im Ausgabeverzeichnis sind.


Damit die Dateien gefunden werden können, müssen Sie den Pfad zum Projektausgabeverzeichnis in den Eigenschaften resource.rc angeben:



Visual Studio erkannte die Datei automatisch als Beschreibung der Ressourcen und fügte sie der Assembly hinzu. Sie müssen dies nicht manuell tun


Jetzt können Sie den Shader-Initialisierungscode schreiben:


 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(); } 

Pufferinitialisierungscode:


 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(); } 

Und der Pufferzugriffsinitialisierungscode vom Computational Shader:


 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(); } 

Als Nächstes sollten Sie den Treiber anweisen, die erstellten Shader und Bundles mit Puffern zu verwenden:


 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); } 

Zur Berechnung der durchschnittlichen Frame-Zeit verwenden wir den folgenden Code:


 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; } 

Und auf jedem Frame - rufen Sie diese Funktion auf:


 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); } 

Falls sich die Fenstergröße geändert hat, müssen wir auch die Größe der Rendering-Puffer ändern:


 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(); } 

Schließlich können Sie eine Nachrichtenverarbeitungsfunktion definieren:


 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; } 

Und die Hauptfunktion:


 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(); } 

Ein Screenshot des laufenden Programms ist im Titel des Artikels zu sehen.


→ Das Projekt ist vollständig auf GitHub hochgeladen

Source: https://habr.com/ru/post/de430202/


All Articles