本文へスキップ

レンダーターゲットとシェーダー ​

レンダーターゲットは、描き込んだ後に普通の画像のように描けるオフスクリーンの画像です。シェーダーは、あるスコープの中で描かれるすべてに掛かるカスタムのフラグメントエフェクトです。この2つで、ポストプロセス、グロー、静的レイヤーのキャッシュ、ミニマップ、軌跡、輪郭線、パレットスワップ、CRT エフェクトをまかなえます。

ts
const scene = draw.createTarget(320, 180);
const crt = draw.createShader(`
  uniform float time;
  vec4 effect(vec4 color, vec2 uv) {
    float scan = 0.85 + 0.15 * sin(uv.y * 900.0);
    return source(uv) * color * vec4(vec3(scan), 1.0);
  }
`);

function frame(t: number) {
  draw.begin("#000", 320, 180, scale);
  draw.into(scene, () => drawWorld(), "#102030");
  crt.set("time", t / 1000);
  draw.withShader(crt, () => draw.sprite(scene, 0, 0, { origin: [0, 0] }));
  draw.end();
}

レンダーターゲット ​

メソッド
createTarget(width, height)新しいターゲット。サイズは整数ピクセルで指定します。
into(target, fn, clear?)描画先を target にして fn を実行し、終わったら元に戻します。
disposeTarget(target)GPU メモリを解放します。その後に描くと、空の状態で作り直されます。

into() の中では、単位はターゲットの左上からのピクセルです。トランスフォーム、クリップ、グローバルアルファはまっさらな状態から始まり、fn が例外を投げても、終了後に外側のものが復元されます。into() は入れ子にできるので、ターゲットを別のターゲットに描けます。

clear 引数は、最初にターゲットを塗りつぶします。

  • 省略:透明。
  • 色:その色。
  • null:中身を残し、新しい描画を上に重ねます。軌跡やモーションブラーはこうして蓄積します。
ts
const trails = draw.createTarget(320, 180);

draw.into(trails, () => {
  draw.rect(0, 0, 320, 180, "#0000000f"); // 前のフレームを少しだけ薄くする
  draw.sprite(comet, comet.x, comet.y);
}, null);
draw.sprite(trails, 0, 0, { origin: [0, 0] });

引き継がれるもの、例外になるもの ​

  • ブレンドモードと有効な withShader は into() でリセットされません。 ターゲットの中でも適用され、fn が終えたときの状態のまま残ります。begin() は毎フレーム両方をリセットします。
  • ターゲットをそれ自身に描くと例外を投げます。描き込み中のターゲットを dispose したり、GPU の MAX_TEXTURE_SIZE より大きいターゲットを作ったりしても同様です。
  • ターゲットは両方のバックエンドで動きます。
  • WebGL のコンテキスト消失後、ターゲットの中身は空になります。draw.contextLost を確認するか、毎フレームターゲットを描き直してください。

シェーダー ​

1ピクセルの色を返す関数 effect(color, uv) を書きます。source(uv) は描画中の画像をサンプリングします。

ts
const wave = draw.createShader(`
  uniform float time;
  vec4 effect(vec4 color, vec2 uv) {
    uv.x += sin(uv.y * 40.0 + time * 6.0) * 0.01;
    return source(uv) * color;
  }
`);

wave.set("time", t);
draw.withShader(wave, () => draw.sprite(hero, x, y));
  • color はその描画のティント × アルファで、**乗算済み(premultiplied)**です。返す色も乗算済み(rgb にアルファを掛けたもの)にしてください。source(uv) * color なら正しくなります。
  • スプライトの flash は、エフェクトの後に適用されます。
  • set(name, value) は float、または [x, y]、[x, y, z]、[x, y, z, w] を受け取ります。影響するのはその後の描画だけなので、同じスコープ内の2つの描画の間で uniform を変えられます。
  • withShader(shader, fn) は終了後に前のシェーダーを復元し、入れ子にできます。
  • シェーダーは最初に使ったときにコンパイルされます。エラーはドライバのメッセージ付きで例外を投げ、行番号は自分のソースの行を指します。コンパイルに失敗したシェーダーはレンダラーを元の状態のまま残し、再び使うとまた例外を投げます。

注意点 ​

  • シェーダーは WebGL2 専用です。Canvas 2D フォールバックでは、withShader の中の描画はエフェクトなしで行われ、コンソールに警告が1回出ます。
  • withShader の中の sprites() は、コピーごとに1クアッドの描画にフォールバックします。結果は正しいですが、一括描画による高速化はありません。

レシピ:全画面ポストプロセス ​

シーンを論理ビューと同じサイズのターゲットに描き、そのターゲットをシェーダーを通して描きます。

ts
const scene = draw.createTarget(320, 180);
const vignette = draw.createShader(`
  uniform float strength;
  vec4 effect(vec4 color, vec2 uv) {
    float d = distance(uv, vec2(0.5));
    return source(uv) * color * vec4(vec3(1.0 - d * strength), 1.0);
  }
`);
vignette.set("strength", 0.8);

function frame() {
  draw.begin("#000", 320, 180, canvas.width / 320);
  draw.into(scene, () => drawWorld(), "#102030");
  draw.withShader(vignette, () => draw.sprite(scene, 0, 0, { origin: [0, 0] }));
  drawHud(); // エフェクトの後に描くので、HUD はくっきりしたまま
  draw.end();
}

ターゲットのサイズはピクセル単位なので、320×180 のターゲットは、canvas のバッキングサイズにかかわらずシーンを 320×180 の解像度で保持します。

レシピ:グロー ​

明るいものを小さなターゲットに描き、それを拡大して加算ブレンドで上に重ねます。

ts
const glow = draw.createTarget(160, 90); // 半分の解像度:軽く、自然にぼける

draw.into(glow, () => {
  draw.scale(0.5);
  draw.sprites(spark, sparks, live, { tint: "#ffd36b" });
});

drawWorld();
draw.blend("add");
draw.sprite(glow, 0, 0, { origin: [0, 0], scaleX: 2, scaleY: 2, smooth: true });
draw.blend("normal");

拡大時の smooth: true が、ブラーのパスなしで半分の解像度のグローを柔らかくします。

検証済みの出力 ​

spryte のブラウザテストは実際の GPU 上で動きます。ターゲットに描いてからそれを等倍で描いた結果が、直接描いた場合とピクセル単位で同じになることを確認しています。

次へ:ドット絵