凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity の Low-Level Native Plugin Interface を使ったテクスチャの更新 (Windows/D3D11 編)

はじめに

本エントリは以下のエントリの続編です。

tips.hecomi.com Low-Level Native Plugin Interface を利用すると OpenGLDirectX から直接テクスチャの生成や更新を行うことが出来ます。前回のエントリでは Mac OS X 環境下での OpenGL を例に紹介しましたので、今回は Windows 環境下での DirectX11 を使った短めのコードを紹介したいと思います。サンプルは前回と同じく OpenCV からのカメラ画の取得の例になります。

環境

Unity 側のコード

前回と同じです。

using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;

public class OpenCvCamera : MonoBehaviour
{
    [DllImport ("NativeTexture")]
    private static extern IntPtr GetCamera(int device);
    [DllImport ("NativeTexture")]
    private static extern void ReleaseCamera(IntPtr ptr);
    [DllImport ("NativeTexture")]
    private static extern int GetCameraWidth(IntPtr ptr);
    [DllImport ("NativeTexture")]
    private static extern int GetCameraHeight(IntPtr ptr);
    [DllImport ("NativeTexture")]
    private static extern void SetCameraTexturePtr(IntPtr ptr, IntPtr texture);
    [DllImport ("NativeTexture")]
    private static extern IntPtr GetRenderEventFunc();

    private IntPtr camera_ = IntPtr.Zero;

    void Start()
    {
        camera_ = GetCamera(0);

        var tex = new Texture2D(
            GetCameraWidth(camera_),
            GetCameraHeight(camera_),
            TextureFormat.ARGB32,
            false);
        GetComponent<Renderer>().material.mainTexture = tex;

        SetCameraTexturePtr(camera_, tex.GetNativeTexturePtr());
        StartCoroutine(OnRender());
    }

    void OnDestroy()
    {
        ReleaseCamera(camera_);
        Debug.Log("Release");
    }

    IEnumerator OnRender()
    {
        for (;;) {
            yield return new WaitForEndOfFrame();
            GL.IssuePluginEvent(GetRenderEventFunc(), 0);
        }
    }
}

C++ 側のコード

いくつかヘッダファイルが必要になります。Low-Level Native Plugin Interface のマニュアルの下部にある「can be downloaded here」からサンプルを取得して下さい。

その後、「NativeRenderingPlugin\RenderingPlugin\Unity」に含まれる以下の 3 つのファイルを自プロジェクトに含めます。

  • IUnityInterface.h
  • IUnityGraphics.h
  • IUnityGraphicsD3D11.h

そして以下の様なプラグイン用のコードを書きます。

#include <opencv2/opencv.hpp>
#include <d3d11.h>
#include <thread>
#include <mutex>

#include "IUnityInterface.h"
#include "IUnityGraphics.h"
#include "IUnityGraphicsD3D11.h"


class Camera
{
public:
    Camera(int device, IUnityInterfaces* unity)
        : camera_(device)
        , width_(static_cast<int>(camera_.get(cv::CAP_PROP_FRAME_WIDTH)))
        , height_(static_cast<int>(camera_.get(cv::CAP_PROP_FRAME_HEIGHT)))
        , unity_(unity)
    {
        StartCapture();
    }

    ~Camera()
    {
        StopCapture();
        camera_.release();
    }

private:
    void StartCapture()
    {
        thread_ = std::thread([this] {
            isRunning_ = true;
            while (isRunning_ && camera_.isOpened()) {
                cv::Mat rgb;
                camera_ >> rgb;
                {
                    std::lock_guard<std::mutex> lock(mutex_);
                    cv::cvtColor(rgb, image_, cv::COLOR_BGR2RGBA);
                }
            }
        });
    }

    void StopCapture()
    {
        isRunning_ = false;
        if (thread_.joinable()) {
            thread_.join();
        }
    }

public:
    void Update()
    {
        if (unity_ == nullptr || texture_ == nullptr || image_.empty()) return;

        std::lock_guard<std::mutex> lock(mutex_);
        auto device = unity_->Get<IUnityGraphicsD3D11>()->GetDevice();
        ID3D11DeviceContext* context;
        device->GetImmediateContext(&context);
        context->UpdateSubresource(texture_, 0, nullptr, image_.data, image_.step, 0);
    }

    int GetWidth() const
    {
        return width_;
    }

    int GetHeight() const
    {
        return height_;
    }

    void SetTexturePtr(void* ptr)
    {
        texture_ = static_cast<ID3D11Texture2D*>(ptr);
    }

private:
    cv::VideoCapture camera_;
    int width_, height_;
    cv::Mat image_;

    IUnityInterfaces* unity_;
    ID3D11Texture2D* texture_ = nullptr;

    std::thread thread_;
    std::mutex mutex_;
    bool isRunning_ = false;
};


