はじめに
レイマーチングとはポリゴンではなく、距離関数(distance function)と呼ばれる数式を元にオブジェクトをレンダリングする方法です。ポリゴンを使わないのでモーフィングや複雑な図形もシンプルな数式で記述することが可能で、GPU のコストが高いという欠点もありますが、面白い見た目を簡単に作り出すことが出来ます。
例えばこれらはキューブのポリゴン(12 ポリゴン)を変形させたものです。
最近のエントリもレイマーチングの内容を中心に書いてきました。というのもレイマーチングを積極的に使ったゲームを作りたいなー、と思っているからです。しかしながら、オブジェクトの形状ごとにシェーダを作成しなければならず、これらを一つ一つ作るのは非常に面倒くさいです。一つ一つのシェーダは基本的には数式以外同じコードを流用するのにも関わらず、一部を書き換えるために多量のシェーダを作るのは、後で大幅な修正が必要になったときに、メンテナンス性の観点からも良くありません。
そこで、数式部分と最低限の設定だけを入力すれば、簡単にレイマーチングするシェーダを量産できるシェーダ群およびエディタ拡張を作成してみました。アセットは GitHub で MIT で公開していて、安定してきたら Asset Store にも出したいな、と思っています。
本エントリでは、その紹介と使い方を説明いたします。
デモ
元となるポリゴンの空間でレイマーチを行う id:i-saint さんの手法(object space raymarching - primitive: blog)を採用しています。詳細は前回のエントリをご参照下さい。また、解説は省略しますが、CommandBuffer
を使った視界全体でのレイマーチもあります。詳細は Examples の Mod World
をご覧ください。
環境
Deferred もレイマーチングも重い処理なので、グラボを搭載していないノート PC 等では十分なパフォーマンスが発揮されないのでご注意下さい。
ダウンロードとテスト
- GitHub - hecomi/uRaymarching: Raymarching Shader Generator in Unity
- Releases · hecomi/uRaymarching · GitHub
はじめはプロジェクト全体または Examples 付きの .unitypackage をダウンロードしてお試しください。インポートして「Raymarching > Examples > Scenes > Hex Floor」を見てみると、次のように表示されると思います。
利用方法
- Rendering Path が「Deferred」になっていることを確認
- Project ウィンドウから「Create > Shader > Raymarching Shader Generator」でシェーダ生成用のアセットを作成
- シェーダの名前を記入
- 好きな Distance Function を記入して Export ボタンを押下するか「Ctrl + R」でシェーダを生成
- シェーダからマテリアルを生成(Create Material ボタンでも可能)
RaymarchingObject
コンポーネントを作成したオブジェクト(Cube または Sphere)にアタッチ- 生成したマテリアルをオブジェクトに適用
専用の UI はマテリアルエディタまたは Generator のエディタに表示されます。
UI
- Basics
- 基本的な設定が表示されています。
- Conditions
- シェーダテンプレートで設定した項目が表示されており、ON/OFF することでシェーダの特性を切り替えることが出来ます。
- Variables
- 変数を設定する必要のある項目が表示されています。テキストフィールドかセレクトボックスで設定します。現状は組み込みではカリングの設定のみ。
- Properties / Distance Function / Post Effect
- コードを直接入力する場所です。シェーダテンプレートによって項目は増えたり減ったりします(現状は 3 つのみ)。
- Material References
- 作成したシェーダを利用する全てのマテリアルがここに表示されます。
- ボタン
- シェーダやマテリアルを生成するボタンがあります。
- Material Properties
- マテリアルの通常のインスペクタが表示されます。
Basics
- Shader Name
- 「Raymarching/Shader Name」という形でシェーダが作成されます。
- Shader Reference
- シェーダを作成するとそのシェーダがセットされます。
- Shader Template
Conditions
- 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
現状は先ほど紹介したカリングの項目だけ追加しています。将来的に何か追加されるかもしれません。また、自身でも後述するシェーダテンプレートの記法に則ってテンプレートを作成すれば追加することが出来ます。
- Culling
Front
、Back
、Off
の 3 つがあります。基本的にはデフォルトのBack
で良いと思います。
Properties / Distance Function / Post Effect
シンタックスハイライト付きの 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; };
ボタン
- Export Shader
- シェーダを出力するボタンです。 「Ctrl + R」でも行うことが出来ます。
- Create Material
- 該当のシェーダを利用してマテリアルを生成します。Project ウィンドウでシェーダを選択して生成するのと同じ挙動です。
- Update Template
- 後述のシェーダテンプレートファイルを変更した際に再読込します。
- Reconvet All
- 全てのシェーダを再出力します。
Material Properties
組み込みのプロパティと 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
を重ね、フォーカスの合う側は透明の文字で描き、描画側はリッチテキストで正規表現でハイライトして表示します。
- uRaymarching/CodeEditor.cs at b28f6f19c2e7ba4f30b930bb0a10e18ca7f64d32 · hecomi/uRaymarching · GitHub
- uRaymarching/ShaderSyntax.cs at b28f6f19c2e7ba4f30b930bb0a10e18ca7f64d32 · hecomi/uRaymarching · GitHub
シリアライズ・デシリアライズ
はじめは「いけにえと雪のセツナ」のロジカルビートさんのチームがやられていたシェーダ自動生成と同じようにやろうと考えました。
しかしながら問題になったのがデータの保存で、記述した距離関数や諸々のパラメタを保存しておいて、後でポチポチ変更しながらトライ・アンド・エラーしたかったので、何かしら保存機構を入れる必要がありました。ただ自前でシリアライズ・デシリアライズをすると大変すぎるので、Unity のシリアライズ機構に頼りたかったため、別の方法を探し、SerializedObject
を使うことにしました。
SerializedObject
は拡張エディタを簡単に作れるので、保存に必要なパラメタを適当に pulbic
にしておけば、serializedObject.ApplyModifiedProperties()
するだけで Redo、Undo 可能で且つ保存される変更が簡単に実現できます。
- uRaymarching/Generator.cs at b28f6f19c2e7ba4f30b930bb0a10e18ca7f64d32 · hecomi/uRaymarching · GitHub
- uRaymarching/GeneratorEditor.cs at b28f6f19c2e7ba4f30b930bb0a10e18ca7f64d32 · hecomi/uRaymarching · GitHub
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" }
おわりに
なるべく汎用的になるようにしたので、テンプレートを変えればレイマーチング以外にも通常のシェーダジェネレータとしても使えると思います。本アセットを作成する上で得られた知見(コード書き換え、シンタックスハイライト付きエディタ等)は、別エントリで紹介したいと思います。