Direct2D 独自Effectの作成(PixelShader利用)

下記記事にて、独自のEffectを作成しました。
Direct2D 独自Effectの作成 - 何でもプログラミング

Effectの効果自体は、組み込みのID2D1OffsetTransformを利用していたので、今回は独自の効果(PixelShader)を定義してみたいと思います。

下図のように、青と赤を入れ替える効果を作成してみます。

f:id:any-programming:20171027153731p:plain

本記事は上記記事をベースとしていますので、特に説明のない部分は上記記事を参照してみてください。

シェーダコンパイラ

コンパイル済みのcsoを準備するために、今回は動的にコンパイルする関数を用意しました。

#include <d3dcompiler.h>
#pragma comment(lib, "d3dcompiler.lib")

ComPtr<ID3DBlob> Compile(std::string code, std::string target)
{
#ifdef _DEBUG
    UINT flags1 = D3DCOMPILE_DEBUG | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS;
#else
    UINT flags1 = D3DCOMPILE_OPTIMIZATION_LEVEL3 | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS;
#endif
    ComPtr<ID3DBlob> compiled, errorMessage;
    D3DCompile(
        code.c_str(),
        code.size(),
        NULL,
        NULL,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        "main",
        target.c_str(),
        flags1,
        0,
        &compiled,
        &errorMessage);
    if (errorMessage)
        throw std::exception((char*)errorMessage->GetBufferPointer());
   
    return compiled;
}


独自Transformクラス

ID2D1DrawTransformを継承して独自のTransformを作成します。

関数がたくさんありますが、重要なのはSetDrawInfoでSetPixelShaderを呼んでいるところくらいです。

PixelShaderの作成自体はEffectクラスのInitializeで行います。

DEFINE_GUID(GUID_MyTransformPS, ....); // 独自のGUIDを定義。

class MyTransform : public ID2D1DrawTransform
{
public:
    MyTransform() : m_refCount(1) {}

    // ここから5つの関数は、ID2D1DrawTransformに必要な関数です。
    IFACEMETHODIMP SetDrawInfo(_In_ ID2D1DrawInfo* pDrawInfo)
    {
        return pDrawInfo->SetPixelShader(GUID_MyTransformPS);
    }
    IFACEMETHODIMP_(UINT32) GetInputCount() const
    {
        return 1;
    }
    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
    )
    {
        if (inputRectCount != 1)
            return E_INVALIDARG;

        *pOutputRect = pInputRects[0];
        *pOutputOpaqueSubRect = pInputOpaqueSubRects[0];

        return S_OK;
    }
    IFACEMETHODIMP 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 MapInvalidRect(
        UINT32 inputIndex,
        D2D1_RECT_L invalidInputRect,
        _Out_ D2D1_RECT_L* pInvalidOutputRect
    ) const
    {
        if (inputIndex != 0)
            return E_INVALIDARG;

        *pInvalidOutputRect = invalidInputRect;

        return S_OK;
    }

    // ここからはCOMに必要な関数です。特別な実装はありません。
    IFACEMETHODIMP_(ULONG) AddRef()
    {
        m_refCount++;
        return m_refCount;
    }
    IFACEMETHODIMP_(ULONG) Release()
    {
        m_refCount--;

        if (m_refCount == 0)
            delete this;

        return m_refCount;
    }
    IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput)
    {
        *ppOutput = nullptr;
        HRESULT hr = S_OK;

        if (riid == __uuidof(ID2D1DrawTransform))
            *ppOutput = (ID2D1DrawTransform*)this;
        else if (riid == __uuidof(ID2D1Transform))
            *ppOutput = (ID2D1Transform*)this;
        else if (riid == __uuidof(ID2D1TransformNode))
            *ppOutput = (ID2D1TransformNode*)this;
        else if (riid == __uuidof(IUnknown))
            *ppOutput = this;
        else
            hr = E_NOINTERFACE;

        if (*ppOutput != nullptr)
            AddRef();

        return hr;
    }
private:
    LONG m_refCount;
};


EffectクラスのInitialize

前回の記事で作成したMyEffectのInitialize関数を、下記で置き換えます。

PixelShaderの入力とTextureは下記の値がDirect2Dから渡されますので、独自の出力を作成します。(VertexShaderを独自で作成した場合は入力を変更できます。)

