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

この記事は[Babylon.js Advent Calendar 2023](https://qiita.com/advent-calendar/2023/babylonjs)の16日目の記事です。

### はじめに

Babylon.js 6.0 で強化された機能の一つに[**Node Material Ray Marching**](https://blogs.windows.com/windowsdeveloper/2023/05/04/part-3-babylon-js-6-0-news-tools/)という記載があったのでどのようなものか調べてみました。

主にMediumの「[Ray Marching in the Babylon.js Node Material Editor](https://babylonjs.medium.com/ray-marching-in-the-babylon-js-node-material-editor-967b5b8c269c)」の記事および[GithubのPR](https://github.com/BabylonJS/Babylon.js/pull/13272)の内容をかいつまんだものになります。

### 実行結果

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

https://playground.babylonjs.com/#WUHY8K
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/7f5e07b1-881c-3a2f-a889-27b86d52ed16.png)](https://playground.babylonjs.com/#WUHY8K)

※ レイマーチングのコードのオリジナルは、Shadertoyの中の人として知られるiq氏の[Snail](https://www.shadertoy.com/view/ld3Gz2)です。

### レイマーチングとは？

レイマーチングは[距離関数と呼ばれる数学的な関数](https://iquilezles.org/articles/distfunctions/)を用いて形状を計算し描画する技術のことです。WebGLでは通常、フラグメントシェーダを用いて計算が行われます。
レイマーチングの作品は[Shadertoy](https://www.shadertoy.com/)などで見ることができます。

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

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

- [Shader Material](https://doc.babylonjs.com/features/featuresDeepDive/materials/shaders/shaderMaterial) … マテリアルとして独自の頂点シェーダとフラグメントシェーダを設定することが出来る機能
- [Procedural Texture](https://doc.babylonjs.com/toolsAndResources/assetLibraries/proceduralTexturesLibrary) … 柄やパターンなどテクスチャにシェーダを使う機能
- [Post Process](https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/usePostProcesses) … 描画の後処理としてシェーダを使う機能（画面全体にたいしてエフェクトを追加）

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

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

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

- 変更
    下記ブロックに「Generate only fragment code（フラグメントコードのみを生成）」のフラグが追加
    - [Lights ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.LightBlock) … フラグメント シェーダにライトを追加するために使用されるブロック
    - [PBRMetallicRoughness ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.PBRMetallicRoughnessBlock) … PBR マテリアルの作成に使用されるブロック
    - [Reflection ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.ReflectionBlock) … PBR マテリアルのリフレクション入力の為に使用されるブロック

- 追加
    - [FragDepth ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.FragDepthBlock) … フラグメント深度の書き込みに使用されるブロック
    - [ShadowMap ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.ShadowMapBlock) … シャドウマップ内で深度を生成するのに使用されるブロック
    - [Tri-Planar ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.TriPlanarBlock) … 3方向からプロジェクションマッピングを行うのに使用されるヘルパーブロック 
    - [Bi-Planar ブロック](https://doc.babylonjs.com/typedoc/classes/BABYLON.BiPlanarBlock) … 2方向からプロジェクションマッピングを行うのに使用されるヘルパーブロック

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

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

https://playground.babylonjs.com/#R2S08V
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/f5f0fa98-e9c4-eb85-42ed-66a4cebc9ce7.png)](https://playground.babylonjs.com/#R2S08V)

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

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

```JavaScript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/28af936f-1d29-f1d2-57d8-67372717289b.png)](https://nme.babylonjs.com/#GD8DSL#26)

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

```glsl
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つのノードは影を作るためのものです。
```JavaScript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26094/ec184e10-6bbd-7345-0ec5-d0166472d046.png)](https://nme.babylonjs.com/#47Y40D#6)

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

### おわりに

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

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

### 参考情報

[Part 3 - Babylon.js 6.0: News Tools - Windows Developer Blog](https://blogs.windows.com/windowsdeveloper/2023/05/04/part-3-babylon-js-6-0-news-tools/)
[Ray Marching in the Babylon.js Node Material Editor | Medium](https://babylonjs.medium.com/ray-marching-in-the-babylon-js-node-material-editor-967b5b8c269c)
[NME: multiple changes to support ray marching in the NME #13272](https://github.com/BabylonJS/Babylon.js/pull/13272)
[Node Material Editor Custom Blocks](https://doc.babylonjs.com/toolsAndResources/nme/customBlocks)
[Ray Marching Custom Block](https://nme.babylonjs.com/#GD8DSL#27)
[TriPlanar block example](https://playground.babylonjs.com/#M3QR7E#34)
[Snail Ray Marching Scene](https://playground.babylonjs.com/#M3QR7E#69)
[Inigo Quilez :: articles :: distance functions](https://iquilezles.org/articles/distfunctions/)
[Snail by Inigo Quilez on Shadertoy](https://www.shadertoy.com/view/ld3Gz2)