The Jerusalem PostZini clashes with Shin Bet officials over need to expand tools used in elections interference casePunchEx-Liverpool defender reveals secret battle with seizuresDaily MaverickWorld Cities Day — Holding the line on eviction protections for the vulnerableZDF heuteEntdecken Sie das ZDF-NachrichtenstudioNHK 社会滋賀 甲賀市で住宅火災 家族3人と連絡取れずRolling StoneMy Chemical Romance Tops the Billboard 200 With 20-Year-Old AlbumInteriaCmentarze rozświetlają się tysiącami świateł. Wyjątkowy dzień w PolsceE! OnlineInfluencer Liam Fitzgerald Dead at 29 After Motorcycle CrashColliderThe 10 Best Thriller Books of All Time, RankedSBS 뉴스이 대통령, 오늘 시정연설…'821조' 예산안 설명Complete SportsEuromatch NPFL: Massive Three Points –Bendel Insurance Media Officer Hails Victory Against Kwara UnitedPremium TimesDecoupling Sovereignty Index (5): How decoupling reinforces itself, By Max Amuchie
The Daily Newsstand · Free, Always
Sunday, November 1, 2026

Direct 2D #15.2 Пользовательский эффект. Пиксельный шейдер. HLSL

Translate

Здравствуйте! В предыдущей статье мы разобрали, что такое шейдеры в целом, и подробно остановились на вычислительном шейдере , который работает вне графического конвейера. Однако есть ещё формирования каждого пикселя на экране. За эту задачу отвечает пиксельный шейдер (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;
}

Что осталось прежним:

  1. COM-инфраструктура: AddRef, Release, QueryInterface (с заменой проверяемого интерфейса), а также приватный деструктор.

  2. Функции MapOutputRectToInputRects, MapInputRectsToOutputRect и MapInvalidRect реализуют попиксельное отображение “один-к-одному”

  3. Загрузка .cso из файла: конструктор по-прежнему открывает HABR_SHADER.cso через CreateFileW, читает его в std::vector и передаёт данные в ID2D1EffectContext

  4. Структура 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. Исходное:

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

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

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.