凹みTips

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

Unity で Windows のウィンドウを個別にキャプチャできるアセットを作成した

はじめに

本エントリでは先ほど公開した uWindowCapture という Windows において個別のウィンドウをキャプチャできるアセットの解説を行います。本アセットで実現できることは以下のようなものです。

  • ウィンドウを個別にキャプチャ
    • 背面やデスクトップ画面の範囲外にある部分もキャプチャします
  • デスクトップをキャプチャ
  • ウィンドウマネージャ
    • ウィンドウの追加・削除や検索
    • タイトルや位置、大きさなどの情報が取れます

デモ

個別画面キャプチャ

f:id:hecomi:20180824011424g:plain

ウィンドウマネージャ

f:id:hecomi:20180825202845g:plain

VR 内ウィンドウ自由配置

作った経緯

VR の中ではディスプレイという概念を取っ払った環境が作れるはず!というコンセプトは色々な人が考えていると思います。それを考える上での題材の一つとして、デスクトップをどうするか、もう少し正確には従来の(非VR)アプリケーション群を VR の中でどう使うか、という問題が挙げられます。これに答える案として、例えば Virtual Desktop や、Bigscreen Beta は、従来の物理的なディスプレイのコンテキストを利用して VR の中でそれらを自由に配置できるようにしています(Bigscreen の方は更にネットワーク越しに共有もできます)。

store.steampowered.com

store.steampowered.com

私も開発をお手伝いした現バージョンでの Mikulus もここに並びます(※コンセプトはもっと先を見据えています)。

vrc.or.jp

panora.tokyo

これに対して、今年頭の Oculus の Rift Core 2.0 から追加された Oculus Dash は個別のウィンドウを空間上に好きに配置できるものとなっており、ディスプレイの制約を取り払ったものになっています:

私もこの発表よりも前の時点で、拙作の uDesktopDuplication と uTouchInjection、そして今回紹介する uWindowCapture を組み合わせた N-Windows という実動作するデモ(前項「VR 内ウィンドウ自由配置」の動画を参照)を公開していました(アプリは配布しておらず動画だけでしたが...)。

tips.hecomi.com

tips.hecomi.com

配布に至らなかったのは、コンセプトは実現できたもののパフォーマンスの関係で実用にはまだ遠い形だったからです。しかしながら、単に画面をキャプチャするアセットとしてもかなり使い途があるのではと思いまして、使いやすいように整理し、またパフォーマンスが出来る限り出るよう修正して公開することにしました。

ダウンロード

ライセンスは MIT です。

github.com

基本的な使い方

いくつかサンプルシーンを利用して出来ることを解説を交えて見ていきたいと思います。Examples 以下にシーンがあるのでそれぞれ開いてみてみてください。

Single Window

これは単体のウィンドウキャプチャを行うシンプルなシーンで、実行すると次のように Unity の画面が表示されると思います。

f:id:hecomi:20180823233905g:plain

Window オブジェクトを見てみると次のような UwcWindowTexture コンポーネントがアタッチされています。このコンポーネントをつけたオブジェクトのメインテクスチャが対象のウィンドウになる仕組みになっています。

f:id:hecomi:20180825142441p:plain

対象のウィンドウは Partial Window Titleでタイトルを部分指定します。書き換えると次のように別のウィンドウに切り替わります。

f:id:hecomi:20180824011424g:plain

コンポーネントの詳細については後述します。

Desktop

コンポーネントTypeWindow から Desktop へ変更するとウィンドウだけでなくデスクトップ画面もキャプチャできるようになります。Desktop を選択すると、ウィンドウ単体ではなく指定したインデックスのモニタ画面全体をキャプチャします。コンポーネントの Target の UI は以下のように変わります。

f:id:hecomi:20180825115337p:plain

キャプチャした様子は次のとおりです。

f:id:hecomi:20180825112604g:plain

Window Object

単体のウィンドウキャプチャだとコンテキストメニューや子ウィンドウがそこに含まれていないため見えません。そこで Create Child Windows にチェックを入れると、子ウィンドウの追加・削除を自動でハンドルして、指定した Child Window Prefab で子ウィンドウを生成するようになります。チェックを入れると次のように UI が変わります。

