iOSで360度パノラマ画像を見れるライブラリ作ってみました。

# 追記
Swift5+SceneKitに変換する記事を書きました。
Qiitaではなく外記事になります。 🙏
[360度パノラマ画像を閲覧するpod更新でGLKitからSceneKitに変えた話](https://blog.mothule.com/ios/scene-kit/ios-scene-kit-abc)


# 概要
THETAみたいなカメラを２つ使って撮影した360度の画像を数学のできない自分でもそれっぽいものができたので、
ライブラリにしました。また簡単な実装方法を踏まえて紹介します。
THETA:https://theta360.com/ja/

### GIF アニメ
![RNSphereImageViewer_Animation3.gif](https://qiita-image-store.s3.amazonaws.com/0/37274/9a6271ac-56ba-b821-d258-799e8d890504.gif)

### GIFだとカクカクしているので、動画もアップしました。
YouTube：https://www.youtube.com/watch?v=crqeokiwsfc

### GitHub
https://github.com/mothule/RNSphereImageViewer


# 使い方

~~~swift
let vc = RNSphereImageViewController()
vc.configuration = RNSIConfiguration() // Required

// リソースのパス設定. UIImageからでもいけます。
let resourceName = "<input image file name.>"
let resourceType = "<input image file type.>"
let path = NSBundle.mainBundle().pathForResource(resourceName, ofType: resourceType)
vc.configuration.filePath = path

vc.configuration.fps = 60 // FPS60. defaultは30
self.navigationController?.pushViewController(vc, animated: true)
~~~

# 作った理由
会社の後輩がTHETAを持っていて、専用ビューワで見たら面白そうだし、なんとなく頭のなかでイメージを描けたので、それが実装できるか試したかったため。
そのため画像は後輩のを借りてます。

## 注意
- レンズが1個しかないタイプの画像は対応していません。
- 画像はメルカトル図法みたいな画像のみ対応しています。（下と上が広がって、左と右がくっついている画像）
- 画像がsRGBだと、GLKTextureLoaderのOptionに GLKTextureLoaderSRGB を追加しないと、恐らく正常な色で読み込んでくれません.

# 実装方法
この実装方法は、数学の弱い自分が思いついた方法なので、これが正規のやり方とは限らず、また理論に間違いがある可能性あるので注意してください。

- 3D空間に球体を作成(自作)
- 描画位置を球体の中に設定(GLKit)
- 球体の内側にUV値を設定(自作)
- テクスチャを貼る(GLKit)
- 画面移動で視点の移動(自作)

自作と書いてある部分は自分でプログラムしたもので、GLKitと書いてある部分は、何か特別な処理はせずGLKitやOpenGLのコマンドを呼んでるだけです。

球体のモデル作成にはsinとcosしか使っていません。
視点の移動には一部atanを使ってます。
カメラも特に必要なかったので、実装していません。

コードは、Xcodeの新規プロジェクトでGameテンプレートでOpenGL ESを選んだら生成されるプロジェクトから不要なコードを削除しながら、必要な処理を追加していきました。

## 球体の作成
XZを水平,Yを垂直とした3軸座標系があったとしたら、XZ平面の円（厳密には円柱）をY軸に向かって作っていっています。
Y-1からY+1に向かって円柱を作成しています。
### なぜ円ではなく円柱なのか

円だと、線にしかならず、ポリゴン（三角形）にならないので、円柱をY-1からY+1に作り続けることで下の円柱と上の円柱をくっつけています
（厳密にはくっついているように見えている）
この時円柱を何個積み重ねるかが球体の分割数になります(Stack)

下図は円柱の一部分を拡大したものだと捉えてください。
このようにポリゴンをY軸を回転させながら作ると360度回転したときに円柱ができあがります。
この時この四角形の縦の線の数が球体の分割数になります。(Slice)
ちなみに球体の分割数が３であれば、四角形は３枚です。上から見ると三角形です。
![RNSphereImageViewer_figure1.png](https://qiita-image-store.s3.amazonaws.com/0/37274/8e2421f8-c963-e6f6-c3b6-c82dc9ccd978.png)


### 円柱が出来上がれば、あとは簡単
XZ平面の円柱の大きさ（半径）をY軸の位置に応じて０〜１に変更させます。
つまりYが-1とYが+1の時は半径は0(最小)、Yが0のときは半径が1(最大）になるようにすれば、球体ができあがります。（半径だけで言えば、分度器を縦にしたときの円周と同じですね。
下図で言うところの、X軸と赤線の交点が半径になります。この赤線の数が球体生成時の分割数(Slice)になります。

![RNSphereImageViewer_figure.png](https://qiita-image-store.s3.amazonaws.com/0/37274/6520e161-e74b-4e3f-d259-8077b57ba18e.png)

# まとめ
以上になります。今回GLKitやOpenGL ESを使うのは初めてで、まともに使い方を覚えていませんが、やりたいことは実現できました。


## 懸念
少し懸念としては、描画方法が GL_TRIANGLES なのと、 IndexBuffer を使っていないので、メモリ効率、描画効率が共に悪いところになります。

## 別策
また別のアプローチとしては、今回は球体を数式を使って一から作り上げましたが、モデリングソフトを使って、UV値が貼られた球体を外部ファイルとして持っておき、球体構築時にはそれを使うでも十分だと思います。

## やはりFloatでも誤差は大して起きない
球体を作ってる途中うまく表示されないバグがあって、原因は計算ミスなのですが、その時は分からず、バグの可能性を潰すために、精度がDoubleではなくFloatによる誤差を疑い、誤差の起きにくいよう最後にだけFloatにしてそれ以外はDoubleで計算して球体を作りましたが、変化ありませんでした。
このことから、Floatでもこの程度の品質であれば十分耐えられることが分かりました。

## やはりC/C++といったポインタ操作ができないと違和感がある
描画類は、バッファに対して、先頭アドレスから指定バイト数ズラすなど、メモリを直接いじることが多い中、Swiftでのポインタ操作に、違和感を感じました。

# 雑談・小言
ちなみに球体の中に入るのは、ゲームなどでは、空の表現として使われることもあります（スカイドーム）
当然それだけだとバレバレなので、スカイドームを何層にも重ねたり、上の方に雲と称したUVアニメが入った板ポリを重ねて(少しずらして)配置したりして、空や雲に厚みを出して空を表現します。PS4やXboxなどリアルにはなっていますが、じっくり見ると、実はやってることは一つ一つなシンプルなものの組み合わせで実現されていることが多々あります。これが海外のFPSやCryEngineなどになると、じっくり見ても分かりづらいもの（ポリゴンが見つけづらいもの）だらけになります。