Здравствуйте! В предыдущей статье мы разобрали, что такое шейдеры в целом, и подробно остановились на вычислительном шейдере, который работает вне графического конвейера. Однако есть ещё формирования каждого пикселя на экране. За эту задачу отвечает пиксельный шейдер (Pixel Shader) — программа, которая выполняется для каждого пикселя и определяет его итоговый цвет.
В первой части мы кратко описали весь конвейер. Освежим в памяти ключевые этапы, чтобы понять, откуда пиксельный шейдер получает данные и что делает с ними дальше.
Input Assembler (IA): Собирает из вершинного и индексного буферов примитивы — треугольники, линии.
Vertex Shader (VS): Выполняется для каждой вершины. Трансформирует координаты из объектного пространства в clip space и вычисляет дополнительные параметры — нормали, UV‑координаты, цвет.
Растеризатор (Rasterizer): Преобразует треугольники в набор фрагментов (потенциальных пикселей). Для каждого фрагмента вычисляются экранные координаты и интерполируются данные, полученные от вершинного шейдера (UV, нормали и так далее).
Pixel Shader (PS): Выполняется для каждого фрагмента. Его главная задача — вычислить итоговый цвет этого фрагмента. В статье про вычислительный шейдер было сказано, что пиксельный шейдер «может читать соседние пиксели из текстур и выполнять сложные вычисления». Именно здесь происходит попиксельное освещение, наложение текстур, смешивание цветов и другие эффекты.
Output Merger (OM): Завершающий этап. Выполняет тесты глубины (Z‑buffer) и трафарета (Stencil), а затем смешивает (blending) новый цвет пикселя с тем, что уже есть в целевом буфере (с учётом прозрачности). Именно здесь решается, будет ли пиксель виден и как он наложится на сцену.
Важное примечание по терминологии: Термин «пиксельный шейдер» (Pixel Shader) в основном используется в DirectX. В OpenGL и других API для обозначения того же самого этапа используется термин «фрагментный шейдер» (Fragment Shader). По сути, это одно и то же.
Как и в случае с вычислительным шейдером, писать мы будем на HLSL (High Level Shading Language). Синтаксис знаком по C, поэтому подробно на скалярах (float, int) и векторах (float2, float3, float4) останавливаться не будем. Отметим лишь ключевые для пиксельного шейдера моменты.
Точка входа: Функция, которая выполняется для каждого фрагмента. Обычно называется main, но может иметь любое имя, указанное при компиляции.
Семантика: Пиксельный шейдер получает данные через входные параметры с определённой семантикой и возвращает результат через выходную. Для пиксельного шейдера ключевой является семантика SV_Target (Shader Model 4.0+) или COLOR (более старые версии). Именно возвращаемое значение с этой семантикой становится цветом фрагмента.
Регистры:
t# — входные текстуры (SRV).
s# — сэмплеры (определяют, как читать текстуру: с фильтрацией, без, с адресацией).
b# — константные буферы (данные с CPU: цвет, коэффициенты, матрицы).
Для начала напишем простейший шейдер, который делает то же, что и вычислительный шейдер из первой статьи, — умножает цвет каждого пикселя на заданный коэффициент. Это позволит провести прямую параллель.
//Объявление входной текстуры. Texture2D - тип текстуры. //Затем имя переменной, затем привязка к слоту t0 (Texture register 0). //Это стандартное место для SRV (Shader Resource View), то есть "только для чтения" Texture2D InputTexture : register(t0); //Константный буфер, как в вычислительном шейдере, только в пиксельном, данные пользователя //Кладутся в b0, а не b1, так как он не занят системой. cbuffer ConstantBuffer : register(b0) // было b1 { float magnitudeScale; float _pad0, _pad1, _pad2; }; float4 main( float4 clipSpaceOutput : SV_POSITION, //Позиция в однородных координатах(clip slace) float4 texelSpaceInput0 : TEXCOORD0 //Координаты в тексельном пространстве (тексель - один пиксель текстуры) ) : SV_Target { float4 c = InputTexture.Load(int3((int2)texelSpaceInput0.xy, 0));// Эта функция читает точный тексель по целочисленным координатам . Второй аргумент 0, это индекс mip-уровня c.rgb = saturate(c.rgb * magnitudeScale); // Умножение RGB на коэффициент с ограничением. У меня в текстуре нет альфа канала, поэтому я выбираю rgb. функция saturate ограничивает координаты в диапазоне [0,1] return float4(c.rgb, 1.0); //1 это альфа канал, задаю непрозрачность. }
Теперь, как в статье про вычислительный шейдер, создадим полноценный пользовательский эффект Direct2D.
Архитектура будет очень похожей: трансформация, которая управляет шейдером, и эффект, который управляет графом трансформаций.
Ключевое отличие — мы будем наследовать интерфейс ID2D1DrawTransform, а не ID2D1ComputeTransform.
Наследник ID2D1DrawTransform отвечает за настройку пиксельного шейдера, передачу ему констант и определение того, как входные данные отображаются на выход.
// HabrPixelShaderHandler.h #pragma once #include "pch.h" DEFINE_GUID(GUID_HABR_PIXEL_SHADER, 0x895136D1, 0x1511, 0x3850, 0xAF, 0x3B, 0x7A, 0xA3, 0xB0, 0x6C, 0x5D, 0xE1); class HabrNodeHandler : public ID2D1DrawTransform { public: explicit HabrNodeHandler(_In_ ID2D1EffectContext* context); IFACEMETHODIMP_(UINT32) GetInputCount() const override { return 1; } IFACEMETHODIMP SetDrawInfo(_In_ ID2D1DrawInfo* pDrawInfo) override; IFACEMETHODIMP MapOutputRectToInputRects( _In_ const D2D1_RECT_L* pOutputRect, _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects, UINT32 inputRectCount) const override; IFACEMETHODIMP MapInputRectsToOutputRect( _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects, _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects, UINT32 inputRectCount, _Out_ D2D1_RECT_L* pOutputRect, _Out_ D2D1_RECT_L* pOutputOpaqueSubRect) override; IFACEMETHODIMP MapInvalidRect( UINT32 inputIndex, D2D1_RECT_L invalidInputRect, _Out_ D2D1_RECT_L* pInvalidOutputRect) const override; // IUnknown - без изменений IFACEMETHODIMP_(ULONG) AddRef() override; IFACEMETHODIMP_(ULONG) Release() override; IFACEMETHODIMP QueryInterface(REFIID riid, _Outptr_ void** ppOutput) override; private: ~HabrNodeHandler() = default; LONG m_cRef; struct ConstantBuffer { float magnitudeScale; float padding[3]; }; static_assert(sizeof(ConstantBuffer) == 16, "ConstantBuffer must be 16 bytes (HLSL cbuffer requirement)"); ConstantBuffer m_constantBuffer = { 2.0f, { 0.0f, 0.0f, 0.0f } }; };
Ключевые отличия от compute‑версии:
Вместо SetComputeInfo мы реализуем SetDrawInfo. Этот метод получает интерфейс ID2D1DrawInfo, через который настраивается графический проход: привязывается пиксельный шейдер, устанавливаются константы и определяется, как шейдер будет использовать входные текстуры.
Методы MapOutputRectToInputRects, MapInputRectsToOutputRect и MapInvalidRect имеют ту же логику, что и в вычислительном шейдере, поскольку наше преобразование также попиксельное и без смещения (отображение 1:1).
Реализация траснформация:
// HabrPixelShaderHandler.cpp #include "pch.h" #include "HabrNodeHandler.h" HabrNodeHandler::HabrNodeHandler(_In_ ID2D1EffectContext* context) : m_cRef(0) { HANDLE hFile = CreateFileW( L"HABR_SHADER.cso", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) { OutputDebugStringW(L"[HabrNodeHandler] HABR_SHADER.cso NOT FOUND next to .exe!\n"); return; } DWORD fileSize = GetFileSize(hFile, nullptr); if (fileSize == INVALID_FILE_SIZE || fileSize == 0) { CloseHandle(hFile); OutputDebugStringW(L"[HabrNodeHandler] HABR_SHADER.cso is empty!\n"); return; } std::vector<BYTE> shaderData(fileSize); DWORD bytesRead = 0; ReadFile(hFile, shaderData.data(), fileSize, &bytesRead, nullptr); CloseHandle(hFile); HRESULT hr = context->LoadPixelShader( GUID_HABR_PIXEL_SHADER, shaderData.data(), static_cast<UINT32>(shaderData.size())); if (FAILED(hr)) { wchar_t buf[128]; swprintf_s(buf, L"[HabrNodeHandler] LoadPixelShader FAILED: 0x%08X\n", hr); OutputDebugStringW(buf); } else { OutputDebugStringW(L"[HabrNodeHandler] LoadPixelShader OK\n"); } } IFACEMETHODIMP HabrNodeHandler::MapInvalidRect( UINT32, D2D1_RECT_L invalidInputRect, _Out_ D2D1_RECT_L* pInvalidOutputRect) const { *pInvalidOutputRect = invalidInputRect; return S_OK; } IFACEMETHODIMP HabrNodeHandler::MapOutputRectToInputRects( _In_ const D2D1_RECT_L* pOutputRect, _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects, UINT32 inputRectCount) const { if (inputRectCount != 1) return E_INVALIDARG; pInputRects[0] = *pOutputRect; return S_OK; } IFACEMETHODIMP HabrNodeHandler::MapInputRectsToOutputRect( _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects, _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects, UINT32 inputRectCount, _Out_ D2D1_RECT_L* pOutputRect, _Out_ D2D1_RECT_L* pOutputOpaqueSubRect) { if (inputRectCount != 1 || !pInputRects || !pOutputRect || !pOutputOpaqueSubRect) return E_INVALIDARG; *pOutputRect = pInputRects[0]; *pOutputOpaqueSubRect = pInputOpaqueSubRects ? pInputOpaqueSubRects[0] : pInputRects[0]; return S_OK; } IFACEMETHODIMP HabrNodeHandler::SetDrawInfo(_In_ ID2D1DrawInfo* pDrawInfo) { HRESULT hr; hr = pDrawInfo->SetPixelShader(GUID_HABR_PIXEL_SHADER); wchar_t buf[160]; swprintf_s(buf, L"[SetDrawInfo] SetPixelShader: 0x%08X\n", hr); OutputDebugStringW(buf); if (FAILED(hr)) return hr; m_constantBuffer.magnitudeScale = 2.0f; hr = pDrawInfo->SetPixelShaderConstantBuffer( reinterpret_cast<const BYTE*>(&m_constantBuffer), sizeof(m_constantBuffer)); swprintf_s(buf, L"[SetDrawInfo] SetPixelShaderConstantBuffer: 0x%08X\n", hr); OutputDebugStringW(buf); if (FAILED(hr)) return hr; D2D1_INPUT_DESCRIPTION inputDesc = {}; inputDesc.filter = D2D1_FILTER_MIN_MAG_MIP_LINEAR; hr = pDrawInfo->SetInputDescription(0, inputDesc); swprintf_s(buf, L"[SetDrawInfo] SetInputDescription: 0x%08X\n", hr); OutputDebugStringW(buf); if (FAILED(hr)) return hr; pDrawInfo->SetInstructionCountHint(10); return S_OK; } IFACEMETHODIMP_(ULONG) HabrNodeHandler::AddRef() { return InterlockedIncrement(&m_cRef); } IFACEMETHODIMP_(ULONG) HabrNodeHandler::Release() { ULONG cRef = static_cast<ULONG>(InterlockedDecrement(&m_cRef)); if (cRef == 0) delete this; return cRef; } IFACEMETHODIMP HabrNodeHandler::QueryInterface(REFIID riid, _Outptr_ void** ppOutput) { *ppOutput = nullptr; HRESULT hr = S_OK; // Было: __uuidof(ID2D1ComputeTransform) if (riid == __uuidof(ID2D1DrawTransform)) *ppOutput = static_cast<ID2D1DrawTransform*>(this); else if (riid == __uuidof(ID2D1Transform)) *ppOutput = static_cast<ID2D1Transform*>(this); else if (riid == __uuidof(ID2D1TransformNode)) *ppOutput = static_cast<ID2D1TransformNode*>(this); else if (riid == __uuidof(IUnknown)) *ppOutput = this; else hr = E_NOINTERFACE; if (*ppOutput) AddRef(); return hr; }
Что осталось прежним:
COM‑инфраструктура: AddRef, Release, QueryInterface (с заменой проверяемого интерфейса), а также приватный деструктор.
Функции MapOutputRectToInputRects, MapInputRectsToOutputRect и MapInvalidRect реализуют попиксельное отображение «один‑к-одному»
Загрузка.cso из файла: конструктор по‑прежнему открывает HABR_SHADER.cso через CreateFileW, читает его в std::vector и передаёт данные в ID2D1EffectContext
Структура ConstantBuffer: поле magnitudeScale и три padding‑поля для выравнивания до 16 байт, чтобы размеры в C++ и HLSL совпадали.
Что нового. Переход от ID2D1ComputeTransform к ID2D1DrawTransform потребовал замены трёх ключевых элементов: способа загрузки шейдера, метода его настройки и набора вызываемых методов внутри этого метода.
Загрузка шейдера: LoadPixelShader вместо LoadComputeShader.
В конструкторе вычислительный шейдер регистрировался через LoadComputeShader. В пиксельном варианте используется LoadPixelShader с тем же GUID (GUID_HABR_PIXEL_SHADER).
Это отражает фундаментальное различие: compute‑шейдер выполняется вне графического конвейера, а пиксельный — внутри него, и Direct2D загружает их разными путями
Метод настройки: SetDrawInfo вместо SetComputeInfo
В compute‑версии настройка происходила в SetComputeInfo, где вызывались SetOutputBuffer, SetComputeShader, SetComputeShaderConstantBuffer и задавался InstructionCountHint.
В пиксельном варианте используется SetDrawInfo, и набор вызовов меняется:
SetPixelShader(GUID) — активирует загруженный шейдер (аналог SetComputeShader).
SetPixelShaderConstantBuffer(…) — передаёт константный буфер (аналог SetComputeShaderConstantBuffer). Важно: в draw‑трансформе этот буфер автоматически привязывается к слоту b0, а не b1. В compute‑версии b0 был зарезервирован под системный буфер (resultRect, outputOffset), а в пиксельном такой необходимости нет, поэтому пользовательские константы могут занимать b0.
SetInputDescription(0, …) — новый вызов, которого не было в compute‑версии. Он настраивает, как Direct2D будет подготавливать входную текстуру для шейдера — в данном случае задаётся линейная фильтрация (D2D1_FILTER_MIN_MAG_MIP_LINEAR). В compute‑шейдере входные данные читались напрямую как SRV, и этот шаг не требовался.
SetOutputBuffer отсутствует — в compute‑версии он явно задавал формат выходного UAV‑буфера (32BPC_FLOAT, 4 канала). В пиксельном шейдере выход определяется целевым render target, поэтому этот вызов не нужен.
Отсутствие CalculateThreadgroups
Этот метод был специфичен для compute‑трансформа: он сообщал Direct2D, сколько групп потоков запустить для обработки выходного прямоугольника. В пиксельном шейдере такой метод не существует, поскольку количество вызовов определяется автоматически растеризатором для каждого фрагмента.
QueryInterface для нового интерфейса
В QueryInterface проверка uuidof(ID2D1ComputeTransform) заменена на uuidof(ID2D1DrawTransform). Остальные проверяемые интерфейсы (ID2D1Transform, ID2D1TransformNode, IUnknown) остались прежними, так как ID2D1DrawTransform также наследует их.
Класс эффекта остаётся практически идентичным тому, что был в статье про вычислительный шейдер. Он управляет графом трансформаций. HabrPixelEffect.h:
#pragma once #include "pch.h" // {88F0F871-18A3-4C55-B528-E738A781351F} DEFINE_GUID(HABR_EFFECT, 0x88F0F871, 0x18A3, 0x4C55, 0xB5, 0x28, 0xE7, 0x38, 0xA7, 0x81, 0x35, 0x1F); class HabrEffect : public ID2D1EffectImpl { public: static HRESULT Register(_In_ ID2D1Factory1* pFactory); static HRESULT __stdcall Create(_Outptr_ IUnknown** ppEffectImpl); // ID2D1EffectImpl IFACEMETHODIMP Initialize( _In_ ID2D1EffectContext* pEffectContext, _In_ ID2D1TransformGraph* pTransformGraph) override; IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType) override; IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pTransformGraph) override; // IUnknown IFACEMETHODIMP_(ULONG) AddRef() override; IFACEMETHODIMP_(ULONG) Release() override; IFACEMETHODIMP QueryInterface(REFIID riid, _Outptr_ void** ppOutput) override; private: HabrEffect(); ~HabrEffect() = default; LONG m_cRef; };
Реализация будет как в прошлой статье. HabrPixelEffect.cpp:
#include "pch.h" #include <initguid.h> #include "HabrEffect.h" #include "HabrNodeHandler.h" #define XML(X) TEXT(#X) HabrEffect::HabrEffect() : m_cRef(0) {} HRESULT HabrEffect::Register(_In_ ID2D1Factory1* pFactory) { PCWSTR pszXml = L"<?xml version='1.0'?>" L"<Effect>" L" <Property name='DisplayName' type='string' value='Habr compute effect'/>" L" <Property name='Author' type='string' value='...'/>" L" <Property name='Category' type='string' value='Transform'/>" L" <Property name='Description' type='string' value='...'/>" L" <Inputs><Input name='Source'/></Inputs>" L"</Effect>"; return pFactory->RegisterEffectFromString( HABR_EFFECT, pszXml, nullptr, 0, Create); } HRESULT __stdcall HabrEffect::Create(_Outptr_ IUnknown** ppEffectImpl) { *ppEffectImpl = static_cast<ID2D1EffectImpl*>(new (std::nothrow) HabrEffect()); if (*ppEffectImpl == nullptr) return E_OUTOFMEMORY; (*ppEffectImpl)->AddRef(); return S_OK; } IFACEMETHODIMP HabrEffect::Initialize( _In_ ID2D1EffectContext* pEffectContext, _In_ ID2D1TransformGraph* pTransformGraph) { // Проверяем, что видеокарта поддерживает compute shader (SM 5.0) if (pEffectContext->IsShaderLoaded(GUID_HABR_PIXEL_SHADER) == FALSE) { // Шейдер ещё не загружен - его загрузит HabrNodeHandler в конструкторе. // Здесь можно было бы загрузить заранее, но оставим как есть. } Microsoft::WRL::ComPtr<HabrNodeHandler> transform = new (std::nothrow) HabrNodeHandler(pEffectContext); if (!transform) return E_OUTOFMEMORY; HRESULT hr = pTransformGraph->AddNode(transform.Get()); if (FAILED(hr)) return hr; hr = pTransformGraph->ConnectToEffectInput(0, transform.Get(), 0); if (FAILED(hr)) return hr; hr = pTransformGraph->SetOutputNode(transform.Get()); if (FAILED(hr)) return hr; return S_OK; } IFACEMETHODIMP HabrEffect::SetGraph(_In_ ID2D1TransformGraph* /*pTransformGraph*/) { return E_NOTIMPL; } IFACEMETHODIMP HabrEffect::PrepareForRender(D2D1_CHANGE_TYPE /*changeType*/) { return S_OK; } IFACEMETHODIMP_(ULONG) HabrEffect::AddRef() { return InterlockedIncrement(&m_cRef); } IFACEMETHODIMP_(ULONG) HabrEffect::Release() { ULONG cRef = static_cast<ULONG>(InterlockedDecrement(&m_cRef)); if (cRef == 0) delete this; return cRef; } IFACEMETHODIMP HabrEffect::QueryInterface(REFIID riid, _Outptr_ void** ppOutput) { *ppOutput = nullptr; HRESULT hr = S_OK; if (riid == __uuidof(ID2D1EffectImpl)) *ppOutput = static_cast<ID2D1EffectImpl*>(this); else if (riid == __uuidof(IUnknown)) *ppOutput = this; else hr = E_NOINTERFACE; if (*ppOutput != nullptr) AddRef(); return hr; }
pch.h:
#pragma once #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0A00 #endif #ifndef NOMINMAX #define NOMINMAX #endif #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <windowsx.h> #include <memory> #include <string> #include <vector> #include <algorithm> #include <cmath> #include <cstdint> #include <cassert> #include <stdexcept> #include <objbase.h> #include <wrl/client.h> #include <d3d11.h> #include <d3d11_1.h> #include <d3d11_2.h> #include <d3d11_3.h> #include <dxgi.h> #include <dxgi1_2.h> #include <d2d1.h> #include <d2d1_1.h> #include <d2d1_2.h> #include <d2d1_3.h> #include <d2d1effectauthor.h> #include <dwrite.h> #include <dwrite_1.h> #include <dwrite_2.h> #include <wincodec.h> #include <DirectXMath.h>
pch.cpp
#include "pch.h"
и главный файл с точкой входа, такой же как и в прошлой статьей. Main.cpp:
#include "pch.h" #include "HabrEffect.h" // ------------------- Глобальные переменные ------------------- HWND g_hwnd = nullptr; bool g_windowClosed = false; // Direct3D 11 Microsoft::WRL::ComPtr<ID3D11Device> g_device; Microsoft::WRL::ComPtr<ID3D11DeviceContext> g_context; Microsoft::WRL::ComPtr<IDXGISwapChain1> g_swapChain; // Direct2D Microsoft::WRL::ComPtr<ID2D1Factory3> g_d2dFactory; Microsoft::WRL::ComPtr<ID2D1Device2> g_d2dDevice; Microsoft::WRL::ComPtr<ID2D1DeviceContext2> g_d2dContext; Microsoft::WRL::ComPtr<ID2D1Bitmap1> g_d2dTargetBitmap; // Эффект и вспомогательные Microsoft::WRL::ComPtr<ID2D1Effect> g_effect; Microsoft::WRL::ComPtr<ID2D1Bitmap1> g_inputBitmap; Microsoft::WRL::ComPtr<IDXGIDevice3> g_dxgiDevice; Microsoft::WRL::ComPtr<IDXGIFactory2> g_dxgiFactory; Microsoft::WRL::ComPtr<IDXGIAdapter> g_dxgiAdapter; D2D1_SIZE_F g_renderTargetSize = { 0, 0 }; float g_dpi = 96.0f; D3D_FEATURE_LEVEL g_featureLevel = D3D_FEATURE_LEVEL_11_0; // ------------------- Прототипы ------------------- LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); bool Initialize(HINSTANCE hInstance, int nCmdShow); void CreateIndependentResources(); void CreateDependentResources(); void OnResize(); void Render(); void Shutdown(); HRESULT LoadTextureFromFile(const std::wstring& filename, Microsoft::WRL::ComPtr<ID2D1Bitmap1>& outBitmap); // ------------------- Точка входа ------------------- int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPSTR, _In_ int nCmdShow) { CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (!Initialize(hInstance, nCmdShow)) { Shutdown(); CoUninitialize(); return 1; } // Главный цикл MSG msg = {}; while (!g_windowClosed) { while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } if (!g_windowClosed) Render(); } Shutdown(); CoUninitialize(); return 0; } // ------------------- Инициализация ------------------- bool Initialize(HINSTANCE hInstance, int nCmdShow) { WNDCLASSEX wcex = {}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.hInstance = hInstance; wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszClassName = L"HabrComputeEffectWindowClass"; if (!RegisterClassEx(&wcex)) return false; RECT rc = { 0, 0, 1920, 1080 }; AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE); g_hwnd = CreateWindowEx(0, L"HabrComputeEffectWindowClass", L"Direct2D Custom Compute Shader Effect", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, nullptr, nullptr, hInstance, nullptr); if (!g_hwnd) return false; RECT clientRect; GetClientRect(g_hwnd, &clientRect); g_renderTargetSize.width = static_cast<FLOAT>(clientRect.right - clientRect.left); g_renderTargetSize.height = static_cast<FLOAT>(clientRect.bottom - clientRect.top); // DPI HMODULE hUser32 = GetModuleHandleW(L"user32.dll"); if (hUser32) { auto pGetDpiForWindow = (UINT(WINAPI*)(HWND))GetProcAddress(hUser32, "GetDpiForWindow"); g_dpi = pGetDpiForWindow ? (float)pGetDpiForWindow(g_hwnd) : 96.0f; } // Фабрика D2D (с debug-слоем в Debug-сборке) D2D1_FACTORY_OPTIONS options = {}; #if defined(_DEBUG) options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; #endif HRESULT hr = D2D1CreateFactory( D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory3), &options, &g_d2dFactory); if (FAILED(hr)) { OutputDebugStringW(L"[Main] D2D1CreateFactory failed\n"); return false; } CreateIndependentResources(); CreateDependentResources(); // Регистрация эффекта hr = HabrEffect::Register(g_d2dFactory.Get()); if (FAILED(hr)) { wchar_t buf[128]; swprintf_s(buf, L"[Main] Register FAILED: 0x%08X\n", hr); OutputDebugStringW(buf); return false; } hr = g_d2dContext->CreateEffect(HABR_EFFECT, &g_effect); if (FAILED(hr) || !g_effect) { OutputDebugStringW(L"[Main] CreateEffect FAILED\n"); return false; } hr = LoadTextureFromFile(L"image.png", g_inputBitmap); if (FAILED(hr) || !g_inputBitmap) { OutputDebugStringW(L"[Main] LoadTexture FAILED\n"); return false; } // ВАЖНО: SetInput возвращает void, а не HRESULT g_effect->SetInput(0, g_inputBitmap.Get()); OutputDebugStringW(L"[Main] Effect input set OK\n"); ShowWindow(g_hwnd, nCmdShow); UpdateWindow(g_hwnd); return true; } // ------------------- Создание устройств ------------------- void CreateIndependentResources() { HRESULT hr = D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, 0, D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION, &g_device, &g_featureLevel, &g_context); if (FAILED(hr)) return; g_device.As(&g_dxgiDevice); g_d2dFactory->CreateDevice(g_dxgiDevice.Get(), &g_d2dDevice); g_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &g_d2dContext); g_dxgiDevice->GetAdapter(&g_dxgiAdapter); g_dxgiAdapter->GetParent(IID_PPV_ARGS(&g_dxgiFactory)); } void CreateDependentResources() { g_d2dTargetBitmap.Reset(); g_swapChain.Reset(); DXGI_SWAP_CHAIN_DESC1 desc = {}; desc.Width = static_cast<UINT>(g_renderTargetSize.width); desc.Height = static_cast<UINT>(g_renderTargetSize.height); desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; desc.Stereo = FALSE; desc.SampleDesc.Count = 1; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.BufferCount = 2; desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; desc.Scaling = DXGI_SCALING_NONE; desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; g_dxgiFactory->CreateSwapChainForHwnd( g_device.Get(), g_hwnd, &desc, nullptr, nullptr, &g_swapChain); g_dxgiDevice->SetMaximumFrameLatency(1); Microsoft::WRL::ComPtr<IDXGISurface2> backBuffer; g_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)); D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), g_dpi, g_dpi); g_d2dContext->CreateBitmapFromDxgiSurface( backBuffer.Get(), &props, &g_d2dTargetBitmap); g_d2dContext->SetTarget(g_d2dTargetBitmap.Get()); } // ------------------- Загрузка текстуры ------------------- HRESULT LoadTextureFromFile(const std::wstring& filename, Microsoft::WRL::ComPtr<ID2D1Bitmap1>& outBitmap) { Microsoft::WRL::ComPtr<IWICImagingFactory> wic; HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wic)); if (FAILED(hr)) return hr; Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder; hr = wic->CreateDecoderFromFilename(filename.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &decoder); if (FAILED(hr)) return hr; Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame; hr = decoder->GetFrame(0, &frame); if (FAILED(hr)) return hr; Microsoft::WRL::ComPtr<IWICFormatConverter> conv; hr = wic->CreateFormatConverter(&conv); if (FAILED(hr)) return hr; hr = conv->Initialize(frame.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.0f, WICBitmapPaletteTypeCustom); if (FAILED(hr)) return hr; return g_d2dContext->CreateBitmapFromWicBitmap(conv.Get(), nullptr, &outBitmap); } // ------------------- Отрисовка ------------------- void Render() { g_d2dContext->BeginDraw(); g_d2dContext->Clear(D2D1::ColorF(D2D1::ColorF::CornflowerBlue)); // Рисуем результат шейдера g_d2dContext->DrawImage( g_effect.Get(), D2D1_INTERPOLATION_MODE_LINEAR, D2D1_COMPOSITE_MODE_SOURCE_OVER); HRESULT hr = g_d2dContext->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { OnResize(); return; } g_swapChain->Present(1, 0); } void OnResize() { if (!g_hwnd) return; RECT rc; GetClientRect(g_hwnd, &rc); g_renderTargetSize.width = static_cast<FLOAT>(rc.right - rc.left); g_renderTargetSize.height = static_cast<FLOAT>(rc.bottom - rc.top); if (g_swapChain) { g_d2dContext->SetTarget(nullptr); g_d2dTargetBitmap.Reset(); if (g_renderTargetSize.width > 0 && g_renderTargetSize.height > 0) { g_swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, 0); CreateDependentResources(); } } } // ------------------- Завершение ------------------- void Shutdown() { g_windowClosed = true; g_effect.Reset(); g_inputBitmap.Reset(); g_d2dTargetBitmap.Reset(); g_d2dContext.Reset(); g_d2dDevice.Reset(); g_d2dFactory.Reset(); g_swapChain.Reset(); g_context.Reset(); g_device.Reset(); g_dxgiDevice.Reset(); g_dxgiAdapter.Reset(); g_dxgiFactory.Reset(); if (g_hwnd) { DestroyWindow(g_hwnd); g_hwnd = nullptr; } } // ------------------- WndProc ------------------- LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_SIZE: if (wParam != SIZE_MINIMIZED) OnResize(); return 0; case WM_DESTROY: g_windowClosed = true; PostQuitMessage(0); return 0; case WM_CLOSE: DestroyWindow(hWnd); return 0; } return DefWindowProc(hWnd, msg, wParam, lParam); }
Собственно собираем проект, шейдер, о том как сделать второе:
fxc /T ps_5_0 /E main /Fo HABR_SHADER.cso HABR_SHADER.hlsl
То есть теперь у нас не cs_5_0, а ps_5_0, собственно ps означает Pixel Shader.
Берём изображение, у меня оно 50 на 50. Исходное:

Результат работы шейдера:

Довольно коротко получилось, осталось рассмотреть: вершинный шейдер и стадию смешивания.