f:id:hecomi:20180825123137p:plain

Child Window Z Distance は子ウィンドウとして追加したゲームオブジェクトを奥行き方向にどれだけ動かすかというものです。実行すると次のようになります。

f:id:hecomi:20180825141719g:plain

現状だと完全な子ウィンドウの識別はできておらず、表示されないケースや DPI スケーリングが子側でうまく働かないなどのバグ(例. Visual Studio)もあります。。見つけた際はアプリケーション名やケースについてご一報いただけると助かります。

Icon

UwcIconTexture コンポーネントを使うとウィンドウのアイコンのテクスチャも取得可能です。

f:id:hecomi:20180826112626g:plain

Cursor Object

UwcCursorTexture を使うとカーソルのテクスチャも個別に取得可能です。マスクも含めてキャプチャするので文字列カーソル等のデスクトップ背面の色によって変化するカーソルも正しく取得可能です。

f:id:hecomi:20180826003207g:plain

コンポーネント解説

前項でいくつか UwcWindowTextureTarget 部分のコンポーネントの解説をしました。ここでは残りも含めてコンポーネント全体について解説を行います。

f:id:hecomi:20180824000155p:plain

Target

f:id:hecomi:20180826175519p:plain

ここではキャプチャする対象となるウィンドウやデスクトップの設定を行います。

  • Type
    • Window、Desktop、Child と 3 つのタイプがあります。Window と Desktop は前項の通りで選択すると Target 内に表示される項目が増減します。Child だけは特殊で uWindowCapture が内部で子ウィンドウをハンドルするために使われます。手動で設定するとエラーが表示されますので設定しないようにしてください。
  • Partial Window Title
    • 部分一致でターゲットとなるウィンドウを指定します。変更があったタイミングで新しいウィンドウを探しに行きます。
    • 部分一致は先頭にマッチするものが優先的に選ばれます。
  • Update Title
    • チェックすると常にタイトルを更新します。
    • タイトル更新は比較的負荷が高いためデフォルトでは初回のみ取得がデフォルトの動作になっています。
  • Desktop Index
    • 指定した ID のモニタをキャプチャ対象とします。現在の仕様ではモニタのインデックス番号は実行のたびに不定で入れ替わることもあります。
  • Alt Tab Window
    • 検索する対象を Alt-Tab で表示されるウィンドウに限定します(子ウィンドウやバックグラウンドでスリープしているウィンドウは含まれません)
  • Create Child Windows
    • チェックするとUwcWindowTextureChildrenManager が自動的にアタッチされ、前項のように子ウィンドウが自動で追加されるようになります。
  • Child Window Prefab
    • 生成する子ウィンドウのプレファブを指定します。子にも Create Child Windows のチェックが入っていれば再帰的に子を生成していきます。
  • Child Window Z Distance
    • 生成する子と親の Z 間距離です。

Capture Settings

f:id:hecomi:20180825150113p:plain

ここではキャプチャの方法に対する設定を行います。

  • Capture Mode
    • キャプチャに使う API です。
    • Print Window(デフォルト)
      • 画面をウィンドウのメニューや枠も含めてキャプチャします。ほとんどの画面はがキャプチャできる代わりに Bit Blt より低速です。
    • Bit Blt
      • Print Window より高速で画面の内部のみをキャプチャします。ただ Chrome などキャプチャ出来ないアプリケーションもあります。デスクトップのキャプチャの場合には強制的に Bit Blt に固定されます。
    • None
      • 指定するとキャプチャしなくなります。