IFACEMETHODIMP Initialize(_In_ ID2D1EffectContext* pContextInternal, _In_ ID2D1TransformGraph* pTransformGraph)
{
    std::string psCode =
        "Texture2D InputTexture : register(t0);                           \n"
        "SamplerState InputSampler : register(s0);                        \n"
        "                                                                 \n"
        "float4 main(                                                     \n"
        "    float4 clipSpaceOutput : SV_POSITION,                        \n"
        "    float4 sceneSpaceOutput : SCENE_POSITION,                    \n"
        "    float4 texelSpaceInput0 : TEXCOORD0                          \n"
        ") : SV_Target                                                    \n"
        "{                                                                \n"
        "    float4 color = InputTexture.Sample(                          \n"
        "        InputSampler,                                            \n"
        "        texelSpaceInput0.xy                                      \n"
        "    );                                                           \n"
        "    return float4(color.b, color.g, color.r, color.a);           \n"
        "}                                                                \n"
        ;
    auto cso = Compile(psCode, "ps_4_0_level_9_1");
    auto hr = pContextInternal->LoadPixelShader(GUID_MyTransformPS, (BYTE*)cso->GetBufferPointer(), cso->GetBufferSize());
    if (FAILED(hr))
        return hr;

    ComPtr<MyTransform> transform;
    transform.Attach(new MyTransform());
    return pTransformGraph->SetSingleTransformNode(transform.Get());
}






Direct2D 独自Effectの作成

下記の記事にて、Direct2Dの組み込みのEffectを利用してみました。
Direct2D Effectの利用 - 何でもプログラミング

今回は独自のEffectを作成してみたいと思います。

組み込みで用意されているOffset効果を利用して、描画結果をずらしてみます。

f:id:any-programming:20171031114928p:plain

本記事は上記記事をベースとしていますので、特に言及のない関数は上記記事を参照してみてください。

独自Effectクラス

ID2D1EffectImplを実装して独自のEffectクラスを作成します。

基本的にはInitialize関数とRegister関数の実装がメインとなります。

#include <initguid.h>
#include <d2d1effectauthor.h>

DEFINE_GUID(CLSID_MyEffect, ...); // 独自のGUIDを定義してください。

class MyEffect : public ID2D1EffectImpl
{
public:
    // ここから3つの関数は、ID2D1EffectImplに必要な関数です。
    IFACEMETHODIMP Initialize(_In_ ID2D1EffectContext* pContextInternal, _In_ ID2D1TransformGraph* pTransformGraph)
    {
        // Offsetを行う効果(組み込み)の作成&登録
        ComPtr<ID2D1OffsetTransform> transform;
        auto hr = pContextInternal->CreateOffsetTransform(D2D1::Point2L(10, 10), &transform);
        if (SUCCEEDED(hr))
            hr = pTransformGraph->SetSingleTransformNode(transform.Get());

        return hr;
    }
    IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType)
    {
        return S_OK;
    }
    IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pGraph)
    {
        return S_OK;
    }

    // ここから2つのstatic関数は、このクラスをID2D1Factory1に登録するのに利用します。(関数名は何でも大丈夫です。)
    static HRESULT Register(_In_ ID2D1Factory1* pFactory)
    {
        // 1画像を入力とするEffectの登録
        auto xml = 
            L"<?xml version = '1.0'?>                                                       "
            L"<Effect>                                                                      "
            L"    <Property name='DisplayName' type='string' value='MyEffect' />            "
            L"    <Property name='Author' type='string' value='Author' />                   "
            L"    <Property name='Category' type='string' value='Category' />               "
            L"    <Property name='Description' type='string' value='Description' />         "
            L"    <Inputs>                                                                  "
            L"        <Input name='Source' />                                               "
            L"    </Inputs>                                                                 "
            L"</Effect>                                                                     "
            ;
        return pFactory->RegisterEffectFromString(CLSID_MyEffect, xml, nullptr, 0, CreateEffect);
    }
    static HRESULT __stdcall CreateEffect(_Outptr_ IUnknown** ppEffectImpl)
    {
        *ppEffectImpl = (ID2D1EffectImpl*)(new MyEffect());

        if (*ppEffectImpl == nullptr)
            return E_OUTOFMEMORY;

        return S_OK;
    }

    // ここからはCOMに必要な関数です。特別な実装はありません。
    IFACEMETHODIMP_(ULONG) AddRef()
    {
        m_refCount++;
        return m_refCount;
    }
    IFACEMETHODIMP_(ULONG) Release()
    {
        m_refCount--;

        if (m_refCount == 0)
            delete this;

        return m_refCount;
    }
    IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput)
    {
        *ppOutput = nullptr;
        HRESULT hr = S_OK;

        if (riid == __uuidof(ID2D1EffectImpl))
            *ppOutput = (ID2D1EffectImpl*)this;
        else if (riid == __uuidof(IUnknown))
            *ppOutput = this;
        else
            hr = E_NOINTERFACE;

        if (*ppOutput != nullptr)
            AddRef();

        return hr;
    }
