概要
この記事では、WebGLを使って時間経過で変化する波状のカラーグラデーションを描画する方法をWebGL初心者の方を対象に、ステップバイステップで手順を説明していきます。
完成イメージ
手順
手順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やシェーダーはとっつきにくいかもしれませんが、何度も書いていると理解が進むと思いますので、まずはぜひいろいろと触って作ってみてみてください。