
Ya hay muchos artículos sobre Habr sobre el uso de sombreadores computacionales con Unity, sin embargo, es difícil encontrar un artículo sobre el uso del sombreador computacional en el Win32 API + DirectX 11 "limpio". Sin embargo, esta tarea no es mucho más complicada, en más detalle, debajo del corte.
Para hacer esto, usaremos:
- Windows 10
- Visual Studio 2017 Community Edition con el módulo "Desarrollo de aplicaciones clásicas en C ++"
Después de crear el proyecto, le diremos al enlazador que use la biblioteca `d3d11.lib`.

Archivos de encabezadoPara calcular la cantidad de fotogramas por segundo, usaremos la biblioteca estándar
#include <time.h>
Emitiremos el número de fotogramas por segundo a través del título de la ventana, para lo cual tendremos que formar la línea correspondiente
#include <stdio.h>
No consideraremos el manejo de errores en detalle, en nuestro caso es suficiente que la aplicación se bloquee en la versión de depuración e indique en el momento del bloqueo:
#include <assert.h>
Archivos de encabezado para WinAPI:
#define WIN32_LEAN_AND_MEAN #include <tchar.h> #include <Windows.h>
Archivos de encabezado para Direct3D 11:
#include <dxgi.h> #include <d3d11.h>
ID de recursos para cargar un sombreador. En su lugar, puede cargar el archivo objeto de sombreado generado por el compilador HLSL en la memoria. La creación de un archivo de recursos se describe más adelante.
#include "resource.h"
Las constantes comunes al sombreador y la parte que llama se declararán en un archivo de encabezado separado.
#include "SharedConst.h"
Declaramos una función para procesar eventos de Windows, que se definirá más adelante:
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
Escribiremos funciones para crear y destruir una ventana.
int windowWidth, windowHeight; HINSTANCE hInstance; HWND hWnd; void InitWindows() {
A continuación se muestra la inicialización de la interfaz para acceder a la tarjeta de video (Device y DeviceContext) y la cadena de búferes de salida (SwapChain):
IDXGISwapChain *swapChain; ID3D11Device *device; ID3D11DeviceContext *deviceContext; void InitSwapChain() { HRESULT result; DXGI_SWAP_CHAIN_DESC swapChainDesc;
Inicialización del acceso desde los sombreadores al búfer en el que se realizará el renderizado:
ID3D11RenderTargetView *renderTargetView; void InitRenderTargetView() { HRESULT result; ID3D11Texture2D *backBuffer;
Antes de inicializar los sombreadores, debe crearlos. Visual Studio puede reconocer la extensión del archivo, por lo que simplemente podemos crear una fuente con la extensión .hlsl
o directamente crear un sombreador a través del menú. Elegí el primer método, porque de todos modos, a través de las propiedades tienes que establecer el uso del Shader Model 5.


Del mismo modo, cree sombreadores de vértices y píxeles.
En el sombreador de vértices, simplemente convertimos las coordenadas de un vector bidimensional (porque las posiciones de los puntos que tenemos son bidimensionales) a cuatro dimensiones (recibidas por la tarjeta de video):
float4 main(float2 input: POSITION): SV_POSITION { return float4(input, 0, 1); }
En el sombreador de píxeles, devolveremos el blanco:
float4 main(float4 input: SV_POSITION): SV_TARGET { return float4(1, 1, 1, 1); }
Ahora un sombreador computacional. Definimos esta fórmula para las interacciones de puntos:
Con la masa adoptada 1
Así es como se verá la implementación de esto en HLSL:
#include "SharedConst.h"
Puede observar que el archivo SharedConst.h
está incluido en el sombreador. Este es el archivo de encabezado con constantes, que se incluye en main.cpp
. Aquí está el contenido de este archivo:
#ifndef PARTICLE_COUNT #define PARTICLE_COUNT (1 << 15) #endif #ifndef NUMTHREADS #define NUMTHREADS 64 #endif
Simplemente declarando el número de partículas y el número de flujos en un grupo. Asignaremos una secuencia a cada partícula, por lo que PARTICLE_COUNT / NUMTHREADS
número de grupos como PARTICLE_COUNT / NUMTHREADS
. Este número debe ser un número entero, por lo que es necesario que el número de partículas se divida por el número de flujos en el grupo.
Cargaremos el bytecode del sombreador compilado utilizando el mecanismo de recursos de Windows. Para hacer esto, cree los siguientes archivos:
resource.h
, que contendrá el ID del recurso correspondiente:
#pragma once #define IDR_BYTECODE_COMPUTE 101 #define IDR_BYTECODE_VERTEX 102 #define IDR_BYTECODE_PIXEL 103
Y resource.rc
, un archivo para generar el recurso correspondiente del siguiente contenido:
#include "resource.h" IDR_BYTECODE_COMPUTE ShaderObject "compute.cso" IDR_BYTECODE_VERTEX ShaderObject "vertex.cso" IDR_BYTECODE_PIXEL ShaderObject "pixel.cso"
Donde ShaderObject
es el tipo de recurso, y compute.cso
, vertex.cso
y pixel.cso
son los nombres correspondientes de los archivos Compiled Shader Object en el directorio de salida.
Para que se encuentren los archivos, debe especificar la ruta al directorio de salida del proyecto en las propiedades resource.rc
:

Visual Studio reconoció automáticamente el archivo como una descripción de los recursos y lo agregó al ensamblado; no es necesario que lo haga manualmente
Ahora puede escribir el código de inicialización del sombreador:
ID3D11ComputeShader *computeShader; ID3D11VertexShader *vertexShader; ID3D11PixelShader *pixelShader; ID3D11InputLayout *inputLayout; void InitShaders() { HRESULT result; HRSRC src; HGLOBAL res;
Código de inicialización del búfer:
ID3D11Buffer *positionBuffer; ID3D11Buffer *velocityBuffer; void InitBuffers() { HRESULT result; float *data = new float[2 * PARTICLE_COUNT];
Y el código de inicialización de acceso al búfer desde el sombreador computacional:
ID3D11UnorderedAccessView *positionUAV; ID3D11UnorderedAccessView *velocityUAV; void InitUAV() { HRESULT result;
A continuación, debe decirle al controlador que use los sombreadores y paquetes creados con buffers:
void InitBindings() {
Para calcular el tiempo de trama promedio, utilizaremos el siguiente 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; }
Y en cada cuadro, llame a esta función:
void Frame() { float frameTime = AverageFrameTime();
En caso de que el tamaño de la ventana haya cambiado, también debemos cambiar el tamaño de los búferes de representación:
void ResizeSwapChain() { HRESULT result; RECT rect;
Finalmente, puede definir una función de procesamiento de mensajes:
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; }
Y la función 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(); }
Se puede ver una captura de pantalla del programa en ejecución en el título del artículo.
→ El proyecto está completamente cargado en GitHub