Imaginantia

思ったことを書きます

Cubes 解説

  • 日本語を丁寧に書くの面倒なので箇条書きスタイルでいきます
    • もうこれからそれでいいのでは?
      • ぶっちゃけ読みやすいと思うんすけど
        • この記事が読みにくいのはもう内容的にしょうがない

もくじ

  • Graphilia改造について
  • 描画について

Graphilia改造について

行ったこと

  • ノード10個の追加
    • Input系: Cube Coord, AudioLink (Bass/Low-Mid/High-Mid/Treble), Last Cube Z
    • Output系: Cube Z, Light, Back Light
    • 計算系: Hash
  • メニューへの追加 (私はメニューの追加はしてないです)
  • ノードの実装

実装 (全ノード共通部分)

ノード追加したければ他のノードがやってることをそのままその通りやれば良い (それはそう)

  • Scripts/GraphiliaNode.cs
    • 定数 (private const int) を追加 (予めメニューを想定して番号を割り当て)
    • UpdateImpl 関数内 nodeDirty 分岐内
      • まず各ノードタイプに合わせて入出力に関する値を設定
        • 入力数 (numInputConnectors) / 出力数 (numOutputConnectors)
        • 即値入力 (スライダー化) の許可 (allowImmediateValues)
          • 不要な場合は省略可っぽい
        • 入力名 (inputNames) / 出力名 (outputNames)
      • 次に各ノードタイプに合わせてに関する値を設定
        • 入力型 (inputTypes) を使って出力型 (outputTypes) を計算
          • n次元ベクトルは n=1 のとき "Scalar", そうでないとき $"Vec{n}"
    • GetNodeName 関数で各ノードの名前を設定
      • これがノード板に表示される名前になる
  • Scripts/GraphiliaMenu.cs
    • GraphiliaNode.cs と同様に private const int の定数を追加
      • 当然数値は GraphiliaNode.cs と一致させる
    • 各ノードに対して Spawn 用イベントを定義
      • public void OnなんとかButton() っていう関数を作る
        • 中身は他ノードと同様に SpawnNode にノードタイプの定数を渡すだけ
      • この名前 (なんとか部分) は後に作るGameObjectの名前と一致させることになる
  • Shaders/GraphiliaCommon.cginc
    • 今度は static const int で定数を追加
  • Shaders/GraphiliaCalculate.cginc
    • 各ノードの計算処理を記述 (Output系以外)
      • GetInput{N}(index)index ノード (現在の処理対象ノード, 要は自分) の N 番目の入力を得られる (float4)
      • SetOutput0(index, value)index ノードの出力を設定 (出力は高々1個しかないので N = 0 しか使えない)
  • この段階で Graphilia は (内部的には) ノードを生成・処理できるようになる
  • メニューへ追加
    • Hierarchy 上の Graphilia/Menu/Panel/Menu を開く
    • 追加したいノードを入れるメニュー板を開く
    • 他のノードを duplicate してボタンを増やす
    • ボタンを追加ノードに合わせて設定
      • 位置をいい感じにする
      • GameObject の名前を GraphiliaMenu.cs で設定したものに合わせる
      • もしもメニュー上の表示を GameObject 名と違うものにしたい場合はそれを Label に設定
        • 改行は \n で入れられる
  • おわり
    • 綺麗にできていたので思った通りにやれば素直にうごきました

Output系の実装

  • 今回作る Output は全て「シーンに唯一」なので、Projector の実装を参考にしました
  • 方針
    • GraphiliaCommon.cginc に、目的の出力に合わせて ConstantBuffer を定義する
    • GraphiliaCalculate.cginc で定義される関数をいい感じに呼ぶシェーダを作る
    • そのシェーダを割り当てた Material を作り、Graphilia (root) に public variable として渡しておく
    • GraphiliaNode.cs の SetupScreen 関数で Material にノード情報を渡す
    • 後はその Material で描画を実行するだけ
  • これ細かく書く必要ある?
    • ちなみに私は3つ出力を作りましたが、1つのシェーダにまとめたらコンパイル長くて心配になったので3つのシェーダに分けました
    • NumNodes の分岐・LocationIndex の分岐を満たさない場合の出力がデフォルトの計算結果として利用できます
  • もうちょっと細かく書いておく
    • まず後々便利なので出力に応じたマクロを define しておく (GRAPHILIA_CUBE_OUT_CUBE_Z みたいにしました)
    • ConstantBufferの定義
      • 自明 (_なんとかNumNodes, _なんとかNodes でノードのデータ、出力ノードの index は _なんとかかんとかIndex )
        • 命名規則は揃える必要がある (GraphiliaNode.cs で利用している為)
    • GraphiliaVertFrag.cginc をコピーしてマクロと関数呼び出しを整備する
      • GRAPHILIA_CALCULATE は計算用関数の関数名 (Calculateなんとか で良さそう)
      • GRAPHILIA_CALCULATE_NUM_NODES はノード数 (_なんとかNumNodes)
      • GRAPHILIA_CALCULATE_NODES はノードのデータ (_なんとかNodes)
      • この3つを定義した状態で #include "GraphiliaCalculate.cginc" する
        • あと呼び出しているのが vertex shader か fragment shader かに依って GRAPHILIA_CALCULATE_VERTGRAPHILIA_CALCULATE_FRAG を define する
      • Calculateなんとか を適当に呼び出す
        • 引数いっぱいあるけどぶっちゃけ全ての引数に 0 を突っ込んでも動く
          • Cube Z では uv だけ入れてそれ以外 0、Light と Back Light では全てに 0 を突っ込んでます
          • 自作の time とか入れるなら引数を増やす必要があるのかな
    • あとは Material を作って Graphilia.cs に public variable として追加、割り当て
    • GraphiliaNode.csSetupScreen 関数で SetupPass 関数などを呼ぶ (他を参考にして書けば良い)
  • こんなもんかな?

