
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-DateienUm 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() {
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;
Initialisierung des Zugriffs von Shadern auf den Puffer, für den das Rendern durchgeführt wird:
ID3D11RenderTargetView *renderTargetView; void InitRenderTargetView() { HRESULT result; ID3D11Texture2D *backBuffer;
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:
Mit der angenommenen Masse 1
So wird die Implementierung auf HLSL aussehen:
#include "SharedConst.h"
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;
Pufferinitialisierungscode:
ID3D11Buffer *positionBuffer; ID3D11Buffer *velocityBuffer; void InitBuffers() { HRESULT result; float *data = new float[2 * PARTICLE_COUNT];
Und der Pufferzugriffsinitialisierungscode vom Computational Shader:
ID3D11UnorderedAccessView *positionUAV; ID3D11UnorderedAccessView *velocityUAV; void InitUAV() { HRESULT result;
Als Nächstes sollten Sie den Treiber anweisen, die erstellten Shader und Bundles mit Puffern zu verwenden:
void InitBindings() {
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();
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;
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