Unityで仮想空間内にデスクトップを表示する方法(を自分なりに改良してみました。)

2016/11/21追記
https://github.com/hecomi/uDesktopDuplication
私自身試した結果から申しますと、凹みさんのこちらのアセットを使ったほうが良いです。
パフォーマンスが段違いなので。



前提
-----
Oculusアプリを開発していてVR空間内にデスクトップを表示させたいなと思って作っていました。
↓こんな感じ
<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">VR空間内のデスクトップ表示、本当にヤバすぎるくらい良かった… <a href="https://t.co/4tv03TZmSh">pic.twitter.com/4tv03TZmSh</a></p>&mdash; Negipoyoc (@CST_negi) <a href="https://twitter.com/CST_negi/status/675536955761819648">2015, 12月 12</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

これを作る中でyuujiiさんの以下の記事が大変参考になりました。
「Unityでデスクトップ1の一部分をクリッピング表示する方法」
(http://2vr.jp/2014/03/02/unity%E3%81%A7%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%971%E3%81%AE%E4%B8%80%E9%83%A8%E5%88%86%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E8%A1%A8%E7%A4%BA%E3%81%99/)


これは
・System.Drawing.Graphics.CopyFromScreenでデスクトップを一枚の静止画で取得
・Byte列を生成
・Texture2D.LoadImage(画像情報のByte列)によって対象のTextureを毎UPDATE時に書き換える
ということをしています。

これを最初はそのまま用いましたが、フルHDのBitmapを同期処理で毎フレームごとに生成しているため処理が重くなってしまい、結果としてOculusアプリなのにカクカクという致命的な状況になってしまいました。(ちなみにクリッピングサイズが小さい場合はカクつきませんでした。)
これを自分なりに改良してみました、というのが本記事の内容です。

**※本記事の以下で説明する自分のソースコードの全体は https://gist.github.com/NegishiTakumi/69b165587fd71f4ab8a3 です。**

やること
----
・Threadを使って処理を分散させる
・UnityAPIは本来MainThreadでしか扱えないので非同期にUnityAPIを操作することができるアセット(SpicyPixel-ConcurrencyKit)を使う。
・+αとしてマウスポインタを描画してポインタの位置も含めてデスクトップを描画させる

まずThreadを使って処理を軽くする。
-----
yuujiiさんのコード

``` DeskTopDisplay.cs
    void Update()
    {
     　  Image img = GetCaptureImage(new Rectangle(0, 0, ClipWidth, ClipHeight));
         var imageBytes = ChangeDataToBytesFromImage.ConvertImageData.ConvertImageToBytes(img);
         img.Dispose();
     
        if (tex == null)
        {
            tex = new Texture2D(TextureWidth, TextureHeight);
        }
        tex.LoadImage(imageBytes);
        targetMaterial.mainTexture = tex;
    }
    private Image GetCaptureImage(Rectangle rect)
    {
        // 指定された範囲と同サイズのBitmapを作成する
        Image img = new Bitmap(
            rect.Width,
            rect.Height,
            System.Drawing.Imaging.PixelFormat.Format32bppArgb);

        // Bitmapにデスクトップのイメージを描画する
        using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(img))
        {
            g.CopyFromScreen(
                rect.X,
                rect.Y,
                0,
                0,
                rect.Size,
                CopyPixelOperation.SourceCopy);
        }
        return img;
    }
```

のうち、GetCaptureImageとimageBytesへの値の格納の処理が重いことがわかりました。
なのでこの部分をThreadを立てて処理を分散させます。

```DeskTopDisplay.cs
public class DesktopDisplay : ConcurrentBehaviour
{
    //省略

    private Thread thread;

    public virtual void Awake()
    {
        base.Awake();
        targetMaterial = GetComponent<Renderer>().material;
        thread = new Thread(Thread1);
        thread.Start();
    }
    
    void Thread1()
    {
        while (true)
        {
            Image img = GetCaptureImage(new Rectangle(0, 0, ClipWidth, ClipHeight));
            var imageBytes = ChangeDataToBytesFromImage.ConvertImageData.ConvertImageToBytes(img);
            img.Dispose();
            taskFactory.StartNew(() => UpdateTexture(imageBytes));
        }
    }

    void UpdateTexture(byte[] imageBytes)
    {

        if (tex == null)
        {
            tex = new Texture2D(TextureWidth, TextureHeight);
        }
        tex.LoadImage(imageBytes);
        targetMaterial.mainTexture = tex;
    }
```

このようにThreadを用いることによって負荷を分散させることができました。
**注意**
コードとして、
・ConcurrentBehaviourというクラスを継承していたり
・Awake()がなぜかvirtual修飾子をつけてたり、base.Awake()してたり。
・UpdateTextureがtaskFactory.StartNew(ラムダ式)の形式で呼ばれていたり
という感じになっています。もちろん理由がありまして、それは
**Unity APIは基本的にはMainThreadでしか呼ぶことができない**という問題点を解決するためです。

非同期でimageBytesに画像情報のByte列を代入できたらTexture書き換えはしたいので次はこれを解決します。
　
　
　
非同期にUnityAPIを操作することができるアセット(SpicyPixel-ConcurrencyKit)を使う。
------
非同期にUnityAPIを操作する
( http://neareal.net/index.php?ComputerGraphics%2FUnity%2FTips%2FAsyncControlUnityAPI )
こちらが参考になりました。

先ほど挙げた
・ConcurrentBehaviourというクラスを継承していたり
・Awake()がなぜかvirtual修飾子をつけてたり、base.Awake()してたり
は、すべて↓を実現するためです。
・UpdateTextureがtaskFactory.StartNew(ラムダ式)の形式で呼ばれていたり

このアセットでは実装したメソッドを非同期な操作から呼び出す場合"taskFactory.StartNew(()=>メソッド名)"だけで済むので楽ですね。
ConcurrentBehaviourの継承はこのためです。

また注意点としては、クラスを継承すると、基底のAwake部分でtaskFactoryメンバなどが初期化されているようなので、Awakeを書く場合はvirtualで書いてやる必要がある。ということです。

以上の点に気をつけて、適当な画像が描画できるタイプのGameObjectにAddComponentしておけばできます。
(最初に言及したyuujiiさんのブログでは、PCモニタの描画機能月3DモデルがPrefabで配布されていたので僕はそれを使っています。)

　
　
　
+αとしてマウスポインタを描画してポインタの位置も含めてデスクトップを描画させる
------
そして自分で動かしてみて気づいたのですが、マウスポインタの位置が全くわからないという問題が発生しました。
自分はUnity以前はWPFの実装をしてた時期があってその時に似たような実装してたの思い出して解決することができました。

以下説明するものは
https://gist.github.com/NegishiTakumi/396ff765768c04021f0c
こちらです。

**using System.Windows.Forms;をする都合上Assets/Plugin以下にSystem.Windows.Forms.dllを置いて参照できるようにしてください。
またその際、"C:\\Program Files (x86)\\Unity\\Editor\\Data\\Mono\\lib\\mono\\2.0"にあるDLLを使ってください。**

といっても別に対して変更はないです。GetCaptureImageメソッドを改良します。

```DeskTopDisplayPlusMousePointer.cs
 private Image GetCaptureImage(Rectangle rect)
    {
        // 指定された範囲と同サイズのBitmapを作成する
        Image img = new Bitmap(
            rect.Width,
            rect.Height,
            System.Drawing.Imaging.PixelFormat.Format32bppArgb);

        // Bitmapにデスクトップのイメージを描画する
        using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(img))
        {
            //カーソルのハンドラを取得
            var cursor = new System.Windows.Forms.Cursor(System.Windows.Forms.Cursor.Current.Handle);
            //カーソルの位置を取得
            var curPoint = System.Windows.Forms.Cursor.Position;
            //ホットスポットを取得
            var hotspot = cursor.HotSpot;
            g.CopyFromScreen(
                rect.X,
                rect.Y,
                0,
                0,
                rect.Size,
                CopyPixelOperation.SourceCopy);
            //位置の微修正
            var p = new Point(curPoint.X - hotspot.X, curPoint.Y - hotspot.Y);
            //赤円の塗りつぶしで描画する
            g.FillEllipse(Brushes.Red,p.X,p.Y,15,15);
        }
        return img;
    }
```

やることは
・カーソルのハンドラを取得
・カーソルの位置を取得
・ホットスポットを取得(この場合カーソルの先端部分です。クリックした時に有効な部分のこと。)
・ホットスポットの位置から描画すべき場所を微修正
・修正した部分に対して赤円(塗りつぶし)で描画する
だけ。
やってることは、スクリーンのキャプチャの上からマウスポインタを重ねて描画しているだけです。簡単ですね。

これをやると画像の右下のようになり、普通にVR空間内でブラウジングとかできるようになります。便利すぎ。
![CropperCapture[22].png](https://qiita-image-store.s3.amazonaws.com/0/88657/01480428-8126-ed61-7111-5b93c2f01f00.png)

その他
-----
ちなみにこれでもフルHDのキャプチャは若干カクつきます。
ここまで書いてきてアレだけど、ネイティブでやったほうが軽くなる気はする(試してません。誰かお願いします。)
<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/CST_negi">@CST_negi</a> これ使えば出来ると思う（Virtual Desktopに使われてる） <a href="https://t.co/FEGZY2L4IL">https://t.co/FEGZY2L4IL</a></p>&mdash; 冬コミに初当選した野生の男＠木曜モ43a (@yasei_no_otoko) <a href="https://twitter.com/yasei_no_otoko/status/675542310474539008">2015, 12月 12</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/CST_negi">@CST_negi</a> こういう重い処理はネイティブdllにしてしまう派（そして泥沼にはまる派）なんですが、Unity APIも使えるアセットもかなり便利そうですね。あとデスクトップキャプチャも応用が広そうで夢が広がります！</p>&mdash; あるしおうね (@AmadeusSVX) <a href="https://twitter.com/AmadeusSVX/status/675588769563541504">2015, 12月 12</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

ちなみにVirtualDesktopというそのものずばりなものがあるのですが、( http://www.vrdesktop.net/ )
<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">もしかしてこれで誰でも、最新ランタイムで動くもののみしか公開されてなかったOculusのVirtual Desktopを任意のランタイムに合わせて作れる！？　<a href="https://t.co/hN7RGr6B3z">https://t.co/hN7RGr6B3z</a></p>&mdash; 早稲田　治慶(本名)＠クロマキーAR (@waseda_fablab) <a href="https://twitter.com/waseda_fablab/status/675716181844619264">2015, 12月 12</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
ということですね。(このメリットは気づきませんでした)
今は0.8.0じゃないとVirtualDesktopは動作しないようですが、この手法ではOculusのランタイムに全く依存しないので大丈夫です。(僕は0.5.0です。)
というわけで使いみちがあるかはわからないけどOculusアプリじゃなくても使える、ということです。

はいこの話おわりー。