🍱

Vite環境のBabylon.jsでシェーダを読み込むtips

2022/12/10に公開

はじめに

この記事はBabylon.js アドベントカレンダー 2022の 11 日目の記事です。

TL;DR

Vite では?rawサフィックスを使うことでアセットを文字列としてインポートできるので、それを ShaderMaterial に渡すことでシェーダをインポートできる。

本記事の概要と対象読者

本記事では Vite + TypeScript で環境構築した Babylon.js プロジェクトでシェーダをインポートする方法をご紹介します。
とても軽い内容で半分筆者の備忘録的な感じですが、お付き合いいただけると嬉しいです。

本記事では Babylon.js や TypeScript・Vite・シェーダに関する初歩的な説明をしませんので、
お読みになる際にはこれらの技術スタックについて知っていることが望ましいです。
しかしそこまで難しい内容を扱うわけではないので、どうぞお気軽にご覧ください。

検証環境

検証環境は次の通りです。

環境 バージョン
Node.js v16.18.0
TypeScript v4.6.4
vite v3.2.3
@babylonjs/core v5.34.0

サンプルプロジェクト

GitHub にてサンプルプロジェクトを公開しておりますので、そちらも合わせてご覧になってください。
https://github.com/drumath2237/babylon-shader-import-sample

デモも見られます。
https://drumath2237.github.io/babylon-shader-import-sample/

シェーダ読み込みの選択肢

Babylon.js ではシェーダの読み込み方法が何通りかあり、下記 docs ページに列挙されています。この中で紹介されている方法を軽く紹介します(どれも今回紹介する方法ではありません)。

https://doc.babylonjs.com/features/featuresDeepDive/materials/shaders/shaderCodeInBjs

CYOS からダウンロードする方法

Babylon.js では CYOS という Playground のシェーダ版みたいなツールがあり、それを使うことでプレビューを見ながら Vertex シェーダと Fragment シェーダを書いていくことが可能です。
https://cyos.babylonjs.com/#8U2ERV#1
また CYOS は編集したシェーダコードをダウンロードできます。プレビュー用のテクスチャファイルと HTML ファイル 1 つが.zip で固められたものがダウンロードされます。それを解凍して HTML にインラインで記述された部分を流用すればシェーダのインポートができますね。
しかしこの方法、なかなか手間です。ローカルプロジェクトへシェーダを反映するイテレーションがかなり回しにくそうなので個人的にあまり好みではありません。

script タグのなかに書く

HTML のスクリプトタグにシェーダコードを記述して、それを Babylon.js で参照するという仕組みがあります。
以下は docs に載っているサンプルコードの引用です。

script タグからシェーダをインポートするサンプルコード
scriptタグの中に書かれたシェーダコード
<script type="application/vertexShader" id="vertexShaderCode">
    #ifdef GL_ES
        precision highp float;
    #endif

    // Attributes
    attribute vec3 position;
    attribute vec2 uv;

    // Uniforms
    uniform mat4 worldViewProjection;

    // Normal
    varying vec2 vUV;

    void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);

    vUV = uv;
    }
</script>

<script type="application/fragmentShader" id="fragmentShaderCode">
    #ifdef GL_ES
        precision mediump float;
    #endif

    varying vec2 vUV;

    uniform sampler2D textureSampler;

    void main(void) {
        gl_FragColor = texture2D(textureSampler, vUV);
    }
</script>
タグに書かれたシェーダをインポートするコード
var shaderMaterial = new BABYLON.ShaderMaterial(
  "shader",
  scene,
  {
    vertexElement: "vertexShaderCode",
    fragmentElement: "fragmentShaderCode",
  },
  {
    attributes: ["position", "normal", "uv"],
    uniforms: ["world", "worldView", "worldViewProjection", "view", "projection"],
  },
);

HTML の中で js コードも記述しているような場合は便利そうです。
しかし今回想定しているような Vite + TypeScript な構成でこれをやると、
ロジックとビューでちゃんとファイル分けされているのにわざわざ HTML のコードも編集しなくちゃいけなくなるのはちょっと嫌ですね......。

.fxファイルからインポートする

