react-pixiでカスタムシェーダー(GLSL)を扱う
Reactで自前のシェーダープログラムを扱いたい人向けの記事です。
また、react-pixi自体の基本的な使い方はここでは扱いません。
@inlet/react-pixi: 6.6.5
pixi.js: 5.3.11
前置き(なぜreact-pixiを選んだか)
前提条件として、2Dテクスチャに対してシェーダーでリッチな表現を行いたく、それに合うライブラリを探していました。
自分自身がWebGL素人であることと、WebGL自体を直接触るとなると色々と書かなければならないことが多いため、そのあたりのwrapperになるというのが選定基準です。
候補に上がったのが
- react-three-fiber
- gl-react
- react-vfx
- react-pixi
の4つです。
ReactでWebGLを扱うとなると、react-three-fiberが有名どころですが、自分の場合は3D情報まで取り扱う必要はなく、もう少し小さくまとまったライブラリを探すことにしました。
次に候補に上がったのがgl-reactです。
gl-reactはReacrでシェーダーを扱うためのライブラリで、完全に目的に合致したコンセプトを持っています。ただ、gl-reactはReactのLegacy Context APIを利用しており、その移行も難航しているようで、今後も継続利用できるかどうかという不安があったため、候補から外しました。
次に見つけたのがreact-vfxでした。
そもそも自分のやりたいこととほぼ同じコンセプトだったので、なんならこのライブラリ使えばカスタムシェーダーを書かなくても良いしれない、と思ったのですが、このライブラリでは頂点シェーダーをサポートしていませんでした。
現状、頂点シェーダーを利用する予定は無いのですが、後々必要になったときのことを考えて頂点シェーダーとフラグメントシェーダーの両方が利用できるライブラリを別に探すことにしました。
(本件とは関係ないですが、画像にGlitch効果を加えるシェーダーを書いた際、react-vfxのシェーダーを参考にさせて頂きました。)
上記の流れがあり、最終的に@inlet/react-pixiを使うことにしました。
react-pixiはpixi.jsをreactで扱うようにしたライブラリなのですが、react-pixiからはpixi.jsを呼び出すところまでに留めており、pixi.jsのドキュメントのまま各種moduleが取り扱えることや、ドキュメントも整備されていることから、役割としては大きいが利用するにあたって困ることはないだろうと判断しました。
react-pixiでのカスタムシェーダー参照
例えばフラグメントシェーダーだけ適用する場合は以下のように書けます。
const width = 200;
const height = 200;
const fs = `
precision mediump float;
void main(void){
gl_FragColor = vec4(1., 0., 0., 1.);
}
`
const [f, setF] = useState<Filter[]>([
new Filter(undefined, fs, {})
]);
return (
<Stage width={width} height={height}>
<Graphics
draw={graphics => {
graphics.clear();
graphics.beginFill(0xffff00);
graphics.drawRect(0, 0, width, height);
graphics.endFill();
}}
filters={f}
/>
</Stage>
)
これでcanvasが赤一色で描画されるはずです。
Filterの第一引数には頂点シェーダー、第二引数にはフラグメントシェーダー、第三引数にはuniform変数のパラメータを渡します。
上記だと、頂点シェーダーを渡すはずの箇所がundefinedとなっていますが、これはpixi.js内部でデフォルトの頂点シェーダーが定義されているため、渡さなくてもフラグメントシェーダーだけ適用させることが可能です。
また、デフォルトで与えられる頂点シェーダーはバージョン記載がないため、 OpenGL ES 3.0
の記述はコンパイルエラーとなります。
デフォルトの頂点シェーダー
もし OpenGL ES 3.0
で記述する場合は頂点シェーダーを書き換えてfilters propsで渡す必要があります。
const vs =`#version 300 es
precision mediump float;
in vec2 aVertexPosition;
in vec2 aTextureCoord;
uniform mat3 projectionMatrix;
out vec2 vTextureCoord;
void main(void){
gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;
}
`
const fs = `#version 300 es
precision mediump float;
out vec4 color;
void main(void){
color = vec4(1., 0., 0., 1.);
}
`
const [f, setF] = useState<Filter[]>([
new Filter(vs, fs, {})
]);
説明がややこしくなるので、以下は頂点シェーダーを渡さない前提で進めます。
resolusionのようなuniform変数を渡したい場合は、上述した通りFilterの第三引数に渡します。
const [f, setF] = useState<Filter[]>([
new Filter(undefined, fs, {uResolution: [width, height]})
]);
ここで注意したいのが resolution
という名前でシェーダーに渡しても、意図した結果で描画されないことです。おそらくpixi.jsの中でresolutionという名前のデータを使っており、それとコンフリクトしてしまうのではないかと予想しています。このあたりソースコードを読んでも解決できなかったので、uResolutionという名前で渡すようにしています(resolutionでなければhogehogeといった名前でも良いはずです)
時間やマウスの位置情報など、動的な値を渡したい場合はusetickを使います
const GraphicsComponent = () => {
const [f, setF] = useState<Filter[]>([
new Filter(undefined, fs, {time: time, uResolution: [width, height]})
]);
useTick((delta) => {
time += delta;
setF([
new Filter(undefined, fs, {time: time, uResolution: [width, height]})
])
})
return (
<Graphics
draw={graphics => {
graphics.clear();
graphics.beginFill(0xffff00);
graphics.drawRect(0, 0, width, height);
graphics.endFill();
}}
filters={f}
/>
)
}
また、テクスチャ情報についてはuSamplerというuniform変数として渡されます。
uSamplerについてはFilterに引数で渡す必要はありません。
const fs = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
`
あとがき
上記のように書くことでReact上から容易にカスタムシェーダの作成・利用ができるようになりました。流石というか、かなり激しいアニメーションでも滑らかに動くため、シェーダーで絵を書く・テクスチャをフィルタリングする、くらいの用途であればかなり使いやすいと思います。
テキストやスプライトの描画もpixi.js側の機能でできるため、pixi.jsの持つフィルター機能ではまかないきれないところだけ自分でシェーダーを書く、という場合にも適しています。
また、上記のように、シェーダープログラムをヒアドキュメントとして書くとハイライトが効かず不便なのですが、vite-plugin-glslというライブラリを使えばシェーダープログラムとReactコンポーネントを分離できてとても便利です。
もしViteを使っている場合はこちらのライブラリもオススメです。
// hogehoge.glsl
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}
Discussion