private:
    LONG m_refCount;
    MyEffect() : m_refCount(1) {}
};


Effectの作成

独自のGUIDでCreateEffectします。

ComPtr<ID2D1Effect> CreateEffect(ID2D1DeviceContext* context)
{
    ComPtr<ID2D1Effect> effect;
    AssertHR(context->CreateEffect(CLSID_MyEffect, &effect));

    return effect;
}


main関数

作成したEffectの登録が追記されています。

ID2D1Factory1を作成した時に同時に登録したほうが簡潔だと思われます。

int main()
{
    HWND hwnd = CreateHWND(WndProc);

    auto dxgiDevice = CreateDXGIDevice();
    g_context = CreateD2DContext(dxgiDevice.Get());
    g_swapChain = CreateSwapChain(dxgiDevice.Get(), hwnd);
    g_backBuffer = CreateBackBufferBitmap(g_context.Get(), g_swapChain.Get());
    g_offscreenBuffer = CreateTargetBitmap(g_context.Get(), g_backBuffer->GetPixelSize());

    ComPtr<ID2D1Factory> factory;
    g_context->GetFactory(&factory);
    ComPtr<ID2D1Factory1> factory1;
    AssertHR(factory.As(&factory1));
    AssertHR(MyEffect::Register(factory1.Get()));

    g_effect = CreateEffect(g_context.Get());

    Run(hwnd);

    return 0;
}






Direct2D Effectの利用

下記記事にて、ID2D1DeviceContextを利用して楕円を描画するところまで実装しました。
Direct2D導入(ID2D1DeviceContext) - 何でもプログラミング

今回はID2D1DeviceContextで利用できるようになったEffectを実装してみたいと思います。

上記記事で描画した楕円画像に、赤と青を入れ替えるEffectを適用してみます。

f:id:any-programming:20171027153731p:plain

記事中に特に説明のない関数は、上記記事を参照してみてください。

Effect作成

既存のEffectを利用して、赤と青を入れ替えるEffectを作成します。

#include <initguid.h>
#include <d2d1_1.h>
#pragma comment(lib, "d2d1.lib")

ComPtr<ID2D1Effect> CreateEffect(ID2D1DeviceContext* context)
{
    ComPtr<ID2D1Effect> effect;
    AssertHR(context->CreateEffect(CLSID_D2D1ColorMatrix, &effect));

    D2D1_MATRIX_5X4_F matrix = D2D1::Matrix5x4F(0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0);
    AssertHR(effect->SetValue(D2D1_COLORMATRIX_PROP_COLOR_MATRIX, matrix));

    return effect;
}


OffscreenBufferの作成

Effectの入力としてBackBufferは利用できないため、一時描画先としてOffscreenBufferを作成します。

ComPtr<ID2D1Bitmap1> CreateTargetBitmap(ID2D1DeviceContext* context, D2D1_SIZE_U size)
{
    auto prop = D2D1::BitmapProperties1(
        D2D1_BITMAP_OPTIONS_TARGET,
        D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE)
    );

    ComPtr<ID2D1Bitmap1> bitmap;
    AssertHR(context->CreateBitmap(size, nullptr, size.width * 4, prop, &bitmap));

    return bitmap;
}


アプリケーションコード

まずOffscreenBufferに楕円を描画し、次にEffectを適用した結果をBackBufferに描画しています。

サイズが変更された場合は、BackBuffer、OffscreenBuffer共に作り直しています。(BackBufferを解放しておかないと、ResizeBuffersで例外が発生します。)

