Direct 2D #15.2 Пользовательский эффект. Пиксельный шейдер. HLSL
Здравствуйте! В предыдущей статье мы разобрали, что такое шейдеры в целом, и подробно остановились на вычислительном шейдере , который работает вне графического конвейера. Однако есть ещё формирования каждого пикселя на экране. За эту задачу отвечает пиксельный шейдер (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. Исходное:

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

Довольно коротко получилось, осталось рассмотреть: вершинный шейдер и стадию смешивания.
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.