Unity×Windowsの画面キャプチャで四苦八苦してる話(1)


#Unity×Windowsの画面キャプチャで四苦八苦してる話(1)

##この記事を書いた人
学生をしながらVRで使えるものを作っている最中の山下です。
表に出ているものは少ないですが、内定先の某社内では共有ライブラリを書き起こしています。

##なぜ必要になったのか
現在作っているアプリの提供機能の一つとして、
Windowsで実行されているほかのアプリケーションの画面をキャプチャし、
内部で空間中に展開できるようにしたいなーと思っていました。
VirtualDesktopで「そのままのデスクトップを出すことしかできない」という部分が不満だったからです。
既存のライブラリなどで丁度よいものが無かったので自作しようとしました。


##結論とこの文章の趣旨
まず最初に結論を言うと 2018/2/26 現在、
結果としては半分成功、半分失敗しています。

成功に関してはデスクトップの画像と、各アプリケーションがWindowsに提供している画像データを取得することはできました。
失敗は、そのアプリケーションから提供されたキャプチャ画像が一部欠損していることです。
VisualStudioやUnityの画面は普通に取得できるのですが、Chromeなどブラウザなどの画面はうまく取得できないという状態になっています。
特に取得が望まれていたブラウザがまともに取得できておらず、先にやりたかったことを凹さん( https://twitter.com/hecomi )が実現されていて息絶えました。

- 凹さんの動画：https://youtu.be/YoFmszqILd4 

とはいえ、調査した資料などをまとめておけば、今後だれかの、何かの役に立つかもしれないと考えて、この文章をかきました。


# 暗中模索の経路
##ブラウザから表示したい
最初はまっとうにWinAPIを叩くつもりはなく、ブラウザでデスクトップ表示するようなものを使えれば大体代用できるよねという発想でそちらについて検索。
とりあえずUnityでブラウザの内容を表示するなんて簡単だろう、すぐできるだろうという発想でした。。。とんでもない。
まず当然ながら既存の情報を検索しましたが、英語も含めて検索したものの、実用性が高いものはみつかりませんでした。
一番助かったのは2014/05/05の凹さんの記事で、こちらは様々なアセットの比較などがされており、現状でも生きている情報。ありがたや。

* Unity で Web をテクスチャとして表示して操作する方法について調べてみた http://tips.hecomi.com/entry/20130818/1376844813

その上で、いくつかブラウザ経由でデスクトップアプリを操作するものを探したのですが、やはりどうにも満足できず。
仕方ないからもう少し正攻法で掘っていくことにしました。


##まっとうにWinAPIをたたく
以下のような内容を参考に、通常のデスクトップの画像と、仮想デスクトップの画像、各ウィンドウの画像を取ろうとしました。

- C#/.NETの標準機能からの取得
 -  ⇒ デスクトップ画像の取得だけなら可能だけど、他が不可能。
 - ウィンドウの一覧や、そのハンドルなどは手に入った。
 - 自分が操作しているメインプロセスの支配下ならFormsから画像がとれますが、ほかのプロセスからはとれません。
- 入手したハンドルを使い、WinAPIのUser32.dllを利用してキャプチャ
 -  ⇒ ブラウザなど、一部のウィンドウが対応していない。
 -  WHY?


##どうしてUser32.dllから取得できないか
あくまで推測ですが、WindowsVista以降で改良されたWindowsの描画プロセスが問題ではないかと推測しています。
ちょっと出典資料がみつからないのですが（だれか知ってたらおしえてください）
初期のWindowsは「CPU側のメインメモリ」と「GPUのメモリ」の両方に対して、各Formの情報をレンダリングしていたらしいです。
しかし、あるバージョンから高速化を理由にその方式を変更し、GPU側のメモリにだけ描画を行うようになっています。
ただし、こうしたバックグラウンドから、実際に描画される部分と、User32がアクセスする領域がうまく一致していない状態が発生しているのではないかと推測しています。


# 今後について
## 実用性と手間
凹さんがすでにWindowCaptureを作られているので、自分が開発を継続する必要はなさそう、というのが正直なところ。
ちょっと自分にはアプローチの難易度が高いなーと思ってきた、というのもあります。
ただ、実際にWindowsAPIを使ったネイティブプラグインや、それを使えるようにする技術について、今まで以上に興味がわいたので非常に実りがあったのは間違いありません。
機会があれば再挑戦したいので(1)としましたが、今回はひとまず凹さんのライブラリが出るのを楽しみに待ちたいと思います。急がなくてよいので。

## 考えていたアプローチ
以下のような方法でアプローチするつもりだったのですが、「こういう手段もあるよ」などのアドバイスがあればご教授願えると嬉しいです。

- DLLインジェクションを行い、個別に内部からスクショし、メモリアロケータを共有して表示。
 - それぞれの内部プロセスからはC#のFormを使ったスクショができるため、この方法が鉄板のように思います。
 - ただし、こちらの方法でもブラウザなどが正しくキャプチャできるかは少し疑問。実験したいですね。
- デバイスドライバを偽装し、仮想ディスプレイと仮想デスクトップをメモリ上に構築、構築された空間内でデスクトップキャプチャすることでウィンドウの画像を取得。
 - これは今後、自分の中でできるようになりたい技術のトップなのですが、どうしたらできるかは五里霧中です。
 - ドライバの自作方法などは調べると出てきますが、存在しないディスプレイを作ってーとなるとまったく分からんです。


#参考資料
以下はこの段階で参考にしていたリンクです。

- C#(.NET)で他のウィンドウのクライアント領域のスクリーンショットを撮る
  http://castaneai.hatenablog.com/entry/2012/03/14/230323
- C#で他のプロセスのウインドウ画像をキャプチャする方法を検討して行き着いた結果
  http://upa-pc.blogspot.jp/2014/02/c.html
- .NET Framework 2.0以降で、Graphics.CopyFromScreenメソッドを使用する方法
  https://dobon.net/vb/dotnet/graphics/screencapture.html
- Get programs currently present in the taskbar
  https://social.msdn.microsoft.com/Forums/vstudio/en-US/fbf3f51b-ed69-4527-a7d1-b24761c790b1/get-programs-currently-present-in-the-taskbar?forum=csharpgeneral
- C# で ウィンドウハンドル を 取得する 方法
  https://garafu.blogspot.jp/2016/08/cs-get-wndhndl.html
 Grabacr07/VirtualDesktop
  https://github.com/Grabacr07/VirtualDesktop
- Windows 10 の仮想デスクトップを制御しようとして失敗した話 - 最終的に完成しました。
  http://grabacr.net/archives/5601
- アクティブなウィンドウを別な仮想デスクトップに飛ばすアプリ
  http://grabacr.net/archives/5701
- Windows 10 のアクティブウィンドウを別の仮想デスクトップに飛ばすアプリを作ったよ
  http://blog.tmyt.jp/entry/2015/09/14/193840
- stackoverflow   stackoverflow Altering Win10 virtual desktop behavior (*)
  https://stackoverflow.com/questions/32416843/altering-win10-virtual-desktop-behavior
- IVirtualDesktopManager interface 
  https://msdn.microsoft.com/en-us/library/windows/desktop/mt186440%28v%3Dvs.85%29.aspx
- VirtualDesktopManager class
  https://msdn.microsoft.com/en-us/library/windows/desktop/mt186461(v=vs.85).aspx
- System.Windows.Forms 名前空間
  https://msdn.microsoft.com/ja-jp/library/system.windows.forms(v=vs.110).aspx
- c# - alt-tabのようなウィンドウを列挙する
  https://code.i-harness.com/ja/q/33648
- 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/
- Take screenshot of multiple desktops of all visible applications and forms
  https://stackoverflow.com/questions/15847637/take-screenshot-of-multiple-desktops-of-all-visible-applications-and-forms
- [Unity]デスクトップをリアルタイムに撮影して表示する
  http://wordpress.notargs.com/blog/blog/2015/08/31/unity%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E3%82%92%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%AB%E6%92%AE%E5%BD%B1%E3%81%97%E3%81%A6%E8%A1%A8%E7%A4%BA%E3%81%99/
- C#(.net)で他のプロセスのメインウィンドウハンドルを取得する
  http://tomoemon.hateblo.jp/entry/20080430/p2
- Windows 10 のアクティブウィンドウを別の仮想デスクトップに飛ばすアプリを作ったよ
  http://blog.tmyt.jp/entry/2015/09/14/193840
  https://github.com/tmyt/VDMHelper
- CreateRemoteThread
  https://msdn.microsoft.com/ja-jp/library/cc429075.aspx
- 64ビット対応のDLLインジェクション
  http://furuya02.hatenablog.com/entry/20120114/1326484897
- Windowsで簡単なデバイスドライバを書いてみる
　http://inaz2.hatenablog.com/entry/2015/09/12/215337
- Windows ドライバーの概要
　https://msdn.microsoft.com/ja-jp/library/windows/hardware/ff554690(v=vs.85).aspx
- windows10でドライバを作成する
　https://qiita.com/blacky_one/items/975319289ed087909f28
- Windows 10 でサンプル ドライバーをビルドするまで
　https://blogs.msdn.microsoft.com/jpwdkblog/2015/08/21/windows-10/