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を用いると可能となります。





JavaScript(ES6)でElm Architecture(Virtual-DOMなし) --- 可変個コントロール

下記記事にてJavaScriptにElmのModelとUpdateの機構を取り入れてみました。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング

今回は引き続き、可変個のコントロールに対応してみたいと思います。

アプリケーションコード

カウンターを追加、削除できるアプリケーションを実装してみたいと思います。

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

可変個のコントロールの定義として、htmlのtemplate要素を利用しています。

また可変個コントロールの動作の実装にbindItemsを利用しています。(実装はのちほど)

その他CellクラスやstartApp関数は下記記事を参照してください。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング

<button id="add">add</button>
<button id="remove">remove</button>

<!-- 可変個カウンタ -->
<div id="counts">
    <template>
        <div>                 
            <button class="dec">dec</button>
            <input type="text" class="count"></input>
            <button class="inc">inc</button>
        </div> 
    </template>
</div>
const initialModel = {
    counts : []
};

const message = {
    addCounter : Symbol(),
    removeCounter : Symbol(),
    increment : Symbol(),
    decrement : Symbol()
}

function update(model, msg){
    switch (msg.id) {
        case message.addCounter :
            return copy(model, {counts : model.counts.insertLast(0)});

        case message.removeCounter :
            return copy(model, {counts : model.counts.removeLast()});
        
        case message.increment : 
            return copy(model, {counts : model.counts.updateAt(msg.index, x => x + 1)});

        case message.decrement :
            return copy(model, {counts : model.counts.updateAt(msg.index, x => x - 1)});

        default : throw "invalid message"
    }
}

function bind(model, send) {
    byId("add").onclick = () => send({ id: message.addCounter });
    byId("remove").onclick = () => send({ id: message.removeCounter });

    // 可変個カウンタBinding
    bindItems(model.map(x => x.counts), "counts", (item, index, element) =>
    {
        const byClass = x => element.getElementsByClassName(x)[0];

        item.listen(x => byClass("count").value = x);
        byClass("dec").onclick = () => send({ id: message.decrement, index: index });
        byClass("inc").onclick = () => send({ id: message.increment, index: index });
    });
}

window.onload = () =>
    startApp(initialModel, update, bind);


bindItems

itemsのCell、親要素のID、コントロールのbindingを受け取る関数になります。

要素内のtemplateを取得し、実際にコントロールを配置するdiv要素を追加しています。

itemsが変更されたときに、変更の送出 or コントロールの作成 or コントロールの削除を行っています。

function bindItems(itemsCell, parentId, bind) {
    const parent = byId(parentId);
    const itemTemplate = parent.getElementsByTagName("template")[0];
    const panel = document.createElement("div");
    parent.appendChild(panel);
    const cells = [];
    itemsCell.listen(items =>
    {
        for (let i = 0; i < items.length; i++)
        {
            // コントロールが存在
            if (i < cells.length)
            {
                cells[i].send(items[i]);
            }
            // コントロール不足
            else
            {
                const cell = new Cell(items[i]);
                const element = document.importNode(itemTemplate.content, true);
                cells.push(cell);
                panel.appendChild(element);
                bind(cell, i, panel.lastElementChild);
            }
        }
        // コントロール過剰
        for (let i = 0; i < cells.length - items.length; i++)
        {
            cells.pop();
            panel.lastElementChild.remove();
        }
    });
}


外側のスコープのCellをbind内で利用するとリーク

スコープ外のCellを利用すると、コントロールの参照がCell内に残り続けてリークを起こしてしまいます。

そのため、スコープ外のCellを利用する場合は、一旦キャプチャしてbind関数の引数に追加する必要があります。

実装例ですが、C#のものが下記にありますので参照してください。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング





JavaScript(ES6)でElm Architecture(Virtual-DOMなし)

Elm自体がJavaScriptを生成するものなので実際には利用することはないと思いますが(さらに生のJavaScriptがよければReact.jsがあります)、ElmのModelとUpdateの仕組みを生のJavaScriptで実装してみました。

WPFC#で実装されたものは、下記記事にて参照できます。
WPFでElm Architecture --- Xaml利用しない版 - 何でもプログラミング

アプリケーションコード

簡単なカウンタアプリを実装します。

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

initialModel 状態の初期値を定義
message update関数で処理させる内容の識別子
update 現在のmodelと、messageを受け取って、新しいmodelを返す
bind modelのCell(現在値+変更通知)と、send(message)関数を、html要素にバインドする

CellはFunctional Rective Programming(FRP)でのBehaviorを意図しており、詳しくは下記を参照してください。
SodiumでFunctional Reactive Programming (F#) --- 導入 - 何でもプログラミング

<body>
    <button id="dec">dec</button>
    <input type="text" id="count"></input>
    <button id="inc">inc</button>
</body>
const initialModel = {
    count : 0,
};

const message = {
    increment : Symbol(),
    decrement : Symbol(),
}

function copy(obj, diff) {
    return Object.assign({}, obj, diff);
}

function update(model, msg){
    switch (msg.id) {
        case message.increment :   
            return copy(model, {count : model.count + 1});
        case message.decrement :
            return copy(model, {count : model.count - 1});
        default : throw "invalid message"
    }
}

function byId(name) {
    return document.getElementById(name);
}

function bind(model, send) {
    model.map(x => x.count).listen(x => byId("count").value = x);

    byId("dec").onclick = () => send({ id: message.decrement });
    byId("inc").onclick = () => send({ id: message.increment });
}

window.onload = () =>
    startApp(initialModel, update, bind);


Cellクラス

listenでコールバックを登録し、sendでデータを送出し、mapでデータの変換を行います。

listenした後、コールバックを解除する機能は実装してありませんので、利用する際(コントロールを動的に追加or削除)はリークに注意してください。

FRPのライブラリSodiumにJavaScript版がありますので、そちらのCellを利用することも可能です。

class Cell {
    constructor(initialValue) {
        this.value = initialValue;
        this.callbacks = [];
    }
    listen(f) {
        f(this.value);
        this.callbacks.push(f);
    }
    send(value) {
        if (this.value === value)
            return;
        this.value = value;
        this.callbacks.forEach(f => f(value));
    }
    map(f) {
        const cell = new Cell(f(this.value));
        this.callbacks.push(x => cell.send(f(x)));
        return cell;
    }
}


startApp

initialModelとupdateから、modelのCellとsend関数を作成し、bindを呼び出しています。

function startApp(initialModel, update, bind){
    const cell = new Cell(initialModel);
    const send = msg => cell.send(update(cell.value, msg));
    bind(cell, send);
}