「ユニフォームバッファ」を使って三角形を描画してみよう

2025年10月8日(水)
大西 武 (オオニシ タケシ)
第9回の今回は、WebGPUでユニフォームバッファを通して行列をユニフォーム定数としてシェーダーに渡し、三角形を描画する解説をします。

はじめに

今回は頂点の座標をモデルごとに一度にまとめてシェーダーに渡すのとは別に、1フレームごとにおいては変化のない定数を「ユニフォームバッファ」からシェーダーに渡します。具体的にはパースペクティブ行列やカメラ行列、ワールド行列、法線行列をユニフォームバッファを通して、シェーダーにユニフォーム定数として渡します。

前回までにコーディングしてきた「lib/Matrix3D.js」「lib/Model3D.js」「lib/UltraMotion3D.js」「lib/Vector3D.js」「lib/WGSL.js」「index.html」に追記して、今回は図1のように遠近法やカメラを考慮した三角形を描画します。まだ三角形だけですが、ワールド行列で回転アニメーションもさせてみます。

図1:遠近法を考慮した三角形を描画

ユニフォームバッファを準備する

各行列はJavaScriptのメインコード側でユニフォームバッファに格納し、その値をWGSLに送ります。

ユニフォームバッファを用意する

次のサンプルコード「lib」→「UltraMotion3D.js」ファイルでは、ユニフォームバッファを用意するためにデバイスの「createBuffer」メソッドの「usage」に「GPUBufferUsage.UNIFORM」を指定して作成します。その「size」には、ここでは「4 * 16 * 4(float32の4byte * 4x4行列がパースペクティブ行列、ビュー行列、ワールド行列、法線行列の4個)」バイトを指定します。

・サンプルコード「lib」→「UltraMotion3D.js」ファイル
var _camera = new Matrix3D();
var _fov = 45.0;
const UNIFORM_BUFFER_SIZE = 4 * 16 * 4; // 4byte(float32) * 4x4 matrix * 4
var _passEncoder = null;

async function initWebGPU(canvas) {
  if (!navigator.gpu) {
    throw Error('WebGPU not supported.');
  }
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    throw Error('Could not request WebGPU adapter.');
  }
  _device = await adapter.requestDevice();
  _canvas = document.getElementById(canvas);
  _context = _canvas.getContext('webgpu');
  _presentationFormat = navigator.gpu.getPreferredCanvasFormat();
  _context.configure({
    device: _device,
    format: _presentationFormat,
    alphaMode: 'premultiplied',
  });
  _pipeline = setPipeline(vertWGSL,fragWGSL);
  _uniformBuffer = _device.createBuffer({
    size: UNIFORM_BUFFER_SIZE,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
  window.addEventListener("resize", resize, false);
	resize();
  init();
  everyFrame();
}

function setPipeline(vertexWGSL,fragmentWGSL) {
(中略)
}

function everyFrame() {
  _commandEncoder = _device.createCommandEncoder();
  const textureView = _context.getCurrentTexture().createView();
  _renderPassDescriptor = {
    colorAttachments: [
      {
        view: textureView,
        clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 },
        loadOp: 'clear',
        storeOp: 'store',
      },
    ],
    depthStencilAttachment: {
      view: _depthTexture.createView(),
      depthClearValue: 1.0,
      depthLoadOp: 'clear',
      depthStoreOp: 'store',
    },
  };
  _passEncoder = _commandEncoder.beginRenderPass(_renderPassDescriptor);
  draw();
  _passEncoder.end();
  _device.queue.submit([_commandEncoder.finish()]);
  requestAnimationFrame(everyFrame.bind(everyFrame));
}

function resize() {
(中略)
}

function degree2radian(degree) {
	return degree * Math.PI / 180;
}

function radian2degree(radian) {
	return radian * 180 / Math.PI;
}

function getPos(event) {
  const clientRect = _canvas.getBoundingClientRect();
  if ( 'ontouchend' in document ) {
    const touch = event.changedTouches.item(0);
    const x = touch.clientX - clientRect.left;
    const y = touch.clientY - clientRect.top;
    return new Vector2D(x,y);
  } else {
    const x = event.clientX - clientRect.left;
    const y = event.clientY - clientRect.top;
    return new Vector2D(x,y);
  }
}

【サンプルコードの解説】
カメラ行列「_camera」変数を用意します。
フィールドオブビュー(視野角)を_fov変数で45度にします。
ユニフォームバッファのサイズを「UNIFORM_BUFFER_SIZE」定数に宣言します。
ユニフォームバッファを「_uniformBuffer」変数に用意します。
「everyFrame」関数が終わるときに、繰り返し「requestAnimationFrame」でeveryFrameをリクエストして呼び出して毎フレーム描画します。
「degree2radian」関数でディグリー角度をラジアン角度に変換し取得します。
「radian2degree」関数でラジアン角度をディグリー角度に変換し取得します。
「getPos」関数でキャンバス上のマウス座標を取得します。

ユニフォームバッファをシェーダーに送る

「Model3D」クラスの「init」メソッドでユニフォームバッファを「uniformBindGroup」にバインド(結び付ける、関連付ける)して、「setTransform」メソッドでユニフォームバッファにプロジェクション行列(ここではパースペクティブ行列)とビュー行列(ここではカメラ行列)とワールド行列(ここでは平行移動・回転・スケーリング行列)をWGSLに渡します。まだ今回は法線行列は渡しません。

