p5.js + Parcelで自作シェーダを作成してファイルを呼び出す
はじめに
p5.jsなどで3Dレンダリングを行う際はシェーダーが必要になりますが、 実際に頂点シェーダーファイルとフラグメントシェーダーファイルを作成してみようと思います。
前記二つのファイルはGLSLで記述されたC言語風のテキストソースファイルですが、 WebGL内メソッドでコンパイルしてくれるのでソースファイルをテキストとしてjavascriptに渡せば使用できます。 GLSLはWebGLにも使用されるシェーディング言語で、GPUから実行することで高速にレンダリングを行うことができます。
p5.jsでは通常、頂点ファイルとフラグメントファイルを呼び出す際はpreload関数内でloadshaderを呼び出しますが、 Parcelでファイルをロードする際はimportが必要なのでこの方法ではできません。
結論から言えば、importしてsetup関数内でcreateShaderを呼び出すだけです。 これでp5.Shaderオブジェクトを作成することができます。
シェーダーファイルを呼び出してシェーダーオブジェクト作成
呼び出し例として、頂点シェーダーとフラグメントシェーダーファイル https://p5js.org/examples/3d-basic-shader.html を使用する例を示します。 シェーダーファイルを保存したパスからimportを行うことで、ソースの文字列がロードされます。 ですので、setup関数内でcreateShaderを呼び出すことでシェーダーオブジェクトを作成することができます。 キャンバスはWEBGLモードにしておく必要があります。 また高密度画素ディスプレイ対策でpixelDensityは1にしておきますが、個人で使用する場合は問題なければあってもなくてもいいと思います。
import p5 from "p5";
import vert from "../shader/basic.vert";
import frag from "../shader/basic.frag";
const sketch = (p: p5) => {
const framerate = 20;
const W = 1080;
const H = 1080;
let shader: p5.Shader;
p.setup = () => {
p.pixelDensity(1);
p.createCanvas(W, H, p.WEBGL);
shader = p.createShader(vert, frag);
};
}
描画
ウィンドウ枠いっぱいにシェーダーをレンダリングする際はrectを使用すればいいです。 これで実行すればグラデーションの画像が出来上がります。
p.draw = () => {
shader.setUniform('u_resolution', [W, H]);
p.noStroke();
p.shader(shader);
p.rect(0, 0, W, H);
}
次に自作のシェーダを作って描画する方法を二つ示しますが、描画される内容は同じです。 どちらか使いやすい方法を使用するといいと思います。 ただし一方はaTexCoordを使用してフラグメントシェーダーにテクスチャ座標を渡す必要があり、 もう一方はカメラを使用した視点移動などができません。
赤緑青色の時間経過とともに動く網目状の模様のシェーダーを作成します。
アニメーション(twitterへ移動): https://twitter.com/tk87__/status/1526217477772181504
シェーダ描画 ver.1
フレーム毎にshaderメソッドを呼び出してcreateShaderで作成したシェーダーオブジェクトをセットし、 その後続いて描画される図形のレンダリングに使用する方法です。 こちらの方法はカメラの視点を変えることができます。
バーテックスファイル(vertファイル)
aPositionはattributeと呼ばれ、頂点の位置情報を保持している読み取り専用の三次元ベクトル変数です。 これをビュー変換(カメラの視点・位置関連の座標変換)と射影変換(ウィンドウ・遠近関連の座標変換)の二つの頂点の座標変換を行い、フラグメントシェーダに渡します。 aTexCoordはシェーダーのテクスチャ座標に一致し、vTexCoordを通じてフラグメントシェーダに渡されます。範囲は[0.0, 1.0]です。 vTexCoordに付与されているvarying修飾子はフラグメントシェーダに渡される変数を表すものになります。
attribute vec3 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uProjectionMatrix;
uniform mat4 uModelViewMatrix;
varying vec2 vTexCoord;
void main() {
vec4 positionVec4 = vec4(aPosition, 1.0);
gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
vTexCoord = aTexCoord;
}
フラグメントファイル(fragファイル)
バーテックスファイルから受け取ったvTexCoordとtypescriptファイルから受け取ったフレームカウント変数(u_frame)で、 それぞれの座標と時間ごとに変化させてながら色を設定します。 u_frameに付与されているuniform修飾子は全頂点間で共通の変数を表すもので、typescript側から渡される変数になります。
#define PI 3.14159265358979323846
uniform int u_frame;
varying vec2 vTexCoord;
void main() {
vec3 t = sin(float(u_frame)/100.0 * PI * 2.0 * vec3(0.5, 0.7, 1.3));
vec3 col = vec3(0.3) + sin((vTexCoord.xyx + t)*30.0) * 0.7 * t;
gl_FragColor = vec4(col,1.0);
}
sketch.ts
setup関数とdraw関数の部分のみ記載します。 shader.setUniformでフラグメントシェーダーにu_frame(フレームカウント)を渡しています。 球と立方体とトーラスを描画するコードとなっています。 また、orbitControl()でカメラの視点移動ができます。
p.setup = () => {
p.createCanvas(W, H, p.WEBGL);
p.frameRate(framerate);
shader = p.createShader(vert, frag);
p.noStroke();
};
p.draw = () => {
p.shader(shader);
shader.setUniform('u_frame', p.frameCount);
p.background(30);
p.orbitControl();
p.translate(0, -H/4, 0);
p.push();
p.translate(0, 0, 0);
p.rotateX(p.fract(p.frameCount / 50)*p.TWO_PI);
p.sphere(40);
p.pop();
p.translate(-W/4, H/4*2, 0);
p.push();
p.rotateX(p.fract(p.frameCount / 50)*p.TWO_PI);
p.torus(40, 10);
p.pop();
p.translate(W/4*2, 0, 0);
p.push();
p.rotateX(p.fract(p.frameCount / 50)*p.TWO_PI);
p.box(50, 50);
p.pop();
};
シェーダ描画 ver.2
もう一つの方法は、シェーダーオブジェクトをテクスチャとして使用する方法です。 こちらはカメラの視点関連は利用できない分、関連する座標変換を行いません。 typescript側でシェーダーオブジェクトをテクスチャに渡すためaPositionの位置情報がテクスチャ座標とほぼ等価になります。
バーテックスファイル(vertファイル)
原点の平行移動とスケールにより[0.0, 1.0]を[-1.0, 1.0] に変換します。 この変換をしないと4分の1しか描画されません。
attribute vec3 aPosition;
void main() {
vec4 positionVec4 = vec4(aPosition, 1.0);
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
gl_Position = positionVec4;
}
フラグメントファイル(fragファイル)
gl_FragCoordを解像度で割って正規化してこれをテクスチャ座標として使用します。 u_resolutionはtypescript側から渡す必要があります。
#define PI 3.14159265358979323846
uniform vec2 u_resolution;
uniform int u_frame;
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 t = sin(float(u_frame)/100.0 * PI * 2.0 * vec3(0.5, 0.7, 1.3));
vec3 col = vec3(0.3) + sin((st.xyx + t)*30.0) * 0.7 * t;
gl_FragColor = vec4(col,1.0);
}
sketch.ts
shtxというGraphicsオブジェクトを作成し、 シェーダを登録してからrectとtextureメソッドを使用してテクスチャに渡しています。
p.setup = () => {
p.createCanvas(W, H, p.WEBGL);
p.frameRate(framerate);
shtx = p.createGraphics(W, H, p.WEBGL);
shader = shtx.createShader(vert, frag);
p.noStroke();
shtx.noStroke();
};
p.draw = () => {
shtx.shader(shader);
shader.setUniform('u_resolution', [W, H]);
shader.setUniform('u_frame', p.frameCount);
shtx.rect(0, 0, W, H);
p.background(30);
p.texture(shtx);
p.translate(0, -H/4, 0);
//以下図形描画処理(省略)
}
まとめ
parcel + typescript経由でp5.jsを使用した自作シェーダーの描画を行うことができました。 バーテックスシェーダーとフラグメントシェーダーは今回使用した描画表現以外にもまだ可能な表現は存在します。 以下のようなサンプル例があるので参考にすると勉強になりそうです。
https://github.com/aferriss/p5jsShaderExamples