凹みTips

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

Unity で Desktop Duplication API を使ってスクリーンキャプチャしてみた

はじめに

以下のエントリではスクリーンキャプチャSystem.Drawing.Graphics.CopyFromScreen() を別スレッドで回して行っていました。

qiita.com

ここで触れられているようにネイティブでやった方が速いということもあり、本エントリでは Windows 8 から使える Desktop Duplication API を利用したスクリーンキャプチャの簡単なサンプルを紹介します。

環境

結果

f:id:hecomi:20160111135205p:plain

サンプル

追記(2016/10/28)

Github に整理したものを上げました。

概要

DirectX 11 および Low-Level Native Plugin Interface を通じてテクスチャを取得しますので、詳細については以下のエントリをご参照下さい。

tips.hecomi.com

tips.hecomi.com

Desktop Duplication API は以下に詳細がまとめられています。

IDXGIOutputDuplication::AcquireNextFrame() を通じてテクスチャやマウスの情報などが受け取れるので、IDXGIOutputDuplicationインスタンスをメインディスプレイとなる IDXGIOutput1 から取得し、毎フレームこの関数を呼んで Unity 側のテクスチャへと反映します。

C++ 側のコード

#include <d3d11.h>
#include <dxgi1_2.h>

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

#pragma comment(lib, "dxgi.lib")


namespace
{
    IUnityInterfaces*       g_unity            = nullptr;
    IDXGIOutputDuplication* g_deskDupl         = nullptr;
    ID3D11Texture2D*        g_texture          = nullptr;
    bool                    g_isPointerVisible = false;
    int                     g_pointerX         = -1;
    int                     g_pointerY         = -1;
    int                     g_width            = -1;
    int                     g_height           = -1;
}


extern "C"
{
    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
    {
        g_unity = unityInterfaces;

        IDXGIFactory1* factory;
        CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(&factory));

        // 全ディスプレイアダプタを調べる
        IDXGIAdapter1* adapter;
        for (int i = 0; (factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND); ++i) 
        {
            // アウトプットを一通り調べてメインモニタを探す
            IDXGIOutput* output;
            for (int j = 0; (adapter->EnumOutputs(j, &output) != DXGI_ERROR_NOT_FOUND); j++) 
            {
                DXGI_OUTPUT_DESC outputDesc;
                output->GetDesc(&outputDesc);

                MONITORINFOEX monitorInfo;
                monitorInfo.cbSize = sizeof(MONITORINFOEX);
                GetMonitorInfo(outputDesc.Monitor, &monitorInfo);

                if (monitorInfo.dwFlags == MONITORINFOF_PRIMARY) 
                {
                    g_width = monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left;
                    g_height = monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top;

                    auto device = g_unity->Get<IUnityGraphicsD3D11>()->GetDevice();
                    IDXGIOutput1* output1;
                    output1 = reinterpret_cast<IDXGIOutput1*>(output);
                    output1->DuplicateOutput(device, &g_deskDupl);

                    output->Release();
                    adapter->Release();
                    factory->Release();

                    return;
                }

                output->Release();
            }
            adapter->Release();
        }

        factory->Release();
    }

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API UnityPluginUnload()
    {
        g_unity = nullptr;
        g_deskDupl->Release();
        g_deskDupl = nullptr;
        g_texture = nullptr;
        g_isPointerVisible = false;
        g_width = -1;
        g_height = -1;
        g_pointerX = -1;
        g_pointerY = -1;
    }

    void UNITY_INTERFACE_API OnRenderEvent(int eventId)
    {
        if (g_deskDupl == nullptr || g_texture == nullptr) return;

        IDXGIResource* resource = nullptr;
        DXGI_OUTDUPL_FRAME_INFO frameInfo;

        const UINT timeout = 500; // ms
        g_deskDupl->AcquireNextFrame(timeout, &frameInfo, &resource);

        g_isPointerVisible = frameInfo.PointerPosition.Visible;
        g_pointerX = frameInfo.PointerPosition.Position.x;
        g_pointerY = frameInfo.PointerPosition.Position.y;

        ID3D11Texture2D* texture;
        resource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&texture));
        resource->Release();

        ID3D11DeviceContext* context;
        auto device = g_unity->Get<IUnityGraphicsD3D11>()->GetDevice();
        device->GetImmediateContext(&context);
        context->CopyResource(g_texture, texture);

        g_deskDupl->ReleaseFrame();
    }

    UNITY_INTERFACE_EXPORT UnityRenderingEvent UNITY_INTERFACE_API GetRenderEventFunc()
    {
        return OnRenderEvent;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetWidth()
    {
        return g_width;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetHeight()
    {
        return g_height;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API IsPointerVisible()
    {
        return g_isPointerVisible;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerX()
    {
        return g_pointerX;
    }

    UNITY_INTERFACE_EXPORT int UNITY_INTERFACE_API GetPointerY()
    {
        return g_pointerY;
    }

    UNITY_INTERFACE_EXPORT void UNITY_INTERFACE_API SetTexturePtr(void* texture)
    {
        g_texture = reinterpret_cast<ID3D11Texture2D*>(texture);
    }
}

Unity 側のコード(C#

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

public class DesktopCapture : MonoBehaviour
{
    [DllImport ("DesktopCapture")]
    private static extern int GetWidth();
    [DllImport ("DesktopCapture")]
    private static extern int GetHeight();
    [DllImport ("DesktopCapture")]
    private static extern bool IsPointerVisible();
    [DllImport ("DesktopCapture")]
    private static extern int GetPointerX();
    [DllImport ("DesktopCapture")]
    private static extern int GetPointerY();
    [DllImport ("DesktopCapture")]
    private static extern int SetTexturePtr(IntPtr ptr);
    [DllImport ("DesktopCapture")]
    private static extern IntPtr GetRenderEventFunc();

    public bool isPointerVisible = false;
    public int pointerX = 0;
    public int pointerY = 0;

    void Start()
    {
        var tex = new Texture2D(GetWidth(), GetHeight(), TextureFormat.BGRA32, false);
        GetComponent<Renderer>().material.mainTexture = tex;

        SetTexturePtr(tex.GetNativeTexturePtr());
        StartCoroutine(OnRender());
    }

    void Update()
    {
        isPointerVisible = IsPointerVisible();
        pointerX = GetPointerX();
        pointerY = GetPointerY();
    }

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

おわりに

Virtual Desktop みたいなアプリがこれで作れるのではないでしょうか。