f:id:hecomi:20180825152230g:plain

  • Capture Priority
    • キャプチャの優先度です*1
    • High
      • 最も優先度の高いキューに追加して追加順に処理されていきます。
    • Middle
      • このキューに追加されると更新の度に先頭のものを 1 つだけ High キューの末尾へ移動します。High で埋まっていてもたまにキャプチャされる形になります。High にない場合はここのものがキャプチャされます。
    • Low
      • High にも Middle にもない場合にキャプチャされます。High と Middle が埋まり続ける状況ではキャプチャされません。
    • Auto(デフォルト)
      • ウィンドウがカーソル下にある場合は High に、ウィンドウが前の方にある場合に Middle に設定されます(どこまでを Middle にするかはパラメタ)。
  • Capture Request Timing
    • キャプチャするタイミングです。
    • Every Frame
      • 画面外にあっても毎フレームキャプチャします。
    • Only When Visible(デフォルト)
      • カメラに映っているとき(OnWillRenderObject() タイミング)にのみキャプチャのリクエストを行います。
      • Scene ビューのみ見ていると更新されなくなるので注意!
    • Manual
      • RequestCapture() を手動で呼ぶとキャプチャを行います。
  • Capture Frame Rate
    • キャプチャを行う目標フレームレート値です。
  • Draw Cursor
    • チェックすると画面にカーソルを描画します。

Scale Settings

f:id:hecomi:20180825150123p:plain

  • Scale Control Type
    • ゲームオブジェクトのスケールをどう調整するかの種類です。
    • Base Scale
      • Scale Per 1000 Pixel で指定したスケールにオブジェクトを自動的に設定します。例えば 2 が設定されていた場合にウィンドウのサイズが 1920px - 1080px であれば、Unity のスケールで 3.84 - 2.16 となります。
    • Fixed Width
      • 横幅を Transform の x で固定し、y を可変にしてアスペクト比をあわせます。
    • Fixed Height
      • 縦幅を Transform の y で固定し、x を可変にしてアスペクト比をあわせます。
    • Manual
      • コンポーネント側では何もせずに、手動で Transform を更新する必要があります。
  • Scale Per 1000 Pixel
    • Base Scale が選択されているときの 1000px 辺りの大きさ(m)です。

Window Information

f:id:hecomi:20180825150135p:plain

ウィンドウの情報の一部が表示されます。

実践的な機能解説

次に少しスクリプトも交えて実践的なサンプルシーンを見ていきましょう。

Horizontal Layout

f:id:hecomi:20180825173700g:plain

Alt-Tab で表示されるウィンドウを横一列に並べるシーンです。Windows オブジェクトに UwcAltTabWindowTextureManagerUwcHorizontalLayouter という 2 つのコンポーネントが追加されています。

  • UwcAltTabWindowTextureManager
    • Alt-Tab ウィンドウを Window Preafab で指定したオブジェクトで生成します。
  • UwcHorizontalLayouter
    • UwcAltTabWindowTextureManager に登録されたウィンドウを横に並べるシンプルなスクリプトです。

UwcHorizontalLayouter は次のようなスクリプトです。

using UnityEngine;

[RequireComponent(typeof(UwcWindowTextureManager))]
public class UwcHorizontalLayouter : MonoBehaviour
{
    UwcWindowTextureManager manager_;

    void Awake()
    {
        manager_ = GetComponent<UwcWindowTextureManager>();
    }

    void Update()
    {
        var pos = Vector3.zero;

        foreach (var kv in manager_.windows) {
            var windowTexture = kv.Value;
            var width = windowTexture.transform.localScale.x;
            pos += new Vector3(width * 0.5f, 0f, 0f);
            windowTexture.transform.localPosition = pos;
            pos += new Vector3(width * 0.5f, 0f, 0f);
        }
    }
}

最初は大量のウィンドウをキャプチャリクエストが走るので、しばらくテクスチャが表示されないウィンドウもあります。全てキャプチャした後は Capture Priority が Auto になっているため、カーソル下にあるウィンドウが優先的にキャプチャされるようになります。

Desktop Layout

f:id:hecomi:20180825202845g:plain

こちらは Horizontal Layout よりも少しだけ複雑な UwcDesktopLayouter を通じて、デスクトップ上の配置を再現するサンプルになります。それぞれのウィンドウには UwcTextureWindowChildrenManager がつくので、Alt-Tab なウィンドウ以外の子ウィンドウも表示するのがこれだけ短いコードで書けます。

using UnityEngine;

[RequireComponent(typeof(UwcWindowTextureManager))]
public class UwcDesktopLayouter : MonoBehaviour
{
    public float scale = 1f;
    public float zMargin = 0.1f;

    UwcWindowTextureManager manager_;

