凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で距離関数の記述だけでレイマーチングができる uRaymarching を Forward / XR 対応した

はじめに

uRaymarching を更新しました。以前の記事はこちら:

tips.hecomi.com

uRaymarching は以下のような機能を持つアセットです(太字が今回更新した内容です)。

  • エディタ上で距離関数の記述だけでレイマーチングができる
  • フォワード / ディファード対応
  • 通常のポリゴンのオブジェクトと混合できる(適切に遮蔽する/される)
  • VR 対応(一部制限)
  • サーフェスシェーダ相当のライティング対応(GI や Lightprobe、シャドウ)
  • Unlit や Transparent も対応
  • オブジェクトスペース(キューブポリゴン)およびフルスクリーン対応(一部制限)
  • インスタンシング対応

前々からフォワード対応と VR 対応の要望が来ていたので、これに対応しつつ他にも使いやすいよう整備を行いました。色々と以前と使い勝手が変わった場所もありますので、一通り使い方を解説します。また、更新にあたってハマったところや既存の問題点などについても触れたいと思います。

デモ

デモはまだ新しいのが作れてないので前と同じですが、以下はフォワード / ディファードどちらでも動きます。

f:id:hecomi:20190127174451g:plain

f:id:hecomi:20190127174447g:plain

また、フォワードでは次のように Unlit で自前で色を記述したり、ライティング + 半透明なども出来たりします。

目次

概要

uRaymarching は uShaderTemplate というエディタ上でポチポチするだけでシェーダのテンプレートからシェーダを生成できるアセットに乗っかった形のアセットになっています。

tips.hecomi.com

これらエディタ上でポチポチする機能は、もともとは uRaymarching の機能だったのですが、汎用的に使えそうなテンプレートおよびエディタ拡張だけ分離した経緯があります。なのでコア部分はシェーダテンプレート及び *.cginc になります。

これまでは扱いの簡単さからディファードのみの対応を行っていましたが、前回の記事の内容を踏まえてフォワードの対応を行いました。

環境

VRChat でも動くようにと Unity 5.6 対応をしていたら VRChat が 2017 になっちゃいました...。

インストール

以下のリリースページから最新版の .unitypackage をダウンロードし、自プロジェクトへ展開してください。

github.com

使い方

  1. Create > Shader > uShaderTemplate > Generator をプロジェクトビューから選択
  2. Shader Name を入力し Shader Template を選択
  3. ConditionsVariablesProperties を作成したいシェーダに合わせて編集
  4. Distance Function および Post Effect を記述
  5. Export ボタンを押下(または Ctrl + R)してシェーダを生成
  6. プロジェクトビューでそのシェーダからマテリアルを生成(または Create Material ボタンを押下)
  7. ヒエラルキー3D Object > Cube でキューブを生成
  8. 生成したマテリアルをこのキューブに適用

ちょっと 2 ~ 4 がわかりにくいと思いますので、この後の章で説明していきます。後はサンプルシーンをいくつか用意しているのでそちらもご参照ください。

インスペクタ

  1. を行うと Generator が生成されます(Generator はテンプレートからシェーダを生成するために必要な情報を格納している ScriptableObject です)。Generator(または生成されたシェーダかマテリアル)を選択すると次のような UI が表示されます。UI は uShaderTemplate により生成されています。

f:id:hecomi:20190126191403p:plain

では各項目を見ていきましょう。

Shader Template

f:id:hecomi:20190127183849p:plain

シェーダテンプレートはシェーダのもととなるファイルで、ここに書かれたルールが Generator のインスペクタに表示され、ポチポチするとシェーダが生成される仕組みです。現在はディファード 2 種類、フォワード 2 種類の計 4 種類のシェーダテンプレートを用意しています。

  • Forward > Standard
  • Forward > Unlit
    • ライティングはされないので自前で書く(フォワード、ForwardBase のみ)
  • Deferred > Standard
    • サーフェスシェーダ相当のライティング(ディファード、Deferred のみ)
  • Deferred > Direct GBuffer
    • ディファードで各 G-Buffer の値を自前で代入する

サーフェスシェーダ相当のものは距離関数で描画されたオブジェクトの表面を自動的に ColorMetallic、および Smoothness に応じて自動的にライティングしてくれるものです。現在のレンダリングのパイプライン及び使いたいケースに応じて選択してください。

Conditions

f:id:hecomi:20190127184102p:plain

チェックの切替でシェーダの設定を切り替えられる項目が並んでいます。内容は多いですが基本的にはデフォルト値で問題ないようにしています。また、選択したテンプレートによって表示される項目は異なります。

  • 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

f:id:hecomi:20190127184500p:plain

変数はここに書きます。

  • Render Type
    • Tags ブロックの RenderType の設定です
  • Render Queue
    • Tags ブロックの Queue の設定です
  • Object Space
    • CUBENONE かどちらかを選択してください
    • CUBE はキューブポリゴンで奥側をクリッピングします
    • NONE はキューブポリゴンを超えて奥まで描画します