ComPtr<ID2D1DeviceContext> g_context;
ComPtr<IDXGISwapChain1> g_swapChain;
ComPtr<ID2D1Bitmap1> g_backBuffer;
ComPtr<ID2D1Bitmap1> g_offscreenBuffer;
ComPtr<ID2D1Effect> g_effect;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
        {
            g_context->BeginDraw();

            g_context->SetTarget(g_offscreenBuffer.Get());

            g_context->Clear(D2D1::ColorF(D2D1::ColorF::LightYellow));

            // 楕円描画
            D2D1_SIZE_F size = g_context->GetSize();
            float rx = size.width / 2;
            float ry = size.height / 2;
            D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(rx, ry), rx, ry);
            ComPtr<ID2D1SolidColorBrush> brush;
            AssertHR(g_context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &brush));
            g_context->FillEllipse(ellipse, brush.Get());

            // Effect適用(赤青いれかえ)
            g_context->SetTarget(g_backBuffer.Get());
            g_effect->SetInput(0, g_offscreenBuffer.Get());
            g_context->DrawImage(g_effect.Get());
            g_effect->SetInput(0, nullptr);

            AssertHR(g_context->EndDraw());
            g_context->SetTarget(nullptr);
                
            DXGI_PRESENT_PARAMETERS params = { 0 };
            AssertHR(g_swapChain->Present1(1, 0, &params));
        }
        return 0;
    case WM_SIZE:
        g_backBuffer = nullptr;
        AssertHR(g_swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, 0)); // サイズ0で、自動でWindowのサイズが設定される
        g_backBuffer = CreateBackBufferBitmap(g_context.Get(), g_swapChain.Get());
        g_offscreenBuffer = CreateTargetBitmap(g_context.Get(), g_backBuffer->GetPixelSize());
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}
int main()
{
    HWND hwnd = CreateHWND(WndProc);

    auto dxgiDevice = CreateDXGIDevice();
    g_context = CreateD2DContext(dxgiDevice.Get());
    g_swapChain = CreateSwapChain(dxgiDevice.Get(), hwnd);
    g_backBuffer = CreateBackBufferBitmap(g_context.Get(), g_swapChain.Get());
    g_offscreenBuffer = CreateTargetBitmap(g_context.Get(), g_backBuffer->GetPixelSize());
    g_effect = CreateEffect(g_context.Get());

    Run(hwnd);

    return 0;
}






Direct2D導入(ID2D1DeviceContext)

下記記事にて、ID2D1HwndRenderTargetを利用してDirect2Dを利用してみました。
Direct2D導入(ID2D1HwndRenderTarget) - 何でもプログラミング

Direct2D 1.1からは、ID2D1DeviceContextを利用して実装するようになっています。

今回はID2D1DeviceContextを用いて上記記事と同じアプリを作成してみたいと思います。

f:id:any-programming:20171026193608p:plain

記事中のCreateHWND関数、Run関数は上記記事を参照してください。

IDXGIDevice1の作成

まずはIDXGIDevice1を作成します。

ID3D11Deviceを作成して、そこからIDXGIDevice1を取得します。

#include <dxgi1_2.h>

#include <d3d11.h>
#pragma comment(lib, "d3d11.lib")

ComPtr<IDXGIDevice1> CreateDXGIDevice()
{
    ComPtr<ID3D11Device> device;
    AssertHR(D3D11CreateDevice(
        nullptr,
        D3D_DRIVER_TYPE_HARDWARE,
        0,
        D3D11_CREATE_DEVICE_BGRA_SUPPORT, // Direct2Dではこのフラグが必要
        nullptr,
        0,
        D3D11_SDK_VERSION,
        &device,
        nullptr,
        nullptr
    ));

    ComPtr<IDXGIDevice1> dxgiDevice;
    AssertHR(device.As(&dxgiDevice));
    AssertHR(dxgiDevice->SetMaximumFrameLatency(1)); // レイテンシを最小にしておく

    return dxgiDevice;
}


ID2D1DeviceContextの作成

IDXGIDevice1を利用して、ID2D1DeviceContextを作成します。

#include <d2d1_1.h>
#pragma comment(lib, "d2d1.lib")

ComPtr<ID2D1DeviceContext> CreateD2DContext(IDXGIDevice1* dxgiDevice)
{
    ComPtr<ID2D1Factory1> factory;
    AssertHR(D2D1CreateFactory<ID2D1Factory1>(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory));

    ComPtr<ID2D1Device> device;
    AssertHR(factory->CreateDevice(dxgiDevice, &device));

    ComPtr<ID2D1DeviceContext> context;
    AssertHR(device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &context));

    return context;
}


IDXGISwapChain1の作成

Windowに描画結果を表示するため、SwapChainを作成します。

またSwapChainのBackBufferのID2D1Bitmap1を作成する関数も準備します。