    void Awake()
    {
        manager_ = GetComponent<UwcWindowTextureManager>();
        manager_.onWindowTextureAdded.AddListener(InitWindow);
    }

    void InitWindow(UwcWindowTexture windowTexture)
    {
        MoveWindow(windowTexture, false);
        ScaleWindow(windowTexture, false);
    }

    void Update()
    {
        foreach (var kv in manager_.windows) {
            var windowTexture = kv.Value;
            CheckWindow(windowTexture);
            MoveWindow(windowTexture);
            ScaleWindow(windowTexture);
        }
    }

    void CheckWindow(UwcWindowTexture windowTexture)
    {
        windowTexture.enabled = !windowTexture.window.isIconic;
    }

    void MoveWindow(UwcWindowTexture windowTexture)
    {
        var window = windowTexture.window;
        var pos = UwcWindowUtil.ConvertDesktopCoordToUnityPosition(window, 1000f / scale);
        pos.z = window.zOrder * zMargin;
        var targetPos = transform.localToWorldMatrix.MultiplyPoint3x4(pos);
        windowTexture.transform.position = Vector3.Slerp(windowTexture.transform.position, targetPos, filter);
    }

    void ScaleWindow(UwcWindowTexture windowTexture)
    {
        windowTexture.scaleControlType = WindowTextureScaleControlType.BaseScale;
        windowTexture.scalePer1000Pixel = scale;
    }
}

Window List

最後はリスト表示です。これは先程の UwcHorizontalLayouterUwcDesktopLayouter の上にリストを表示し、選択したものだけ表示させる、というものになっています。

f:id:hecomi:20180826004503g:plain

より詳細は UwcWindowList.cs を見てみてください。

スクリプトからの利用

ここからはスクリプトからの利用を見ていきましょう。スクリプトは基本的に利用しなくても使いやすいようになっていると思いますが、凝ったことをしたい場合は必要になってきます。基本的にはサンプルで基本的な使い方は網羅していると思いますので、サンプルのコンポーネントを直接見ていただくとより理解が深まると思います。

UwcManager

実行すると UwcManager がアタッチされた uWindowCapture と名前のついたゲームオブジェクトが 1 つだけ作成されます(予め配置しておいても構いません)。ここでは DLL との橋渡しを行っており、次のようなスクリプトになっています。

public class UwcManager : MonoBehaviour
{
    public static UwcManager instance { get; }
    public static UwcWindowEvent onWindowAdded { get; }
    public static UwcWindowEvent onWindowRemoved { get; }
    public static UwcEvent onCursorCaptured { get; }
    public static Dictionary<int, UwcWindow> windows { get; }
    public static UwcWindow cursorWindow { get; }
    public static UwcCursor cursor { get; }
    public static int desktopCount { get; }

    public static UwcWindow Find(int id);
    public static UwcWindow Find(string partialTitle, bool isAltTabWindow = true);
    public static UwcWindow Find(System.IntPtr handle);
    public static UwcWindow Find(System.Func<UwcWindow, bool> func);
    public static List<UwcWindow> FindAll(string title);
    public static UwcWindow FindParent(int id);
    public static UwcWindow FindDesktop(int index);

    public static void UpdateAllWindowTitles();
    public static void UpdateAltTabWindowTitles();
}

それぞれ次のような機能になっています。

ウィンドウの検索

ウィンドウは UwcManager を経由して検索します。検索方法はいくつかあります。

タイトルの部分一致

ウィンドウのタイトルで検索する方法は 2 つあります。

// Unity と名前のついたウィンドウを 1 つだけ返す
UwcWindow unity = UwcManager.Find("Unity");

// Chrome と名前のついたウィンドウ全てを返す
List<UwcWindow> chromes = UwcManager.FindAll("Chrome");

なお、UwcManager.Find() の第 2 引数は、Alt-Tab ウィンドウだけを探す場合は true(デフォルト)、そうでない場合は false を与えることで子ウィンドウも検索できます。

ハンドル

何らかの方法でウィンドウハンドルを取得しているのであればそれを使って UwcWindow を取得できます。

System.IntPtr hWnd = ...;
var window = UwcManager.Find(hWnd);
条件で検索

