[WebGL]スマホでもナビボール(姿勢指示器)が見たい!

### 訂正 : 20201130 計算が根本的に間違っていたので修正
カメラのrotationプロパティでカメラの方向を変更していましたが、
そもそもこの方法では使えず、Quatanionによる回転が必要だったので修正しました。

# 作ったもの
スマホでアクセスしてください。
iPoheSE(初代) iOS14、safariにて確認。(Androidは手元にないので未確認です)
#### https://tokishirazu.llc/navball/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/5a81b502-24ee-8b23-fd7f-4073601fd74b.png)

スクショ
<img width="160px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/c4b8aeb3-6a7e-f8fc-a23c-aa58c389b199.png" />

# ナビボールへのお誘い
全国1万人くらいのKSP及びメーデーファンのみなさんこんにちは! (まさかすでにタグがあるなんて!)
みなさんは長い人生を歩んでいく中で、道に迷ったり、つまづいて方向がわからなくなることありますよね?
そんな時、このナビボール(姿勢指示器)があったら素敵だと思いませんか?
探してみるとアプリでもHTMLでもなかったので適当に作ってみました。

# 真面目な解説
ナビボールまたは姿勢指示器とは、航空機やロケットの計器の一つ。
暗闇や宇宙空間などで自分が今どの方向を向いているのかがわかります。
方位、仰角・俯角、傾きをセンサーから読み取りHTMLで表示しています。

# 技術的なこと
ソースはこちら https://github.com/alivelime/tokishirazu/tree/master/navball
センサーの取得とWebGLでの描画のセットになっています。

#### 座標系とカメラの傾きについて
すみません! 3次元の数学の話はよくわからないので、こちらのWeb仕様書の日本語訳のページをご覧ください。
一つ言えるのは、センサの座標系の傾きが、そのままカメラの回転座標として使えるわけではないといことです。
それをうまくやるためにQuatanionというものを使います。
あとはセンサの±に気をつけつつ、合わせてやる感じになります。
https://triple-underscore.github.io/deviceorientation-ja.html


#### デバイスの傾き情報の取得

iOs13以降ではボタンを押した後にデバイスへのアクセス許可を取らないといけません。
Androidでは許可はいらないはずですが、確認できていません。
センサ情報取得にはhttpsが必須です。

```javascript
window.onload = () => {
  // この辺りはこちらを参考に https://qiita.com/umi_kappa/items/38499c03792b2aac49ad
  document.getElementById("start").addEventListener('click', () => {
    document.getElementById("start").style.display = "none"
    let camera = render()

    // これはダメ
    // camera.rotation.x = THREE.MathUtils.degToRad(0) // 仰角・俯角
    // camera.rotation.y = THREE.MathUtils.degToRad(90) // 方位
    // camera.rotation.z = THREE.MathUtils.degToRad(0)  // ねじれ

    // 現在の球に対しては、仰角・傾き(+180)・方位
    var qa = getQuaternion(-30, 180, 90);
    var q = new THREE.Quaternion(qa[0], qa[1], qa[2], qa[3]);
    camera.quaternion.copy(q);

    if (typeof DeviceOrientationEvent !== "undefined") {
      if (typeof DeviceOrientationEvent.requestPermission === 'function') {
        // iOS 13+ の Safari
        // 許可を取得
        DeviceOrientationEvent.requestPermission()
          .then(permissionState => {
            if (permissionState === 'granted') {
              setDeviceOrientationEvent(camera)
            } else {
              // 許可を得られなかった場合の処理
              alert("センサへのアクセスが拒否されました。もう一度やり直してください。")
            }
          })
          .catch(err => alert("センサ情報が取得できませんでした。" + err.toString())) // https通信でない場合などで許可を取得できなかった場合
      } else {
        // 上記以外のブラウザ
        setDeviceOrientationEvent(camera)
      }
    } else {
      alert("このデバイスには地磁気センサがありません")
    }
  }, false)
};
```

#### WebGLと傾きのマッピング

以前記事に書いた360°WebRTCを元に、天球にナビボールの画像を貼り付けています。
また、デバイスから取得した傾きをカメラに反映しています。


```javascript
function render() {
  // この辺りはこちらを参考 https://ics.media/entry/14490/
  let width = window.innerWidth
  let height = window.innerHeight
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(75, width / height, 1, 1000);
  camera.position.set(0, 0, 0);
  scene.add(camera);

  var geometry = new THREE.SphereGeometry(5, 60, 40);
  geometry.scale(-1, 1, 1);
	//　マテリアルの作成
	// 画像はこちらのをお借りしました
	// https://forum.kerbalspaceprogram.com/index.php?/topic/164158-13-navballtexturechanger-v16-8717/
  let texture = new THREE.TextureLoader().load("/img/navball.png")
	let material = new THREE.MeshBasicMaterial({
		map: texture
	});
	sphere = new THREE.Mesh(geometry, material);
	scene.add(sphere);

	// レンダラー
	renderer = new THREE.WebGLRenderer({antialias: true});
	renderer.setSize(width, height);
	renderer.setClearColor({color: 0xffccff});
	element = renderer.domElement;
	document.getElementById("navball").appendChild(element);
  function r() {
    requestAnimationFrame(r)
    renderer.render(scene, camera)
  }
  requestAnimationFrame(r)
  return camera
}

function setDeviceOrientationEvent(camera) {
  // 許可を得られた場合、deviceorientationをイベントリスナーに追加
  window.addEventListener('deviceorientation', e => {
    e.preventDefault()

    // これは使えない
    // camera.rotation.x = THREE.MathUtils.degToRad(- e.beta) // 仰角・俯角
    // camera.rotation.y = THREE.MathUtils.degToRad(e.alpha + 90) // 方位
    // camera.rotation.z = THREE.MathUtils.degToRad(- e.gamma)  // ねじれ

    // 自分の座標系に対する回転後の座標を適用させる
    var qa = getQuaternion( - e.beta, e.gamma + 180, - e.alpha);
    var q = new THREE.Quaternion(qa[0], qa[1], qa[2], qa[3]);
    camera.quaternion.copy(q);

  })
}

function getQuaternion( alpha, beta, gamma ) {

  let  degtorad = Math.PI / 180;
  var _x = beta  ? beta  * degtorad : 0; // β 値
  var _y = gamma ? gamma * degtorad : 0; // γ 値
  var _z = alpha ? alpha * degtorad : 0; // α 値

  var cX = Math.cos( _x/2 );
  var cY = Math.cos( _y/2 );
  var cZ = Math.cos( _z/2 );
  var sX = Math.sin( _x/2 );
  var sY = Math.sin( _y/2 );
  var sZ = Math.sin( _z/2 );

  /*  ZXY 四元数の構築  */

  var w = cX * cY * cZ - sX * sY * sZ;
  var x = sX * cY * cZ - cX * sY * sZ;
  var y = cX * sY * cZ + sX * cY * sZ;
  var z = cX * cY * sZ + sX * sY * cZ;

  return [ w, x, y, z ];
}
```


#### ご注意

- おそらくxyz軸が間違っています
- スマホを横にすると画面が回転しますが未対応です。
- カメラの制御には three.js のDeviceOrientationControlsが使えるはずですが、ボタンのクリックイベントと紐付けるのが面倒なので使っていません。
- 西側用です。万一バズったら東側用も作るかもしれません。(捻るむきが逆になるだけかと)