はじめに
uRaymarching を更新しました。以前の記事はこちら:
uRaymarching は以下のような機能を持つアセットです(太字が今回更新した内容です)。
- エディタ上で距離関数の記述だけでレイマーチングができる
- フォワード / ディファード対応
- 通常のポリゴンのオブジェクトと混合できる(適切に遮蔽する/される)
- VR 対応(一部制限)
- サーフェスシェーダ相当のライティング対応(GI や Lightprobe、シャドウ)
- Unlit や Transparent も対応
- オブジェクトスペース(キューブポリゴン)およびフルスクリーン対応(一部制限)
- インスタンシング対応
前々からフォワード対応と VR 対応の要望が来ていたので、これに対応しつつ他にも使いやすいよう整備を行いました。色々と以前と使い勝手が変わった場所もありますので、一通り使い方を解説します。また、更新にあたってハマったところや既存の問題点などについても触れたいと思います。
デモ
デモはまだ新しいのが作れてないので前と同じですが、以下はフォワード / ディファードどちらでも動きます。
また、フォワードでは次のように Unlit で自前で色を記述したり、ライティング + 半透明なども出来たりします。
uRaymarching の Forward 対応、影とライティングも含めて正しい画が出るようになった~。図はサーフェスシェーダ相当の Opaque および Transparent と Unlit pic.twitter.com/cLfnS8HirY
— 凹 (@hecomi) 2019年1月8日
目次
- はじめに
- デモ
- 目次
- 概要
- 環境
- インストール
- 使い方
- インスペクタ
- Properties
- Distance Function
- Post Effect
- フルスクリーン
- 以前との差分
- 既知の問題点
- その他 Tips
- おわりに
- uRaymarching を使って下さっているプロジェクト
概要
uRaymarching は uShaderTemplate というエディタ上でポチポチするだけでシェーダのテンプレートからシェーダを生成できるアセットに乗っかった形のアセットになっています。
これらエディタ上でポチポチする機能は、もともとは uRaymarching の機能だったのですが、汎用的に使えそうなテンプレートおよびエディタ拡張だけ分離した経緯があります。なのでコア部分はシェーダテンプレート及び *.cginc になります。
これまでは扱いの簡単さからディファードのみの対応を行っていましたが、前回の記事の内容を踏まえてフォワードの対応を行いました。
環境
VRChat でも動くようにと Unity 5.6 対応をしていたら VRChat が 2017 になっちゃいました...。
インストール
以下のリリースページから最新版の .unitypackage をダウンロードし、自プロジェクトへ展開してください。
使い方
- Create > Shader > uShaderTemplate > Generator をプロジェクトビューから選択
- Shader Name を入力し Shader Template を選択
- Conditions、Variables、Properties を作成したいシェーダに合わせて編集
- Distance Function および Post Effect を記述
- Export ボタンを押下(または Ctrl + R)してシェーダを生成
- プロジェクトビューでそのシェーダからマテリアルを生成(または Create Material ボタンを押下)
- ヒエラルキーで 3D Object > Cube でキューブを生成
- 生成したマテリアルをこのキューブに適用
ちょっと 2 ~ 4 がわかりにくいと思いますので、この後の章で説明していきます。後はサンプルシーンをいくつか用意しているのでそちらもご参照ください。
インスペクタ
- を行うと Generator が生成されます(Generator はテンプレートからシェーダを生成するために必要な情報を格納している
ScriptableObject
です)。Generator(または生成されたシェーダかマテリアル)を選択すると次のような UI が表示されます。UI は uShaderTemplate により生成されています。
では各項目を見ていきましょう。
Shader Template
シェーダテンプレートはシェーダのもととなるファイルで、ここに書かれたルールが Generator
のインスペクタに表示され、ポチポチするとシェーダが生成される仕組みです。現在はディファード 2 種類、フォワード 2 種類の計 4 種類のシェーダテンプレートを用意しています。
- Forward > Standard
- Forward > Unlit
- ライティングはされないので自前で書く(フォワード、ForwardBase のみ)
- Deferred > Standard
- サーフェスシェーダ相当のライティング(ディファード、Deferred のみ)
- Deferred > Direct GBuffer
- ディファードで各 G-Buffer の値を自前で代入する
サーフェスシェーダ相当のものは距離関数で描画されたオブジェクトの表面を自動的に Color、Metallic、および Smoothness に応じて自動的にライティングしてくれるものです。現在のレンダリングのパイプライン及び使いたいケースに応じて選択してください。
Conditions
チェックの切替でシェーダの設定を切り替えられる項目が並んでいます。内容は多いですが基本的にはデフォルト値で問題ないようにしています。また、選択したテンプレートによって表示される項目は異なります。
- Blend
- シェーダ内に
Blend X X
なラインを生成します - 設定自体はマテリアルから行ってください
- 半透明なオブジェクトを生成した際に設定してください
- シェーダ内に
- Shadow Caster
- 影を生成する
ShadowCaster
パスを生成します - レイマーチングしたオブジェクトの影を生成したい場合はチェックしてください
- 影を生成する
- Full Screen
- 後述する
RaymarchingRenderer
を使ってフルスクリーンの描画を行う際にチェックを入れます - チェックを入れるとレイの開始点がカメラのニアクリップ面になります(しない場合はポリゴン表面)
- 後述する
- World Space
- 距離関数内に降ってくる
pos
変数がワールド座標になります - チェックを入れないとオブジェクト(キューブ)のローカル座標が返ってきます(移動や回転追従)
- フルスクリーンのレンダリング時向け
- 距離関数内に降ってくる
- Follow Object Scale
- チェックを入れないとオブジェクトのスケールは無視します(冒頭の六角タイルのような表現)
- チェックを入れると距離関数の
pos
にオブジェクトのスケールを考慮した値が降ってくるようになります
- Camera Inside Object
- チェックを入れるとオブジェクト内部にカメラのニアクリップ平面が入り込んだ時にレイの開始点をそこから行うようになります(しない場合は向こう側の面の表面になってしまう)
Cull Off
またはCull Front
と併用してください
- Use Raymarching Depth
- レイマーチングした結果のデプスをデプスバッファで使用します
- 結果、オブジェクト同士の遮蔽はレイマーチングの結果で正しく行われるようになります
- 遮蔽が正しくなくても良いケースでパフォーマンスが気になる場合は OFF にしてください
- また、半透明オブジェクトの場合はこちらを OFF にして次の Use Camera Depth Texture を ON にすると良いです
- Use Camera Depth Texture
- フォワードで遮蔽を
CameraDepthTexture
を使って計算します - チェックするとレイの最大長が
CameraDepthTexture
までの距離になり、通り過ぎるとdiscard
されます - 半透明オブジェクトの場合は
ZWrite Off
とこれを組み合わせてください
- フォワードで遮蔽を
- Disable View Culling
- Camera Inside Object フラグが立っているときに遠方のポリゴンがビューフラスタムカリングされて何も描画されなくなるのを防ぎます
- 後述するフルスクリーンの項目で詳しく述べます
- Spherical Harmonics Per Pixel
- 頂点単位で行われる球面調和ライティングをピクセル単位で計算します
- キューブから大きく変形するような形の場合にチェックを入れると計算は重くなりますがより正しい結果が出力されます
- Forward Add
ForwardAdd
パスを生成します- 追加のライトのライティングが欲しい場合はチェックを入れてください
- 現状このパスの中でも再度レイマーチングが走るので結構重いです
- Fallback To Standard Shader
- チェックすると
Fallback
の行が挿入されデフォルトのサーフェスシェーダへフォールバックされます - チェックしないと
Fallback Off
になります - 例えば Shadow Caster をオフにしてこちらをチェックするとポリゴンの影が生成されます(軽い)
- チェックすると
Variables
変数はここに書きます。
- Render Type
Tags
ブロックのRenderType
の設定です
- Render Queue
Tags
ブロックのQueue
の設定です
- Object Space
CUBE
かNONE
かどちらかを選択してくださいCUBE
はキューブポリゴンで奥側をクリッピングしますNONE
はキューブポリゴンを超えて奥まで描画します
CUBE
NONE
Properties
Properties
ブロックに追記したい変数をここに書きます。例を以下に示します。
[Header(Additional Parameters)] _Grid("Grid", 2D) = "" {}
Distance Function
本命の距離関数をここに記述します。例えば次のようなコードを書くと冒頭の無限に続く球とキューブのモーフィングみたいなやつになります。
inline float DistanceFunction(float3 pos) { float r = abs(sin(2 * PI * _Time.y / 2.0)); float d1 = RoundBox(Repeat(pos, float3(6, 6, 6)), 1 - r, r); float d2 = Sphere(pos, 3.0); float d3 = Plane(pos - float3(0, -3, 0), float3(0, 1, 0)); return SmoothMin(SmoothMin(d1, d2, 1.0), d3, 1.0); }
Post Effect
サーフェスシェーダ相当の色付けをしたい場合はここで行います。サーフェスシェーダとの違いはレイマーチングへの入出力が RaymarchInfo
という構造体に詰まってやってくるのでそれを使える点です。また PostEffectOutput
は使用するテンプレートによって異なります。サーフェスシェーダ相当の Standard 系は SurfaceOutputStandard
になり、Unlit は float4
がそのままやってきます。
RaymarchInfo
は次のような構造体です。
struct RaymarchInfo { // 入力 float3 startPos; // レイの開始点 float3 rayDir; // レイの向き float3 polyNormal; // キューブポリゴンの法線方向 float minDistance; // 計算を終了するための最小距離(パラメタ指定) float maxDistance; // レイの最大長 int maxLoop; // 最大ループ数(パラメタ指定) // 出力 int loop; // 計算に要したループ数 float3 endPos; // 最終距離(距離関数によって与えられる図形の表面位置) float lastDistance; // 収束に至った最終ループでの距離関数の出力 float totalLength; // 合計のレイの長さ float depth; // デプス(エンコード済み) float3 normal; // 法線(エンコード済み) };
次の例はレイマーチングのステップ数を AO とアルファに使うもので、六角タイルのデモに適用したコードです。
float4 _TopColor; inline void PostEffect(RaymarchInfo ray, inout PostEffectOutput o) { float3 localPos = ToLocal(ray.endPos); o.Emission += smoothstep(0.48, 0.50, localPos.y) * _TopColor; // ray.loop がかかったループ数、ray.maxLoop がマテリアルのパラメタで指定した最大ループ数 o.Occlusion *= 1.0 - 1.0 * ray.loop / ray.maxLoop; }
AO に関してはもっと真面目に計算したほうが結果がよくなるので、このあたりは以下のがむさんの資料をご参照ください。
フルスクリーン
RaymarchingRenderer
コンポーネントを任意のゲームオブジェクトにアタッチしてそれ用のマテリアルを適用すると、フルスクリーンのレイマーチングを行うことが出来ます。
Conditions の設定は次のとおりです。Full Screen にチェックをし、World Space および Use Raymarching Depth をチェックしてください。
ただ、いくつか既知の問題点があるので後の「既知の問題点」の章をご参照ください。
以前との差分
スクリプトに関しての大きな変更は以下 2 点になります。
RaymarchingObject
の廃止
v1.0.0 からは以前にあった RaymarchingObject
コンポーネントを必要としなくなります。主にオブジェクトのスケールを与えるためにこのコンポーネントを必要としていたのですが、unity_ObjectToWorld
行列からスケールを抽出する形にしました。これにより、シェーダだけで完結するようになりました。
inline float3 GetScale() { return float3( length(float3(unity_ObjectToWorld[0].x, unity_ObjectToWorld[1].x, unity_ObjectToWorld[2].x)), length(float3(unity_ObjectToWorld[0].y, unity_ObjectToWorld[1].y, unity_ObjectToWorld[2].y)), length(float3(unity_ObjectToWorld[0].z, unity_ObjectToWorld[1].z, unity_ObjectToWorld[2].z))); }
RaymarchingRenderer
によるカメラへの CommandBuffer
の追加
以前は OnWillRenderObject
を使ってシーンビューも含めたカメラすべてにコマンドバッファを付与する方式を取っていましたが、これだとそれぞれのカメラにオブジェクトが映らないとコマンドバッファが登録されず面倒でした。v1.0.0 からは普通に Update
のタイミングですべてのカメラをさらうようにしました。
foreach (var camera in Camera.allCameras) { UpdateCommandBuffer(camera); } #if UNITY_EDITOR foreach (SceneView view in SceneView.sceneViews) { if (view != null) { UpdateCommandBuffer(view.camera); } } #endif
既知の問題点
フォワードに関してはいろいろと問題点があります。。
フォワードで DepthNormalTexture を使う場合
例えば Post Processing で Scalable Ambient Obscurance を使う場合、次のような画になってしまいます。
これは Scalable Ambient Obscurance では DepthNormalsTexture
を内部的に使用しているのですが、現状レイマーチングしたオブジェクトは DepthNormalsTexture
に正しい深度と法線を出力できないためです。この理由は、DepthNormalsTexture
は Unity の Replaced Shaders という仕組みを利用しているからです。
具体的には、Opaque などの RenderType が指定されたシェーダは、通常のオブジェクトのレンダリングに先立ち、ビルトインの Internal-DepthNormalsTexture.shader で描画され、この中ではポリゴンの情報を EncodeDepthNormals()
した結果が DepthNormalsTexture
に格納されています。この時にビルトインのシェーダで描画するために Replaced Shaders の仕組みが使われています。レイマーチングしたオブジェクトはポリゴンの深度を改変して独自の深度と法線を吐き出しています。この部分がポリゴンの情報を出力するシェーダに差し替えられてしまうので上手のような結果になってしまいます。
このビルトインのシェーダは Graphics の設定から別のシェーダに置き換えることも出来るので、無理すれば正しい深度と法線を吐き出す対応もできますが、レイマーチングのオブジェクトが一つ増えるたびに RenderType と専用の SubShader をこの中に追記していかなければならないのでちょっと大変すぎるため諦めています。ディファードでは G-Buffer に出力された値を使うので問題ありません。なので、フォワードで深度だけでなく法線情報を使うようなケースにおいては注意してください。
なお AO に関しては代わりに Multi Scale Volumetric Obscurance を使用すれば本問題を避けることができます。
フォワードで RaymarchingRenderer
を使った際にライティングが反映されない
RaymarchingRenderer
では CommandBuffer
を使って描画を行っていますが、このときライティングに関係するキーワードが定義されていないなどの理由により、シェーダの中でライティングが反映されない形になってしまいます。Unlit なシェーダを使う場合には問題ないですが、フルスクリーンのレイマーチングをしつつライティングの反映が欲しい場合には、カメラの子要素に大きなキューブポリゴンまたは全面に大きなプレーンを配置し、ここに Cull Front
または Cull Off
して Camera Inside Object
および World Scale
を ON にしたシェーダを適用してください。こうすれば通常のオブジェクトの描画ループのタイミングで描画されることになり、ライティングのキーワードも適切にシェーダの中で使えます。
詳細は Mod World for VR シーンを参考にしてみてください。
RaymarchingRenderer
を使うと VR で立体視できない
同じく RaymarchingRenderer
を使うと VR で立体視ができなくなります。StereoEyeIndex
は正常に降ってきているのですが、どうしても左右のカメラの位置が同じになってしまいました。なので、VR で使う際は前述の問題と同じような対応を行って通常の描画ループで描画されるようにしてください。
なお、issue でコメントも頂いているので、これは次のバージョンで試してみようと思っています。
フォワードで Static なオブジェクトのライティングが変
ライトマップを焼き込むときの調整をしていないので、現状スタティックなオブジェクトに関しては、キューブから大きく変更するような形状で頂点単位の影響が見えてしまいます。以下の写真の球はレイマーチングによるもので、左がダイナミック、右がスタティックなものです。
Meta パスの追加で対応できるかどうかは将来的に試してみます。
その他 Tips
今回のアップデートに際していろいろと工夫した点など細かい話です。
DepthTexture の生成
フォワードレンダリングにおいて、Camrea.depthTextureMode
を設定するか、影を生成するライトがあるときは、オブジェクトの深度を書き込んだ CameraDepthTexture
が生成されます。影の描画は ForwardBase
や ForwardAdd
で行われるので、これらの走るメイン描画ループよりも前に生成されます。さきほど DepthNormalsTexture
は Replaced Shaders の仕組みを利用して生成されると述べましたが、こちらは ShadowCaster
パスを使って生成されます。
通常 ShadowCaster
パスはシャドウマップ用にデプスを出力するので問題ないのですが、レイマーチングを行う際はレイの方向や開始位置に関して異なる設定を使う必要があったりします。なので同じパスで走ってしまうには都合がよくありません。しかし切り分けるフラグはないので無理やり判定しなければなりません。
今回は unity_LightShadowBias
の値に注目して場合分けすることにしました。カメラのデプス出力時は 0 になっている一方、シャドウマップの場合はその仕組み上、たいてい 0 よりも大きな値がセットされています。次のようなコードで場合分けを行っています。
inline bool IsCameraPerspective() { return any(UNITY_MATRIX_P[3].xyz); } void Frag( v2f i, out float4 outColor : SV_Target, out float outDepth : SV_Depth) { ... if (IsCameraPerspective()) { if (abs(unity_LightShadowBias.x) < 1e-5) { // ここはカメラのデプス出力 ... } else { // ここはスポットライト } } else { // ここはディレクショナルライト } ... }
ピクセル単位のフォグの計算
通常は頂点シェーダ内で UNITY_TRANSFER_FOG()
してそれをフラグメントシェーダで UNITY_APPLY_FOG()
して色の調整を行います。しかしながらピクセルごとに深度の改変をする場合は、頂点の深度を使ってしまうとおかしな描画になってしまいます。
そこで、次のようにフラグメントシェーダ内でレイマーチングの結果の座標を使ってフォグ用の座標を再計算する処理を行っています。
FragOutput Frag(VertOutput i) { ... #if (defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)) i.fogCoord.x = mul(UNITY_MATRIX_VP, float4(ray.endPos, 1.0)).z; #endif UNITY_APPLY_FOG(i.fogCoord, o.color); ... }
ビューフラスタムカリングを避ける
フルスクリーンの描画で「既知の問題点」の中で述べた Camera Inside Object な中にカメラを配置する方法では、外側のキューブがカメラのファークリップよりも大きすぎると、裏面がビューフラスタムカリングによって消えてしまい、そもそも何も描画されなくなってしまいます。これを避けるために Disable View Culling にチェックを入れると、カリングされないようにクリップ空間の z
を無理やり書き換えます。
VertOutput Vert(appdata_full v) { ... o.pos = UnityObjectToClipPos(v.vertex); #ifdef DISABLE_VIEW_CULLING o.pos.z = 1; #endif ... }
ポリゴンの座標を出力するときに ForwardAdd
でノイズが入る
深度が一致しないからかポリゴン面でクリッピングされる場所でノイズが入ってしまいました。
ForwardAdd も追加した。デプスが一致しないのかノイズが…。 pic.twitter.com/5kt4nux1cH
— 凹 (@hecomi) 2019年1月3日
これに対してはちょっと出力するデプスをいじってあげると避けることが出来ました。
ポリゴンで Raymarching オブジェクトがクリッピングされてしまうときのデプスをちょっとだけ(1.0e-6 くらい)小さくするので対応した。 pic.twitter.com/DHbbYkSXZb
— 凹 (@hecomi) 2019年1月3日
void Raymarch(inout RaymarchInfo ray) { ... float initLength = length(ray.startPos - GetCameraPosition()); if (ray.totalLength - initLength < ray.minDistance) { ray.normal = EncodeNormal(ray.polyNormal); ray.depth = EncodeDepth(ray.startPos) - 1e-6; } else { float3 normal = GetDistanceFunctionNormal(ray.endPos); ray.normal = EncodeNormal(normal); ray.depth = EncodeDepth(ray.endPos); } }
おわりに
まだまだ問題点は残ってますが、注意して使えば問題なく使えるようになってきたのではないでしょうか。要望やフィードバックをいただけると嬉しいです。
uRaymarching を使って下さっているプロジェクト
ありがとうございます!