UwcWindow を逐次見つつ当てはまるものを検索することも出来ます。

var window = UwcManager.Find(
    window => window.isAltTabWindow && window.title.IndexOf("Unity") != -1);
タイトルの一括更新

タイトルの更新はコストがかかるので、updateTitle というフィールドを UwcWindowTexture に追加しているのですが、この関係でデフォルトではウィンドウのタイトルが更新されず、タイトルでの検索時に困ることがあります。具体的には例えばブラウザを開いていて、開始時は Google 検索画面を開いていて、途中で Twitter を開き、「Twitter」というキーワードで対象のブラウザを見つけたかったとしても、起動時の「Google」で該当の UwcWindowtitle が固定されてしまっています。そこでマネージャには全てのタイトルを更新する UpdateAllWindowTitles() と Alt-Tab ウィンドウのタイトルだけを更新する UpdateAltTabWindowTitles() が用意されています。呼び出し直後に更新されるわけではなく、あくまで更新リクエストをして非同期で更新されるものなので注意してください。

コールバック

ウィンドウの追加・削除は以下のコールバックから取得できます。

  • UwcManager.onWindowAdded
  • UwcManager.onWindowRemoved

デスクトップを表示するサンプルではこれを使用しています。

ウィンドウ一覧の取得

UwcManager.windows で現在管理しているウィンドウを全て取得することができます。リストのサンプルではこれを使っています。

UwcWindow

さて、上記のように得られた UwcWindow はどういったものなのか見ていきます。細かい実装は省いてメンバのみ見ていきましょう。

public class UwcWindow
{
    // プロパティ
    public int id { get; set; }
    public UwcWindow parentWindow { get; set; }
    public System.IntPtr handle { get; set; }
    public System.IntPtr ownerHandle { get; set; }
    public System.IntPtr parentHandle { get; set; }
    public System.IntPtr instance { get; set; }
    public int processId { get; set; }
    public int threadId { get; set; }
    public bool isValid { get; set; }
    public bool isAlive { get; set; }
    public bool isRoot { get; set; }
    public bool isChild { get; set; }
    public bool isVisible { get; set; }
    public bool isAltTabWindow { get; set; }
    public bool isDesktop { get; set; }
    public bool isEnabled { get; set; }
    public bool isUnicode { get; set; }
    public bool isZoomed  { get; set; }
    public bool isMaximized { get; set; }
    public bool isIconic { get; set; }
    public bool isMinimized { get; set; }
    public bool isHungup { get; set; }
    public bool isTouchable { get; set; }
    public bool isStoreApp { get; set; }
    public bool isBackground { get; set; }
    public string title { get; set; }
    public string className { get; set; }
    public int x { get; set; }
    public int y { get; set; }
    public int width { get; set; }
    public int height { get; set; }
    public int zOrder { get; set; }
    public int bufferWidth { get; set; }
    public int bufferHeight { get; set; }
    public int iconWidth { get; set; }
    public int iconHeight { get; set; }
    public Texture2D texture { get; set; }
    public bool hasIconTexture { get; set; }
    public Texture2D iconTexture { get; set; }
    public CaptureMode captureMode { get; set; }
    public bool cursorDraw { get; set; }

    // コールバック
    public class ChildAddedEvent : UnityEvent<UwcWindow> {}
    public class ChildRemovedEvent : UnityEvent<UwcWindow> {}
    public UnityEvent onCaptured  { get; set; }
    public UnityEvent onSizeChanged { get; set; }
    public UnityEvent onIconCaptured  { get; set; }
    public ChildAddedEvent onChildAdded { get; set; }
    public ChildRemovedEvent onChildRemoved { get; set; }

    // 関数
    public void RequestCaptureIcon()
    public void RequestCapture(CapturePriority priority = CapturePriority.High)
}

これは DLL を通じて C++ 側で管理されているウィンドウの情報とやりとりをするラッパークラスになっています。ほとんどのメンバは Win32API を叩いて得られる結果と同じです。ほとんどの情報は別スレッドで非同期に取得しているので、プロパティを呼び出してもそのタイミングで Win32API がコールされるわけではありませんので付加的に大きなインパクトはありません。UwcWindowTextureUwcIconTexture を利用せずとも、この UwcWindow を管理すれば自前で更新タイミングやテクスチャの管理をコントロールすることも出来ます。