ComPtr<IDXGISwapChain1> CreateSwapChain(IDXGIDevice1* device, HWND hwnd)
{
    ComPtr<IDXGIAdapter> adapter;
    AssertHR(device->GetAdapter(&adapter));

    ComPtr<IDXGIFactory2> factory;
    AssertHR(adapter->GetParent(IID_PPV_ARGS(&factory)));

    DXGI_SWAP_CHAIN_DESC1 desc = { 0 };
    desc.Width = 0; // 自動でWindowのサイズが設定される
    desc.Height = 0;
    desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    desc.Stereo = false;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    desc.BufferCount = 2;
    desc.Scaling = DXGI_SCALING_NONE;
    desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
    desc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
    desc.Flags = 0;

    ComPtr<IDXGISwapChain1> swapChain;
    AssertHR(factory->CreateSwapChainForHwnd(
        device,
        hwnd,
        &desc,
        nullptr,
        nullptr,
        &swapChain
    ));

    return swapChain;
}
ComPtr<ID2D1Bitmap1> CreateBackBufferBitmap(ID2D1DeviceContext* context, IDXGISwapChain1* swapChain)
{
    ComPtr<IDXGISurface> backBuffer;
    AssertHR(swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)));

    auto prop = D2D1::BitmapProperties1(
        D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
        D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE)
    );
    ComPtr<ID2D1Bitmap1> bitmap;
    AssertHR(context->CreateBitmapFromDxgiSurface(backBuffer.Get(), &prop, &bitmap));

    return bitmap;
}


楕円描画関数

Windowいっぱいに楕円を描画します。

void Draw(ID2D1DeviceContext* context)
{
    context->BeginDraw();

    context->Clear(D2D1::ColorF(D2D1::ColorF::LightYellow));

    D2D1_SIZE_F size = context->GetSize();
    float rx = size.width / 2;
    float ry = size.height / 2;
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(rx, ry), rx, ry);
    ComPtr<ID2D1SolidColorBrush> brush;
    AssertHR(context->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &brush));
    context->FillEllipse(ellipse, brush.Get());

    AssertHR(context->EndDraw());
}


アプリケーションコード

Drawで描画した際、Windowに表示するためにSwapChainのPresentを呼び出す必要があります。

サイズ変更は、一度ContextのTargetから外し、ResizeBuffersで変更した後、再度Targetに設定する必要があります。

ComPtr<ID2D1DeviceContext> g_context;
ComPtr<IDXGISwapChain1> g_swapChain;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
        {
            Draw(g_context.Get());
            DXGI_PRESENT_PARAMETERS params = { 0 };
            AssertHR(g_swapChain->Present1(1, 0, &params));
        }
        return 0;
    case WM_SIZE:
        g_context->SetTarget(nullptr);
        AssertHR(g_swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_UNKNOWN, 0)); // サイズ0で、自動でWindowのサイズが設定される
        g_context->SetTarget(CreateBackBufferBitmap(g_context.Get(), g_swapChain.Get()).Get());
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}
int main()
{
    HWND hwnd = CreateHWND(WndProc);

    auto dxgiDevice = CreateDXGIDevice();
    g_context = CreateD2DContext(dxgiDevice.Get());
    g_swapChain = CreateSwapChain(dxgiDevice.Get(), hwnd);
    g_context->SetTarget(CreateBackBufferBitmap(g_context.Get(), g_swapChain.Get()).Get());

    Run(hwnd);

    return 0;
}






Direct2D導入(ID2D1HwndRenderTarget)

今回はDirect2Dを用いて、下記のような描画を行うところまで実装してみたいと思います。

f:id:any-programming:20171026193608p:plain

ウィンドウ生成、表示関数

Direct2Dの描画先として、通常のWindowsのウィンドウを作成、実行する関数を準備します。

内容は一般的なもので、特に変わったものではありません。

HWND CreateHWND(WNDPROC wndProc)
{
    WNDCLASSEX wndclass;
    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    wndclass.lpfnWndProc = wndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = GetModuleHandle(NULL);
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = NULL;
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = L"Main Window";
    wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    RegisterClassEx(&wndclass);

    HWND hwnd = CreateWindow(
        L"Main Window",
        L"Main Window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL,
        NULL,
        wndclass.hInstance,
        NULL
    );

    return hwnd;
}
void Run(HWND hwnd)
{
    ShowWindow(hwnd, SW_SHOWNORMAL);

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}


Direct2D初期化

ID2D1HwndRenderTargetを生成する関数を作成します。

ID2D1Factory経由で作成を行います。

#include <d2d1.h>
#pragma comment(lib, "d2d1.lib")

#include <wrl\client.h>
using namespace Microsoft::WRL;

