Amebientの世界描画1

phi16です。Amebient Advent Calendar 7日目、今回は延々と描画の話をする回です。

食器とかビルはまぁいいとして、あの世界にある**見えるモノ**それぞれについて、書きます。今回はシェーダ系100%のお話です。
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

( 続きもあります: [Amebientの世界描画2](https://qiita.com/phi16/items/1b51042e20f78a0b179b) )

## Render Queue

まず水がテーマの一部である点で半透明的物体は必須です。描画順が最悪になることはわかりきっていたので最初から管理は徹底していました。

- `Overlay+1`: Overlay (`OverlayGrab`)
- `Transparent`: Font
- `Transparent-519`: DropDisplay
- `Transparent-520`: InputBoard
- `Transparent-525`: Air, DropAir
- `Transparent-527`: DropRain
- `Transparent-528`: Thunder
- `Transparent-529`: FallRain (`RainGrab`)
- `Transparent-530`: Surface
- `Transparent-534`: Bubble
- `Transparent-535`: Ocean
- `Transparent-536`: Underwater (`SurfaceGrab`)
- `Transparent-540`: Skybox

最終的にはこんな形になりました (メモに書いてあった)。これはいろんな制約条件から導かれた結論です。

- depthのある物体で空を覆わないと元来のSkybox (`Transparent-500`) が上書きしてしまうので、その前に空を書き終えなければいけない
- AlphaTest (`Transparent-550`) よりも後に半透明系の描画を開始しなければいけない
- その他相互関連
  - 空 (Skybox) は全てに映るので当然最初
  - 海面 (Ocean) は水中 (Underwater) より後に描画しないと ZTest Off によって上書きされる
  - 海面 (Ocean) は泡 (Bubble) より先に描画しないと泡が消える
  - 食器の水面 (Surface) は雨粒 (FallRain, DropRain) を通過させてはいけないので ZWrite On にして先に描画
  - 食器の水面 (Surface) を見るときは原則として海 (Ocean, Underwater, Bubble) を見ていないので GrabPass を共通化して良い
  - 降雨 (FallRain) は海や水面を屈折させるので新たに GrabPass が必要
  - 雷 (Thunder) が落ちる瞬間ははっきり見せたいので降雨 (FallRain) よりも後
  - リズム用の雨 (DropRain) は雷 (Thunder) より重要なのでさらに後
  - 加算合成系 (Air, DropAir, InputBoard, DropDisplay) は最後にする
  - 視界へのエフェクト (Overlay) は全ての描画の最後

これでも解ききれなかった制約がいくつかあります。

- 海中でも食器の水面が見える
    - 水が張っているのがわかりやすいという機能的側面で解釈することにしました。
- 水中から雷が見えすぎる
    - 描画順ではどうにもならないんですが直接計算してしまえばまぁ…？
- 水の張ったバケツに入っても水中っぽくない
    - これをやるには「視点がどこかの水中」であることを調べなきゃいけないんですが厳しい。
- 水の張ったバケツに入ると雨が見えない
    - 描画順では無理、判定するのも厳しい。

まぁ、許容範囲というところです。

では各々の解説をします…。機械系 (Font, DropDisplay, InputBoard) と水面 (Surface) はそれぞれ別の日に書くことにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/d7e49281-3982-4e10-138f-5c99e8e2c960.png)
Hierarchyはこんな感じ。動的制御が多く入ってる`DropRain`と`DropAir`は`Mecha`に属していて、それ以外は`Environment`です。まぁ別に何が正解というわけではない。

えー、では… 簡単そうなものから順にいきます。

## Overlay

- 雷に合わせて暗くする・画面を揺らす
- respawn時の暗転
- ポスプロの代行

を行います。

雷が鳴るまでの時間・鳴ってからの時間を `ThunderTime` として受け取り、それに従ってGrabTextureを拾うUVをいい感じに揺らします。雷の強さというのが一応あって、定期的に鳴る雷は世界遷移の雷よりちょっと弱いです (あと海だともっと弱くなります)。`float2(cos(a), sin(a)) * exp(-r*3) * exp(-t*4) * 0.03` みたいな感じらしいです (`a`と`r`は乱数、`t`は経過時間)、移動量が`exp`付きのランダムになってるとなんだか「揺れ」って感じがするんですよね。あとは時間に関する指数関数的減衰ね。ちなみにちゃんとGrabTextureのアス比は合わせてます。あと暗さの方は単純な指数関数的減衰ですね、はい。

respawnの暗転はUdon制御のパラメータを乗算しているだけです。Amebientは**ある高さまで落ちると以後respawnが確定する**ようなマップになっている[^1]ので「段々フェードアウトする」をするのが比較的容易です。これだけならシェーダだけでも良かったんですが、フェードインをする為にはUdon制御が必要でした。

[^1]: そうしてもらった

> respawnにフェードが入ってるのは、まぁ「見える世界を連続にする為」です。急激な変化は基本的に嬉しくありません (それを期待しない限りは)。加えてAmebientだと水中だったので「段々何も見えなくなる」のはとても自然なんですよね。逆に空の世界だと段々白くなっていくとかが自然なのかしら。
> それはそうと、あのフェードアウトは「死」の印象を持っている部分もあると思います。それによって飛び込みの「仮想上の重み」みたいなのが現れてる気がするのです、水中だとゆっくりになりますし…。ちょっぴり悲しいけど穏やかな死です。Amebientの雰囲気に合っていると思ってます。

ポスプロについては… 最初は普通にPostProcessingStackV2のVignettingとChromaticAberrationを使っていました。でもどうやら**Pimaxだとこの影響が強すぎる**っぽいのですね…。特に左目右目での位置による効果の差が大きいと言えます。
それを確認して、これらについては自前実装をすることにしました。`#if defined(USING_STEREO_MATRICES)` で分岐を取って、VR上だとこれらの効果を掛けないことにしています。写真には載って欲しいんだけど視界には邪魔なこと、そこそこあるんですよね…。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Amebientのバグ修正をしました: 第一関節の無いアバターの指をコンソールが認識しない点・VRの視界で色収差が過剰なことがある点　よろしくおねがいします <a href="https://t.co/XqCmyOxmuf">pic.twitter.com/XqCmyOxmuf</a></p>&mdash; phi16 (@phi16_) <a href="https://twitter.com/phi16_/status/1288752366766837760?ref_src=twsrc%5Etfw">July 30, 2020</a></blockquote> 

公開のタイミングからはちょっと遅れてしまいましたが、対応出来てよかったとは思います。…本当にどうしようもないデバイス依存ケースではなくて良かった…。スクリーンショットをお願いして撮って頂いた[リリーヤマユリさん](https://twitter.com/lilly_vrc)に感謝です。ちなみに自前でPimaxの視界シミュレータを書いたりもした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/a08bf6fe-d760-5c48-cd62-976bed6a30f8.png)
左目と右目で分岐していい感じに表示するやつ。パラメータは正直わからんかったので眼で調節しました。これで初めてどうなってるか知ることが出来た。検証環境、だいじです。

## Air

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/78df62c0-3433-48df-bc68-acc234abdc25.png)
このふわふわ浮いてるやつです。

**空間の奥行きと恒常的変化と湿度感の強調**が主な役割で、加えて**風の強さ**を表してたりもします。世界遷移するときに急激に速くなったりしてるんですよ。

実装は…65536頂点のメッシュをいい感じにシェーダでパーティクルにしています。よく言うアレです。ただ色々細かいことはしていますね…。

- メッシュはエディタ拡張製で、boundsが予め巨大化されてる (カリング防止)
- ソフトパーティクル (壁から唐突に現れないようにいい感じにする) の実装
    - 海に対しても似たように振る舞う
    - あとカメラに近すぎてもフェードアウト
- ランダムサイズ (`rand`)・ランダム移動 (`noise`)
- 微妙に上下方向にグラデーションが入ってる (立体感が出る)
- 一定範囲で繰り返しにすることで「常に自分の周囲に表示」を行う (無限に出てくるようにする)
    - 半分は`(20,10,20)`、もう半分は`(40,20,40)`で繰り返すことで自分周辺の密度を上昇
    - ついでに遠景系のほうが浮上速度が速い

動かすのに使っている時間のパラメータは`_Time`ではなくUdon制御で、後から速度を変えたりしやすくなっています。ただ世界遷移時の急激な速度上昇はこれではなく`TransitTime`っていうパラメータを使っていて、**これは世界遷移が始まる瞬間に値が不連続変化します**。これはどういうことかというと**世界遷移が始まる瞬間、粒の位置が変わる**ということなんですが、多分気づいてる人は誰も居ない (私も気づいてない)。

> 終わる時だと気づくかもしれないな、と思ったのでデフォルト値が「遷移終了の時間」になってたりします。

パーティクルの作り方についてはそういえば[ソースを公開](https://gist.github.com/phi16/7d4c9dff08eb4adf96a926a621bcdaaa)していたので観たい人は読んでください。Airのソースから色々抜いて上下の移動方向変えただけなので。

## Bubble

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/8c878425-e269-1239-ecdf-85a636e5ed19.png)
役割は**水中におけるAir**です。海中だと空気はこういう形をとるということで、加えて少しだけ**音の説得力**をもたらしていますね (でも本来はもっと激しく振動するんだと思う)。

実装もほぼ同じですが、繰り返し単位は `(40,40,40)` と `(30,30,30)` を使っているようです。あと大きな違いは勿論見た目です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/0a56b6b0-9bc2-61fa-0748-0a053a881364.png)
reflectionが入ってます。泡ってそういうものかなと思ったので。UVからローカル法線を擬似的に計算(`(uv.x,uv.y,-sqrt(1-dot(uv,uv)))`)して、パーティクルを生成する際の`normal, tangent, binormal`を用いてワールド法線を算出しています。

