1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebGPUで単色グラデーション表現を実装する - テクスチャ座標を使った描画パターンの作り方

Posted at

概要

前回の記事ではWebGPUで実践的な開発環境を整える方法を紹介しました。

今回は、以前WebGLで実装したグラデーション表現をWebGPUで書き直します。

テクスチャ座標を使った様々な描画パターンを作成し、WebGLとの違いを確認します。

描画範囲を画面全体に拡大

前回までは中央に小さな矩形を描画していましたが、今回は画面全体に描画します。

頂点座標を[-0.5, 0.5]から[-1.0, 1.0]に変更します。

src/main.ts
  // 頂点データの定義(四角形の4つの頂点)
  const vertices = new Float32Array([
-   -0.5, -0.5, // 左下
-    0.5, -0.5, // 右下
-   -0.5,  0.5, // 左上
-    0.5,  0.5, // 右上
+   -1.0, -1.0, // 左下
+    1.0, -1.0, // 右下
+   -1.0,  1.0, // 左上
+    1.0,  1.0, // 右上
  ]);

テクスチャ座標の準備

グラデーション表現のために、頂点シェーダーでテクスチャ座標を計算します。

座標を[-1, 1]から[0, 1]に正規化し、フラグメントシェーダーに渡します。

src/shaders/plane.vert.wgsl
struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) textureCoord: vec2f,
}

@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
  var output: VertexOutput;
  output.position = vec4f(position, 0.0, 1.0);
  // 座標を [-1, 1] から [0, 1] に正規化
  output.textureCoord = position * 0.5 + 0.5;
  return output;
}

WGSLの主要な概念

ここで、初めて登場するWGSLの概念を説明します。

struct

複数の値をまとめて扱うためのデータ型です。
頂点シェーダーからフラグメントシェーダーに複数の値を渡す際に使用します。

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) textureCoord: vec2f,
}

@builtin

GPUの組み込み変数を指定する属性です。

@builtin(position)は頂点の画面上の位置を表す特別な変数で、必ず出力する必要があります。

@location

任意のデータをシェーダー間で受け渡しするための属性です。

入力側(関数の引数)では、頂点バッファのどの属性を受け取るかを指定します。

@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
  // @location(0) は頂点バッファの0番目の属性(今回はposition)
}

出力側(構造体のフィールド)では、次のシェーダーステージに渡すデータの場所を指定します。

struct VertexOutput {
  @location(0) textureCoord: vec2f,  // フラグメントシェーダーに渡される
}

フラグメントシェーダーでは、同じ@location(0)で受け取ります。

@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  // 頂点シェーダーから渡された textureCoord を受け取る
}

WebGLとの違い

WebGLでは、頂点シェーダーでoutキーワードを使って変数を宣言し、フラグメントシェーダーで同名のin変数として受け取っていました。

// WebGL版頂点シェーダー
out vec2 vTextureCoord;
void main() {
  gl_Position = vec4(aPosition, 0.0, 1.0);
  vTextureCoord = aPosition * 0.5 + 0.5;
}

// WebGL版フラグメントシェーダー
in vec2 vTextureCoord;

WebGPUでは、構造体を使って複数の値をまとめて返します。

@builtin(position)は組み込みの出力、@location(0)は任意のデータを渡すための出力です。

// WebGPU版
struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) textureCoord: vec2f,
}

パターン1: 水平グラデーション

左から右に向かって黒→赤に変化するグラデーションを作ります。

src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  // パターン1: 水平グラデーション
  let r = textureCoord.x;
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

textureCoord.xは0から1の範囲で変化するため、赤色成分が左から右に向かって増えていきます。

WebGLとの違い

WebGLでは以下のように書いていました。

// WebGL版
precision mediump float;
in vec2 vTextureCoord;
out vec4 fragColor;

void main() {
  float r = vTextureCoord.x;
  float g = 0.0;
  float b = 0.0;
  fragColor = vec4(r, g, b, 1.0);
}

細かな違いはありますが、ここまでと比べればほぼ同じ考え方・書き方ができています。

パターン2: 垂直グラデーション

下から上に向かって黒→赤に変化するグラデーションを作ります。

src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  // パターン2: 垂直グラデーション
  let r = textureCoord.y;
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

textureCoord.xtextureCoord.yに変えるだけで、縦方向のグラデーションになります。

パターン3: 斜めグラデーション

左下から右上に向かって黒→赤に変化するグラデーションを作ります。

src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  // パターン3: 斜めグラデーション
  let r = (textureCoord.x + textureCoord.y) / 2.0;
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

XとYの座標を平均することで、対角線方向のグラデーションが得られます。

パターン4: 波のようなパターン

ここまでの単なるグラデーションならCSSで書いた方が簡単です。
しかし、WebGPUの場合はより複雑な計算もできます。

例えば、斜めグラデーションに三角関数を組み合わせると、以下のようになります。

src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  let pi = acos(-1.0);
  let r = abs(sin((textureCoord.x + textureCoord.y) * pi * 3.0));
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

斜めのグラデーションにしても、波のような複雑なパターンができました。

さらに式に手を入れてみます。

パターン5: 千鳥格子風

XとYの三角関数を組み合わせて、千鳥格子風の複雑なパターンを作ります。

src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  let pi = acos(-1.0);
  // パターン5: 千鳥格子風
  let r = sin(textureCoord.x * pi * 10.0) * cos(textureCoord.y * pi * 5.0) * 0.5 + 0.5;
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

