読者です 読者をやめる 読者になる 読者になる

HTC ViveでペンタVR上でクリスタが使えるように改良する

はじめに

Viveが来たんですよ!Viveが!

さて、今回はわっふるめーかー氏=>waffle_maker@TBA (@waffle_maker) | Twitterが開発した『ペンタVR』に関して、
少ない手間で大きな成果をあげられそうなことを思いついたので、実験してみました。
(今回開発したものはペンタVRともわっふるめーかー氏とも関係ありませんが、実質同じもの&既出のアイデアなため、改良とさせていただきました。)

ペンタVRとは

 今話題のVR上にタブレット(=キャンバス)を表示して、仮想空間内でお絵かきをしよう!というもの。去年くらいに話題になりましたね。
開発の経緯 => よー清水さんの提案する仮想空間お絵描きVRシステムと実現方法について #ペンタVR - Togetterまとめ
実装方法 => ペンタVRの実装方式について | わっふるぷれーと

ペンタVRの利点

 上記のリンクでも述べられていますが

  1. 安価な板タブレットを、高価な液晶タブレットのように使える!
  2. 仮想空間内に3Dキャラクターや家具を配置、それを立体的に観察しながら絵を描くことが出来る!
  3. 画像などの資料も見える範囲でいくらでも配置出来る!
  4. 最近のWacomの板タブレットはワイヤレス化が出来るため、VRHMDを被ってさえいれば好きな場所、好きな姿勢でお絵描きが出来る!
  5. タブレットは液晶タブレットのように夏場熱くならない!

ペンタVRの欠点

  1. VRHMD越しで仮想画面を見るタイプ(PSVRのシアターとか)に共通の問題ですが、離れた分だけ解像度が低くなってしまう
  2. かといって画面を大きくすると現実世界との齟齬が大きくなる!(でも、そういう板タブ+仮想画面的な使い方もアリ)
  3. VRHMDつけてないとお絵描き出来ない!!重い!!夏場は絶対熱い!!
  4. お絵描き中に水が飲めない!(Viveにはシャペロンという単眼カメラがついてるので、外の様子もわかります)

現状のペンタVRと、その問題点(情報が古かったらすみません)

 ペンタVRの実装方式について | わっふるぷれーとの最後に描いてある通り、現状のペンタVRは

ゲームエンジンの機能を利用してテクスチャの更新を行う方式です。
お絵描きアプリをフルスクラッチで実装するような手間が掛かりますが、VRならではの表現を行いやすいメリットがあります。

となっています。
これの一番の問題は、プロアマ問わずイラストレーターさんの多くが愛用している『Photoshop』や『Sai』や『CLIP STUDIO PAINT』などが使えないということですね。

改善策

 上記リンクの1の方法

既存のお絵描きアプリでキャンバスを管理
デスクトップのキャプチャ画像をテクスチャに貼り付ける方式です。
既存のお絵描きアプリの機能をそのまま仮想空間に持ち込むことが出来ますが、現状では遅延が大きく使い物にならないようです。

キャプチャアプリからGPU側のテクスチャ情報を直接更新できれば、遅延が改善するかも知れません。
平面のお絵描きに限定すれば、APIを提供することでお絵描きアプリ側にペンタVR連携機能を組み込むことが出来る可能性があります。

で作ればいいわけです。

ここまでは特に新しいアイデアはないです。
以下、新しいアイデア

ペンタブのトラッキング

ペンタVRでは

多くのOculus Rift向けVRコンテンツでは、仮想空間内のカメラ位置にプレイヤーを一致させますが、
今回は、タブレットの上部にOculus Riftの赤外線カメラを固定することで、現実の赤外線カメラとHMDの相対位置をそのまま仮想空間に持ち込みます。
これにより、現実と仮想空間でタブレットの位置を一致させることが出来ます。
ただし、タブレットの傾きは反映されません。

という方法でしたが……

 さて、ここにHTC ViveというVRHMDシステムがあります。
こいつが優れものでして、ルームスケールのトラッキングシステムや、それを利用したモーションコントローラーが標準装備されております。このViveのトラッキングシステムはかなり優秀で、コントローラの位置も回転も正確に補足してくれます。

 ならペンタブにViveコンくっつければいいじゃない!ということで取り付けたのが以下
f:id:IARIKE:20160514220230j:plain(カメラくっつけるのとレベルは変わっていない)

遅延の少ないデスクトップのキャプチャ

 上では難しいといわれていた高速なデスクトップキャプチャですが、今どきはVirtual Desktopなんていうソフトがマルチディスプレイ環境でもバンバンやっているので、出来るにきまってるのです。
でも難しそうだな……と思っていたら既にやられている方がいました。

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

もう難しいことはありません。
あとは凹みさんのスクリプトに多少変更を加えたり、Viveコンでトラッキング出来るようにしただけです。

手順

  1. 上記のように、ペンタブにViveコンを物理的に貼り付けます(粘着面付きのマジックテープを使っています)
  2. Unity5.4.0bをインストール
  3. プロジェクトを新規作成
  4. アセットストアからSteamVRをインポート
  5. 凹みさんのパッケージをインポート
  6. 新規シーンを作成
  7. SteamVR->Prefabs->[CameraRig]プレハブをシーンに追加
  8. シーン内の[CameraRig]->Controller(right)の中に、[DesktopCapture]->[Scenes]->MainシーンにあるPlaneを持ってくる
  9. シーン内の[CameraRig]->Controller(right)->ModelのSteam VR_Render Modelコンポーネントを切る(コントローラの非表示)
  10. Planeのサイズをペンタブの描画エリアと一致させます。Planeはスケール1で1mではなく10mなところに注意
  11. 張り付けたViveコントローラとの位置関係を考えてPlaneをずらしたり、回転させたりする