まぁ、こんな近くではフェードアウトするようになってるんですけどね…。中心からの距離`d`に対して`pow(d,3)`でalpha載ってるし、ほぼリングみたいなもんになってます。多少立体感はあるかな。

## DropAir

これは楽器から出てきた空気粒のこと。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/07aada09-f886-ea49-3d77-d5c5648aaebd.png)
役割は**演奏と世界変化の因果を結ぶモノ**です。楽器を鳴らすと空気が生まれ、それが空に浮かんでいく。それによって空模様はどんどん悪くなっていく。そんな気持ち。

> 直接的に示すよりは雰囲気で持って行きたかった。気づいたらめっちゃパーティクル出てる、くらいの認識を意図しています。それまではきっとAirと混ざって区別できない。

えー、このパーティクル群は**CustomRenderTextureで位置制御**されてます。`WaterServer`というUdonが要請することで、衝突位置に新たなパーティクルが発生。各点はcurl noiseと適当な力場…最初は+Z方向、段々+Y方向に強まる力、に従って動いていきます。

急に生えてくるのは先の意図と反するので、出現後8秒程は大きさが0になっていて段々大きくなり、逆に50秒以降段々小さくなっていきます (どうせ遠いので)。

あとそういえばFanAirという名前でファンが回ってるときにもパーティクルが飛ぶんですが、まぁ同じ仕組みなので特筆することは無いかな？あっちはファンの付近だとちょっと加速するようになってますね。ちなみにMaterialは分かれてます。この子たちの元となるメッシュは1024頂点ですね。

