- 日本語を丁寧に書くの面倒なので箇条書きスタイルでいきます
- もうこれからそれでいいのでは?
- ぶっちゃけ読みやすいと思うんすけど
- この記事が読みにくいのはもう内容的にしょうがない
- ぶっちゃけ読みやすいと思うんすけど
- もうこれからそれでいいのでは?
きゅーぶがいっぱい動くわーるどを Community Labs にあげました 好きに遊んでね "Cubes (experimental)" https://t.co/EJwtgOHLLl #VRChat_world紹介 pic.twitter.com/EnHHBDQp5g
— phi16 (@phi16_) 2022年7月15日
もくじ
- 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}"
- n次元ベクトルは n=1 のとき
- 入力型 (
- まず各ノードタイプに合わせて入出力に関する値を設定
GetNodeName
関数で各ノードの名前を設定- これがノード板に表示される名前になる
- Scripts/GraphiliaMenu.cs
- GraphiliaNode.cs と同様に private const int の定数を追加
- 当然数値は GraphiliaNode.cs と一致させる
- 各ノードに対して Spawn 用イベントを定義
public void OnなんとかButton()
っていう関数を作る- 中身は他ノードと同様に
SpawnNode
にノードタイプの定数を渡すだけ
- 中身は他ノードと同様に
- この名前 (
なんとか
部分) は後に作るGameObjectの名前と一致させることになる
- GraphiliaNode.cs と同様に private const int の定数を追加
- Shaders/GraphiliaCommon.cginc
- 今度は static const int で定数を追加
- Shaders/GraphiliaCalculate.cginc
- 各ノードの計算処理を記述 (Output系以外)
GetInput{N}(index)
でindex
ノード (現在の処理対象ノード, 要は自分) のN
番目の入力を得られる (float4)SetOutput0(index, value)
でindex
ノードの出力を設定 (出力は高々1個しかないので N = 0 しか使えない)
- 各ノードの計算処理を記述 (Output系以外)
- この段階で 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_VERT
かGRAPHILIA_CALCULATE_FRAG
を define する
- あと呼び出しているのが vertex shader か fragment shader かに依って
Calculateなんとか
を適当に呼び出す- 引数いっぱいあるけどぶっちゃけ全ての引数に 0 を突っ込んでも動く
- Cube Z では
uv
だけ入れてそれ以外 0、Light と Back Light では全てに 0 を突っ込んでます - 自作の time とか入れるなら引数を増やす必要があるのかな
- Cube Z では
- 引数いっぱいあるけどぶっちゃけ全ての引数に 0 を突っ込んでも動く
- あとは Material を作って Graphilia.cs に public variable として追加、割り当て
GraphiliaNode.cs
のSetupScreen
関数でSetupPass
関数などを呼ぶ (他を参考にして書けば良い)
- まず後々便利なので出力に応じたマクロを define しておく (
- こんなもんかな?
ちなみに出力はこんな感じで行われてます (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 として寄与を計算
- 面光源の uniform sample に向かって適当にレイを投げる
- ランダム選択はデバイス座標での Blue Noise を使って算出 (よくあるやつ)
- ある程度高周波ノイズにしてやることで後でぼかしたときにノイズが消えやすくなる
- デバイス座標、
SV_POSITION
になってる値のxy
を frag で取ったらそれっぽかったのでそれを使ってます
- Ambient Occlusion
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
をいじるbakeryVertForwardBase
とbakeryVertForwardAdd
の最初の方 (マクロの後?) でv.vertex
を直接書き換えて vertex 弄りをしています- このタイミングなら何やっても大丈夫なはず (メッシュがもともとそういうデータだったことと区別がつかないため)
bakeryFragForwardBase
の末尾付近 (UNITY_BRDF_PBS
の直前) でgi.indirect.diffuse
とgi.indirect.specular
を好き勝手に書き換え- LightProbeを読み出して
float4(normal, 1)
とdot
を取るなどする- そういえば
SphericalHarmonicsL2
に入っている成分は 1,y,z,x の順番なので注意 (テクスチャ化のときに xyz1 に揃えた)
- そういえば
- 後は LTC の寄与を突っ込んだりする
- ちなみに
specular
成分はBakerySH
関数のBAKERY_LMSPEC
部分を参考に適当にでっちあげました- 何故か
focus
使われてなかったけど使っておきました - まぁこのシーンではほぼ意味ないんですが…
- 何故か
- LightProbeを読み出して
- ついでに
bakeryFragForwardBase
の最後に emission 項を直接加算
- FilamentedStandard
FilamentLightIndirect.cginc
をいじるevaluateIBL
の (UnityGI_Irradiance
後の)derivedLight
にベイクされた値が入ってるっぽい (unity_Irradiance
にもなんか入ってるっぽい) (よくわかってない)derivedLight.attenuation
にAO項を乗算derivedLight.colorIntensity.rgb
にバックライトの明かりを乗算してから LTC 光源の寄与を突っ込む
こんなかんじです。
おわり。