ComPtr<ID2D1HwndRenderTarget> CreateRenderTarget(HWND hwnd)
{
    ComPtr<ID2D1Factory> factory;
    AssertHR(D2D1CreateFactory<ID2D1Factory>(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory));

    ComPtr<ID2D1HwndRenderTarget> renderTarget;
    AssertHR(factory->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(), 
        D2D1::HwndRenderTargetProperties(hwnd), 
        &renderTarget
    ));

    return renderTarget;
}


楕円描画

RenderTargetいっぱいに楕円を描画する関数となります。

void Draw(ID2D1HwndRenderTarget* renderTarget)
{
    renderTarget->BeginDraw();

    renderTarget->Clear(D2D1::ColorF(D2D1::ColorF::LightYellow));

    D2D1_SIZE_F size = renderTarget->GetSize();
    float rx = size.width / 2;
    float ry = size.height / 2;
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(rx, ry), rx, ry);
    ComPtr<ID2D1SolidColorBrush> brush;
    AssertHR(renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &brush));
    renderTarget->FillEllipse(ellipse, brush.Get());

    AssertHR(renderTarget->EndDraw());
}


WndProc

WM_SIZEにてRenderTargetのサイズを変更し、WM_PAINTにて上記のDraw関数を呼ぶよう実装します。

ComPtr<ID2D1HwndRenderTarget> g_renderTarget;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
        Draw(g_renderTarget.Get());
        return 0;
    case WM_SIZE:
        g_renderTarget->Resize(D2D1::SizeU(LOWORD(lParam), HIWORD(lParam)));
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}


main

最終的にmain関数はこのようになります。

int main()
{
    HWND hwnd = CreateHWND(WndProc);
    g_renderTarget = CreateRenderTarget(hwnd);
    Run(hwnd);

    return 0;
}






glTFファイルの読み込み(WPF3Dで表示)

3Dデータ用のフォーマットは3dsCOLLADA等たくさん存在しますが、2015年にKhronos GroupよりglTFというフォーマットが公開されました。
GitHub - KhronosGroup/glTF: glTF – Runtime 3D Asset Delivery

JSONベースで記述されていますが、頂点データなどは別ファイルにバイナリとして保存されているため、高速に読み込むことが可能です。

今回はglTFを読み込んで表示してみたいと思います。

読み込むデータは下記リンクにあるDuckを利用して実装していきたいと思います。
https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Duck/glTF
f:id:any-programming:20170814212650p:plain

glTFパース

現状すでにたくさんのパーサが存在していますが、今回はJSONライブラリを利用して自前でパースしてみたいと思います。

JSONパーサは下記のNewtonsoft.Jsonを利用しました。
NuGet Gallery | Json.NET 10.0.3

今回は必要最低限のデータをパースするよう、下記の様にGLTFクラスを準備しました。

public enum ComponentType
{
    Int8   = 5120,
    UInt8  = 5121,
    Int16  = 5122,
    UInt16 = 5123,
    Float  = 5126
}
public enum DataType
{
    VEC3,
    VEC2,
    SCALAR
}
public class Attributes
{
    public int Normal { get; set; }
    public int Position { get; set; }
    public int Texcoord_0 { get; set; }
}
public class Primitive
{
    public Attributes Attributes { get; set; }
    public int Indices { get; set; }
}
public class Mesh
{
    public Primitive[] Primitives { get; set; }
}
public class Accessor
{
    public int BufferView { get; set; }
    public int ByteOffset { get; set; }
    public ComponentType ComponentType { get; set; }
    public int Count { get; set; }
    public DataType Type { get; set; }
}
public class Image
{
    public string Uri { get; set; }
}
public class BufferView
{
    public int Buffer { get; set; }
    public int ByteOffset { get; set; }
    public int ByteLength { get; set; }
}
public class Buffer
{
    public string Uri { get; set; }
}
public class GLTF
{
    public Mesh[] Meshes { get; set; }
    public Accessor[] Accessors { get; set; }
    public Image[] Images { get; set; }
    public BufferView[] BufferViews { get; set; }
    public Buffer[] Buffers { get; set; }
}

gltfをテキストとして読み込み、GLTFとしてDeserializeObjectを呼ぶことで、各々のプロパティにデータがセットされます。

Jsonテキストとデシリアライズ先のクラスにおいて、階層とプロパティ名が同じ(デフォルトでは大文字小文字関係なし)場合、そこにデータがデシリアライズされるようになっており、Jsonテキスト側かクラス側のどちらかにしか存在しないプロパティは無視されるようになっています。

