使用DirectX 11在C ++中编写成对相互作用的粒子系统


在Habr上已经 很多关于在Unity上使用计算着色器 文章 ,但是,很难找到关于在“干净的” Win32 API + DirectX 11上使用计算着色器的文章。 但是,此任务并没有复杂得多,更详细地说是“削减”。


为此,我们将使用:


  • Windows 10
  • Visual Studio 2017社区版,带有模块“用C ++开发经典应用程序”
    创建项目后,我们将告诉链接器使用`d3d11.lib`库。


头文件

要计算每秒的帧数,我们将使用标准库


#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。




同样,创建顶点和像素着色器。


在顶点着色器中,我们只需将坐标从二维矢量(因为我们拥有的点的位置为二维)转换为四维(由视频卡接收):


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

现在是一个计算着色器。 我们为点的相互作用定义以下公式:


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


大众采用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 。 这是此文件的内容:


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

仅声明一组中的粒子数和流数。 我们将为每个粒子分配一个流,因此我们将PARTICLE_COUNT / NUMTHREADSPARTICLE_COUNT / NUMTHREADSPARTICLE_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.csovertex.csopixel.cso是输出目录中已编译Shader Object文件的对应名称。


为了找到文件,您应该在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(); } 

在文章标题中可以看到正在运行的程序的屏幕截图。


→该项目已完全上传到GitHub

Source: https://habr.com/ru/post/zh-CN430202/


All Articles