LoginSignup
3
1

レイマーチングをBabylon.jsのシーンに召喚する方法

Posted at

この記事はBabylon.js Advent Calendar 2023の16日目の記事です。

はじめに

Babylon.js 6.0 で強化された機能の一つにNode Material Ray Marchingという記載があったのでどのようなものか調べてみました。

主にMediumの「Ray Marching in the Babylon.js Node Material Editor」の記事およびGithubのPRの内容をかいつまんだものになります。

実行結果

以下は実行結果になります。Babylon.jsのシーンにレイマーチングのサンプルを組み合わせた結果になります。
あたかもカタツムリの3Dモデルがシーン内に読み込まれたかのように見えますが、実際は、レイマーチングの技術を利用してGPUで計算された結果が反映されています。

https://playground.babylonjs.com/#WUHY8K
image.png

※ レイマーチングのコードのオリジナルは、Shadertoyの中の人として知られるiq氏のSnailです。

レイマーチングとは?

レイマーチングは距離関数と呼ばれる数学的な関数を用いて形状を計算し描画する技術のことです。WebGLでは通常、フラグメントシェーダを用いて計算が行われます。
レイマーチングの作品はShadertoyなどで見ることができます。

Babylon.jsとレイマーチングについて

従来の Babylon.js でも独自のシェーダコードを組み込むことは出来た為、一応、レイマーチングを利用することはできました。

  • Shader Material … マテリアルとして独自の頂点シェーダとフラグメントシェーダを設定することが出来る機能
  • Procedural Texture … 柄やパターンなどテクスチャにシェーダを使う機能
  • Post Process … 描画の後処理としてシェーダを使う機能(画面全体にたいしてエフェクトを追加)

しかしながら、シェーダが適用される個所としてテクスチャの一部であったり、画面全体に対してだったりと、Babylon.js のシーンになじむ形での組み込みは実現できていませんでした。

ノードマテリアルでのレイマーチングのサポートとは?

Babylon.jsのNode Material Editorは、その名の通り、ノードベースのマテリアルエディタです。
Babylon.js 6.0 ではノードマテリアルでのレイマーチングのサポート強化として主に以下の機能が追加・変更されているようです。
主な変更点の1つは、いくつかのブロック(Lights、PBRMetallicRoughness、Reflection)がフラグメントシェーダ内のみでコード生成が行えるようになったことのようです。

  • 変更
    下記ブロックに「Generate only fragment code(フラグメントコードのみを生成)」のフラグが追加

  • 追加

    • FragDepth ブロック … フラグメント深度の書き込みに使用されるブロック
    • ShadowMap ブロック … シャドウマップ内で深度を生成するのに使用されるブロック
    • Tri-Planar ブロック … 3方向からプロジェクションマッピングを行うのに使用されるヘルパーブロック
    • Bi-Planar ブロック … 2方向からプロジェクションマッピングを行うのに使用されるヘルパーブロック

シンプルなレイマーチングの例

記事の最初に提示したサンプルは PBR マテリアルやテクスチャを用いている例であった為、少し複雑なものでした。
ここでは、説明を簡単にする為、テクスチャを用いないシンプルな例を提示したいと思います。

https://playground.babylonjs.com/#R2S08V
image.png

この Babylon.js Playground のサンプルでは ParseFromSnippetAsync() が2か所使われています。
引数で指定されている ID がノードを識別する ID となっており、1つがレイマーチングの形状を計算するノード、もう1つが影を計算するノードになります。

まずは1つ目のノードを見ていきましょう。

const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {diameter: 2, segments: 32}, scene);
const mat = await BABYLON.NodeMaterial.ParseFromSnippetAsync("#GD8DSL#26", scene);
sphere.material = mat;

赤枠の部分が RayMaching の本体となるノードです。

https://nme.babylonjs.com/#GD8DSL#26
image.png

RayMaching ブロックは CustomBlock として登録されており、CustomBlock の中身は GLSL のコードです。sdf() が形状を求めるための距離関数で球体と立方体が時間に応じて結合しているされているという感じのコードになっています。

float sdSphere( vec3 p, float s )
{
  return length(p)-s;
}

float sdRoundBox( vec3 p, vec3 b, float r )
{
  vec3 q = abs(p) - b;
  return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
}

float opSmoothUnion( float d1, float d2, float k ) {
    float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
    return mix( d2, d1, h ) - k*h*(1.0-h);
}

float sdf(vec3 p) {
    float ds = sdSphere(p, 0.4+0.3*sin(u_Time*1.5));
    float dc = sdRoundBox(p, vec3(1.4, 0.5, 0.2)/2., 0.2+0.1*sin(u_Time*2.2));
    return opSmoothUnion(ds, dc, 0.05);
}

vec3 calcNormal( in vec3 p )
{
    const float h = 0.0001;
    const vec2 k = vec2(1,-1);
    return normalize( k.xyy*sdf( p + k.xyy*h ) + 
                      k.yyx*sdf( p + k.yyx*h ) + 
                      k.yxy*sdf( p + k.yxy*h ) + 
                      k.xxx*sdf( p + k.xxx*h ) );
}

void raymarch(vec3 worldPosIn, vec3 worldCameraPos, float time, mat4 world, out vec4 worldPos, out vec4 worldNormal) {
    #if defined(SM_LIGHTTYPE_DIRECTIONALLIGHT)
        vec3 ro = worldPosIn;
        vec3 rd = normalize(lightDataSM);
    #else
        vec3 ro = worldCameraPos;
        vec3 rd = normalize(worldPosIn - ro);
    #endif
    float dO = 0.;
    mat4 invWorld = inverse(world);
    for(int i=0;i<100;i++)
    {
        vec4 p = vec4(ro + rd * dO, 1.0);
        float ds = sdf((invWorld * p).xyz);
        dO += ds;
        if(dO > 100. || ds < 0.01) break;
    }
    if (dO > 100.) discard;
    worldPos = vec4(ro + rd * dO, 1.);
    worldNormal = vec4(calcNormal((invWorld * worldPos).xyz), 0.);
}

もう1つのノードは影を作るためのものです。

const matShadow = await BABYLON.NodeMaterial.ParseFromSnippetAsync("#47Y40D#6", scene);
sphere.material.shadowDepthWrapper = new BABYLON.ShadowDepthWrapper(matShadow, scene, { standalone: true, doNotInjectCode: true });

https://nme.babylonjs.com/#47Y40D#6
image.png

上記のコードを実行することにより、球体と立方体が時間とともに結合し、その結果、影が床に投影されるという結果になります。

おわりに

2023年4月末にリリースされた Babylon.js v6.0 は、物理エンジンの Havok が採用されたことが大きく取り上げられていた為、他の改善箇所があまり周知されていなかったのではないかと思います。
その機能の1つが Node Material Editor へのレイマーチングサポートです。

レイマーチング自体、数学的知識を要する為、各自のプロジェクトに組み込むには少しハードルが高いかもしれません。しかしながら見栄えとしてはかなり目を引くものとなっていますので、うまく使いこなせばインパクトのある作品ができるのではと思いました。

参考情報

Part 3 - Babylon.js 6.0: News Tools - Windows Developer Blog
Ray Marching in the Babylon.js Node Material Editor | Medium
NME: multiple changes to support ray marching in the NME #13272
Node Material Editor Custom Blocks
Ray Marching Custom Block
TriPlanar block example
Snail Ray Marching Scene
Inigo Quilez :: articles :: distance functions
Snail by Inigo Quilez on Shadertoy

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1