「draw」メソッドで「setTransform」メソッドを呼び出し、パスエンコーダーの「setBindGroup」メソッドで「uniformBindGroup」をグループ0番にバインドします。

・「lib」→「Model3D.js」ファイル
class Model3D {
  constructor() {
    this.vertices = [0,100,0,1, -100,-100,0,1, 100,-100,0,1];
    this.indices = [0,1,2];
    this.pos = new Vector3D(0,0,0);
    this.rotate = new Vector3D(0,0,0);
    this.scale = new Vector3D(1,1,1);
 }
  initBuffers() {
    this.vertexArray = new Float32Array(this.vertices);
    this.verticesBuffer = _device.createBuffer({
      size: this.vertexArray.byteLength,
      usage: GPUBufferUsage.VERTEX,
      mappedAtCreation: true,
    });
    new Float32Array(this.verticesBuffer.getMappedRange()).set(this.vertexArray);
    this.verticesBuffer.unmap();
    this.init();
  }
  async init() {
    this.uniformBindGroup = _device.createBindGroup({
      layout: _pipeline.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: {
            buffer: _uniformBuffer,
            offset: 0,
            size: UNIFORM_BUFFER_SIZE,
          },
        },
      ],
    });
  }
  getVertexCount() {
    return ~~(this.vertexArray.length/4);
  }
  draw() {
    if (this.uniformBindGroup) {
      _passEncoder.setPipeline(_pipeline);
      this.setTransform();
      _passEncoder.setBindGroup(0,this.uniformBindGroup);
      _passEncoder.setVertexBuffer(0,this.verticesBuffer);
      _passEncoder.draw(this.getVertexCount());
    }
  }
  setTransform() {
    const projectionMatrix = this.getProjectionMatrix();
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 0,
      projectionMatrix.e.buffer,
      0,
      4 * 16
    );
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 1,
      _camera.e.buffer,
      0,
      4 * 16
    );
    const worldMatrix = this.getWorldMatrix();
    _device.queue.writeBuffer(
      _uniformBuffer,
      4 * 16 * 2,
      worldMatrix.e.buffer,
      0,
      4 * 16
    );
  }
  getProjectionMatrix() {
    const projectionMatrix = new Matrix3D();
    const aspect = _canvas.width/_canvas.height;
    projectionMatrix.perspective(_fov, aspect, 1, 100000.0);
    return projectionMatrix;
  }
  getWorldMatrix() {
    const worldMatrix = new Matrix3D();
    worldMatrix.translate(this.pos);
    worldMatrix.rotateX(degree2radian(this.rotate.x));
    worldMatrix.rotateY(degree2radian(this.rotate.y));
    worldMatrix.rotateZ(degree2radian(this.rotate.z));
    worldMatrix.scale(this.scale);
    return worldMatrix;
  }
}

【サンプルコードの解説】
Model3Dクラスで「pos(位置)」「rotate(回転)」「scale(拡大縮小)」プロパティを用意します。
「initBuffers」メソッドで「init」メソッドを呼び出します。
「init」メソッド内でデバイスの「createBindGroup」メソッドを使い、「_unifomBuffer」変数を「binding」の0番にバインドします。
「draw」メソッドで「setTransform」メソッドを呼び出し、「uniformBindGroup」プロパティをグループ0番にバインドします。
「setTransform」メソッドでプロジェクション行列を4*16*0オフセットに4*16バイト用意します。
ビュー行列を4*16*1オフセットに4*16バイト用意します。
ワールド行列を4*16*2オフセットに4*16バイト用意します(4*16*3オフセットが開いていますがそれは次回法線行列を用意します)。
「getProjectionMatrix」メソッドでパースペクティブ行列(遠近法)を取得します。
「getWorldMatrix」メソッドでワールド行列を取得します。

【コラム】「スケジュールを待ち受けに」

スマホアプリとして、待ち受け画面にスケジュールを書くアイデアを考えました。それに派生して、リアルの世界では曜日が書かれた磁石でスケジュールのメモを冷蔵庫にくっつけたらどうかと考えました。でも、もう以前から磁石がくっつかない冷蔵庫が当たり前でした。筆者は昭和の古い人間だったと悲しくなります…。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版する作家。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞するアーティスト。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。
https://profile.vixar.jp

連載バックナンバー

開発言語技術解説
第9回

「ユニフォームバッファ」を使って三角形を描画してみよう

2025/10/8
第9回の今回は、WebGPUでユニフォームバッファを通して行列をユニフォーム定数としてシェーダーに渡し、三角形を描画する解説をします。
開発言語技術解説
第8回

オブジェクトの頂点変換やピクセルを色付けするシェーダー言語「WGSL」の文法を学ぼう

2025/9/17
第8回の今回は、WebGPUで使うシェーダー言語「WGSL」の頂点シェーダーとフラグメントシェーダーの文法を解説します。
開発言語技術解説
第7回

3Dの移動や回転、拡大・縮小する「行列」について学ぼう

2025/8/27
第7回の今回は、ベクトル座標を移動したり、カメラの視線に合わせて変形したり、遠近法で変形したりする「行列」について解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています