凹みTips

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

Unity でレイマーチングするシェーダを簡単に作成できるツールを作ってみた

はじめに

レイマーチングとはポリゴンではなく、距離関数(distance function)と呼ばれる数式を元にオブジェクトをレンダリングする方法です。ポリゴンを使わないのでモーフィングや複雑な図形もシンプルな数式で記述することが可能で、GPU のコストが高いという欠点もありますが、面白い見た目を簡単に作り出すことが出来ます。

例えばこれらはキューブのポリゴン(12 ポリゴン)を変形させたものです。

最近のエントリもレイマーチングの内容を中心に書いてきました。というのもレイマーチングを積極的に使ったゲームを作りたいなー、と思っているからです。しかしながら、オブジェクトの形状ごとにシェーダを作成しなければならず、これらを一つ一つ作るのは非常に面倒くさいです。一つ一つのシェーダは基本的には数式以外同じコードを流用するのにも関わらず、一部を書き換えるために多量のシェーダを作るのは、後で大幅な修正が必要になったときに、メンテナンス性の観点からも良くありません。

そこで、数式部分と最低限の設定だけを入力すれば、簡単にレイマーチングするシェーダを量産できるシェーダ群およびエディタ拡張を作成してみました。アセットは GitHub で MIT で公開していて、安定してきたら Asset Store にも出したいな、と思っています。

本エントリでは、その紹介と使い方を説明いたします。

デモ

元となるポリゴンの空間でレイマーチを行う id:i-saint さんの手法(object space raymarching - primitive: blog)を採用しています。詳細は前回のエントリをご参照下さい。また、解説は省略しますが、CommandBuffer を使った視界全体でのレイマーチもあります。詳細は Examples の Mod World をご覧ください。

環境

Deferred もレイマーチングも重い処理なので、グラボを搭載していないノート PC 等では十分なパフォーマンスが発揮されないのでご注意下さい。

ダウンロードとテスト

はじめはプロジェクト全体または Examples 付きの .unitypackage をダウンロードしてお試しください。インポートして「Raymarching > Examples > Scenes > Hex Floor」を見てみると、次のように表示されると思います。

f:id:hecomi:20161011134130p:plain

利用方法

  1. Rendering Path が「Deferred」になっていることを確認
  2. Project ウィンドウから「Create > Shader > Raymarching Shader Generator」でシェーダ生成用のアセットを作成
  3. シェーダの名前を記入
  4. 好きな Distance Function を記入して Export ボタンを押下するか「Ctrl + R」でシェーダを生成
  5. シェーダからマテリアルを生成(Create Material ボタンでも可能)
  6. RaymarchingObject コンポーネントを作成したオブジェクト(Cube または Sphere)にアタッチ
  7. 生成したマテリアルをオブジェクトに適用

専用の UI はマテリアルエディタまたは Generator のエディタに表示されます。

UI

f:id:hecomi:20161011204814p:plain

  • Basics
    • 基本的な設定が表示されています。
  • Conditions
    • シェーダテンプレートで設定した項目が表示されており、ON/OFF することでシェーダの特性を切り替えることが出来ます。
  • Variables
    • 変数を設定する必要のある項目が表示されています。テキストフィールドかセレクトボックスで設定します。現状は組み込みではカリングの設定のみ。
  • Properties / Distance Function / Post Effect
    • コードを直接入力する場所です。シェーダテンプレートによって項目は増えたり減ったりします(現状は 3 つのみ)。
  • Material References
    • 作成したシェーダを利用する全てのマテリアルがここに表示されます。
  • ボタン
    • シェーダやマテリアルを生成するボタンがあります。
  • Material Properties
    • マテリアルの通常のインスペクタが表示されます。

Basics

f:id:hecomi:20161011231120p:plain

  • Shader Name
    • Raymarching/Shader Name」という形でシェーダが作成されます。
  • Shader Reference
    • シェーダを作成するとそのシェーダがセットされます。
  • Shader Template
    • スタンダードサーフェスシェーダ相当版がデフォルトでセットされていて、こちらを変更すると Conditions や Variables の項目も変わります。詳細は「シェーダテンプレートのカスタマイズ」の項で解説します。

Conditions

f:id:hecomi:20161011230914p:plain

  • Shadow Caster
    • レイマーチングした影を出力するかどうかを切り替えます。
  • Follow Object Scale
    • チェックするとレイマーチしたオブジェクトのスケールが直接変わるようになります。しない場合はスケールは空間として使われ、外側の世界のスケールと一致します。
  • Do Not Output Depth
    • フラグメントシェーダの出力に深度値を含めないようにします。つまりポリゴンの深度値がそのまま出力されます。デプスを使うポストエフェクトやライティングが正しく行われないデメリットはありますが速度面のメリットがあります。
  • Spherical Harmonics Per Pixel
    • 球面調和によるライティングをピクセルごとに行います。元のポリゴンから大きく形状が変化する場合、頂点単位で計算するライトプローブの影響が変に見えることがあります。その際、コスト高ですがピクセル単位で球面調和の計算を行うことで、より正しい結果が得らるようになります。
  • Camera Inside Object
    • 後述の Variables で Culling を Off にする状態と組み合わせることで、レイマーチしたオブジェクトの中に入り込めるようになります。オブジェクトにインターセクトしても裏側が見える、という状態にはならずボリュームのある図形として描かれます。詳細は後述します。
  • Fallback To Standard Shader
    • Standard Shader へフォールバックします。Shadow Caster が Off の状態でチェックを入れると、ポリゴンの影が表示されるようになります。また、インスペクタに正しくプレビューが表示されるようになります。

Variables

f:id:hecomi:20161011231133p:plain

現状は先ほど紹介したカリングの項目だけ追加しています。将来的に何か追加されるかもしれません。また、自身でも後述するシェーダテンプレートの記法に則ってテンプレートを作成すれば追加することが出来ます。

  • Culling
    • FrontBackOff の 3 つがあります。基本的にはデフォルトの Back で良いと思います。

Properties / Distance Function / Post Effect

f:id:hecomi:20161011221548p:plain

シンタックスハイライト付きの TextArea でコードを記述できます。また、出力されたシェーダファイルを直接編集してもこちらに反映されるので、キーバインド等が気に入らない場合はそういった運用でも良いと思います(ただし同期には、「// @block DistanceFunction ~ // @endblock といったコメントを残しておく必要があります」)。

Properties はシェーダの Properties ブロックの中に直接組み込まれ、追加のプロパティを設定することが出来ます。

Distance Function は距離関数を記入します。使用できる関数や変数は「Raymarching/Shaders/Include」に含まれる「Math.cginc」や「Primitives.cginc」をご参照下さい。

Post Effect はレイマーチング終了後に呼ばれる関数です。PostEffectOutput は Shader Template が Standard の場合は SurfaceOutputStandard になります。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;
};

ボタン

f:id:hecomi:20161011231026p:plain

  • Export Shader
    • シェーダを出力するボタンです。 「Ctrl + R」でも行うことが出来ます。
  • Create Material
    • 該当のシェーダを利用してマテリアルを生成します。Project ウィンドウでシェーダを選択して生成するのと同じ挙動です。
  • Update Template
    • 後述のシェーダテンプレートファイルを変更した際に再読込します。
  • Reconvet All
    • 全てのシェーダを再出力します。

Material Properties

f:id:hecomi:20161011220526p:plain

組み込みのプロパティと Properties のエディタで追加したプロパティが追加されます。組み込みのプロパティは PBS は物理ベースシェーディングのパラメタ、Raymarching は次のような値です。

  • Loop
    • レイマーチングの計算に使うループ数。多いほど奥までレイが到達して正しい形状になるが、ループ数が増えるため負荷も高い
  • Minimum Distance
    • レイマーチングを終了する最短距離。小さいほど正確な図形になるが、ループ数も増える。
  • Shadow Loop
    • シャドウの計算のためのループ数。図形に因っては小さい値でもそれっぽく見える。
  • Shadow Minimum Distance
    • こちらも同最短距離。大きい値でもそれっぽく見える。

