一括スプライト
draw.sprites(frame, positions, count?, options?, each?) は、1つのフレームを多数の位置に1回の呼び出しで描きます。WebGL2 では、使い回した Float32Array がそのまま GPU にアップロードされ、コピーごとの JavaScript 処理はありません。パーティクル、弾、群れ、星空などに使います。
const MAX = 20_000;
const pos = new Float32Array(MAX * 2); // x0, y0, x1, y1, ...
const vel = new Float32Array(MAX * 2);
let live = 0;
function update(dt: number) {
for (let i = 0; i < live * 2; i++) pos[i] += vel[i] * dt;
}
function render() {
draw.sprites(spark, pos, live, { tint: "#ffd36b", alpha: 0.9 });
}引数
frame:draw.sprite()と同じく、画像またはFrame。positions:x, y のペア。毎フレーム同じFloat32Arrayを渡してください。count:描くコピーの数。省略するとpositionsのすべてのペアを描きます。options:sprite()と同じSpriteDrawOptions(origin、scaleX、scaleY、rotation、flipX、flipY、alpha、tint、flash、smooth)。すべてのコピーに適用されます。each:コピーごとの配列(省略可)。下で説明します。
each でコピーごとの値を渡す
縮んでいく火花、回転する破片、消えていく火の粉。こうしたものには、コピーごとの配列を第5引数に渡します。
const scale = new Float32Array(MAX);
const rotation = new Float32Array(MAX);
const color = new Uint32Array(MAX); // 0xRRGGBBAA
function spawn(i: number, x: number, y: number) {
pos[i * 2] = x;
pos[i * 2 + 1] = y;
scale[i] = 1;
rotation[i] = Math.random() * Math.PI * 2;
color[i] = 0xffd36bff;
}
function fade(i: number, lifeLeft: number) {
scale[i] = lifeLeft;
color[i] = (0xffd36b00 | Math.round(255 * lifeLeft)) >>> 0;
}
draw.sprites(spark, pos, live, {}, { scale, rotation, color });scale(Float32Array)は共通のscaleXとscaleYに掛け合わされます。rotation(Float32Array、ラジアン)は共通のrotationに加算されます。color(0xRRGGBBAAのUint32Array)は共通のtintとalphaに掛け合わされます。値を組み立てるときは、符号なしのままにするため>>> 0を残してください。- 必要な配列だけを渡せば十分です。これらもそのままアップロードされます。
なぜ速いのか
draw.sprite() のループでは、スプライトごとに少しだけ JavaScript の処理が走ります。オプションを読み、トランスフォームを計算し、インスタンスを書き込みます。WebGL2 で sprites() を使うと、spryte は Float32Array(と each の配列)をそのまま GPU に渡し、GPU が各コピーを展開します。パーティクルごとの処理は、自分の更新ループだけです。positions にほかの配列型を渡しても動きますが、先にステージング用の配列へコピーされます。
出力は、同じ値で sprite() をループした場合とピクセル単位で同一です。ピクセルスナップも含みます。smooth がオフなら、回転していないコピーは整数ピクセル上に置かれます。spryte はこれを実際の GPU 上のブラウザテストで確認しているので、エフェクトをループから sprites() に切り替えても見た目は変わりません。
spryte 自身のベンチマーク(デスクトップ Chrome、RTX 5060 搭載ノート PC、3ラウンド)では、パーティクルごとの拡大率、回転、ティント、アルファがすべて変化する20万パーティクルが1フレーム 7.7 ms でした。PixiJS 8 の ParticleContainer は 28.9 ms です。計測方法と注意点はベンチマークのページにあります。
制限
- 1回の呼び出しにつき1フレーム。 多くの画像から数百のスプライトを描くなら、
draw.sprite()のループのほうが簡単です。 withShaderの中では、sprites()はコピーごとに1クアッドの描画にフォールバックします。結果は正しいですが、一括描画による高速化はありません。- Canvas 2D フォールバックでは、各コピーが個別のクアッドとして描かれます。
countは渡した配列のうち最も短いもの(positionsのペア数、scale、rotation、color)に切り詰められるので、短い配列のせいでゴミを読むことはありません。NaNや負の数なら何も描きません。- バッチを分ける1回のドローコール。 WebGL2 では
sprites()はそれ自体が1回のドローコールなので、パーティクルごとに呼ぶと意味がありません。レイヤーごとに1回呼んでください。
描画順
sprites() は、それより前のものすべての後に、それより後のものすべての前に描かれます。各レイヤー(奥に煙、手前に火花)を別々の呼び出しにして、描きたい順に並べてください。
draw.sprites(smoke, smokePos, smokeLive, { alpha: 0.5 });
draw.sprite(player, px, py);
draw.blend("add");
draw.sprites(spark, sparkPos, sparkLive, { tint: "#ffd36b" }, { color: sparkColor });
draw.blend("normal");次へ:図形とテキスト