WindowsでUnityでデスクトップを表示したいとなった場合、一番パフォーマンスがいいのがDesktop Duplication APIを使用したものでしょう。
こちらは凹みさんがuDesktopDuplicationとしてプラグインを作成されています。
ただ、環境によってはこのプラグインを使ってデスクトップを表示できない場合があり、そういった環境でもデスクトップを表示させたいというのがこの投稿の趣旨です。
Win32Api
C#でデスクトップの画像を取得する方法としては、System.DrawingのBitmapを使用したものが有名ですが、しかし、UnityだとSystem.Drawingを使用することができなくはないですがめんどいです。
めんどいというのは、一応、使用することは可能でUnity側でのコンパイルも通るのですが、UnityでDLLやコードを追加したりするとプロジェクトのリロードが走り、その際に参照設定で追加したSystem.Drawingが削除されてしまい、VSからUnityにアタッチしてデバッグするときにVS側でコンパイルが走りVS側でコンパイルエラーとなってしまい、コンパイルを通すために再度参照設定で追加しなければならなくなります。
また、スクリーンのサイズを取得する場合、特にマルチモニターを考慮する場合、System.Windows.Forms.Screen.AllScreensを使用しますが、これもまた、System.Windows.Formsを参照設定しなければなりませんが、同様にプロジェクトのリロードが走ると参照設定から削除されてしまいます。
ですので、System.DrawingやSystem.Windows.Formsなどの名前空間を使用しない方法となるとWin32 APIを使用した方法となります。
CreateCompatibleBitmap / CreateDIBSection
Win32 APIにおけるBitmapオブジェクトを作成する方法として、CreateCompatibleBitmap()とCreateDIBSection()の2つがあります。
CreateDIBSectionは、名前からわかるように厳密にはDIBSectionを作成するもので、BitmapオブジェクトではありませんがBitmapオブジェクトと同じように扱えます。
パフォーマンス的には、BitmapオブジェクトよりDIBSectionのほうがいいようです。
また、CreateDIBSectionは、ピクセルデータへのポインターも取得することができます。
LoadRawTextureData()には、ポインター(IntPtr)を引数にとるオーバーロードがあるため、このポインターを利用することでbyte配列を用意することなくテクスチャーを作成することができます。
ちょっとマルチモニター環境でのキャプチャ方法がわからなかったので、シングルモニターでのキャプチャコードが以下になります。
(本来であればGetSystemMetrics()等を使用してモニターサイズを取得すべきですがべた書きしています)
using System;
using System.Runtime.InteropServices;
class Win32Api
{
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hwnd, IntPtr hdc);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC([In] IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC([In] IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BITMAPINFO pbmi, DIB_Color_Mode pila, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject([In] IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern int BitBlt(IntPtr hDestDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, TernaryRasterOperations dwRop);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width
{
get
{
return Right - Left;
}
}
public int Height
{
get
{
return Bottom - Top;
}
}
}
public enum DIB_Color_Mode : uint
{
DIB_RGB_COLORS = 0,
DIB_PAL_COLORS = 1
}
public enum TernaryRasterOperations : uint
{
SRCCOPY = 0x00CC0020,
CAPTUREBLT = 0x40000000 //only if WinVer >= 5.0.0 (see wingdi.h)
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct BITMAPFILEHEADER
{
public ushort bfType;
public uint bfSize;
public ushort bfReserved1;
public ushort bfReserved2;
public uint bfOffBits;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
public RGBQUAD bmiColors;
}
[StructLayout(LayoutKind.Sequential)]
public struct BITMAPINFOHEADER
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public uint biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
public const int BI_RGB = 0;
public void Init()
{
biSize = (uint)Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct RGBQUAD
{
public byte rgbBlue;
public byte rgbGreen;
public byte rgbRed;
public byte rgbReserved;
}
static BITMAPINFO bmpInfo;
static IntPtr desktopWnd;
static IntPtr desktopDC;
static IntPtr cmpDC;
static IntPtr cmpBmp;
static IntPtr oldBmp;
static bool inited;
public static IntPtr pPixelData;
public static void BeginDesktopCapture(int width, int height)
{
bmpInfo = new BITMAPINFO();
bmpInfo.bmiHeader = new BITMAPINFOHEADER();
bmpInfo.bmiHeader.Init();
bmpInfo.bmiHeader.biBitCount = 32;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biWidth = width;
bmpInfo.bmiHeader.biHeight = height;
desktopWnd = GetDesktopWindow();
desktopDC = GetDC(desktopWnd);
cmpDC = CreateCompatibleDC(desktopDC);
cmpBmp = CreateDIBSection(cmpDC, ref bmpInfo, DIB_Color_Mode.DIB_RGB_COLORS, out pPixelData, IntPtr.Zero, 0);
oldBmp = SelectObject(cmpDC, cmpBmp);
inited = true;
}
public static void DesktopCapture()
{
BitBlt(cmpDC, 0, 0, bmpInfo.bmiHeader.biWidth, bmpInfo.bmiHeader.biHeight, desktopDC, 0, 0, TernaryRasterOperations.SRCCOPY);
}
public static void EndDesktopCapture()
{
if (!inited) return;
SelectObject(cmpDC, oldBmp);
DeleteObject(cmpBmp);
DeleteDC(cmpDC);
ReleaseDC(desktopWnd, desktopDC);
inited = false;
}
}
using UnityEngine;
public class Win32ApiDesktopCapture : MonoBehaviour {
private long cnt;
private Texture2D tex;
void Start () {
Win32Api.BeginDesktopCapture(1920, 1080);
tex = new Texture2D(1920, 1080, TextureFormat.BGRA32, false);
GetComponent<Renderer>().material.mainTexture = tex;
}
void Update()
{
Win32Api.DesktopCapture();
tex.LoadRawTextureData(Win32Api.pPixelData, 1920 * 1080 * 4);
tex.Apply();
}
private void OnDestroy()
{
Win32Api.EndDesktopCapture();
}
}
あとがき
グラボ(GeForce1080)がのっかっているスペックいいマシンしか手元になくこの環境下でかつ単純に上記のコードだけのプロジェクトでほかに負荷がかかるコードは一切ない状況下でですが、一応、60fps以上はキープしました。