本文へスキップ

一括スプライト ​

draw.sprites(frame, positions, count?, options?, each?) は、1つのフレームを多数の位置に1回の呼び出しで描きます。WebGL2 では、使い回した Float32Array がそのまま GPU にアップロードされ、コピーごとの JavaScript 処理はありません。パーティクル、弾、群れ、星空などに使います。

ts
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引数に渡します。

ts
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() は、それより前のものすべての後に、それより後のものすべての前に描かれます。各レイヤー(奥に煙、手前に火花)を別々の呼び出しにして、描きたい順に並べてください。

ts
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");

次へ:図形とテキスト