WebGPUを試してみる:線画と面描画を別パイプラインで混在させて立方体を回してみた
はじめに
こんにちは。今回は、自作の3D計算・タイマー管理ライブラリ(nas6lib)をベースに、モダンブラウザの標準となりつつある WebGPU を使って、線と面が混在する立方体の3Dアニメーションを描画するデモを作成しました。
デモページはこちらです:
🔗 3Dプログラミング講座・その33:WEBGPUテスト (nas6.net)
WebGPUの仕様上、1回のドローコールで異なるプリミティブトポロジ(line-list と triangle-list)を直接混在させられない制限があるため、「2つのパイプラインを切り替えて描画する」というアプローチをとっています。その実装のポイントを備忘録としてシェアします。
デモの概要
- 描画内容: ワイヤーフレーム(緑色)の立方体を描きつつ、上面だけを三角形2枚(薄い黄緑)で塗りつぶす。
-
アニメーション: 自作のタイマーマネージャー(
N6LTimerMan)と回転行列・透視射影関数を使い、毎フレーム座標を計算して描画を更新。 - WebGPU特有の工夫:
- 線描画用(
line-list)と面描画用(triangle-list)の2つのGPURenderPipelineを用意。 - 1つのレンダーパス内でパイプラインを切り替えながら、頂点バッファを共有して効率的に描画。
注釈:簡略化された透視射影の概念図
実装のポイント:線と面をどう共存させるか?
WebGPUでは、パイプライン作成時に primitive.topology(line-list や triangle-list など)を固定します。そのため、同じオブジェクト内で「ワイヤーフレーム」と「塗りつぶし面」を両立させるには、レンダーパス内でパイプラインを切り替える必要があります。
実際の描画ループ(GLoop)のコードの骨子は以下のようになっています。
function GLoop(id) {
time += Math.PI / 180;
// 1. 3D座標の回転・透視投影計算(自作関数を使用)
// ... (省略: 頂点座標の計算&NDC変換) ...
const encoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: textureView,
loadOp: 'clear',
storeOp: 'store',
clearValue: { r: 0, g: 0, b: 0, a: 1 }, // 背景:黒
}],
});
// --- 2. 上面の塗りつぶし(三角形描画) ---
{
const tverts = [];
// 三角形1・2の頂点を構築してバッファに書き込む
// ...
device.queue.writeBuffer(vertexBuffer, 0, tdata.buffer);
pass.setPipeline(trianglePipeline); // 面用パイプライン
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(6, 1, 0, 0); // 三角形2枚(6頂点)
}
// --- 3. 輪郭線の描画(線描画) ---
{
// 線の頂点データを構築してバッファに書き込む
// ...
device.queue.writeBuffer(vertexBuffer, 0, data.buffer);
pass.setPipeline(linePipeline); // 線用パイプライン
pass.setVertexBuffer(0, vertexBuffer);
for (let i = 0; i < edges.length; i++) {
pass.draw(2, 1, i * 2, 0); // 1本ずつ線を描画
}
}
pass.end();
device.queue.submit([encoder.finish()]);
// 次フレームの予約
TMan.timer[0].setalerm(() => GLoop(0), 50);
}
工夫した点
頂点バッファの動的更新: フレームごとに計算される頂点データを device.queue.writeBuffer を使って効率よくGPU側へ転送しています。
シェーダーコード(WGSL)
頂点シェーダーは共通(位置をそのまま受け渡す)、フラグメントシェーダーでそれぞれ「緑の線」「薄い黄緑の面」の色を返しています。
// 線描画用シェーダーモジュール
const shaderCode = `
@vertex
fn vs_main(@location(0) inPos : vec2<f32>) -> @builtin(position) vec4<f32> {
return vec4<f32>(inPos, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0); // 緑
}
`;
// 面描画用シェーダーモジュール
const triangleshaderCode = `
@vertex
fn vs_main(@location(0) inPos : vec2<f32>) -> @builtin(position) vec4<f32> {
return vec4<f32>(inPos, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(0.7, 1.0, 0.7, 1.0); // 薄い黄緑
}
`;
おわりに
WebGPUへ移行するにあたり、パイプラインやレンダーパスの概念をしっかり理解する必要がありましたが、このように自前の数学ルーチンと組み合わせることで、クリーンかつモダンな描画環境を構築することができました。
実際の動きや全ソースコードは以下のページから確認できますので、興味のある方はぜひ触ってみてください!
🔗 デモページを見る
作者
GitHub: https://github.com/NAS6mixfoolv
X(旧Twitter): https://x.com/NAS6_oxo
作者HP: https://nas6.net
気に入っていただけたら GitHub に ⭐ をいただけると嬉しいです!

