7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebGLで波状のカラーグラデーションを描画する

Last updated at Posted at 2024-10-31

概要

この記事では、WebGLを使って時間経過で変化する波状のカラーグラデーションを描画する方法をWebGL初心者の方を対象に、ステップバイステップで手順を説明していきます。

完成イメージ

gradation_1.gif

手順

手順1: HTMLファイルの作成

まずは、WebGLを描画するためのキャンバス要素をHTMLに配置します。以下のHTMLコードをコピーして、index.htmlなどの名前で保存してください。<style>タグでキャンバスを画面全体に表示するように設定しています。

<!DOCTYPE html>
<html>
<head>
  <title>Wavy Color Gradient</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
  // ここにJavaScriptコードが入ります (後述)
</script>
</body>
</html>

手順2: JavaScriptでWebGLコンテキストを取得

次に、JavaScriptでWebGLコンテキストを取得します。<script>タグ内に以下のコードを追加します。

const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gl = canvas.getContext('webgl');

これで、gl変数を通じてWebGLの機能を利用できるようになります。

手順3: シェーダーの作成

WebGLでは、頂点シェーダーとフラグメントシェーダーという2種類のシェーダーを使って描画を行います。

  • 頂点シェーダー: 頂点の位置を決定します。
  • フラグメントシェーダー: 各ピクセルの色を決定します。

まずは、これらのシェーダーのコードを文字列として定義します。

// 頂点シェーダー - 単純に頂点をそのまま出力
const vertexShaderSource = `
  attribute vec2 position;
  void main() {
    gl_Position = vec4(position, 0.0, 1.0);
  }
`;

// フラグメントシェーダー - 時間経過で変化する波状のカラーグラデーションを生成
const fragmentShaderSource = `
  precision mediump float;
  uniform float time;
  uniform vec2 resolution;

  // 7色のグラデーションカラーを定義
  vec3 colorA = vec3(1.0, 0.2, 0.2); // Red
  vec3 colorB = vec3(0.93, 0.47, 0.0); // Orange
  vec3 colorC = vec3(1.0, 0.87, 0.0); // Yellow
  vec3 colorD = vec3(0.0, 0.66, 0.37); // Green
  vec3 colorE = vec3(0.2, 0.6, 1.0); // Blue
  vec3 colorF = vec3(0.61, 0.45, 0.7); // Purple
  vec3 colorG = vec3(0.87, 0.52, 0.64); // Pink

  // 時間に基づいてグラデーションカラーを返す関数
  vec3 getColor(float t) {
    t = fract(t);
    if (t < 1.0/7.0) return mix(colorA, colorB, t * 7.0);
    if (t < 2.0/7.0) return mix(colorB, colorC, (t - 1.0/7.0) * 7.0);
    if (t < 3.0/7.0) return mix(colorC, colorD, (t - 2.0/7.0) * 7.0);
    if (t < 4.0/7.0) return mix(colorD, colorE, (t - 3.0/7.0) * 7.0);
    if (t < 5.0/7.0) return mix(colorE, colorF, (t - 4.0/7.0) * 7.0);
    if (t < 6.0/7.0) return mix(colorF, colorG, (t - 5.0/7.0) * 7.0);
    return mix(colorG, colorA, (t - 6.0/7.0) * 7.0);
  }

  void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    float distortion = sin(uv.x * 10.0 + time) * 0.1 * sin(uv.y * 10.0 + time * 2.0);
    gl_FragColor = vec4(getColor(uv.x + distortion), 1.0);
  }
`;

頂点シェーダーは、入力として頂点座標(position)を受け取り、出力としてgl_Positionを設定します。フラグメントシェーダーには、時間(time)とキャンバスの解像度(resolution)を渡し、getColor関数で時間に基づいてグラデーションカラーを計算します。

手順4: シェーダーのコンパイルとリンク

WebGLはシェーダーのコードを直接理解できないため、コンパイルとリンクという手順が必要です。以下の関数を追加します。

function createShaderProgram(vertexSource, fragmentSource) {
  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(vertexShader, vertexSource);
  gl.shaderSource(fragmentShader, fragmentSource);
  gl.compileShader(vertexShader);
  gl.compileShader(fragmentShader);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

const shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

このcreateShaderProgram関数は、与えられた頂点シェーダーとフラグメントシェーダーのソースコードをコンパイルし、リンクして、シェーダープログラムを返します。

手順5: シェーダープログラムの属性とuniform変数の取得

シェーダープログラム内の属性とuniform変数の場所を取得します。

const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'position');
const timeUniformLocation = gl.getUniformLocation(shaderProgram, 'time');
const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'resolution');

手順6: 頂点バッファの作成

頂点バッファを作成し、頂点データをWebGLに渡します。

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  -1.0, -1.0, // 左下
  1.0, -1.0, // 右下
  -1.0,  1.0, // 左上
  -1.0,  1.0, // 左上
  1.0, -1.0, // 右下
  1.0,  1.0, // 右上
]), gl.STATIC_DRAW);

このコードでは、2つの三角形を描画するための頂点座標を指定しています。

手順7: 描画ループ

requestAnimationFrameを使って描画ループを実装します。

function render(timestamp) {
  const time = timestamp * 0.001;

  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(shaderProgram);

  gl.uniform1f(timeUniformLocation, time);
  gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);

  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

  gl.drawArrays(gl.TRIANGLES, 0, 6);

  requestAnimationFrame(render);
}

requestAnimationFrame(render);

このrender関数は、毎フレーム画面をクリアし、時間と解像度をシェーダーに渡して、三角形を描画します。

完成版のコード

上記のコードを全て<script>タグ内にまとめた完成版は以下の通りです。

