【Unity】VR空間にペンタブレットを持ち込んで板タブを液タブみたいに使ってみた

#はじめに
こんにちは、@iykuetbooと申します｜
去年に引き続き、[UT-virtualアドベントカレンダー2019](https://qiita.com/advent-calendar/2019/ut-virtual) に13日目の担当として参加しました。

今回は、UnityとOculusRiftSを使って、VR空間でペンタブレットを使ってみようというお話です。

#環境
Windows 10
Unity2018.4.19f1
Wacom Intuos Pro Medium
タブレットドライバのバージョン 6.3.37-3

#経緯
ペイントソフトでのイラスト製作や3DCGソフトでのモデリングなどによく使われるペンタブレットですが、大きく2種類に分かれています。

・**板タブレット（板タブ）**
板状無地のタブレット上にペンを走らせることでカーソルを動かせる。ディスプレイを見ながら操作する。
・**液晶タブレット（液タブ）**
タブレット自体が液晶画面になっていて、手元を見ながらペンで直接書くように操作できる。板タブより高い。

で、板タブの方を持っていて使っているのですが、**画面を見ながら手元のペンで字や絵を描くのって結構難しい**んですよ。
液タブ使いて～、でも高いよね～、なんて友人と話した後、ふと思いつきました。**VR機器を使えば板タブでも手元に絵を表示しながら描ける**のでは？？

#実装
というわけで、板タブでも手元を見ながら絵を描ける機能を目指して、Unityを起動しました。
##アセットの導入
実現にあたり、なくてはならない3つの機能**「ペンタブとUnityの連携」「テクスチャペイント」「VR対応」**それぞれに対し、**超絶便利なアセットが存在しました**。先人達に感謝。

###uWintab
凹みTips様の、[Unity でペンタブの入力を受け取れるアセットを作ってみた](http://tips.hecomi.com/entry/2017/06/11/144246) を見ながらunitypackageをインストールします。
Windowsのタブレットドライバのapiから情報を引っ張ってきてくれるアセットです。

インストールしてサンプルシーン開いただけで簡単にUnityペンタブ連携ができちゃいました。

###InkPainter
@Es_Program 様の [Unityでリアルタイムテクスチャペイント](https://qiita.com/Es_Program/items/fe9c66afc4cbddc7fdfd) を使用しました。

```PaintTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Es.InkPainter; //追加する

public class PaintTest : MonoBehaviour
{
    public InkCanvas canvas;
    public Brush brush;
    public Vector3 paintPosition;

    void Start()
    {
        canvas.Paint(brush,paintPosition));
    }
}
```
このようにPaintメソッドを呼び出すことでテクスチャにペイントすることができます。詳細はリンク先へ。
nomalmapやheightmapをペイントできたり、インクが垂れる表現ができたりと、ただの平面お絵かきに使うのはもったいなく感じてしまう高機能アセットでした。

###OculusIntegration
最後はVR用のアセット。今回はRiftSを使うので、Oculus公式の[Oculus Integration](https://assetstore.unity.com/packages/tools/integration/oculus-integration-82022)をインストールします。
Oculus/VR/Prefabs内にあるOVRCameraRig.prefabをシーンに置くだけで使えます。
機器に依存する処理はしていないので、QuestやRiftを使ったり、SteamVRでVIVEを使ったりしても大丈夫です。

##ペンタブで入力した位置にペイントする
InkPainterのPaintメソッドは、ペイントしたい位置へのRaycastHitまたはその位置のWorld座標を引数に必要とします。
一方、uWintabで取得できるのはタブレット内のペン位置（xとy,ともに0-1）です。

このままではペンの入力位置にペイントすることが出来ないので、取得したペン位置をWorld座標に変換することにします。
uWintabのTabletクラスを継承し、ワールド座標を取得できるようにした新しいクラスを生成してそれに置き換えます。

```TabletExtend.cs
using uWintab;

public class TabletExtend : Tablet
{
    public Transform board;

    public Vector3 GetLocalTouchPosion
    {
        get
        {
            float x_, y_, z_;
            x_ = (x - 0.5f) * board.localScale.x;
            y_ = (1f - 0.5f) * board.localScale.y;
            z_ = (y - 0.5f) * board.localScale.z;

            return board.localPosition + new Vector3(x_, y_, z_);
        }
    }

    public Vector3 GetWorldTouchPosion
    {
        get
        {
            return transform.TransformPoint(GetLocalTouchPosion);
        }
    }
}
```

とりあえずuWintabのサンプルシーンからペンやタブレットのオブジェクトをコピーしてきて、TabletにアタッチされているTablet.csを上記に置き換えます。そして、子のBoardオブジェクトにInkPainterのInkCanvas.csをアタッチしました。

これでWorld座標の取得が可能になったので、実際にペイントするためのスクリプトを用意します。

```PaintOnBoard.cs
public class PaintOnBoard : MonoBehaviour
{
    [SerializeField] InkCanvas canvas;
    [SerializeField] TabletExtend tablet;
    public Brush brush;
    
    // Update is called once per frame
    void Update()
    {
        if (tablet.pressure > 0.1f)
        {
            canvas.Paint(brush, tablet.GetWorldTouchPosion);
        }
    }
}
```

これを適当なゲームオブジェクトにアタッチし、参照先をインスペクタから設定すれば、、、
ペンタブの入力位置にペイントできるようになりました！！（VRじゃなくてもこれだけで楽しい）

###VRでやってみる
先ほどのシーンにOVRCameraRigを配置し、位置や大きさを調整してヘッドセットで覗いてみます。

！！

思った通りの線が描ける！！！
手元を見ながら描ける！！！
細かい文字も綺麗に書ける！！！

###さらに色々
せっかくVRなんだ、タブレットよりもデカいキャンバスに描きたい！
というわけで、Inkcanvas.csを別のプレーンにアタッチして、取得できるWorld座標と合わせることでタブレットより大きいキャンバスに書き込めるようしたり。

せっかくペンの筆圧取得できるんだ、筆圧に応じて太さ変えたい！
というわけで、Tabletからpressure取得してその値をbrushのScaleに反映させたり。

せっかくブラシの設定できるんだ、ブラシの色とか大きさとかテクスチャ変えたい！
というわけで、TabletのexpKeyでブラシの色や大きさを変えられるようにしたり。

ペンを速く動かした時に点線になってしまうのは嫌だ！
というわけで、Paintメソッドの呼び出し回数を増やしてみたり。

色々やった結果こんな感じになりました！
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310255/f626ee5d-f278-d87f-d859-88e7fdf73405.gif)
（実装方法詳細書いてるとアドベントカレンダー間に合わないから雑でも許して）

<details><summary>コード詳細</summary><div>

```BrushSizeChange.cs
using Es.InkPainter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BrushSizeChange : MonoBehaviour
{
    [SerializeField] TabletExtend tablet;

    [SerializeField] PaintOnBoard painter;
    Brush brush;

    [SerializeField] float[] sizes;//変更先候補のサイズの配列
    float brushSize;//筆圧最大の時のBrushのサイズ（基本サイズ）
    int index;

    [SerializeField] Transform colorSampleRect;//前方にBrushの見本をUI表示

    // Start is called before the first frame update
    void Start()
    {
        ChangeTo(sizes[0]);
        brush = painter.brush;
    }

    // Update is called once per frame
    void Update()
    {
        if (tablet.GetExpKeyDown(1))
        {
            index = (index + 1) % sizes.Length;
            ChangeTo(sizes[index]);
        }

        if(tablet.pressure > 0)
        {
            brush.Scale = brushSize * tablet.pressure;//基本サイズに筆圧に応じて倍率をかける
        }

    }

    void ChangeTo(float size)//基本サイズを変更
    {
        colorSampleRect.localScale = size / sizes[sizes.Length-1] * Vector3.one;
        brushSize = size;
    }
}
```

```BrushColorChange.cs
using Es.InkPainter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BrushColorChange : MonoBehaviour
{
    [SerializeField] TabletExtend tablet;

    [SerializeField] PaintOnBoard painter;
    Brush brush;

    [SerializeField] Color[] colors;//変更先候補の色配列
    int index;

    [SerializeField] Material colorSampleMat;

    // Start is called before the first frame update
    void Start()
    {
        brush = painter.brush;
    }

    // Update is called once per frame
    void Update()
    {
        if (tablet.GetExpKeyDown(0))
        {
            index = (index + 1) % colors.Length;//変更先の色インデックスを変更
            ChangeTo(colors[index]);
        }
    }

    void ChangeTo(Color color)//Brushの色変更
    {
        colorSampleMat.color = color;
        brush.Color = color;
    }
}
```

```PaintOnBoard.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Es.InkPainter;

public class PaintOnBoard : MonoBehaviour
{
    [SerializeField] InkCanvas canvas;
    [SerializeField] TabletExtend tablet;
    public Brush brush;

    Vector3 currentTouchPosition;
    Vector3 preTouchPosition;

    bool drawLine = false;
    [Range(1,10)] public int Density;//点線になるのを防ぐ

    // Update is called once per frame
    void Update()
    {

        if (tablet.pressure > 0.1f)
        {
            currentTouchPosition = tablet.GetWorldTouchPosion;
            canvas.Paint(brush, currentTouchPosition);

            if(drawLine)//前フレームでPaintしていた場合
            {
                if(Density > 1)
                {
                    for(int i = 1; i < Density; i++)
                    {
                        canvas.Paint(brush, Vector3.Lerp(currentTouchPosition, preTouchPosition, 1f/Density * i));//補間して滑らかな直線に
                    }
                }

                preTouchPosition = currentTouchPosition;
            }
            else
            {
                drawLine = true;
                preTouchPosition = currentTouchPosition;
            }
        }
        else
        {
            drawLine = false;//書くのをやめたときにフラグを下す
        }
    }
}
```
</div></details>

###詰まった点
途中困った点とか謎のバグとかあったのでいくつか触れておきます。

**・GetExpKeyDownが上手く動かない**
uWintabのTablet.csのGetExpKeyDownメソッドが、ExpKeyを押し始めたタイミングのみTrueになってほしいのに、押している間常にTrueとなってしまいました。WintabAPIとの連携のところでの処理回数の違いとかが原因なのかなと素人ながらに想像していますが詳細不明...。

**・タブレットの指操作を取得したい**
ペンタブレットはペン入力だけでなく、設定によってはトラックパッドと同様に指での操作も可能です。ペンで描きながら指でキャンバスを移動させたいなーと思い指入力の有効化の方法を調べると、[Wacom「Intuos Pro」タッチパネルで出来る事とオンオフの方法](http://efude.info/wacom%E3%80%8Cintuos-pro%E3%80%8D%E3%82%BF%E3%83%83%E3%83%81%E3%83%91%E3%83%8D%E3%83%AB%E3%81%A7%E5%87%BA%E6%9D%A5%E3%82%8B%E4%BA%8B%E3%81%A8%E3%82%AA%E3%83%B3%E3%82%AA%E3%83%95%E3%81%AE%E6%96%B9/)というサイトが。しかし設定を開いても**「タッチ入力を有効化する」というトグルが見当たりません**でした。その後いろいろ調べると、このタブレットは**本体のハード側にタッチ入力の切り替えボタンが付いていて解決**。
タッチ入力をunityで取得しようとすると、どうやら通常のトラックパッドと同様の処理になっているらしく、Input.mousePosition()を使うしかなさそう。マウスとは別に処理したいのですが、その方法は未発見です。

#感想
実際にやってみると、ディスプレイを見ながらペンタブを使うのに比べ、相当正確に書けるというのが分かった。**液タブみたいな使用感という当初の目標はバッチリ達成**。

今回じつは、VR空間と現実のペンタブの位置はトラッキングしているわけではなく、シーン上で位置を合わせているだけでした。それでも、手元、特に**ペン先が見えてると現実で描くのとほぼ同じ感覚で細かい字まで思い通りに書けました**。**視覚をダマす**のはやっぱり効きますね。もっといろんなブラシとか搭載したり、ペイントソフトと連携したりして本格的に描けるようにしたら絶対楽しい。

イラスト方面なら、参考資料の絵とか3Dオブジェクトとか前方に浮かべて見ながら描いたり、VR書斎つくってHMD被るだけで散らかった部屋でも一瞬で創作環境に入れたり...
他にも、文字とかも普通に書けるので色々整えれば**VRでの会議**とかにバリ使えそう。文字認識とかと組み合わせてもよさげ。
などなど、色々妄想が膨らんで楽しかったです。

#余談
冒頭で、液晶タブレット高いという話を出しましたが、VR機材もそれなりに高い。どうなんだろうなと思い値段比較してみました。
さっと調べただけなので参考程度に...(2019.12現在)

**液タブ**
安：[XP-PEN Artist12](https://www.storexppen.jp/buy/artist12.html?channel=officialwebsite)  ￥21,998
中：[Wacom Cintiq 16 (DTK1660K0D)](https://store.wacom.jp/products/detail.php?product_id=4197) ￥74,580
高：[Wacom Cintiq Pro 32 (DTH-3220/K0)](https://store.wacom.jp/products/detail.php?product_id=4162)  ￥404,800
**VRヘッドセット**
安：[Oculus Go 32GB](https://www.oculus.com/go/?locale=ja_JP) ￥17,500
中：[Oculus Quest 64GB](https://www.oculus.com/quest/?locale=ja_JP) ￥49,800
高：[VIVE Pro Full Kit](https://www.vive.com/jp/product/vive-pro-full-kit/) ￥147,880

やっぱりピンキリで求める性能によって全然違ってきそうですが、VR機材の方が高そうなイメージだったのにそうでもなかった...。