円周率の扱い

WGSLには円周率の組み込み定数がないため、自分で計算する必要があります。

acos(-1.0)で円周率を求めることができます。

let pi = acos(-1.0);

WebGLとの違い

WebGLでも同様にacos(-1.0)で円周率を計算できました。

// WebGL版
float pi = acos(-1.0);

WGSLでも同じ方法が使えます。

// WebGPU版
let pi = acos(-1.0);

sin()cos()などの三角関数は、WebGLと同じ名前で使用できます。

完全なコード

最終的なコードは以下のようになります。

src/main.ts
import './style.css';
import vertexShaderCode from './shaders/plane.vert.wgsl?raw';
import fragmentShaderCode from './shaders/plane.frag.wgsl?raw';

async function initWebGPU() {
  // Canvas要素の取得
  const canvas = document.getElementById('webgl-canvas') as HTMLCanvasElement;

  // GPUアダプターとデバイスの取得
  const adapter = await navigator.gpu.requestAdapter() as GPUAdapter;
  const device = await adapter.requestDevice();

  // Canvasコンテキストの取得
  const context = canvas.getContext('webgpu') as GPUCanvasContext;
  const canvasFormat = navigator.gpu.getPreferredCanvasFormat();

  // 頂点データの定義(四角形の4つの頂点)
  const vertices = new Float32Array([
    -1.0, -1.0, // 左下
     1.0, -1.0, // 右下
    -1.0,  1.0, // 左上
     1.0,  1.0, // 右上
  ]);

  // 頂点バッファの作成
  const vertexBuffer = device.createBuffer({
    size: vertices.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(vertexBuffer, 0, vertices);

  // WGSLシェーダーの定義(別ファイルから読み込み)
  const shaderModule = device.createShaderModule({
    code: vertexShaderCode + fragmentShaderCode,
  });

  // レンダーパイプラインの構築
  const pipeline = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: shaderModule,
      entryPoint: 'vertexMain',
      buffers: [
        {
          arrayStride: 8, // 2 floats * 4 bytes
          attributes: [
            {
              shaderLocation: 0,
              offset: 0,
              format: 'float32x2',
            },
          ],
        },
      ],
    },
    fragment: {
      module: shaderModule,
      entryPoint: 'fragmentMain',
      targets: [
        {
          format: canvasFormat,
        },
      ],
    },
    primitive: {
      topology: 'triangle-strip',
    },
  });

  // 描画処理
  function render() {
    const encoder = device.createCommandEncoder();
    const textureView = context.getCurrentTexture().createView();
    const renderPass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: textureView,
          loadOp: 'clear',
          clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // 黒色
          storeOp: 'store',
        },
      ],
    });
    renderPass.setPipeline(pipeline);
    renderPass.setVertexBuffer(0, vertexBuffer);
    renderPass.draw(4); // 4つの頂点を描画
    renderPass.end();
    device.queue.submit([encoder.finish()]);
  }

  // キャンバスサイズの調整とコンテキスト設定
  function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    context.configure({
      device: device,
      format: canvasFormat,
    });

    // リサイズ後に再描画
    render();
  }

  // 初期化時とリサイズ時にキャンバスサイズを調整
  resizeCanvas();
  window.addEventListener('resize', resizeCanvas);
}

initWebGPU();
src/shaders/plane.vert.wgsl
struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) textureCoord: vec2f,
}

@vertex
fn vertexMain(@location(0) position: vec2f) -> VertexOutput {
  var output: VertexOutput;
  output.position = vec4f(position, 0.0, 1.0);
  // 座標を [-1, 1] から [0, 1] に正規化
  output.textureCoord = position * 0.5 + 0.5;
  return output;
}
src/shaders/plane.frag.wgsl
@fragment
fn fragmentMain(@location(0) textureCoord: vec2f) -> @location(0) vec4f {
  let pi = acos(-1.0);

  // パターン1: 水平グラデーション
  // let r = textureCoord.x;
  // let g = 0.0;
  // let b = 0.0;

  // パターン2: 垂直グラデーション
  // let r = textureCoord.y;
  // let g = 0.0;
  // let b = 0.0;

  // パターン3: 斜めグラデーション
  // let r = (textureCoord.x + textureCoord.y) / 2.0;
  // let g = 0.0;
  // let b = 0.0;

  // パターン4: 波のようなパターン
  // let r = abs(sin((textureCoord.x + textureCoord.y) * pi * 3.0));
  // let g = 0.0;
  // let b = 0.0;

  // パターン5: 千鳥格子風
  let r = sin(textureCoord.x * pi * 10.0) * cos(textureCoord.y * pi * 5.0) * 0.5 + 0.5;
  let g = 0.0;
  let b = 0.0;

  return vec4f(r, g, b, 1.0);
}

まとめ

WebGPUでテクスチャ座標を使ったグラデーション表現を実装しました。

1つ目の記事では、WebGLとWebGPUの設計思想の違いが大きく、コード量もかなり違いました。
しかし今回は、シェーダーの記述方法に若干の違いはあるものの、基本的な考え方や処理の流れはほぼ同じでした。

構造体を使ったデータの受け渡しや、in/outの代わりに関数の引数と戻り値を使う点など、WGSL特有の書き方はありますが、グラデーションを作成するロジック自体(座標を使った計算や三角関数の使用)は似ていて、新しく学ぶにしても助かりました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?