Vite環境のBabylon.jsでシェーダを読み込むtips
はじめに
この記事は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 にてサンプルプロジェクトを公開しておりますので、そちらも合わせてご覧になってください。
デモも見られます。
シェーダ読み込みの選択肢
Babylon.js ではシェーダの読み込み方法が何通りかあり、下記 docs ページに列挙されています。この中で紹介されている方法を軽く紹介します(どれも今回紹介する方法ではありません)。
CYOS からダウンロードする方法
Babylon.js では CYOS という Playground のシェーダ版みたいなツールがあり、それを使うことでプレビューを見ながら Vertex シェーダと Fragment シェーダを書いていくことが可能です。
しかしこの方法、なかなか手間です。ローカルプロジェクトへシェーダを反映するイテレーションがかなり回しにくそうなので個人的にあまり好みではありません。
script タグのなかに書く
HTML のスクリプトタグにシェーダコードを記述して、それを Babylon.js で参照するという仕組みがあります。
以下は docs に載っているサンプルコードの引用です。
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 をご覧下さい。
この仕組みを使って Vite + Babylon.js の環境で glTF ファイルを読み込む記事も以前に書きましたので合わせてご覧ください。
?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 にシェーダをインポートして使うまでを解説しました。外部のテキストファイルを文字列として受け取れる仕組みはシェーダと相性がよく、個人的に気に入っているプラクティスなので備忘録的に書けて良かったです。
こちらの内容が皆様のお役に立てれば幸いです。最後まで読んでいただきありがとうございました。
参考文献
Discussion