Escrevendo um sistema de partículas que interagem com pares em C ++ usando o DirectX 11


Já existem muitos artigos no Habr sobre o uso de sombreadores computacionais com o Unity, no entanto, é difícil encontrar um artigo sobre o uso do sombreador computacional na API "limpa" do Win32 + DirectX 11. No entanto, essa tarefa não é muito mais complicada, com mais detalhes - sob o corte.


Para fazer isso, usaremos:


  • Windows 10
  • Visual Studio 2017 Community Edition com o módulo "Desenvolvimento de aplicativos clássicos em C ++"
    Após criar o projeto, instruiremos o vinculador a usar a biblioteca `d3d11.lib`.


Arquivos de cabeçalho

Para calcular o número de quadros por segundo, usaremos a biblioteca padrão


#include <time.h> 

Nós produziremos o número de quadros por segundo através do título da janela, para o qual precisaremos formar a linha correspondente


 #include <stdio.h> 

Não consideraremos o tratamento de erros em detalhes; no nosso caso, basta que o aplicativo trate na versão de depuração e indique no momento da queda:


 #include <assert.h> 

Arquivos de cabeçalho para WinAPI:


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

Arquivos de cabeçalho do Direct3D 11:


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

IDs de recursos para carregar um sombreador. Em vez disso, você pode carregar o arquivo do objeto shader gerado pelo compilador HLSL na memória. A criação de um arquivo de recurso é descrita posteriormente.


 #include "resource.h" 

As constantes comuns ao sombreador e à parte de chamada serão declaradas em um arquivo de cabeçalho separado.


 #include "SharedConst.h" 

Declaramos uma função para processar eventos do Windows, que será definida posteriormente:


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

Escreveremos funções para criar e destruir uma janela


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

A seguir, é apresentada a inicialização da interface para acessar a placa de vídeo (Device e DeviceContext) e a cadeia de buffers de saída (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(); } 

Inicialização do acesso dos shaders ao buffer no qual a renderização será executada:


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

Antes de inicializar os shaders, você precisa criá-los. O Visual Studio pode reconhecer a extensão do arquivo, para que possamos simplesmente criar uma fonte com a extensão .hlsl ou criar diretamente um sombreador através do menu. Eu escolhi o primeiro método, porque de qualquer maneira, através das propriedades, você deve definir o uso do Shader Model 5.




Da mesma forma, crie sombreadores de vértice e pixel.


No sombreador de vértices, simplesmente convertemos as coordenadas de um vetor bidimensional (porque as posições dos pontos que temos são bidimensionais) para quadridimensionais (recebidas pela placa de vídeo):


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

No pixel shader, retornaremos branco:


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

Agora, um sombreador computacional. Definimos essa fórmula para as interações dos pontos:


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


Com a massa adotada 1


É assim que a implementação disso no HLSL ficará:


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

Você pode perceber que o arquivo SharedConst.h está incluído no sombreador. Este é o arquivo de cabeçalho com constantes, incluído no main.cpp . Aqui está o conteúdo deste arquivo:


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

Apenas declarando o número de partículas e o número de fluxos em um grupo. Como alocaremos um fluxo para cada partícula, PARTICLE_COUNT / NUMTHREADS número de grupos como PARTICLE_COUNT / NUMTHREADS . Esse número deve ser um número inteiro; portanto, é necessário que o número de partículas seja dividido pelo número de fluxos no grupo.


Carregaremos o bytecode do shader compilado usando o mecanismo de recursos do Windows. Para fazer isso, crie os seguintes arquivos:


resource.h , que conterá o ID do recurso correspondente:


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

E resource.rc , um arquivo para gerar o recurso correspondente do seguinte conteúdo:


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

Onde ShaderObject é o tipo de recurso e compute.cso , vertex.cso e pixel.cso são os nomes correspondentes dos arquivos do Objeto Shader Compilado no diretório de saída.


Para que os arquivos sejam encontrados, você deve especificar o caminho para o diretório de saída do projeto nas propriedades resource.rc :



O Visual Studio reconheceu automaticamente o arquivo como uma descrição dos recursos e o adicionou ao assembly. Você não precisa fazer isso manualmente


Agora você pode escrever o código de inicialização do shader:


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

Código de inicialização do buffer:


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

E o código de inicialização de acesso ao buffer do shader computacional:


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

Em seguida, você deve informar ao driver para usar os shaders e pacotes configuráveis ​​criados com buffers:


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

Para calcular o tempo médio de quadro, usaremos o seguinte código:


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

E em cada quadro - chame esta função:


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

Caso o tamanho da janela tenha sido alterado, também precisamos alterar o tamanho dos buffers de renderização:


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

Por fim, você pode definir uma função de processamento de mensagens:


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

E a main função:


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

Uma captura de tela do programa em execução pode ser vista no título do artigo.


→ O projeto foi totalmente carregado no GitHub

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


All Articles