namespace
{
    IUnityInterfaces* g_unity    = nullptr;
    Camera*           g_camera   = nullptr;
}


extern "C"
{
    // Low-Level Native Plugin Interface で Unity 側から呼ばれる
    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
    {
        g_unity = unityInterfaces;
    }

    // Low-Level Native Plugin Interface で Unity 側から呼ばれる
    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginUnload()
    {
        // 特に終了処理は必要なし
    }

    // Unity 側で GL.IssuePlugin(この関数のポインタ, eventId) を呼ぶとレンダリングスレッドから呼ばれる
    void UNITY_INTERFACE_API OnRenderEvent(int eventId)
    {
        if (g_camera) g_camera->Update();
    }

    // GL.IssuePlugin で登録するコールバック関数のポインタを返す
    UNITY_INTERFACE_EXPORT UnityRenderingEvent UNITY_INTERFACE_API GetRenderEventFunc()
    {
        return OnRenderEvent;
    }

    UNITY_INTERFACE_EXPORT void* UNITY_INTERFACE_API GetCamera(int device)
    {
        g_camera = new Camera(device, g_unity);
        return g_camera;
    }

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API ReleaseCamera(void* ptr)
    {
        auto camera = reinterpret_cast<Camera*>(ptr);
        if (g_camera == camera) g_camera = nullptr;
        delete camera;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetCameraWidth(void* ptr)
    {
        auto camera = reinterpret_cast<Camera*>(ptr);
        return camera->GetWidth();
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetCameraHeight(void* ptr)
    {
        auto camera = reinterpret_cast<Camera*>(ptr);
        return camera->GetHeight();
    }

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetCameraTexturePtr(void* ptr, void* texture)
    {
        auto camera = reinterpret_cast<Camera*>(ptr);
        camera->SetTexturePtr(texture);
    }
}

Unity 側で作られた ID3D11Device を通じてテクスチャの更新を行う必要があるため、定義すると Unity から自動で呼ばれる UnityPluginLoad() を通じてデバイスを取得する必要があります。本来はここでバージョン(D3D9、D3D11、D3D12、OpenGL など)をチェックしなければならないのですが、詳細は公式サンプルを見て下さい。ここでは簡単のために D3D11 だけを例に書いています。

UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    g_unity = unityInterfaces;
}

IUnityInterfaces*IUnityInterface.h に定義されていて Get() テンプレートメンバ関数を通じて予め Register() された型のインスタンスを取り出すことが出来ます。IUnityGraphicsD3D11.h ではこの仕組を利用して ID3D11Device* を取り出す GetDevice() メンバ関数を持つ IUnityGraphicsD3D11 が宣言されていますので、これを利用してテクスチャの更新に必要な ID3D11DeviceContext* を取り出します。文章で書くとアレですが、実際にコードを見ると以下のように短いものになります。

// ID3D11Device* unity_;
// cv::Mat image_;
auto device = unity_->Get<IUnityGraphicsD3D11>()->GetDevice();
ID3D11DeviceContext* context;
device->GetImmediateContext(&context);
context->UpdateSubresource(texture_, 0, nullptr, image_.data, image_.step, 0);

取り出した ID3D11DeviceContextメンバ関数である UpdateSubresource() を利用して Unity 側から GetNativeTexturePtr() を通じて得られた ID3D11Texture2D* を更新します。D3D11 の面倒な Desc を定義して生成したりする部分は Unity の内部で行われているため、とても簡単なコードになりますね。

結果

前回と同じですが実行すると以下のようにカメラ画が OpenCvCamera.cs をアタッチしたオブジェクトに表示されます。

f:id:hecomi:20160110171554p:plain

OpenCV を利用した変換(失敗編)

OpenCV 3.0 から cv::directx::convertToD3D11Texture2D() が追加され、内部的には OpenCLclCreateFromD3D11Texture2DKHR() を使って cv::Mat を直接 DirectXTexture2D へと変換できるようなので試してみました。

...
#include <opencv2/core/directx.hpp>

class Camera
{
public:
    Camera(int device, IUnityInterfaces* unity)
    ...
    {
        auto d3d11Device  = unity_->Get<IUnityGraphicsD3D11>()->GetDevice();
        cv::directx::ocl::initializeContextFromD3D11Device(d3d11Device);
        ...
    }

    ...

    void Update()
    {
        ...
        cv::directx::convertToD3D11Texture2D(image_, texture_);
    }

    ...
};

...

最初の 1 回目の実行時には正常に表示されるのですが、2 回目以降の実行時には Runtime Error で落ちてしまいます。もし原因が分かる方がいらっしゃいましたら教えて頂けると嬉しいです。

おわりに

ここまでで Win / Mac で利用する方法を紹介しました。Android / iOS 編は使う機会が出てきたら書きます。