
Il y a déjà beaucoup d' articles sur Habr sur l'utilisation des shaders de calcul avec Unity, cependant, il est difficile de trouver un article sur l'utilisation du shader de calcul sur l'API Win32 "propre" + DirectX 11. Cependant, cette tâche n'est pas beaucoup plus compliquée, plus en détail - sous la coupe.
Pour ce faire, nous utiliserons:
- Windows 10
- Visual Studio 2017 Community Edition avec le module "Développement d'applications classiques en C ++"
Après avoir créé le projet, nous dirons à l'éditeur de liens d'utiliser la bibliothèque `d3d11.lib`.

Fichiers d'en-têtePour calculer le nombre d'images par seconde, nous utiliserons la bibliothèque standard
#include <time.h>
Nous afficherons le nombre d'images par seconde via le titre de la fenêtre, pour lequel nous devons former la ligne correspondante
#include <stdio.h>
Nous ne considérerons pas la gestion des erreurs en détail, dans notre cas il suffit que l'application plante dans la version de débogage et indique au moment du crash:
#include <assert.h>
Fichiers d'en-tête pour WinAPI:
#define WIN32_LEAN_AND_MEAN #include <tchar.h> #include <Windows.h>
Fichiers d'en-tête pour Direct3D 11:
#include <dxgi.h> #include <d3d11.h>
ID de ressource pour charger un shader. Au lieu de cela, vous pouvez charger le fichier objet shader généré par le compilateur HLSL en mémoire. La création d'un fichier de ressources est décrite plus loin.
#include "resource.h"
Les constantes communes au shader et à la partie appelante seront déclarées dans un fichier d'en-tête séparé.
#include "SharedConst.h"
Nous déclarons une fonction de traitement des événements Windows, qui sera définie ultérieurement:
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
Nous allons écrire des fonctions pour créer et détruire une fenêtre
int windowWidth, windowHeight; HINSTANCE hInstance; HWND hWnd; void InitWindows() {
Vient ensuite l'initialisation de l'interface pour accéder à la carte vidéo (Device et DeviceContext) et à la chaîne de tampons de sortie (SwapChain):
IDXGISwapChain *swapChain; ID3D11Device *device; ID3D11DeviceContext *deviceContext; void InitSwapChain() { HRESULT result; DXGI_SWAP_CHAIN_DESC swapChainDesc;
Initialisation de l'accès des shaders au tampon sur lequel le rendu sera effectué:
ID3D11RenderTargetView *renderTargetView; void InitRenderTargetView() { HRESULT result; ID3D11Texture2D *backBuffer;
Avant d'initialiser les shaders, vous devez les créer. Visual Studio peut reconnaître l'extension du fichier, nous pouvons donc simplement créer une source avec l'extension .hlsl
, ou créer directement un shader via le menu. J'ai choisi la première méthode, car de toute façon, grâce aux propriétés, vous devez définir l'utilisation de Shader Model 5.


De même, créez des vertex et des pixel shaders.
Dans le vertex shader, nous convertissons simplement les coordonnées d'un vecteur à deux dimensions (parce que les positions des points que nous avons sont à deux dimensions) en quatre dimensions (reçues par la carte vidéo):
float4 main(float2 input: POSITION): SV_POSITION { return float4(input, 0, 1); }
Dans le pixel shader, nous retournerons le blanc:
float4 main(float4 input: SV_POSITION): SV_TARGET { return float4(1, 1, 1, 1); }
Maintenant un shader de calcul. Nous définissons cette formule pour les interactions des points:
Avec la masse adoptée 1
Voici à quoi ressemblera l'implémentation de ceci sur HLSL:
#include "SharedConst.h"
Vous pouvez remarquer que le fichier SharedConst.h
est inclus dans le shader. Il s'agit du fichier d'en-tête avec des constantes, qui est inclus dans main.cpp
. Voici le contenu de ce fichier:
#ifndef PARTICLE_COUNT #define PARTICLE_COUNT (1 << 15) #endif #ifndef NUMTHREADS #define NUMTHREADS 64 #endif
Il suffit de déclarer le nombre de particules et le nombre de flux dans un groupe. Nous allons allouer un flux à chaque particule, nous allons donc PARTICLE_COUNT / NUMTHREADS
nombre de groupes comme PARTICLE_COUNT / NUMTHREADS
. Ce nombre doit être un entier, il est donc nécessaire que le nombre de particules soit divisé par le nombre de flux dans le groupe.
Nous chargerons le bytecode shader compilé en utilisant le mécanisme de ressources Windows. Pour ce faire, créez les fichiers suivants:
resource.h
, qui contiendra l'ID de la ressource correspondante:
#pragma once #define IDR_BYTECODE_COMPUTE 101 #define IDR_BYTECODE_VERTEX 102 #define IDR_BYTECODE_PIXEL 103
Et resource.rc
, un fichier pour générer la ressource correspondante du contenu suivant:
#include "resource.h" IDR_BYTECODE_COMPUTE ShaderObject "compute.cso" IDR_BYTECODE_VERTEX ShaderObject "vertex.cso" IDR_BYTECODE_PIXEL ShaderObject "pixel.cso"
Où ShaderObject
est le type de ressource et compute.cso
, vertex.cso
et pixel.cso
sont les noms correspondants des fichiers d'objets Shader compilés dans le répertoire de sortie.
Pour que les fichiers soient trouvés, vous devez spécifier le chemin d'accès au répertoire de sortie du projet dans les propriétés resource.rc
:

Visual Studio a automatiquement reconnu le fichier comme une description des ressources et l'a ajouté à l'assembly, vous n'avez pas besoin de le faire manuellement
Vous pouvez maintenant écrire le code d'initialisation du shader:
ID3D11ComputeShader *computeShader; ID3D11VertexShader *vertexShader; ID3D11PixelShader *pixelShader; ID3D11InputLayout *inputLayout; void InitShaders() { HRESULT result; HRSRC src; HGLOBAL res;
Code d'initialisation du tampon:
ID3D11Buffer *positionBuffer; ID3D11Buffer *velocityBuffer; void InitBuffers() { HRESULT result; float *data = new float[2 * PARTICLE_COUNT];
Et le code d'initialisation d'accès au tampon du shader de calcul:
ID3D11UnorderedAccessView *positionUAV; ID3D11UnorderedAccessView *velocityUAV; void InitUAV() { HRESULT result;
Ensuite, vous devez dire au pilote d'utiliser les shaders et les bundles créés avec des tampons:
void InitBindings() {
Pour calculer le temps d'image moyen, nous utiliserons le code suivant:
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; }
Et sur chaque image - appelez cette fonction:
void Frame() { float frameTime = AverageFrameTime();
Dans le cas où la taille de la fenêtre a changé, nous devons également changer la taille des tampons de rendu:
void ResizeSwapChain() { HRESULT result; RECT rect;
Enfin, vous pouvez définir une fonction de traitement des messages:
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; }
Et la fonction 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(); }
Une capture d'écran du programme en cours d'exécution peut être vue dans le titre de l'article.
→ Le projet est entièrement téléchargé sur GitHub