uGUIの上で本をめくる

ゲーム内のUIで本をめくりたくないですか?
僕はめくりたかったんです.

f:id:FriendSea:20181228202146g:plain

github.com

本みたいなUI

製作中のゲーム(ゲンソウファインダー)では手帳をイメージしたUIが出てきます.
なので設定画面などの遷移ではページを捲りたかったのですが,面倒だったので今までは現在の画面が退出して別の手帳がスライドインしてくるという謎仕様でした. それがやっと本来やりたかった見た目になったんです.

f:id:FriendSea:20181229031627p:plain

(いつまで2018秋のつもりなんだ?)

使い方

BookUI

まず見開きサイズとなるRectTransformを配置して,その子オブジェクトとして必要な見開きの数だけUIを横につなげて配置します.
親の方にBookUIアサインして実行すればUIがよしなに変形されて本の形に整うはずです.開いてるページはBookUI.CurrentPageで操作できる.

f:id:FriendSea:20181229034015p:plain

UI要素を横方向に並べるときはHorizontalLayoutGroupが便利.

PageSelector

BookUIと同じオブジェクトにアサインすれば動きます.本の中のUI要素が選択されたとき,その要素のあるページを開くようにBookUI.CurrentPageを更新するやつです.

BeseMeshEffectと頂点シェーダ

見ての通り本の形への整形では頂点を動かす必要があります.uGUIの場合BaseMeshEffectを利用することで描画時にメッシュを編集することができます.
ただ,当然ですが渡されるメッシュはローカルな座標空間でのものです.しかし今回は本を基準とした本座標系(今名前つけた)で変形をを行いたい.これをBaseMeshEffectで行おうと思うとUI要素毎にそれぞれローカル座標系と本座標系の変換行列を用意する必要があります.
一方で頂点シェーダに渡される頂点はCanvas内での位置であり,どのオブジェクトでも共通した座標系となっています.これは多分バッチ処理で複数のメッシュをまとめて描画するために全体で共通した座標系を使っているのでしょう.今回もこれが好都合で,ローカル座標系(Canvas座標系にまとまる)と本座標系の変換行列が全体で共通の1つで済みます.

f:id:FriendSea:20181229044058p:plain

というわけで結局シェーダを使って変形してます.

テッセレーション

メッシュは頂点単位で折り曲げられています.だから折り目近くに頂点がない場合グシャってなっちゃう.
というか変なとこで貫通して来たりします.

  f:id:FriendSea:20181229052247p:plain

だったらテッセレーションみたいなことをして頂点を増やせばいいよね.これはローカル座標系で処理でる上に,全体に適用する必要もないのでBaseMeshEffectを使って実装しました.
ページの背景みたいに大きな要素や折り目部分にまたがっている要素にアサインしてます.

問題点

張り切って作ったんですが結構扱いづらい点が多いです.

マウス/タッチで操作できない

UIのポインター系のイベントはraycastで対象のオブジェクトを取ってます.頂点シェーダで変形してしまうと,Raycast targetには影響がないので正しく選択できません.
固定されたUIでページを捲って本の中では絵を見せるだけなら平気ですが,本の中のUIを操作したければキーボードやコントローラでカーソルを移動することになります.

Maskを使えない

uGUIのMaskの下に本を置くと何故かカスタムシェーダのいくつかのパラメータが初期値に固定されます.なんで?(不服)
具体的には変換行列がIdentityに固定されてしまうのでまともに動いてくれません.
シリアライズされないパラメータが初期化されちゃうのかな?」と考えてMatrix4x4が2つの代わりにVectorを8つ与えると正しく変換できました.アホくさ.
でも今度は現在のページを指定するパラメータが固定された.なんで?
結局Maskは諦めて普通に行列与えてます.

負のスケールを使えない

UI要素のXかYのスケールを負にすることで画像などを反転させて利用できます.でも今回のシェーダはCullBackしてるので無理です.
負のスケールにすると裏側のページに現れちゃいます.

おしまい

本ができたよ.ドンドン使っていきたいね.
正直Maskが使えないのはかなり痛いですが運用でカバーしような.