typegpu について
FrameVideoでWebGPUの映像を制作するためのスキルです。シェーダーやGPU計算に加え、タイムラインとフレームキャプチャの同期方法を扱います。
主な機能
- 01WebGPUシェーダーと計算のパターン
- 02FrameVideoのシークに応じた描画
- 03GSAPタイムラインの同期登録
- 04GPUフレームキャプチャの同期
- 05決定論的なビジュアル描画
活用シーン
- 01パーティクルや物理シミュレーションの作成
- 02カスタムシェーダーやポストエフェクトの構築
- 03FrameVideoの指定時刻に合わせたエフェクト描画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chanjing-ai/ framevideo --skill typegpuインストール後、Claude Code で/typegpuを入力して読み込まれたか確認します。
詳細
FrameVideo 向け TypeGPU / WebGPU
使用する場面
次の用途には TypeGPU/WebGPU を使用します。
- GPU コンピュートシェーダー — パーティクルシステム、物理シミュレーション
- カスタム WGSL シェーダー — リキッドグラス、メタボール、レイマーチング
- 高パフォーマンスのエフェクト — 複雑なフラグメントシェーダー、ポストプロセッシング
- 高度な視覚効果 — ノイズフィールド、反応拡散、流体シミュレーション
- 計算負荷の高いビジュアル — Three.js では抽象度が高すぎる場合
使用しない場面
次の用途には TypeGPU/WebGPU を使用しないでください。
- シンプルな 3D シーン —
threeを使用します(より簡単で、成熟しています) - 2D アニメーション —
gsapを使用します(はるかにシンプルです) - 標準的なエフェクト — WebGPU はブラウザーの対応が限られているため、WebGL の代替手段を使用します
- テキスト/UI アニメーション — HTML + GSAP を使用します(レンダリング品質が優れています)
クイックスタート
FrameVideo での基本的な WebGPU フラグメントシェーダー:
<canvas id="gpu-layer" width="1920" height="1080"></canvas>
<script>
(async () => {
if (!navigator.gpu) return;
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById("gpu-layer");
const ctx = canvas.getContext("webgpu");
ctx.configure({
device,
format: navigator.gpu.getPreferredCanvasFormat()
});
// Create time uniform buffer
const timeUniform = new Float32Array([0]);
const timeBuf = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// Listen for FrameVideo seek events
window.addEventListener("fv-seek", async (event) => {
const time = event.detail.time;
// Update time uniform
timeUniform[0] = time;
device.queue.writeBuffer(timeBuf, 0, timeUniform);
// Render your pipeline...
// [render pass code here]
// IMPORTANT: flush GPU work before frame capture
await device.queue.onSubmittedWorkDone();
});
})();
</script>
ポイント:
fv-seekイベントをリッスンしますevent.detail.timeでユニフォームを更新します- フレームをキャプチャする前に
await device.queue.onSubmittedWorkDone()を呼び出します - WebGPU が利用可能かどうかを確認します
契約
- WebGPU は非同期で初期化します(
await navigator.gpu.requestAdapter())。ただし、GSAP のすべてのトゥイーンは 同期的に(awaitより前に)登録してください。FrameVideo プレーヤーはページ読み込み直後にタイムラインを読み取ります。 performance.now()ではなく、FrameVideo の時刻を使ってレンダリングします。fv-seekイベントをリッスンし、その時刻ちょうどに再レンダリングします。- WebGPU が利用できない環境に備えて確認処理を行います。アダプターは代わりに確認してくれません。
- 動画をレンダリングする場合は、GPU の処理を送信した後に
await device.queue.onSubmittedWorkDone()を呼び出し、フレームのキャプチャ前にキャンバスがフラッシュされるようにします。
アダプターは、シークのたびに window.__fvTypegpuTime を設定し、new CustomEvent("fv-seek", { detail: { time } }) をディスパッチします。
基本パターン
<canvas id="gpu-layer"></canvas>
<script>
(async () => {
if (!navigator.gpu) return;
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return;
const device = await adapter.requestDevice();
const canvas = document.getElementById("gpu-layer");
canvas.width = 1920;
canvas.height = 1080;
const ctx = canvas.getContext("webgpu");
const fmt = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format: fmt, alphaMode: "opaque" });
// Build your pipeline, buffers, bind groups...
const timeUniform = new Float32Array([0]);
const timeBuf = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
function render(t) {
timeUniform[0] = t;
device.queue.writeBuffer(timeBuf, 0, timeUniform);
const enc = device.createCommandEncoder();
const pass = enc.beginRenderPass({
colorAttachments: [
{
view: ctx.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0, a: 1 },
storeOp: "store",
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([enc.finish()]);
}
render(0);
window.addEventListener("fv-seek", (e) => render(e.detail.time));
})();
</script>
タイムラインの登録
テキスト、キャプション、HTML 要素を制御する GSAP のトゥイーンは、同期的に(await より前に)登録する必要があります。
const tl = gsap.timeline({ paused: true });
// Caption tweens: synchronous, added before WebGPU init
gsap.set(".cap", { opacity: 0 });
tl.to("#cap-1", { opacity: 1, duration: 0.3 }, 1.0);
tl.to("#cap-1", { opacity: 0, duration: 0.2 }, 3.5);
window.__timelines["my-comp"] = tl;
// GPU-dependent tweens can go inside the async IIFE
(async () => {
// ... WebGPU init ...
const proxy = { value: 0 };
tl.to(proxy, { value: 1, duration: 2, onUpdate: render }, 0.5);
})();
動画を使ったエフェクト(リキッドグラス、歪み)
<video> を GPU 入力テクスチャとして使うには:
const videoEl = document.getElementById("aroll");
// Wait for video metadata before creating the texture
await new Promise((r) => {
if (videoEl.readyState >= 1) r();
else videoEl.addEventListener("loadedmetadata", r, { once: true });
});
// Create texture at the video's NATIVE resolution
const vw = videoEl.videoWidth,
vh = videoEl.videoHeight;
const bgTex = device.createTexture({
size: [vw, vh],
format: "rgba8unorm",
usage:
GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
});
function render(t) {
try {
device.queue.copyExternalImageToTexture({ source: videoEl }, { texture: bgTex }, [vw, vh]);
} catch (_) {
/* frame not decoded yet */
}
// ... draw ...
}
レンダーモードに関する注意: ヘッドレス Chrome では、動画要素に対する copyExternalImageToTexture が失敗することがあります。本番環境でレンダリングする場合は、あらかじめ FFmpeg でキーフレームを PNG として抽出し、代わりに画像テクスチャとして読み込んでください。
ダウンサンプルパスによるすりガラス状のぼかし
単一パスのガウスカーネルでは、ガラスのようなすりガラス状のぼかしを十分に表現できません。次の 2 パス方式を使用します。
- パス 1 — ダウンサンプル: フル解像度のテクスチャを小さなテクスチャ(解像度を 1/6 にしたもの)にレンダリングします。ダウンサンプル時のバイリニアフィルタリングによって、ピクセルが自然に平均化されます。
- パス 2 — ガラスの合成: すりガラス部分には小さなテクスチャ(バイリニアアップスケーリングにより強く滑らかなぼかし)を、シャープな部分や色収差の屈折にはフル解像度のテクスチャをサンプリングします。
これは、ミップマップを生成せずに TypeGPU の textureSampleBias によるミップレベル指定の方法を再現します。
透過キャンバスと不透明キャンバス
alphaMode: 'opaque'— GPU キャンバスがフレーム全体(動画 + エフェクト)をレンダリングします。GPU パイプラインですべての視覚コンテンツを処理する場合に使用します。alphaMode: 'premultiplied'— GPU キャンバスは alpha = 0 の部分が透過になり、その下にある HTML 要素が表示されます。通常の<video>要素に重ねるオーバーレイ(パーティクル、パスアニメーション)に使用します。
WGSL の全画面三角形
全画面エフェクトで使用する標準的な頂点シェーダーです(頂点バッファーは不要です)。
struct Vo { @builtin(position) pos: vec4f, @location(0) uv: vec2f }
@vertex fn vs(@builtin(vertex_index) vi: u32) -> Vo {
let ps = array<vec2f, 3>(vec2f(-1., -1.), vec2f(3., -1.), vec2f(-1., 3.));
let ts = array<vec2f, 3>(vec2f(0., 1.), vec2f(2., 1.), vec2f(0., -1.));
return Vo(vec4f(ps[vi], 0., 1.), ts[vi]);
}
pass.draw(3) で描画します。ビューポート全体を覆う三角形が 1 つ描画されます。
角丸長方形 SDF(リキッドグラスのピル)
fn sdf_box(p: vec2f, half_size: vec2f, corner_radius: f32) -> f32 {
let d = abs(p) - half_size + vec2f(corner_radius);
return length(max(d, vec2f(0.))) + min(max(d.x, d.y), 0.) - corner_radius;
}
これを使って、ガラスエフェクトの内側/リング/外側のゾーンを定義します。負の値は図形の内側を表します。
決定論的なレンダリング
Math.random()は使用しません。シード付きの PRNG を使用します。- レンダーループに
requestAnimationFrameは使用しません。fv-seekに応じてのみレンダリングします。 - アニメーション時刻に
performance.now()は使用しません。window.__fvTypegpuTimeまたはe.detail.timeを読み取ります。 - GPU 処理を送信した後、レンダーモードでフレームをキャプチャする場合は
await device.queue.onSubmittedWorkDone()を呼び出します。