風にいい感じに乗ってる感じにするためにそこそこの調節時間が掛かったんだけどあんまりそういうのは解説には書けないのよね。まぁ。

## FallRain

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/0fff574d-f043-2cdd-5f07-576609267ef4.png)
リズムを持たない雨です。

役割は**恒常的変化**と、**雨の強さによる空気感**ですかね…。最初は穏やか、段々強くなっていって、世界遷移の時にはめちゃくちゃ激しくなります。

- 指定された強さに応じて量が変わる
- 世界遷移の時だけ色が付く
- 床(または海)に到達すると波紋になる

床に到達したかどうかは予めBakeされた**空から見た建物の高さマップ**を使っています。これのことですよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/656d5853-2d74-fb98-ecb5-bb715352d80b.png)
青成分が距離を表しています。これによって距離が判定でき、そして赤と緑成分の表す**法線方向**[^2]によって「傾いた床」に対しても正しく波紋を出すことが出来ています。

[^2]: このモニタは`abs`した結果を表示しているものです

現在地点が床より上なら「水滴」としてメッシュを構成 (軸固定ビルボード・疑似法線)、床より下なら「波紋」として構成 (床と法線が一致するQuad) しています。

あと波紋が床の大きさをはみ出るのはなんか違うと思ったのでここでは**ソフトパーティクルの逆**をやっています[^3]。床のあるところにしか表示させない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/9095b3a3-cc06-7e0f-d32b-3ba24ac1ebaf.png)
微妙にフェードアウトしているのが見えますね。

[^3]: 厳密には`groundDepth`を使ってるのでちょっと違うけど本質は同じ

あとは… 前と同じ繰り返し方法で「自分の周り」に描画していると自分が建物の中に居る時にほぼ屋根に遮られてしまうということが起き、奥の雨がほぼ見えなくなってしまいます。そこでFallRainの半分は**建物周辺専用**、それ以外は**建物周辺以外専用**として扱われ、前者は自分の位置に追従せず、後者は自分の位置に追従しつつ建物周辺では描画されません。これによって密度がほぼ一定な感じになります。

&nbsp;
そしてシェーディングの話。原則として**そんなに主張しない**ことを願っていて、それは世界遷移のときの衝撃を持ち上げる効果にもなります。空間に溶け込んでいた物が「現れる」。

