
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çalhoPara 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() {
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;
Inicialização do acesso dos shaders ao buffer no qual a renderização será executada:
ID3D11RenderTargetView *renderTargetView; void InitRenderTargetView() { HRESULT result; ID3D11Texture2D *backBuffer;
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:
Com a massa adotada 1
É assim que a implementação disso no HLSL ficará:
#include "SharedConst.h"
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;
Código de inicialização do buffer:
ID3D11Buffer *positionBuffer; ID3D11Buffer *velocityBuffer; void InitBuffers() { HRESULT result; float *data = new float[2 * PARTICLE_COUNT];
E o código de inicialização de acesso ao buffer do shader computacional:
ID3D11UnorderedAccessView *positionUAV; ID3D11UnorderedAccessView *velocityUAV; void InitUAV() { HRESULT result;
Em seguida, você deve informar ao driver para usar os shaders e pacotes configuráveis criados com buffers:
void InitBindings() {
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();
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;
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