ちなみに出力はこんな感じで行われてます (32x16 RGBAFloat16 の RenderTexture に Camera で描画してる)

ここまでくればもう好きに使えば良い

描画について

やってること

  • vertex shader
    • 押し出したりする
    • 位置が同じキューブはくっつける (最も左下のキューブを拡大させて、他を全て消す)
  • fragment shader
    • pixel 毎に Light Probe を読み出してライティングさせる
    • raytrace して ambient occlusion を計算
    • 面光源の直接光を Linearly Transformed Cosines で計算
    • raytrace して LTC による光の影を計算
  • おまけ
    • AOと影は GrabPass でいい感じにぼかす

Light Probe は この記事 の内容を L1 まで拡張して (RGBAに突っ込んで) あげただけ

レイトレ部分について

  • 今回はシーンが「2次元のセル上の四角柱たち」なので、そのセルを直接 raytrace すれば良い (QuadTree Traversal, はやい)
    • これはまぁがんばる
  • AOと影を計算する (どちらも 4 samples per pixel)
    • Ambient Occlusion
      • 法線を向いた Cosine-weighted Hemisphere に向かって適当にレイを投げる
      • 衝突までの距離 L [m] に対して saturate(L) で寄与を計算 (遠いと1、近いと0)
        • どうせAOなんて最初から擬似的なもんなのでそれっぽければよい
    • 影 (面光源の見える面積として影を近似)
      • 面光源の uniform sample に向かって適当にレイを投げる
        • 真値ではないです (本来は見かけ上の面積で uniform sample するべきだと思う)
      • 光源まで当たれば 1、そうでなければ 0 として寄与を計算
    • ランダム選択はデバイス座標での Blue Noise を使って算出 (よくあるやつ)
      • ある程度高周波ノイズにしてやることで後でぼかしたときにノイズが消えやすくなる
      • バイス座標、SV_POSITION になってる値の xy を frag で取ったらそれっぽかったのでそれを使ってます

AOと影を表示するとこんな感じ (Rが影 (じゃない部分)、BがAO成分)

で、これをいい感じにぼかすことで綺麗にします

これがこう。

  • 仕組み
    • 概略
      • ぼかす為にめっちゃ GrabPass を使います
      • ついでに余計にぼかさない為に normal/depth がほしいのでそれも作ります (GBuffer みたいなもん)
      • その為にシーンの物体は3つずつおいてあります
    • 実際の処理
      • 真っ黒を描画
      • Normal を描画
      • NamedGrabPass しつつ ZWrite On, ZTest Always で o.vertex.z を far clip の depth 値にして空間を覆う (初期化)
      • レイトレ結果を描画
      • GrabPass しつつぼかす(1)
      • GrabPass しつつぼかす(2)
      • GrabPass しつつぼかす(4)
      • GrabPass しつつぼかす(8)
      • NamedGrabPass して再度 depth を初期化
      • 作ったバッファを利用してシーンをいい感じに描画する

Normal の GrabTexture はこんなかんじ (自明)

ちなみにキューブには bevel が入ってるので微妙に平坦ではない

ぼかしの実装は これ とかを参考にしました (1+4 sample で重み付き平均を取っていく)

(edge-avoid する為に normal と position を使った適当な項を突っ込んでる感じです) (position は depth から復元します)

それっぽいのでまんぞく。

Standard系拡張

  • 実はキューブ達は BakeryStandard の派生、部屋全体は FilamentedStandard の派生
    • 最初の状態だと部屋の奥の方が Reflection Probe を過剰に取ってるようにみえたので
    • どうせ最終的にはほぼ見えなくなったわけですが
  • BakeryStandard
    • Bakery.cginc をいじる
    • bakeryVertForwardBasebakeryVertForwardAdd の最初の方 (マクロの後?) で v.vertex を直接書き換えて vertex 弄りをしています
      • このタイミングなら何やっても大丈夫なはず (メッシュがもともとそういうデータだったことと区別がつかないため)
    • bakeryFragForwardBase の末尾付近 (UNITY_BRDF_PBS の直前) で gi.indirect.diffusegi.indirect.specular を好き勝手に書き換え
      • LightProbeを読み出して float4(normal, 1)dot を取るなどする
        • そういえば SphericalHarmonicsL2 に入っている成分は 1,y,z,x の順番なので注意 (テクスチャ化のときに xyz1 に揃えた)
      • 後は LTC の寄与を突っ込んだりする
      • ちなみに specular 成分は BakerySH 関数の BAKERY_LMSPEC 部分を参考に適当にでっちあげました
        • 何故か focus 使われてなかったけど使っておきました
        • まぁこのシーンではほぼ意味ないんですが…
    • ついでに bakeryFragForwardBase の最後に emission 項を直接加算
  • FilamentedStandard
    • FilamentLightIndirect.cginc をいじる
    • evaluateIBL の (UnityGI_Irradiance 後の) derivedLight にベイクされた値が入ってるっぽい (unity_Irradiance にもなんか入ってるっぽい) (よくわかってない)
    • derivedLight.attenuation にAO項を乗算
    • derivedLight.colorIntensity.rgb にバックライトの明かりを乗算してから LTC 光源の寄与を突っ込む

こんなかんじです。

おわり。