そのため上記の様にクラスを定義することで、読み込みたいデータのみをパース出来るようになっています。

string str = File.ReadAllText(path);
GLTF gltf = JsonConvert.DeserializeObject<GLTF>(str);

詳しいフォーマットはGitHubのglTFサイトを参照してください。

パースしたglTFからメッシュデータに変換

glTFでは最終的にAccessorで定義された条件でバッファを参照することとなります。

GLTFクラスからバッファデータを取得する拡張メソッドを下記の様に定義しました。

バイナリデータは別のファイルに格納されているため、ファイルの存在するディレクトリパスを渡しています。(通常はgltfファイルと同じ場所にあると思います。)

public static int Size(this ComponentType type) =>
    type == ComponentType.Float  ? 4 :
    type == ComponentType.Int16  ? 2 :
    type == ComponentType.UInt16 ? 2 :
    1;
public static int Size(this DataType type) =>
    type == DataType.SCALAR ? 1 :
    type == DataType.VEC2   ? 2 :
    3;
public static byte[][] AccessorBytes(this GLTF gltf, string directory)
{
    var buffers =
        gltf.Buffers
        .Select(x => File.ReadAllBytes($"{directory}\\{x.Uri}"))
        .ToArray();

    var bufferViews =
        gltf.BufferViews
        .Select(x => buffers[x.Buffer]
                     .Skip(x.ByteOffset)
                     .Take(x.ByteLength))
        .ToArray();

    var accessors =
        gltf.Accessors
        .Select(x => bufferViews[x.BufferView]
                     .Skip(x.ByteOffset)
                     .Take(x.Count * x.ComponentType.Size() * x.Type.Size())
                     .ToArray())
        .ToArray();

    return accessors;
}

さらにバッファデータから実際にDuckメッシュを作成する、Duckクラスを定義します。

Primitive.IndicesやPrimitive.Attributes.Positionなどにバッファのインデクスが格納されているので、そのバッファからPoint3D[]などを読み込みます。(WPF3Dで表示するため)

本来、テクスチャはMaterial→Texture→Imageの階層で参照するのですが、簡単のためImageから直接読み込んでいます。

class Duck
{
    public Point3D[] Positions { get; }
    public Vector3D[] Normals { get; }
    public Point[] Texcoords { get; }
    public int[] Indices { get; }
    public ImageSource Texture { get; }
    public Duck(string directory, GLTF gltf)
    {
        var accessorBytes = gltf.AccessorBytes(directory);

        var primitive = gltf.Meshes[0].Primitives[0];

        Indices = ConvertTo(accessorBytes[primitive.Indices], 2, (bytes, start) => 
            (int)BitConverter.ToUInt16(bytes, start)
        );
        Positions = ConvertTo(accessorBytes[primitive.Attributes.Position], 12, (bytes, start) =>
            new Point3D(BitConverter.ToSingle(bytes, start), 
                        BitConverter.ToSingle(bytes, start + 4), 
                        BitConverter.ToSingle(bytes, start + 8))
        );
        Normals = ConvertTo(accessorBytes[primitive.Attributes.Normal], 12, (bytes, start) =>
            new Vector3D(BitConverter.ToSingle(bytes, start), 
                         BitConverter.ToSingle(bytes, start + 4), 
                         BitConverter.ToSingle(bytes, start + 8))
        );
        Texcoords = ConvertTo(accessorBytes[primitive.Attributes.Texcoord_0], 8, (bytes, start) =>
            new Point(BitConverter.ToSingle(bytes, start), 
                      BitConverter.ToSingle(bytes, start + 4))
        );
        Texture = gltf.Images.Select(x => new BitmapImage(new Uri($"{directory}\\{x.Uri}"))).Single();
    }
    T[] ConvertTo<T>(byte[] bytes, int stepBytes, Func<byte[], int, T> convert)
    {
        T[] dst = new T[bytes.Length / stepBytes];
        for (int i = 0; i < dst.Length; ++i)
            dst[i] = convert(bytes, stepBytes * i);
        return dst;
    }
}


WPF3Dで表示

MeshGeometry3Dにデータをセットし、Viewport3Dを作成します。

詳しくは下記記事を参照してください。
WPFのクラスで3Dプログラミング - 何でもプログラミング

