凹みTips

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

Unity で Windows のマルチタッチ操作をエミュレートできる uTouchInjection を作った

はじめに

VR 向けのバーチャルデスクトップを作っているとハンドコントローラを使ったポインタによる UI 等で指した場所の操作をしたくなります。こういった場合は kernel32.dll 経由でマウス操作をエミュレーションすることが多いと思いますが、Windows ではタッチスクリーンを使ったマルチタッチを備えているため、これを使いたくなる時があるかもしれません。例えばスクロール操作をクリックして引っ張れば出来たり、複数のハンドコントローラでピンチ操作を行ったりといったことはマウス x スクロール操作よりも便利な場合も多いです。Windows ではこのタッチ操作をエミュレーションできる仕組みである Touch Injection というものが用意されています。

この Touch Injection を Unity 向けに簡単に使えるようにラップした uTouchInjectionというアセットを作成しました。今回はこの解説を行います。作ったのは大分前なのですが、使い方を書いていなかったので書くことにしました。

デモ

ダウンロード

github.com

Releases から最新のものをダウンロードしてください(執筆時点では v0.0.2)。

使い方

まずは簡単な使い方から見てみましょう。

uTI_MovePointerSample.cs
using UnityEngine;

public class uTI_[f:id:hecomi:20180721170922p:plain]MovePointerSample : MonoBehaviour
{
    uTouchInjection.Pointer pointer0;
    uTouchInjection.Pointer pointer1;

    [SerializeField] int areaSize = 5;
    [SerializeField] float t0 = 0f;
    [SerializeField] float t1 = 2f;
    [SerializeField] float t2 = 4f;
    [SerializeField] float t3 = 6f;
    [SerializeField] float t4 = 8f;
    [SerializeField] Vector2 start0 = new Vector2(400, 300);
    [SerializeField] Vector2 end0 = new Vector2(400, 800);
    [SerializeField] Vector2 start1 = new Vector2(500, 300);
    [SerializeField] Vector2 end1 = new Vector2(500, 800);
    float t = 0f;

    void Start()
    {
        pointer0 = uTouchInjection.Manager.GetPointer(0);
        pointer1 = uTouchInjection.Manager.GetPointer(1);
        pointer0.areaSize = areaSize;
        pointer1.areaSize = areaSize;
    }

    void Update()
    {
        if (t < t0) 
        {
            pointer0.Release(start0);
            pointer1.Release(start1);
        }
        else if (t < t1) 
        {
            pointer0.Hover(start0);
            pointer1.Hover(start1);
        }
        else if (t < t2) 
        {
            var a = (t - t1) / (t3 - t2);
            pointer0.Touch(start0 + (end0 - start0) * a);
            pointer1.Touch(start1 + (end1 - start1) * a);
        } 
        else if (t < t3) 
        {
            pointer0.Hover(end0);
            pointer1.Hover(end1);
        } 
        else if (t < t4) 
        {
            pointer0.Release(end0);
            pointer1.Release(end1);
        }
        else 
        {
#if UNITY_EDITOR
            UnityEditor.EditorApplication.ExecuteMenuItem("Edit/Play");
#else
            Application.Quit();
#endif
        }

        t += Time.deltaTime;
    }
}

これを実行すると 2 点のタッチイベントが start0end0 および start1end1 を動くように OS へ送られます。コードを 1 つずつ見ていきましょう。

まず、タッチを制御する uTouchInjection.Pointer を次のように uTouchInjection.Manager から取得します。

pointer0 = uTouchInjection.Manager.GetPointer(0);
pointer1 = uTouchInjection.Manager.GetPointer(1);

これを経由して 2 点タッチを制御することになります。PointerRelease()Hover()Touch() を備えていて、これらを毎フレーム呼び出すことでタッチイベントを呼び出すことが出来ます。

if (t < t0) 
{
    pointer0.Release(start0);
    pointer1.Release(start1);
}
else if (t < t1) 
{
    pointer0.Hover(start0);
    pointer1.Hover(start1);
}
else if (t < t2) 
{
    var a = (t - t1) / (t3 - t2);
    pointer0.Touch(start0 + (end0 - start0) * a);
    pointer1.Touch(start1 + (end1 - start1) * a);
} 
else if ...

次のように別々で与えても構いません。

...
else if (t < t1) 
{
    pointer0.Hover();
    pointer0.position = start0;
    ...
}
...

実行すると画面の左上辺りにタッチのフィードバックエフェクトが見えると思います。

VR での使い方(uDD との併用)

uDesktopDuplication(uDD)との併用でタッチ操作できる VR デスクトップアプリを作成することが出来ます。

tips.hecomi.com

uDD ではレイを飛ばして指定された座標を取ってくる API があるのでこれを利用します。

github.com

具体的には、uDesktopDuplication.Texture.RaycastAll(Vector3 from, Vector3 dir) をすると、以下のような構造体に結果が格納されて返ってくるので、この中の desktopCoord を利用してそこにタッチを送るようにします。