その他

形状

利用できる形状は基本的には Cube または Sphere になります。それぞれの形状に応じて RaymarchingObject コンポーネントshape フィールドを設定して下さい。これはレイが図形を貫通したかどうかの判定を数式で行っているためです(ポリゴン貫通判定が出来ないため)。None を指定すると無限遠まで図形を描画する形になります。

オブジェクト進入

少し説明のところで触れましたが、Culling を Off に設定し、Camera Inside Object フラグを On にするとオブジェクトの中に入ることが出来ます(動画参照)。レイ開始点がオブジェクト内部にいるかを判定し、その場合はレイの開始点をカメラの Near Clip への位置へと移動させることによって実現しています。レイ開始点がレイマーチングしたオブジェクトの内部にいる時はその位置のデプスを出力してレイの逆方向の法線を出力するので、内部にめり込むとポリゴンのときのように向こう側が見えるわけではなく、ボリュームのある形状にめり込んだような形になります。ただし、判定分の処理負荷は増すので注意が必要です。

個人的には、ステージをレイマーチングで作る際に、カメラまたはライトの Command Buffer を使ったレイマーチングだと影のレンダリングパス(Shadows.RenderJob)に組み込めないのですが、こちらだと ShadowCaster に入れるだけでいい感じにやってくれて、無限でなく範囲も制限できる分、有利かなと思っています。

仕組み

コードエディタ

シンタックスハイライト付きのエディタを作るのも苦労しました…。詳細は別エントリにまとめますが、方法としては 2 枚の TextArea を重ね、フォーカスの合う側は透明の文字で描き、描画側はリッチテキストで正規表現でハイライトして表示します。

シリアライズ・デシリアライズ

はじめは「いけにえと雪のセツナ」のロジカルビートさんのチームがやられていたシェーダ自動生成と同じようにやろうと考えました。

しかしながら問題になったのがデータの保存で、記述した距離関数や諸々のパラメタを保存しておいて、後でポチポチ変更しながらトライ・アンド・エラーしたかったので、何かしら保存機構を入れる必要がありました。ただ自前でシリアライズ・デシリアライズをすると大変すぎるので、Unity のシリアライズ機構に頼りたかったため、別の方法を探し、SerializedObject を使うことにしました。

SerializedObject は拡張エディタを簡単に作れるので、保存に必要なパラメタを適当に pulbic にしておけば、serializedObject.ApplyModifiedProperties() するだけで Redo、Undo 可能で且つ保存される変更が簡単に実現できます。

ShaderGUI を使うとシェーダやマテリアルのインスペクタを改造できますが、これだけだと Unity のシリアライズに頼ることが出来ない(自由なパラメタを保存できる対象のアセットがない)ため、SeralizedObject のエディタを内部で生成して表示する形式にしました。これによりマテリアルを選択した時やゲームオブジェクトを選択したときにも編集用の UI が表示されます。

シェーダテンプレートのカスタマイズ

Raymarching/Editor/Resources/ShaderTemplates/ディレクトリ下に置かれているテキストファイルが Shader Template セレクトボックスに表示されていて、Standard Surface Shader 相当版(ライトマップなども適切にライティングされる)と直接 G-Buffer の出力に書き出す版(ライトマップなどは反映されないがちょっと軽い)がデフォルトで入っています。ここに自分のテンプレートを追加すると、セレクトボックスに自動で表示されるようになります。

テンプレートでは以下の文法が使用できます。

// IF 文、Conditions に「Hoge Hoge」がトグルとして表示される。
// チェックされた時だけ書き出し対象になる
@if HogeHoge
#define HOGEHOGE
@endif 

// else も追加でき、デフォルト値も指定可能
@if HogeHoge2 : false
#define HOGEHOGE2
@else
#define HOGEHOGE3
@endif

// 変数文、<> で囲んだ名前が Variables に表示され、テキストフィールドで書き換えられる。
// = でデフォルトを指定可能。 = | でセレクトボックスになる
#define Hoge <Hoge>
#define Fuga <Fuga=Hoge> // 
#define Piyo <Piyo=Hoge|Fuga|Piyo>