var model = new GeometryModel3D()
{
    Geometry = new MeshGeometry3D()
    {
        Positions = new Point3DCollection(mesh.Positions),
        Normals = new Vector3DCollection(mesh.Normals),
        TextureCoordinates = new PointCollection(mesh.Texcoords),
        TriangleIndices = new Int32Collection(mesh.Indices)
    },
    Material = new DiffuseMaterial(new ImageBrush(mesh.Texture)),
};

var light = new DirectionalLight(Colors.White, new Vector3D(0, 0, -1));

var camera = new PerspectiveCamera(new Point3D(0, 100, 300), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45);

Viewport3D viewport3D = new Viewport3D();
viewport3D.Camera = camera;
viewport3D.Children.Add(new ModelVisual3D() { Content = model });
viewport3D.Children.Add(new ModelVisual3D() { Content = light });

Content = viewport3D;

f:id:any-programming:20170814220540p:plain





WPFのクラスで3Dプログラミング

WPFで3Dプログラミングをする場合、通常は裏でDirect3Dを用意してレンダリングを行います。

ただ、とても簡単な3D描画を行いたい場合は、WPFが用意する3D描画クラスを利用することができます。

今回は簡単な実装をしてみたいと思います。

アプリケーション

今回はキーボードの上下左右で回転する立方体を表示してみたいと思います。
f:id:any-programming:20170801015918p:plain

コントロールの主体はViewport3Dで、そこにカメラやライト、メッシュを追加していく形となります。

法線は自動生成されるため、今回は作成していません。

回転はModel3DのTransformを更新することで行っています。

下記はC#で実装していますが、Xamlで記述することも可能です。

// Cubeの各三角形(12枚)を作成
var vertices = new []
{
    new Point3D(-1,  1,  1),
    new Point3D(-1, -1,  1),
    new Point3D( 1, -1,  1),
    new Point3D( 1,  1,  1),
    new Point3D(-1,  1, -1),
    new Point3D(-1, -1, -1),
    new Point3D( 1, -1, -1),
    new Point3D( 1,  1, -1)
};
Point3D[] face(int i1, int i2, int i3, int i4) =>
    new[] { i1, i2, i3, i1, i3, i4 }.Select(x => vertices[x]).ToArray();
Point3D[] positions = new[]
{
    face(0, 1, 2, 3),
    face(3, 2, 6, 7),
    face(7, 6, 5, 4),
    face(4, 5, 1, 0),
    face(0, 3, 7, 4),
    face(5, 6, 2, 1),
}.SelectMany(x => x).ToArray();

// 頂点座標と色をセット
var model = new GeometryModel3D()
{
    Geometry = new MeshGeometry3D() { Positions = new Point3DCollection(positions) },
    Material = new DiffuseMaterial(Brushes.LightGreen),
};

// ライト作成
var light = new DirectionalLight(Colors.White, new Vector3D(0, 0, -1));

// カメラ作成
var camera = new PerspectiveCamera(new Point3D(0, 0, 5), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45);

// 表示用コントロールの作成
Viewport3D viewport3D = new Viewport3D();
viewport3D.Camera = camera;
viewport3D.Children.Add(new ModelVisual3D() { Content = model });
viewport3D.Children.Add(new ModelVisual3D() { Content = light });

Content = viewport3D;

// KeyDownでCubeが回転するよう実装
var quaternion = new Quaternion();
KeyDown += (s, e) =>
{
    var q =
        e.Key == Key.Left  ? new Quaternion(new Vector3D(0, 1, 0), -1) :
        e.Key == Key.Right ? new Quaternion(new Vector3D(0, 1, 0),  1) :
        e.Key == Key.Up    ? new Quaternion(new Vector3D(1, 0, 0), -1) :
        e.Key == Key.Down  ? new Quaternion(new Vector3D(1, 0, 0),  1) :
        Quaternion.Identity;

    quaternion = q * quaternion;
    model.Transform = new RotateTransform3D(new QuaternionRotation3D(quaternion));
};


法線やインデックス

MeshGeometry3DにはNormalsとTriangleIndicesが設定可能となっています。

テクスチャ

MeshGeometry3DのTextureCoordinatesを設定し、DiffuseMaterialにImageBrushを設定します。

カリングOFF

GeometryModel3DのBackMaterialを設定するとOFFになります。

ライン描画

残念ながら対応していません。細いポリゴンを描くしかなさそうです。

オブジェクト毎のマウスイベントハンドリング

ModelVisual3Dの代わりにModelUIElement3Dを利用すると、MouseMoveなどが登録できるようになります。

2Dコントロール(Buttonなど)を3D上に配置

Viewport2DVisual3Dを用いると可能となります。