プロパティ

ウィンドウの属性を取得するためのプロパティが一通り用意されています。これ以外にも欲しいものがあれば GitHub の issue 等でリクエストをください。

テクスチャ

テクスチャはウィンドウのテクスチャである texture と、アイコンのテクスチャである iconTexture があります。これらはデフォルトではキャプチャされていないので、手動で RequestCapture()RequestCaptureIcon() を呼ぶ必要があります。

コールバック

キャプチャ完了時や画面のサイズが変更されたとき、子が追加・削除されたときなどにフックして処理を行えるよういくつかコールバックが用意されています。

UwcWindowTexture

そしてこの UwcWindowMonoBehaviour 経由で使いやすくしたものが UwcWindowTexture です。インスペクタに出ているものは一通り public なプロパティまたは変数を通じてアクセス可能です。

public class UwcWindowTexture : MonoBehaviour
{
    // プロパティ / 変数
    public WindowTextureType type { get; set; }
    public bool altTabWindow { get; set; }
    public bool createChildWindows { get; set; }
    public GameObject childWindowPrefab;
    public string partialWindowTitle  { get; set; }
    public float childWindowZDistance;
    public int desktopIndex { get; set; }
    public CaptureMode captureMode = CaptureMode.PrintWindow;
    public CapturePriority capturePriority = CapturePriority.Auto;
    public WindowTextureCaptureTiming captureRequestTiming = WindowTextureCaptureTiming.OnlyWhenVisible;
    public int captureFrameRate = 30;
    public bool drawCursor = true;
    public bool updateTitle = true;
    public WindowTextureScaleControlType scaleControlType = WindowTextureScaleControlType.BaseScale;
    public float scalePer1000Pixel = 1f;
    public UwcWindow window { get; set; }
    public UwcWindowTexture parent { get; set; }
    public UwcWindowChangeEvent onWindowChanged { get; set; }
    public bool isValid { get; }

    // メソッド
    public void RequestCapture();
}
ウィンドウの手動セット

UwcWindow を検索した後、直接 UwcWindowTexture.window に代入すれば、そのウィンドウを UwcWindowTexture がハンドルするようになります。

UwcWindowTextureManager / UwcAltTabWindowTextureManager

ウィンドウマネージャを作成したい場合はこれらを参考にしてください。UwcWindowTextureManager がウィンドウの追加・削除・リストといった基本的な機能を持っているので、UwcAltTabWindowTextureManager はそれを継承する形になっています。

using UnityEngine;

public class UwcAltTabWindowTextureManager : UwcWindowTextureManager
{
    void Start()
    {
        // マネージャのコールバックに登録
        UwcManager.onWindowAdded.AddListener(OnWindowAdded);
        UwcManager.onWindowRemoved.AddListener(OnWindowRemoved);

        // 現時点ですでにあるウィンドウを処理
        foreach (var pair in UwcManager.windows) {
            OnWindowAdded(pair.Value);
        }
    }

    void OnWindowAdded(UwcWindow window)
    {
        // 子ウィンドウは UwcWindowTextureChildrenManager で管理
        if (window.parentWindow != null) return;

        // 可視、Alt-Tab 表示でないもの、かつスリープしているストアアプリを除外
        if (!window.isVisible || !window.isAltTabWindow || window.isBackground) return;

        // 初回だけ高優先度でキャプチャ
        window.RequestCapture();

        // 管理対象へ追加(UwcWindowTextureManager のリストに追加)
        AddWindowTexture(window);
    }

    void OnWindowRemoved(UwcWindow window)
    {
        // 管理対象から除外(UwcWindowTextureManager のリストから削除)
        RemoveWindowTexture(window);
    }
}

UwcCursor

UwcWindow のカーソル版が UwcCursor で、マネージャの中に 1 つだけ存在しています。

using UnityEngine;

public class UwcCursor
{
    public int x { get; }
    public int y { get; }
    public int width { get; }
    public int height { get; }
    public Texture2D texture { get; }
    public UwcEvent onCaptured { get; }
    public UwcEvent onTextureChanged { get; }
    public void RequestCapture();
}

