OculusGoからRemoteでPCを操作したい#2(PC画面操作)

これは[前回](https://qiita.com/selfPolygon/items/47ab59ebd7be289861d3)の続きです。

#Step3    レーザーポインタの位置をPCへ送信する
こちらは非常に簡単です。画像データ送信の逆のことをすればいい＋データ量は多くない＋オレオレデータで良いからです。
前回も紹介しましたが、[この方の投稿](https://qiita.com/noitak/items/f442a45c2d772d9d8269)を参考にし、レーザーポインタを表示して取得した値を送信できるようにしました。

```c#:Pointer.cs
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using UnityEngine;

public class Pointer : MonoBehaviour {

    [SerializeField]
    private Transform _RightHandAnchor;
    [SerializeField]
    private Transform _LeftHandAnchor;
    [SerializeField]
    private Transform _CenterEyeAnchor;
    [SerializeField]
    private float _MaxDistance = 100.0f;
    [SerializeField]
    private LineRenderer _LaserPointerRenderer;

    [SerializeField]
    Transform p;

    Vector3 pos;//レーザーポインタで指した位置
    Gradient def;//グラデーション初期値(レーザーポインタの色を変えたとき用)
    bool isp = true;//レーザーポインタの色を透明にするかどうか

    private Transform PointerRay {
        get {
            // 現在アクティブなコントローラーを取得
            var controller = OVRInput.GetActiveController();
            if (controller == OVRInput.Controller.RTrackedRemote) {
                return _RightHandAnchor;
            } else if (controller == OVRInput.Controller.LTrackedRemote) {
                return _LeftHandAnchor;
            }
            // どちらも取れなければ目の間からビームが出る
            return _CenterEyeAnchor;
        }
    }

    static UdpClient udp;
    int PORT = 5998;//操作用ポート

    void Start () {
        udp = new UdpClient("192.168.1.XXX", PORT);//PCのIPアドレス
        def = _LaserPointerRenderer.colorGradient;//レーザーポインタのグラデーション
    }

    // Update is called once per frame
    void Update () {
        var pointer = PointerRay;
        if (pointer == null || _LaserPointerRenderer == null) {
            return;
        }
        // コントローラー位置からRayを飛ばす
        Ray pointerRay = new Ray(pointer.position, pointer.forward);

        // レーザーの起点
        _LaserPointerRenderer.SetPosition(0, pointerRay.origin);

        RaycastHit hitInfo;
        if (Physics.Raycast(pointerRay, out hitInfo, _MaxDistance)) {
            // Rayがヒットしたらそこまで
            _LaserPointerRenderer.SetPosition(1, hitInfo.point);
            pos = hitInfo.point;
        } else {
            // Rayがヒットしなかったら向いている方向にMaxDistance伸ばす
            _LaserPointerRenderer.SetPosition(1, pointerRay.origin + pointerRay.direction * _MaxDistance);
            pos = Vector3.negativeInfinity;
        }

        //レーザーポインタの色を透明にする
        if (OVRInput.GetDown(OVRInput.Button.PrimaryTouchpad)) {
            if (isp) {
                // カラーキーの配列
                var colorKeys = new[]{
                    new GradientColorKey( Color.black, 0 ),
                    new GradientColorKey( Color.black, 1 ),
                };
                // アルファキーの配列
                var alphaKeys = new[]{
                    new GradientAlphaKey( 0, 0 ),
                    new GradientAlphaKey( 0, 1 ),
                };
                var gradient = new Gradient();
                gradient.SetKeys(colorKeys, alphaKeys);
                _LaserPointerRenderer.colorGradient = gradient;
                isp = false;
            } else {
                _LaserPointerRenderer.colorGradient = def;
                isp = true;
            }
        }

        //クリック操作
        if (OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger)) {
            p.position = pos;//取得したワールド座標をスクリーンの子に入れる
            pos = p.localPosition;//ローカル座標変換を行い、画面のどの位置にあっても同じ値が取れるようにする

            //x軸
            pos.x -= 5f;//中心が(0,0)なので、左上が(0.0)となるように調整している
            pos.x *= -9.6f;//画面サイズと相談して適当に調整

            //y軸
            pos.z += 5f;
            pos.z *= 5.4f;

            //"C x座標 y座標 z座標"となるように送信(今回y座標は0固定なので正直送信しなくてもいい)
            byte[] bmsg = System.Text.Encoding.ASCII.GetBytes(
            "C " + pos.x.ToString("f2") + " " + pos.y.ToString("f2") + " " + pos.z.ToString("f2"));
            udp.Send(bmsg, bmsg.Length);
        }
    }

    void OnApplicationQuit() {
        udp.Close();
    }
}

```

```Transform p;```はスクリーンの子で、スクリーンがどの位置に存在しても同じ値を返すようにする為に存在します。
```byte[] bmsg = System.Text.Encoding.ASCII.GetBytes...```は今後キーボードを送ることを考えて「コマンド X座標 Y座標 Z座標」を送信としました。(CはクリックのCです(なんでもいい))
因みに、Rayが画面外の場合```pos = Vector3.negativeInfinity;```として有りえない値を入れています。

#Step4    PC側でコマンドを受け取って処理する
昔はPowerShellとかでサーバー立ててた気がしたんですけど、今回はPythonで行います。(画像データ送信もPythonだったので)

操作系のPythonライブラリは[PyAutoGui](https://pyautogui.readthedocs.io/en/latest/#)を使用しました。
pip等からインストールしてください。

```python:ope.py
import pyautogui
import socket

UDP_IP = ""
UDP_PORT = 5998    #操作用ポート

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

while True:
    data, addr = sock.recvfrom(1024)
    print ("rcvd:", data)
    cmd = data.decode(encoding = "ascii").split()#spllitの引数指定しないとスペースで区切られる
    
    if cmd[0] == "C":
        x_val = -10000.0
        y_val = -10000.0
        if cmd[1] != "-Infinity" and cmd[1] != "NaN":
            x_val = float(cmd[1])
        if cmd[3] != "-Infinity" and cmd[3] != "NaN":
            y_val = float(cmd[3])#yとか言ってるが、実際にはz座標を使用してる
        
        if x_val != -10000.0 and y_val != -10000.0:#画面外でなければ処理を行う
            pyautogui.click(x_val*20, y_val*20)#実際の画面と比率が1/20なので20倍してやる
```

これsock.recvfrom中はCtrl+Cが効かないですね。PID表示させてタスクマネージャー(またはCMD)から終了させるか、OculusGo側からクリック操作してやってsock.recvfromを抜ける必要があります。

と、これでOculusGo側からトリガーを引けばクリックができるようになりました。

#Step5    スクリーンの位置を移動する
このままでは座ってしかゲームができません。そこで寝転んでできるようにUnity側のスクリプトを追加します。
OculusGoのコントローラースクリプトは[この方の記事](http://bibinbaleo.hatenablog.com/entry/2018/05/14/171744)を参考にしました。

```c#:SCPos.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SCPos : MonoBehaviour {

    [SerializeField]
    private Camera cam;
	
	// Update is called once per frame
	void Update () {

        if (OVRInput.GetDown(OVRInput.Button.Back)) {//思いやりのある人は真似しないように
            Vector3 v = cam.transform.localPosition + cam.transform.forward * 60;
            transform.position = v;//カメラの正面方向にスクリーンがくるようにする

            Vector3 v3 = cam.transform.localRotation.eulerAngles;
            v3.x -= 90f;//元々x軸を-90度にしていた為、一度分解して-90度を加える
            var look = Quaternion.Euler(v3);
            transform.localRotation = look;
        }

        // タッチパッドの上か下で画面を近づけたり離したりできるようにする
        Vector2 pt = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad); ///タッチパッドの位置
        if (pt.y < -0.5 && -0.5 < pt.x && pt.x < 0.5)///下側？
        {
            transform.position += transform.up * Time.deltaTime;
        }
        if (pt.y > 0.5 && -0.5 < pt.x && pt.x < 0.5)///上側？
        {
            transform.position -= transform.up * Time.deltaTime;
        }
    }
}
```

OVRInput.GetDown(OVRInput.Button.Back)は戻るボタンです。普通ならOculusマークのボタン長押しリセットをトリガにしてください。ただし、私はアプリ開始時にも発火してしまうとどこかで目にしてしまったので戻るボタンにしました。(デバックがしづらくなる)
また、途中-90度に傾けているのは、予め-90度に設定してあったスクリーンの回転もリセットされるため、線しか見えなくなるからです。

実はこのスクリプトにはバグが有って(n回目)　正面を0度としたとき、仰向けで寝転がって90度を超えるとスクリーンが消失します。なので80度くらいの位置で画面調整すると良いです。

#所感
1.実用的かと言われれば全くそんなことないですが、いつかバグ修正と機能追加をするときに忘れてそうなことを書きました。
2.安定するまで(安定しても時折)ノイズが結構乗ってます。受信側でもう少し工夫が必要です。
3.画像データ送信側も横着せずにエラー処理を行うべき。(というかPC側のサーバーまとめなさい)
4.ゲームはフルスクリーンにするとなんとか読める感じです。
5.画質が流石にしょっぱいのでここも改善の必要ありです(単に1/2リサイズをやめるとチカチカして目が疲れる)
**6.映画館でエロゲしてるみたい(興奮)**

#おわり
![output.gif](https://qiita-image-store.s3.amazonaws.com/0/281660/74ebe294-5eea-b2ce-98cb-e04dbccbe673.gif)