// ブロック文、囲まれた場所がコードエディタとして表示される
// 出力されたシェーダにも // @block ~ // @endblock として書き出され、
// そちらを編集するとエディタ上にフィードバックされる
@block Moge
float Moge2() { return _Move * _Moge; }
@endblock

組み込みのテンプレートはこんな感じになっています。

Shader "Raymarching/<Name>"
{

Properties
{
    [Header(PBS)]
    _Color("Color", Color) = (1.0, 1.0, 1.0, 1.0)
    _Metallic("Metallic", Range(0.0, 1.0)) = 0.5
    _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5

    [Header(Raymarching Settings)]
    _Loop("Loop", Range(1, 100)) = 30
    _MinDistance("Minimum Distance", Range(0.001, 0.1)) = 0.01
@if ShadowCaster : true
    _ShadowLoop("Shadow Loop", Range(1, 100)) = 10
    _ShadowMinDistance("Shadow Minimum Distance", Range(0.001, 0.1)) = 0.01
@endif

@block Properties
    _Color2("Color2", Color) = (1.0, 1.0, 1.0, 1.0)
@endblock
}

SubShader
{

Tags
{
    "RenderType" = "Opaque"
    "DisableBatching" = "True"
}

Cull <Culling=Back|Front|Off>

CGINCLUDE

@if FollowObjectScale : false
#define OBJECT_SCALE
@endif
@if DoNotOutputDepth : false
#define DO_NOT_OUTPUT_DEPTH
@endif
@if SphericalHarmonicsPerPixel : true
#define SPHERICAL_HARMONICS_PER_PIXEL
@endif
@if CameraInsideObject : false
#define CAMERA_INSIDE_OBJECT
@endif

#define DISTANCE_FUNCTION DistanceFunction
#define POST_EFFECT PostEffect
#define PostEffectOutput SurfaceOutputStandard

#include "<RaymarchingShaderDirectory>/Common.cginc"

@block DistanceFunction
inline float DistanceFunction(float3 pos)
{
    return Sphere(pos, 0.5);
}
@endblock

@block PostEffect
inline void PostEffect(RaymarchInfo ray, inout PostEffectOutput o)
{
}
@endblock

#include "<RaymarchingShaderDirectory>/Raymarching.cginc"

ENDCG

Pass
{
    Tags { "LightMode" = "Deferred" }

    Stencil
    {
        Comp Always
        Pass Replace
        Ref 128
    }

    CGPROGRAM
    #include "<RaymarchingShaderDirectory>/VertFragStandardObject.cginc"
    #pragma target 3.0
    #pragma vertex Vert
    #pragma fragment Frag
    #pragma multi_compile_prepassfinal
    #pragma multi_compile OBJECT_SHAPE_CUBE OBJECT_SHAPE_SPHERE ___
    #pragma exclude_renderers nomrt
    ENDCG
}

@if ShadowCaster
Pass
{
    Tags { "LightMode" = "ShadowCaster" }

    CGPROGRAM
    #include "<RaymarchingShaderDirectory>/VertFragShadowObject.cginc"
    #pragma target 3.0
    #pragma vertex Vert
    #pragma fragment Frag
    #pragma multi_compile_shadowcaster
    #pragma multi_compile OBJECT_SHAPE_CUBE OBJECT_SHAPE_SPHERE ___
    #pragma fragmentoption ARB_precision_hint_fastest
    ENDCG
}
@endif

}

@if FallbackToStandardShader : true
Fallback "Raymarching/Fallbacks/StandardSurfaceShader"
@else
Fallback Off
@endif

CustomEditor "Raymarching.MaterialEditor"

}

おわりに

なるべく汎用的になるようにしたので、テンプレートを変えればレイマーチング以外にも通常のシェーダジェネレータとしても使えると思います。本アセットを作成する上で得られた知見(コード書き換え、シンタックスハイライト付きエディタ等)は、別エントリで紹介したいと思います。