CUBE

f:id:hecomi:20190127185823g:plain

NONE

f:id:hecomi:20190127185906g:plain

Properties

Properties ブロックに追記したい変数をここに書きます。例を以下に示します。

[Header(Additional Parameters)]
_Grid("Grid", 2D) = "" {}

f:id:hecomi:20190127190058p:plain

Distance Function

f:id:hecomi:20190127190227p:plain

本命の距離関数をここに記述します。例えば次のようなコードを書くと冒頭の無限に続く球とキューブのモーフィングみたいなやつになります。

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

f:id:hecomi:20190127195316p:plain

サーフェスシェーダ相当の色付けをしたい場合はここで行います。サーフェスシェーダとの違いはレイマーチングへの入出力が RaymarchInfo という構造体に詰まってやってくるのでそれを使える点です。また PostEffectOutput は使用するテンプレートによって異なります。サーフェスシェーダ相当の Standard 系は SurfaceOutputStandard になり、Unlitfloat4 がそのままやってきます。

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;
}

f:id:hecomi:20190127174451g:plain

AO に関してはもっと真面目に計算したほうが結果がよくなるので、このあたりは以下のがむさんの資料をご参照ください。

speakerdeck.com

フルスクリーン

RaymarchingRenderer コンポーネントを任意のゲームオブジェクトにアタッチしてそれ用のマテリアルを適用すると、フルスクリーンのレイマーチングを行うことが出来ます。

f:id:hecomi:20190127224613p:plain

Conditions の設定は次のとおりです。Full Screen にチェックをし、World Space および Use Raymarching Depth をチェックしてください。

f:id:hecomi:20190127224701p:plain

ただ、いくつか既知の問題点があるので後の「既知の問題点」の章をご参照ください。

以前との差分

スクリプトに関しての大きな変更は以下 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 を使う場合、次のような画になってしまいます。

f:id:hecomi:20190126182402p:plain

これは 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 を使用すれば本問題を避けることができます。

f:id:hecomi:20190126185431p:plain

フォワードで RaymarchingRenderer を使った際にライティングが反映されない

RaymarchingRenderer では CommandBuffer を使って描画を行っていますが、このときライティングに関係するキーワードが定義されていないなどの理由により、シェーダの中でライティングが反映されない形になってしまいます。Unlit なシェーダを使う場合には問題ないですが、フルスクリーンのレイマーチングをしつつライティングの反映が欲しい場合には、カメラの子要素に大きなキューブポリゴンまたは全面に大きなプレーンを配置し、ここに Cull Front または Cull Off して Camera Inside Object および World Scale を ON にしたシェーダを適用してください。こうすれば通常のオブジェクトの描画ループのタイミングで描画されることになり、ライティングのキーワードも適切にシェーダの中で使えます。

詳細は Mod World for VR シーンを参考にしてみてください。

f:id:hecomi:20190127231934p:plain

RaymarchingRenderer を使うと VR立体視できない

同じく RaymarchingRenderer を使うと VR立体視ができなくなります。StereoEyeIndex は正常に降ってきているのですが、どうしても左右のカメラの位置が同じになってしまいました。なので、VR で使う際は前述の問題と同じような対応を行って通常の描画ループで描画されるようにしてください。

なお、issue でコメントも頂いているので、これは次のバージョンで試してみようと思っています。

フォワードで Static なオブジェクトのライティングが変

ライトマップを焼き込むときの調整をしていないので、現状スタティックなオブジェクトに関しては、キューブから大きく変更するような形状で頂点単位の影響が見えてしまいます。以下の写真の球はレイマーチングによるもので、左がダイナミック、右がスタティックなものです。 f:id:hecomi:20190127164942p:plain

Meta パスの追加で対応できるかどうかは将来的に試してみます。

その他 Tips

今回のアップデートに際していろいろと工夫した点など細かい話です。

DepthTexture の生成

フォワードレンダリングにおいて、Camrea.depthTextureMode を設定するか、影を生成するライトがあるときは、オブジェクトの深度を書き込んだ CameraDepthTexture が生成されます。影の描画は ForwardBaseForwardAdd で行われるので、これらの走るメイン描画ループよりも前に生成されます。さきほど DepthNormalsTextureReplaced Shaders の仕組みを利用して生成されると述べましたが、こちらは ShadowCaster パスを使って生成されます。

docs.unity3d.com

通常 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 でノイズが入る

深度が一致しないからかポリゴン面でクリッピングされる場所でノイズが入ってしまいました。

これに対してはちょっと出力するデプスをいじってあげると避けることが出来ました。

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 を使って下さっているプロジェクト

ありがとうございます!

gam0022.net

blog.gmork.in

connect.unity.com