次のような条件を満たすことで Babylon.js が良しなにシェーダデータをフェッチしてインポートしてくれます。

  • HTML ファイルとシェーダファイルが同じディレクトリにある
  • Vertex シェーダファイルの名前はxxx.vertex.fxというフォーマット
  • Fragment シェーダファイルの名前はxxx.fragment.fxというフォーマット

この条件を満たしたうえで次のようなコードによってシェーダファイルをインポートできます。

var shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, "./xxx", {
  attributes: ["position", "normal", "uv"],
  uniforms: ["world", "worldView", "worldViewProjection", "view", "projection"],
});

一見スマートそうですが、ディレクトリ構成の制約が少し汚く感じました。
特にバンドラや JS フレームワークと一緒に使う場合には追加の設定が必要になる可能性があります。

Vite の仕組みを使って読み込む

Vite の静的アセットインポート

Vite には静的アセットを良しなにインポートしてくれるしてくれる仕組みが用意されています。
詳しくは次の docs をご覧下さい。

https://ja.vitejs.dev/guide/assets.html

この仕組みを使って Vite + Babylon.js の環境で glTF ファイルを読み込む記事も以前に書きましたので合わせてご覧ください。

https://zenn.dev/drumath2237/articles/88d49bb2be9baf

?rawを使って読み込む

?rawサフィックスを使うとアセットファイルの中身を文字列としてインポートできます。これを使うことによってシェーダファイルからコードの文字列を取得し、ShaderMaterial に渡すことでシェーダをインポートできます。例えば次のようなディレクトリ構成のプロジェクトを想定してみます。

ディレクトリ構成
/
├─ src/
│   ├─ shaders/
│   │   ├─ sampleShader.vert
│   │   └─ sampleShader.frag
│   └─ main.ts
├─ index.html
└─ package.json

Vite CLI で Vanilla-ts を選択するとできるプロジェクトで、/src/shaders/以下にシェーダファイルがある状態です。
例として、時間によってマジェンタになったり黒になったりするシンプルなシェーダを書いてみました。

シェーダ
// sampleShader.vert
precision highp float;
attribute vec3 position;
uniform mat4 worldViewProjection;

void main() {
    vec4 p = vec4(position, 1.);
    gl_Position = worldViewProjecti
}

// sampleShader.frag
precision highp float;
uniform float time;

void main(void) {
    float sinValue = sin(time) / 2.0 + 0.5;
    vec3 base = vec3(sinValue, 0, sinValue);

    gl_FragColor = vec4(base, 1.);
}

これらを/src/main.tsからインポートし、Babylon.js の ShaderMaterial を作成するコードは次のようになります(サンプルプロジェクトより抜粋)。

import {
  Engine,
  MeshBuilder,
  Scene,
  ShaderMaterial,
  Vector3,
} from "@babylonjs/core";
import "./style.scss";

import vertShader from "./shaders/sampleShader.vert?raw";
import fragShader from "./shaders/sampleShader.frag?raw";

const main = () => {
  // 中略

  const shaderMaterial = new ShaderMaterial(
    "sampleShader",
    scene,
    {
      vertexSource: vertShader,
      fragmentSource: fragShader,
    },
    {
      attributes: ["position"],
      uniforms: ["worldViewProjection"],
    }
  );

  let time = 0;
  scene.registerBeforeRender(() => {
    shaderMaterial.setFloat("time", time);
    time += 0.03;
  });

  // 中略
};

main();

スッキリと書けましたね。もちろん Vite のホットリロードもちゃんと効きます。
ShaderMaterial を適用するには、メッシュオブジェクトなどの Material を変更すれば大丈夫です。

cube.material = shaderMaterial;

おわりに

今回の記事では Vite の機能を使うことで Babylon.js にシェーダをインポートして使うまでを解説しました。外部のテキストファイルを文字列として受け取れる仕組みはシェーダと相性がよく、個人的に気に入っているプラクティスなので備忘録的に書けて良かったです。
こちらの内容が皆様のお役に立てれば幸いです。最後まで読んでいただきありがとうございました。

参考文献

https://ja.vitejs.dev/guide/assets.html

https://doc.babylonjs.com/features/featuresDeepDive/materials/shaders/shaderCodeInBjs

GitHubで編集を提案

Discussion