だから色味は持たなくて、やるのは弱い屈折くらい。ただそれだけだと**本当に溶けちゃう**ので、色を**1.3倍**してます。それでちょっと明るく見える。

元々「波紋」というのは**水の記号化**だと思うので、それに相応しい主張度になったのでは、と思っています。この世界では抽象がそこそこ実在を持っているんです。空気もそうね。
よく言うように水滴は現実世界では縦長ではありません。見えているのは**速度を持った水滴の抽象**です。そしてそれをそのまま作ることは、この世界では合法だと思うのです。

一応水滴の終端速度とか調べたんだけどまぁ無視していいか、ということにしたのよね。

## Thunder

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/7dbdb2c8-8d8f-5975-3054-557fef9ca8f0.png)
今見ると正直もっと頑張りたかったなと思う。

雷が落ちることになったので雷を作らなきゃいけませんでした。だから役割は**在ること**です。

これの制作の話は実は [日記 0706](https://phi16.hatenablog.com/entry/2020/07/06/034601) で少し解説をしていて、付随情報とかも色々載ってるのでまぁいいんじゃないかな…と思ったけど、少し書きます。

まず**ある点からある点へ落ちる雷**。私はよくあるフラクタル地形の作り方をベースに考えました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/56a02deb-757a-0cd7-2bd4-958c31116ded.png)
現在の線群の各中心点を、その線の移動方向と垂直な向きに適当にずらす。この時線分の長さを移動量に乗算しているので段々移動が落ち着いていき、ランダムっぽい曲線になります。多分もうちょっとこれには調節の余地があって、分岐点は中心よりも天に近いほうが良かったのかもしれない…？わからないけど。

これをいくらか繰り返してメインの雷が出来ます。そして加えていくつか、**メインの雷とその周辺のどこかの点を結ぶ雷**を作ります。同じ方法です。そしてそれが終わったらそれらのどこから…という感じ。データの格納は0番地から順番に行っているのでランダムの範囲を広げるだけで「メインの雷」「サブの雷」の選択を同時に行えます。

ちなみに内部バッファ (の可視化) がこれです。DropAirのCustomRenderTextureと共通です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/45196b1a-10bf-b4a2-de4b-d2aee2d47d8b.png)
**雷が出現する32拍後** (または32拍前) に更新されるのがよく見るとわかります。雷の形は完成するまでいくらか時間が掛かるので、絶対に安全になるように半周期ずらして計算を開始しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/d191fe5c-0a95-26cf-4e9b-f240ffbf8ba0.png)
近くで見るとこんな感じ。ただのカプセルのビルボードの集合体です。交差点が光りすぎないようにしなきゃいけないので好き勝手な色出せないんですよね…。そこそこ色を高めに出力しているのでBloomの影響でちょっと光った感じにはなってます。でも本来はもっと大きなメッシュにして自前で光らせるべきだったんでしょうね…。

世界遷移のときの雷は建物に一番近い電柱に落ちるようになっていますが、周期的に降ってくるやつはランダムにどこかに落ちます。衝撃的であってほしいときはその効果を持ち上げるために近く強く。それ以外はゆるく存在させたいので、逆に**一番近くの電柱には絶対に落ちません**。

常に同じところに落ちるとあまりにも予想出来すぎるわけですが、見た目的に中央付近の電柱に一番落ちそうだと思ったので、7割の確率でここに落ちるようになってます。他は一様です。

&nbsp;
この辺の解釈は…難しいですよね…。完全に筋が通っているわけではないです。

ただ、**あの鉄塔を中心に雲が渦巻いている**という**事実**がまずあるはずで、そうすると鉄塔の特異性が推測されるところ。何故鉄塔に雷が落ちないのかというと「落としても距離が短くて面白くないから」なんですけど、意味としては「鉄塔から離れるように電子群が降っていく」のだとは思っています。そして電柱に当たって、電線を伝う。本来ならそんなことは起きないとは思うんですが、**線は繋がっている**ので、まぁ許されないこともないと思う。現実の機械をそのまま使っているわけではないのですから。

私達の起動した機械の信号はあの因果の線を伝って鉄塔に届き、そして各ビルに届いて、**鐘を鳴らし始めます**。そういうことになっています。

## 続く

ここまではアレです。おまけです。

[Amebientの世界描画2](https://qiita.com/phi16/items/1b51042e20f78a0b179b)
<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">Amebientの世界描画2 <a href="https://t.co/Ba8vnEe4kZ">https://t.co/Ba8vnEe4kZ</a></p>&mdash; phi16 (@phi16_) <a href="https://twitter.com/phi16_/status/1335600131614199808?ref_src=twsrc%5Etfw">December 6, 2020</a></blockquote>