struct uDesktopDuplication.Texture.RayCastResult
{
    public bool hit;
    public uDesktopDuplication.Texture texture;
    public Vector3 position;
    public Vector3 normal;
    public Vector2 coords;
    public Vector2 desktopCoord;
}

以下、サンプルスクリプトが雑なのと長くてアレなのですが...、サンプルに付属しているソースコードになります。Oculus Touch でトリガに触れるとホバーを、押し込むとタッチを送るという形になっています。操作しやすいように desktopCoord に少しスムージングを掛けています。

uDD_TouchDispatcher.cs
using UnityEngine;
using uTouchInjection;

public class uDD_TouchDispatcher : MonoBehaviour
{
    private static int currentId = 0;

    Pointer pointer_;
    bool isFirstTouch_ = true;

    [SerializeField] OVRInput.RawButton touchInputTrigger = OVRInput.RawButton.LIndexTrigger;
    [SerializeField] OVRInput.RawTouch hoverInputTrigger = OVRInput.RawTouch.LIndexTrigger;
    [SerializeField, Range(0f, 1f)] float filter = 0.8f;
    [SerializeField] float maxRayDistance = 9999f;

    public uDesktopDuplication.Texture.RayCastResult result 
    { 
        get; 
        private set; 
    }

    public bool isPrimaryPointer
    {
        get { return pointer_ != null && pointer_.id == 0; }
    }

    public enum State
    {
        Release,
        Hover,
        Touch,
    }

    public State state 
    { 
        get; 
        set; 
    }

    public Vector2 filteredDesktopCoord 
    { 
        get; 
        private set; 
    }

    void GetPointer()
    {
        if (pointer_ != null) return;

        pointer_ = uTouchInjection.Manager.GetPointer(currentId);
        currentId++;
    }

    void ReleasePointer()
    {
        if (pointer_ == null) return;

        pointer_.Release();
        pointer_ = null;
        currentId--;
    }

    void Start()
    {
        state = State.Release;
    }

    void Update()
    {
        UpdateTouch();
        UpdateState();
    }

    void UpdateTouch()
    {
        result = uDesktopDuplication.Texture.RayCastAll(transform.position, transform.forward * maxRayDistance);

        if (pointer_ == null) return;

        if (result.hit) 
        {
            if (isFirstTouch_) 
            {
                filteredDesktopCoord = result.desktopCoord;
                isFirstTouch_ = false;
            } 
            else 
            {
                filteredDesktopCoord += (result.desktopCoord - filteredDesktopCoord) * (Time.deltaTime * 60) * (1f - filter);
            }
        }

        pointer_.position = filteredDesktopCoord;
    }

    void UpdateState()
    {
        if (!result.hit) 
        {
            StartRelease();
            return;
        }

        switch (state) 
        {
            case State.Release:
                if (OVRInput.Get(hoverInputTrigger)) 
                {
                    StartHover();
                }
                break;
            case State.Hover:
                Hover();
                if (OVRInput.Get(touchInputTrigger)) 
                {
                    StartTouch();
                } 
                else if (!OVRInput.Get(hoverInputTrigger)) 
                {
                    StartRelease();
                }
                break;
            case State.Touch:
                Touch();
                if (!OVRInput.Get(touchInputTrigger)) 
                {
                    StartHover();
                }
                break;
        }
    }

    void StartRelease()
    {
        ReleasePointer();
        state = State.Release;
    }

    void StartHover()
    {
        GetPointer();
        state = State.Hover;
    }

    void StartTouch()
    {
        isFirstTouch_ = true;
        state = State.Touch;
    }

    void Hover()
    {
        GetPointer();
        pointer_.Hover();
    }

    void Touch()
    {
        GetPointer();
        pointer_.Touch();
    }
}

また付属の uDD_PointerDrawer をアタッチすると、動画のように指している場所が描画されるようになります。

その他

タッチ点を増やしたい場合

uTouchInjection.Manager にアクセスしたときに自動的にこのコンポーネントを付与したオブジェクトが生成されるのですが、予めシーンに配置しておくことも出来ます。この touchNum の数値を増やせばタッチ点を増やすことが出来ます。

f:id:hecomi:20180721170144p:plain

その他のメンバ

uTouchInjection.Pointer には areaSize というプロパティがあり、これは POINTER_TOUCH_INFOrcContact に与える大きさになります。

position に値を代入した際に、以下のようなコードが DLL 内で走ります。

void Pointer::SetPosition(int x, int y)
{
    contact_.pointerInfo.ptPixelLocation.x = x;
    contact_.pointerInfo.ptPixelLocation.y = y;
    contact_.rcContact.top    = y - areaSize_;
    contact_.rcContact.bottom = y + areaSize_;
    contact_.rcContact.left   = x - areaSize_;
    contact_.rcContact.right  = x + areaSize_;
}

イベントの送信タイミング

イベントの送信は uTouchInjection.ManagerLateUpdate() のタイミングでまとめて行うので、Pointer のメソッドを呼び出した瞬間に呼ばれるわけではありません。

おわりに

VR だけでなく Leap Motion との併用やネットワークを介してタッチイベントを別 PC に送るなど、工夫次第で色々な用途に使えると思いますのでぜひお試しください。