UnityでRaymarching
まえがき
タイトルの通りで、UnityでRaymarchingを試してみました。以下でリポジトリを公開しています。
既にやっている方のを真似させて頂いただけですので、詳しく知りたい方はid:i-saintさんとid:hecomiさんの記事を参考にしてください。 この記事の目的は、実装してみて必要になった知識を自分なりにまとめてメモしておくことです。
今回行うRaymarchingでは、今まで記事として残してきたUnityのレンダリングパイプラインやシェーダーに関する知識が必要です。
- Deferred Shadingについてまとめてみた - しゅみぷろ
- Forward Renderingについてまとめてみた - しゅみぷろ
- ForwardRenderingとDeferredShading - しゅみぷろ
- Unityのシェーダーまとめスライドを作ってみた - しゅみぷろ
UnityCG.cgincの定義値は以下を参考にしました。
座標変換に関して参考にさせていただいた記事が以下になります。
今回の手法ではCommandBufferを用いるのですが、これに関しては以下の記事を参考にさせていただきました。
また、サンプルとして掲載しているものに使用した距離関数はid:i-saintさんの記事で紹介されていた以下のサイトのモノをほぼそのまま使わせていただきました。
Raymarchingについてはid:hecomiさんの記事で紹介されていた以下のサイトを参考にしました。
wgld.org 全能感UP! GLSLで進めレイマーチング « demoscene.jp
やったことを大雑把におさらいしてみる
ここでは順を追って、何から実装していったのかをメモしておきます。 あくまで大雑把なおさらいなので、詳細は後述してます。
1. Raymarchingで球を描画してみる
まずはじめにやったのは、Raymarchingによる球の描画です。
細かい注意点などは参考にさせて頂いた記事の方で詳しく書いてくれていたので、そちらを参考に書いていきました。
余談というかしょうもない話なのですが、実はこの段階で何故かDeferredShadingでグラフィックスドライバが応答停止してしまうという問題に遭遇しました(4時間持って行かれました...)。本当に単純なミスなのですが、レイを飛ばすループ変数のインクリメント部分で、誤ってループ用の変数ではなくフラグメントシェーダーのパラメーター変数iに対してインクリメントを行ってしまっていたのが原因でした。死にたくなりました。皆さんもこんなことがないように注意しましょうね(無い)...。
2. 距離感数をいじってみる
次に、距離関数を色々と弄ってみました。以後サンプルとして使用しているのはGLSL Sandboxのコードをほぼそのまま使っています。
3. シェーダーのリファクタリングとバリアント作成
ここまで1つのシェーダーファイルにすべての処理をコリゴリ書き込んでいました。 なので、シェーダーをの処理をいくつかのincludeファイルに分割し、距離関数についてはバリアントを定義することで複数のシェーダーファイルに分割させました。
KeywordEnum属性でシェーダーの変更ができるようにしてあります。インスペクターからマテリアルのドロップダウンリストを変更することで距離関数のアルゴリズムを変更しています。
4. 陰影を確認してみる
オブジェクトをシーンに配置し、影の描画処理が行われているかを確かめました。
ちゃんと影が描画されているのが確認できると思います。
5. SceneビューでのRaymarching描画を廃止
先ほどまでの工程ではCommandBufferをカメラに追加する処理をOnWillRenderObjectによって行っていました。OnWillRenderObjectは、ExecuteInEditMode属性が付加されているスクリプトだとSceneビューを映し出すカメラについても呼び出しを行ってくれているみたいで、参考にしたコードではCommandBufferとCommandBufferを追加したカメラをDictionaryとして保持し、OnWillRenderObjectが呼び出されるすべてのカメラがRaymarchingレンダリングの対象となるように作られていました。
Raymarchingによる描画が複雑でなければこのままの方がやりやすくて良いのですが、距離関数によってはライトやその他のオブジェクトの配置が難しくなってしまいました。なので、SceneビューとGameビュー双方を見ながらシーンを構成していけるように、カメラ用のスクリプトの変更と追加を行いました。
球の描画について
ここでは球を描画するまでの流れと実装について書いていきます。
まずやったことは、Quadを生成し、CommandBufferでG-Bufferパス後にこのQuadを描画させることです。
このQuadのレンダリングには独自で定義したMRTによるG-Bufferの生成を行うシェーダーを用います。
Lightingパス前にこれを行うことで、ライティングに関してはUnity標準の一貫したライティングが施されるようになります。
つまり、このQuadのレンダリング工程ではG-Bufferを好きなように創って、あとのライティングなどの処理はUnityに移譲するといったことをやっています。
以下のスクリプトをカメラにアタッチすることで実現しています。
using System; using UnityEngine; using UnityEngine.Rendering; [ExecuteInEditMode, RequireComponent(typeof(Camera))] public class RaymarchingRenderer : MonoBehaviour, IDisposable { private const CameraEvent RENDER_PASS = CameraEvent.AfterGBuffer; private Camera cam; private CommandBuffer command; private Mesh quad; [SerializeField] private Material material = null; public void Dispose() { if(cam != null && command != null) cam.RemoveCommandBuffer(RENDER_PASS, command); cam = null; command = null; } private Mesh CreateQuad() { var mesh = new Mesh(); mesh.vertices = new Vector3[4] { new Vector3( 1.0f , 1.0f, 0.0f), new Vector3(-1.0f , 1.0f, 0.0f), new Vector3(-1.0f ,-1.0f, 0.0f), new Vector3( 1.0f ,-1.0f, 0.0f), }; mesh.triangles = new int[6] { 0, 1, 2, 2, 3, 0 }; return mesh; } private void OnDestroy() { Dispose(); } private void OnDisable() { Dispose(); } private void OnPreRender() { if(cam != null) return; if(quad == null) quad = CreateQuad(); cam = GetComponent<Camera>(); var com = new CommandBuffer(); com.name = "Raymarching"; com.DrawMesh(quad, Matrix4x4.identity, material, 0, 0); cam.AddCommandBuffer(RENDER_PASS, com); command = com; } }
次に、MRTでG-Bufferを出力するシェーダーを書きました。
Shader "Raymarching/e#23658" { SubShader { Pass { Tags { "LightMode" = "Deferred" } Cull Off Stencil { Comp Always Pass Replace Ref 128 } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 3.0 #include "Raymarching.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float4 screen : TEXCOORD0; }; //MRTにより出力するG-Buffer struct gbuffer { half4 diffuse : SV_Target0; // rgb: diffuse, a: occlusion half4 specular : SV_Target1; // rgb: specular, a: smoothness half4 normal : SV_Target2; // rgb: normal, a: unused half4 emission : SV_Target3; // rgb: emission, a: unused float depth : SV_Depth; }; v2f vert(appdata v) { v2f o; o.vertex = v.vertex; o.screen = v.vertex; return o; } gbuffer frag(v2f i) { ....省略(ここでG-Bufferに出力するデータを計算).... gbuffer o; //この例では適当な値を入れてるだけ o.diffuse = float4(0.5, 0.5, 0.5, 0.5); o.specular = float4(1, 1, 1, 1); o.emission = float4(0.1, 0.1, 0.1, 1); o.depth = depth; o.normal = float4(normal, 1); return o; } ENDCG } } }
省略している部分もありますが、概ねこういった感じでG-Bufferを出力します。
注意:ステンシルバッファの7ビット目(128)が立っていない場合、描画は無視されます。描画を行うには、ステンシルテストに合格させ、7ビット目に書き込みを行います。
ここまででG-Bufferへの書き込みの準備が整いました。 今回はRaymarching法で計算したデータをG-Bufferへの出力にします。書き込むデータは、距離関数を用いて計算される仮想的な空間のモデル情報です。
図の灰色で塗りつぶされた空間は距離関数で表現される実世界(Unityで言うScene上)には存在しない空間です。この空間の座標系とUnityの座標系を一致させてあげる必要があります。この部分はid:i-saintさんの記事で詳しく紹介されています。
raymarching でレンダリングするオブジェクトと Unity のシーン上のオブジェクトの位置関係を正しくするには、当然ながら raymarching で用いる座標系と Unity のシーンの座標系を一致させる必要があります。このために必要な情報は、カメラの位置、正面方向、上方向、focal length ( 1.0/tan(fovy*0.5) ) になります。結論としては以下のコードで必要な情報を抽出できます。
float3 GetCameraPosition() { return _WorldSpaceCameraPos; } float3 GetCameraForward() { return -UNITY_MATRIX_V[2].xyz; } float3 GetCameraUp() { return UNITY_MATRIX_V[1].xyz; } float3 GetCameraRight() { return UNITY_MATRIX_V[0].xyz; } float GetCameraFocalLength() { return abs(UNITY_MATRIX_P[1][1]); }
これらをまとめた定義は以下になります。
#ifndef RAYMARCHING #define RAYMARCHING #include "UnityCG.cginc" #include "DistanceFunctionVariant.cginc" //ワールド座標系のカメラの位置 float3 get_cam_pos() { return _WorldSpaceCameraPos; } //変換行列からカメラの情報を取得 float3 get_cam_fwd() { return -UNITY_MATRIX_V[2].xyz; } float3 get_cam_up() { return UNITY_MATRIX_V[1].xyz; } float3 get_cam_right() { return UNITY_MATRIX_V[0].xyz; } float get_cam_focal_len() { return abs(UNITY_MATRIX_P[1][1]); } //_ProjectionParamsのyはカメラのClippingPlanes[Near]、zは[Far] //カメラがレンダリングする距離を算出する float get_cam_visibl_len() { return _ProjectionParams.z - _ProjectionParams.y; } //レイの進むべき方向を算出する //カメラ -> レンダリングするピクセル float3 compute_ray_dir(float4 screen) { //UNITY_UV_START_AT_TOPはV値のトップ位置 //Direct3Dでは1、 OpenGL系では0 #if UNITY_UV_STARTS_AT_TOP //Direct3Dの場合、yを反転させることで凌ぐ screen.y *= -1.0; #endif //_ScreenParamsのxはレンダリングターゲットのピクセルの幅、yはピクセルの高さ screen.x *= _ScreenParams.x / _ScreenParams.y; //カメラの情報とピクセル位置から カメラ -> ピクセル のレイの方向ベクトルを求める float3 camDir = get_cam_fwd(); float3 camUp = get_cam_up(); float3 camSide = get_cam_right(); float focalLen = get_cam_focal_len(); return normalize((camSide * screen.x) + (camUp * screen.y) + (camDir * focalLen)); } //ある位置(レイがオブジェクトにヒットした位置)の深度値を取得する。 //depthの算出方法はDirect3DとOpenGL系で違う。 float compute_depth(float4 pos) { #if UNITY_UV_STARTS_AT_TOP return pos.z / pos.w; #else return (pos.z / pos.w) * 0.5 + 0.5; #endif } //ある位置(レイがオブジェクトにヒットした位置)におけるオブジェクトの法線を取得する。 //x,y,zは偏微分によって求められる。 //G-Bfferのnormal(法線バッファ)は符号なしデータ(RGBA 10, 10, 10, 2 bits)で格納されるため //格納時は *0.5+0.5 //取得時は *2.0-1.0 //してやる必要がある(格納時に負数をなくし、取得時に再現する)。 float3 compute_normal(float3 pos) { const float delta = 0.001; return normalize(float3( distance_func(pos + float3(delta, 0.0, 0.0)) - distance_func(pos + float3(-delta, 0.0, 0.0)), distance_func(pos + float3(0.0, delta, 0.0)) - distance_func(pos + float3(0.0, -delta, 0.0)), distance_func(pos + float3(0.0, 0.0, delta)) - distance_func(pos + float3(0.0, 0.0, -delta)))) * 0.5 + 0.5; } #endif //RAYMARCHING
DistanceFunctionVariant.cgincファイルをincludeしていますが、これには後でDistanceFunctionを定義します。
compute_depth関数では、depthの算出方法がDirect3DとOpenGL系で違うため処理の分岐が必要です。id:hecomiさんの記事で書かれているのですが
#if defined(SHADER_TARGET_GLSL) return (vpPos.z / vpPos.w) * 0.5 + 0.5; #else return vpPos.z / vpPos.w; #endif
とした場合、Mac上(OpenGL 4.1)ではSHADER_TARGET_GLSLプリプロセッサーマクロが未定義になり、depthがおかしくなる(Direct3Dでのdepth算出をしてしまう)ようです。何故なのかはよくわかりませんが、UNITY_UV_STARTS_AT_TOPで判定することで凌いでいます(Mac、Win共に正常な動作をしました)。
normal関数ではオブジェクトにヒットした位置の近隣ピクセルの勾配から法線を推測して返しています。normalはG-Bufferに(RGBA 10, 10, 10, 2 bits)で格納されるため、格納時は 0.5+0.5、取得時は 2.0-1.0 する必要があります。
距離関数について
距離関数は位置を引数にとり、その位置から一番近いオブジェクトへの距離を返す関数です。0に近い値が返ってくればオブジェクトとの距離が近い( = Rayが接触した)と仮定することができます。 つまり引数に指定したある位置において0に近い値が返ってきたら、その位置情報をもとにオブジェクトを描画するようにします。
裏を返せば、描画させたい位置で0に近い値を返すような距離関数を作ればゴリ押しで好きなモデルを書くこともできます。
距離関数は今後色々と実験的に作っていこうと思っているので、面白いものが出来たら別記事で紹介させていただくかもしれません。
今回のサンプルで利用した距離関数のバリアントを定義したファイルは以下のようになりました。
/******************************* *http://glslsandbox.com/e#23658* ********************************/ #ifndef DISTANCE_FUNCTION #define DISTANCE_FUNCTION #ifndef PI #define PI 3.14159265359 #endif //PI float3 mod(float3 a, float3 b) { return frac(abs(a / b)) * abs(b); } float3 rotateX(float3 p, float angle) { float c = cos(angle); float s = sin(angle); return float3(p.x, c*p.y + s*p.z, -s*p.y + c*p.z); } float3 rotateY(float3 p, float angle) { float c = cos(angle); float s = sin(angle); return float3(c*p.x - s*p.z, p.y, s*p.x + c*p.z); } float3 rotateZ(float3 p, float angle) { float c = cos(angle); float s = sin(angle); return float3(c*p.x + s*p.y, -s*p.x + c*p.y, p.z); } float kaleidoscopic_IFS(float3 z) { const int FRACT_ITER = 20; float FRACT_SCALE = 1.8; float FRACT_OFFSET = 1.0; float c = 2.0; z.y = mod(z.y, c) - c / 2.0; z = rotateZ(z, PI / 2.0); float r; int n1 = 0; for (int n = 0; n < FRACT_ITER; n++) { float rotate = PI*0.5; z = rotateX(z, rotate); z = rotateY(z, rotate); z = rotateZ(z, rotate); z.xy = abs(z.xy); if (z.x + z.y < 0.0) z.xy = -z.yx; // fold 1 if (z.x + z.z < 0.0) z.xz = -z.zx; // fold 2 if (z.y + z.z < 0.0) z.zy = -z.yz; // fold 3 z = z*FRACT_SCALE - FRACT_OFFSET*(FRACT_SCALE - 1.0); } return (length(z)) * pow(FRACT_SCALE, -float(FRACT_ITER)); } float tglad_formula(float3 z0) { z0 = mod(z0, 2.); float mr = 0.25, mxr = 1.0; float4 scale = float4(-3.12, -3.12, -3.12, 3.12), p0 = float4(0.0, 1.59, -1.0, 0.0); float4 z = float4(z0, 1.0); for (int n = 0; n < 3; n++) { z.xyz = clamp(z.xyz, -0.94, 0.94)*2.0 - z.xyz; z *= scale / clamp(dot(z.xyz, z.xyz), mr, mxr)*1.; z += p0; } float dS = (length(max(abs(z.xyz) - float3(1.2, 49.0, 1.4), 0.0)) - 0.06) / z.w; return dS; } // distance function from Hartverdrahtet // ( http://www.pouet.net/prod.php?which=59086 ) float hartverdrahtet(float3 f) { float3 cs = float3(.808, .808, 1.167); float fs = 1.; float3 fc = float3(0, 0, 0); float fu = 10.; float fd = .763; // scene selection int i = mod(_Time.x / 2.0, 9.0); if (i == 0) cs.y = .58; if (i == 1) cs.xy = float2(0.5, 0.5); if (i == 2) cs.xy = float2(0.5, 0.5); if (i == 3) fu = 1.01, cs.x = .9; if (i == 4) fu = 1.01, cs.x = .9; if (i == 6) cs = float3(.5, .5, 1.04); if (i == 5) fu = .9; if (i == 7) fd = .7, fs = 1.34, cs.xy = float2(0.5, 0.5); if (i == 8) fc.z = -.38; //cs += sin(time)*0.2; float v = 1.; for (int loop = 0; loop < 12; loop++) { f = 2.*clamp(f, -cs, cs) - f; float c = max(fs / dot(f, f), 1.); f *= c; v *= c; f += fc; } float z = length(f.xy) - fu; return fd*max(z, abs(length(f.xy)*f.z) / sqrt(dot(f, f))) / abs(v); } float pseudo_kleinian(float3 p) { const float3 CSize = float3(0.92436, 0.90756, 0.92436); const float Size = 1.0; const float3 C = float3(0.0, 0.0, 0.0); float DEfactor = 1.; const float3 Offset = float3(0.0, 0.0, 0.0); float3 ap = p + 1.; for (int i = 0; i < 10; i++) { ap = p; p = 2.*clamp(p, -CSize, CSize) - p; float r2 = dot(p, p); float k = max(Size / r2, 1.); p *= k; DEfactor *= k; p += C; } float r = abs(0.5*abs(p.z - Offset.z) / DEfactor); return r; } float pseudo_knightyan(float3 p) { const float3 CSize = float3(0.63248, 0.78632, 0.775); float DEfactor = 1.; for (int i = 0; i < 6; i++) { p = 2.*clamp(p, -CSize, CSize) - p; float k = max(0.70968 / dot(p, p), 1.); p *= k; DEfactor *= k*1.1; } float rxy = length(p.xy); return max(rxy - 0.92784, abs(rxy*p.z) / length(p)) / DEfactor; } float map(float3 p) { #ifdef _MAP_KALEIDOSCOPIC_IFS return kaleidoscopic_IFS(p); #elif _MAP_TGLAD_FORMULA return tglad_formula(p); #elif _MAP_HARTVERDRAHTET return hartverdrahtet(p); #elif _MAP_PSEUDO_KLEINIAN return pseudo_kleinian(p); #elif _MAP_PSEUDO_KNIGHTYAN return pseudo_knightyan(p); #else return length(p) - 1; #endif } //空間内の点の位置を受け取り, 図形と点との最短距離を返す //この関数の値が0となる点の集合が図形の表面となる //つまり0に近い値を返した場合は描画されることになる float distance_func(float3 pos) { return map(pos); } #endif //DISTANCE_FUNCTION
シェーダーバリアントの利用に関して
DistanceFunctionのシェーダーバリアントですが、このプロジェクトではRaymarchingの確認を行えれば十分で、DistanceFunctionアルコリズムの切り替えに便利だと判断して使っています。
今後このバリアントを有効に使っていくために考えていることが、G-Bufferへの出力を決定するアルゴリズムのバリアントを作り、それと組み合わせて使っていくことです。