<!DOCTYPE html>
<html>
<head>
  <title>Wavy Color Gradient</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
  // キャンバス要素を取得
  const canvas = document.getElementById('canvas');
  // キャンバスのサイズを設定
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  // WebGLコンテキストを取得
  const gl = canvas.getContext('webgl');

  // 頂点シェーダー - 単純に頂点をそのまま出力
  const vertexShaderSource = `
    attribute vec2 position; // 頂点座標
    void main() {
      gl_Position = vec4(position, 0.0, 1.0); // 出力座標
    }
  `;

  // フラグメントシェーダー - 時間経過で変化する波状のカラーグラデーションを生成
  const fragmentShaderSource = `
    precision mediump float; // 中程度の精度の浮動小数点数を指定
    uniform float time; // 時間経過を表す変数
    uniform vec2 resolution; // キャンバスの解像度

    // 7色のグラデーションカラーを定義
    vec3 colorA = vec3(255.0/255.0, 51.0/255.0, 51.0/255.0); // Red
    vec3 colorB = vec3(238.0/255.0, 120.0/255.0, 0.0/255.0); // Orange
    vec3 colorC = vec3(255.0/255.0, 220.0/255.0, 0.0/255.0); // Yellow
    vec3 colorD = vec3(0.0/255.0, 169.0/255.0, 96.0/255.0); // Green
    vec3 colorE = vec3(51.0/255.0, 153.0/255.0, 255.0/255.0); // Blue
    vec3 colorF = vec3(155.0/255.0, 114.0/255.0, 178.0/255.0); // Purple
    vec3 colorG = vec3(222.0/255.0, 130.0/255.0, 167.0/255.0); // Pink

    // 時間に基づいてグラデーションカラーを返す関数
    vec3 getColor(float t) {
      // 時間を 0.0 - 1.0 の範囲にクランプ
      t = fract(t);
      // 時間に応じて適切なカラーを線形補間で計算して返す
      if (t < 1.0/7.0) return mix(colorA, colorB, t * 7.0);
      if (t < 2.0/7.0) return mix(colorB, colorC, (t - 1.0/7.0) * 7.0);
      if (t < 3.0/7.0) return mix(colorC, colorD, (t - 2.0/7.0) * 7.0);
      if (t < 4.0/7.0) return mix(colorD, colorE, (t - 3.0/7.0) * 7.0);
      if (t < 5.0/7.0) return mix(colorE, colorF, (t - 4.0/7.0) * 7.0);
      if (t < 6.0/7.0) return mix(colorF, colorG, (t - 5.0/7.0) * 7.0);
      return mix(colorG, colorA, (t - 6.0/7.0) * 7.0);
    }

    void main() {
      // 画面上の座標を正規化
      vec2 uv = gl_FragCoord.xy / resolution.xy;
      // 時間と座標に基づいて歪みを計算
      float distortion = sin(uv.x * 10.0 + time) * 0.1 * sin(uv.y * 10.0 + time * 2.0);
      // 歪みを適用した座標でグラデーションを取得
      gl_FragColor = vec4(getColor(uv.x + distortion), 1.0); // 出力カラー
    }
  `;

  // シェーダーをコンパイルしてプログラムを作成する関数
  function createShaderProgram(vertexSource, fragmentSource) {
    // 頂点シェーダーを作成
    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    // フラグメントシェーダーを作成
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    // シェーダーにソースコードを設定
    gl.shaderSource(vertexShader, vertexSource);
    gl.shaderSource(fragmentShader, fragmentSource);
    // シェーダーをコンパイル
    gl.compileShader(vertexShader);
    gl.compileShader(fragmentShader);
    // シェーダープログラムを作成
    const shaderProgram = gl.createProgram();
    // シェーダープログラムにシェーダーをアタッチ
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    // シェーダープログラムをリンク
    gl.linkProgram(shaderProgram);
    // シェーダープログラムを返す
    return shaderProgram;
  }

  // シェーダープログラムを作成
  const shaderProgram = createShaderProgram(vertexShaderSource, fragmentShaderSource);

  // シェーダープログラムの属性を取得
  const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'position'); // 頂点座標属性の場所を取得
  const timeUniformLocation = gl.getUniformLocation(shaderProgram, 'time'); // 時間経過を表すuniform変数の場所を取得
  const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'resolution'); // キャンバスの解像度を表すuniform変数の場所を取得

  // 頂点バッファを作成
  const positionBuffer = gl.createBuffer();
  // 頂点バッファをバインド
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  // 頂点バッファにデータをセット
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    -1.0, -1.0, // 左下
    1.0, -1.0, // 右下
    -1.0,  1.0, // 左上
    -1.0,  1.0, // 左上
    1.0, -1.0, // 右下
    1.0,  1.0, // 右上
  ]), gl.STATIC_DRAW);

  // アニメーションループ
  function render(timestamp) {
    // 時間経過を計算
    const time = timestamp * 0.001;

    // キャンバスをクリア
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // シェーダープログラムを使用
    gl.useProgram(shaderProgram);

    // 時間と解像度をシェーダーに渡す
    gl.uniform1f(timeUniformLocation, time);
    gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);

    // 頂点バッファをバインドして有効化
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    // 三角形を描画
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // 次のフレームを要求
    requestAnimationFrame(render);
  }

  // アニメーションを開始
  requestAnimationFrame(render);
</script>
</body>
</html>

このコードをブラウザで開くと、7色の波状のカラーグラデーションが表示され、時間経過とともに変化していく様子が確認できます。

WebGLやシェーダーはとっつきにくいかもしれませんが、何度も書いていると理解が進むと思いますので、まずはぜひいろいろと触って作ってみてみてください。

7
7
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
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?