(RightにつけてますがLeftのほうがいいかも……)

これで最初の実験は出来るはずです。
(うちの環境だとDesktopCaptureのdllがEditor上で落ちまくり、Buildしてもディスプレイ間を移動させたりすると落ちます。ソースを全然見てないので、今の私の力ではどうしようもありません)
(メインディスプレイ上にUnityEditorを置くと落ちなかったりします。Build後は画面が見えないようにウィンドウを動かしてやると落ちない。そんな気もします)

続けて

  1. 発光するディスプレイにしたいので、mainTextureとしてではなく_EmissionTexとしてキャプチャ画像を渡してやります
  2. テクスチャの更新頻度が多いせいか、VR的なフレームレートが落ちるので、キャプチャ(転送)間隔を少し長くとります(GPUの転送速度次第?)

さらに

  1. マウスポインタが表示されないので、自分で描画する必要があります
  2. arrowというテクスチャがあったので、それでマテリアルを作ります。
  3. Planeにそのマテリアルを割り当て、後述のスクリプトをつけます。
  4. 画面の拡大率を上げると文字が見やすくなるかも?

Intuos4 LargeでCLIPSTUDIOPAINTを動かしてみているところ。
f:id:IARIKE:20160514225114j:plain
実際にはこんな感じに見えます。やはり解像度が低いです。
UIの文字を読むためには、結構頭を近づける必要があります。

以下ソースコードです。

凹みさんのを改造した
DesktopCapture.cs

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

public class DesktopCapture : MonoBehaviour
{
    [DllImport ("DesktopCapture")]
    private static extern void Initialize();
    [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;

    public bool updateScreenCapture;  // <- 変更
    void Start()
    {
        var tex = new Texture2D(GetWidth(), GetHeight(), TextureFormat.BGRA32, false);
        GetComponent<Renderer>().material.SetTexture("_EmissionMap", tex);  // <- 変更

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

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

        if (Time.frameCount % 3 == 0)    // <- 変更
            updateScreenCapture = true;  // <- 変更
    }

    IEnumerator OnRender()
    {
        for (;;)
        {
            yield return new WaitForEndOfFrame();
            if (updateScreenCapture)  // <- 変更
            {
                updateScreenCapture = false;  // <- 変更
                GL.IssuePluginEvent(GetRenderEventFunc(), 0);
            }  // <- 変更
        }
    }
}

マウスポインタを描画するやつ。Unity上での描画。
DrawPointer.cs

using UnityEngine;
using System.Collections;

public class DrawPointer : MonoBehaviour
{
    public DesktopCapture capture;
    public int ScreenWidth = 1920;
    public int ScreenHeight = 1080;

    private Vector2 PointerPos;
    
    
    public float PointerAltitude = 0.001f;

	void Update ()
    {
        if (capture.isPointerVisible)
        {
            // DesktopCaptureの中心位置からのズレ分を計算
            // 10を掛けているのはPlaneの大きさのため
            PointerPos.x = (capture.pointerX / (float)ScreenWidth - 0.5f) * capture.transform.localScale.x * 10;
            PointerPos.y = (capture.pointerY / (float)ScreenHeight - 0.5f) * capture.transform.localScale.z * 10;
        }
        
        // DesktopCaptureは回転するので、その方向に合わせてオフセットさせる
        // 更に自身の左上をポインタ地点に合わせるために、localScaleでずらしている
        // 5を掛けているのはPlaneの大きさのため
        transform.localPosition = capture.transform.position -
            capture.transform.right * (PointerPos.x + transform.localScale.x * 5) +
            capture.transform.up * PointerAltitude -
            capture.transform.forward * (PointerPos.y - transform.localScale.z * 5);
	}
}


HTC Viveの開発に関しては、こちらフレームシンセシス 技術ブログを参考にしています。

まとめ

 開発環境は Windows10Pro64bit,Unity5.4.0b14、i7-3930K、Radeon R9 390です。

 今回は先人たちの成果をごちゃごちゃーっと組み合わせて割と簡単にできました。
クリスタなど既存のソフトが使えるということで実用に近づきましたが、解像度に関してはまだまだ辛いものがあります。
(UIが絡む操作をするときだけは、目の前にある大きなスクリーンで板タブ的に使う、というのはありかもしれません。同じマテリアルのPlaneを量産すればいいだけで、クローンを作るのは簡単なことです。)


機能的にはあと

  • プリミティブの配置、ライト類の設置、マテリアルの変更
  • 外部から画像を読み込んで、資料として配置できる機能
  • 外部からOBJ形式などのモデルを読み込んで、配置できる機能
  • 配置した物体をもう片方のViveコントローラで移動したりスケールしたりする機能

などありますが、手間がかかるだけで難しいことではないですね。

おそらくBlenderやZBrushなどのモデリングソフトも動かせるはず(性能の問題を除けば)なので、特定のフォルダにOBJ出力するとペンタVR上にモデルがサッ(一瞬止まる)と現れて立体的に回り込んで確認できる!
とか、作ると面白いかもしれませんね(言うだけ言っておく)