UwcWindow と同じく必要なときに RequestCapture() してテクスチャを更新する必要があります。こういったことを自動でやってくれるように MonoBehaviour でラップしたものが UwcCursorTexture で、アタッチしたオブジェクトのマテリアルのメインテクスチャがカーソルのテクスチャになります。

UwcIconTexture

アイコン表示の基本的な使い方は Window Texture へ表示したいアイコンの UwcWindowTexture コンポーネントをインスペクタから設定する形ですが、直接 windowTextureスクリプトからセットしても構いません。また、UwcWindowTexture 同様に、直接 window プロパティに UwcWindow をセットしても大丈夫です。UwcWindowTexture が不要でアイコンのみ必要な時にこういった利用も可能です。

仕組み

f:id:hecomi:20180826223648p:plain

Unity のメインスレッドおよびレンダリングスレッドへ極力負荷を掛けない形で設計しています。まず、専用のウィンドウの情報を集めてくるスレッドで EnumWindows() し、得られたウィンドウハンドルからそのウィンドウの情報を回収しておきます。このウィンドウの追加・削除・属性といった情報を Unity 側に伝え、Unity 側では先程のスレッドでキャッシュされた情報を利用して必要なウィンドウを選別、キャプチャのリクエストを行います。キャプチャのリクエストは専用のキャプチャスレッドへ伝えられ、この中で PrintWindow()BitBlt() が行われます。キャプチャが終わったら以降は順に処理されていきます。キャプチャが終わるとアップロードスレッド内で Shared Texture へ UpdateSubresource() して書き込みを行い、その書き込みが終わると最後に Unity のレンダリングスレッドでそれを CopyResource() する、という流れになっています。

PrintWindow() を並列化しても速くならないため、これ以上速度を上げるには、更新のあった領域のみ処理する方法や直接バッファをもらってくる方法を見つける以外難しそうです。

トラブルシューティング

画像の反転

デフォルトで使用している Prefab についているメッシュの UV はいじってあるので問題ないのですが、テクスチャの向きが合わない場合は Tiling をマイナスに設定して調整してください。

f:id:hecomi:20180826005805p:plain

UwcWindow の取得タイミング

ゲームを開始した後に DLL の内部で状態を初期化し、ウィンドウを探しに行くため、場合によっては最初の数フレームはウィンドウが見つからないことがあります。Start() だけでウィンドウを探しに行く処理を書いている場合はこれでハマることがありますので、Update() で見つかるまで探す処理を書いてください。

クラッシュ

まだ完全にクラッシュが防げていないケースもあります。何か変な挙動を見つけた際は、実行場所(Editor 上ではプロジェクトのルート、実行ファイルでは .exe の隣)に uWindowCapture.log が出力されているので、そのログを送っていただけると参考になります。

ログファイルを出力したくない

また、ログファイルを出力したくない場合は UwcManager.instance.debugModeDebugMode.None に設定してください。予めシーンにマネージャを配置して、インスペクタから設定しても大丈夫です。

現状の問題点や将来の改善点

テクスチャの向き

現状、自動スケールが XY 方向にしか対応していません(標準のオブジェクトなら Quad は OK、Plane は NG)。将来的に XZ でも可能にしようと思いますが、手動でスケーリングしたい場合は Scale Control TypeManual に設定してください。

パフォーマンス

現状のボトルネックPrintWindow() の速度です。環境にも依存しますが自宅の環境では 4K のテクスチャのキャプチャの 50 ~ 60 ms 程度の時間を要します(= 15 fps 程度が限界)。小さいウィンドウほど速いので 60 fps 出るかもしれませんが、ゲーム実況と行った用途にはまだ難しいです。。もしどなたか別のキャプチャ方法について詳しい資料などご存知でしたら教えていただけますと幸いです。

おわりに

uDesktopDuplication も VR 内作業ツールや VTuber 向けツールなど色々な場所で利用していただいているので、ぜひこちらも利用してフィードバックなど貰えると嬉しいです。

*1:将来的にフィードバックを受けてアルゴリズムは変更するかもしれません