風呂瀬 信五

p5.jsとGLSLシェーダーの共鳴:GPUで光を操る詩的入門

p5.jsで描く円や四角形。その一つ一つの図形が、画面という静かな水面に投げ込まれた小石だとしたら、私たちはその波紋の広がりをただ見つめているだけなのかもしれません。もし、水面そのものを、その分子一つ一つを直接揺り動かすことができたなら、どのような光景が広がるのでしょうか。p5.jsの表現力をさらに深め、GPUの持つ膨大な計算能力を借りて、ピクセル単位で光と色を操る。その鍵となるのが GLSLシェーダー です。しかし、その概念はどこか抽象的で、コードは異質な響きを持ち、多くの探求者がその扉の前で足踏みしています。この記事は、そんなシェーダーという静かなる巨人との対話の始め方を探る、思索の旅です。

序章:描画の根源を問う ─ CPUからGPUへ

私たちがp5.jsで rect(0, 0, 100, 100); と書くとき、一体何が起きているのでしょうか。コンピュータの心臓部であるCPUが、一つ一つの命令を解釈し、四角形を描くために必要なピクセルの座標と色を計算し、画面に送り出しています。これは、一人の職人がキャンバスに向かい、設計図に従って丁寧に筆を運ぶ姿に似ています。しかし、画面が何十万、何百万というピクセルの集合体であるとき、この逐次的な手法には限界が見え始めます。

ここで登場するのが、もう一人の巨人、GPU (Graphics Processing Unit) です。もともと3Dグラフィックスを高速に描画するために生まれたGPUは、単純な計算を並列で行うことに特化しています。数千の小さなコアが、まるで無数の職人集団のように、画面上のすべてのピクセルに対して「同時に」計算を実行できるのです。

シェーダーとは、このGPUという職人集団に指示を出すための、小さなプログラムに他なりません。p5.jsの標準的な描画がCPUという一人の画家の筆致に依存するのに対し、シェーダーはGPUを介して、光そのものを素材として扱うような、根源的な描画手法です。これは単なる高速化ではなく、描画という行為そのものの哲学的な転換を意味します。命令によって形を描くのではなく、法則を与えて世界を生成する。そのパラダイムシフトの入り口に、私たちは立っています。

GLSLの詩学:ピクセルに生命を吹き込む言語

GPUと対話するための言葉が、GLSL (OpenGL Shading Language) です。C言語に似た構文を持つこの言語で書かれたコードは、GPU上で直接実行され、ピクセルの最終的な色を決定します。p5.jsと組み合わせる文脈で特に重要になるのが、「フラグメントシェーダー」と呼ばれるものです。これは、画面上のすべてのピクセル(フラグメント)に対して並列に実行される、いわば「ピクセルの色を決定するためだけの関数」です。

フラグメントシェーダーの核心は、驚くほどシンプルです。各ピクセルは、自分自身の座標 (gl_FragCoord) を知っており、最終的に自分が何色になるべきか (gl_FragColor) を宣言するだけで良いのです。

例えば、このような短い詩(コード)を考えてみましょう。

// フラグメントシェーダーのコード
precision mediump float;

// p5.jsから渡される、正規化された座標 (0.0 ~ 1.0)
varying vec2 vTexCoord;

void main() {
  // 横軸の座標を赤、縦軸の座標を緑に割り当てる
  gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 0.5, 1.0);
}

このコードは、画面上のすべてのピクセルにこう語りかけます。「あなたの色は、あなたの水平位置(赤)、垂直位置(緑)、そして少しの青(0.5)と完全な不透明度(1.0)から成り立っている」。その結果、画面には左から右へ、下から上へと滑らかに変化する色のグラデーションが生まれます。ここでは for ループも、特定のピクセルを指定する命令もありません。ただ、すべてのピクセルに適用される一つの法則があるだけです。この普遍的な法則が、個々のピクセルの振る舞いを決定し、全体として一つの調和したイメージを織りなすのです。

シェーダーと時間:静止画から動的な表現へ

ピクセルに座標に基づいた色を与えるだけでは、生まれるのは静的なイメージに過ぎません。ジェネラティブアートに生命の息吹を吹き込むためには、「時間」という概念が不可欠です。p5.jsの世界からシェーダーの世界へ、時間の流れを伝えるにはどうすればよいのでしょうか。

その架け橋となるのが uniform 変数です。uniform は、CPU(p5.js)側からGPU(シェーダー)側へ送られる、全ピクセルで共通の(uniformな)値です。p5.jsの draw() ループ内で経過時間を計算し、それを uniform 変数としてシェーダーに渡すことで、静的な世界に動きが生まれます。

precision mediump float;

uniform vec2 u_resolution; // キャンバスの解像度
uniform float u_time;     // 経過時間 (秒)

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution.xy;
  
  // 時間の経過と共に変化する値を計算
  float r = sin(st.x * 10.0 + u_time) * 0.5 + 0.5;
  float g = cos(st.y * 10.0 + u_time) * 0.5 + 0.5;
  float b = 0.6;

  gl_FragColor = vec4(r, g, b, 1.0);
}

このコードでは、u_time という変数が時間の流れをシェーダーに伝えています。三角関数 sin()cos() と組み合わせることで、色は波のように揺らめき、明滅します。もはや静的なグラデーションではありません。それは、時間の流れの中で刻一刻と表情を変える、生きたパターンです。u_time は、シェーダーという閉じた宇宙に外部から差し込む光であり、ジェネラティブアートに予測不可能なゆらぎと生命感を与えるための、最も重要な鍵の一つなのです。

色彩の錬金術:シェーダーで創り出す無限のグラデーション

シェーダーの真価は、その圧倒的な計算能力によって、複雑で有機的な色彩やパターンをリアルタイムに生成できる点にあります。p5.jsの noise() 関数が生成するようなパーリンノイズのアルゴリズムをGLSLで実装すれば、CPUで計算するよりも遥かに高速に、雲や水面のような自然なテクスチャを画面全体に描画できます。

また、GLSLには mix() という強力な組み込み関数があります。これは二つの色(あるいはベクトル)を、指定した割合で線形補間する関数です。p5.jsにおける lerpColor() のような働きを、全ピクセルで同時に実行する想像をしてみてください。

// ...
uniform float u_time;

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution.xy;

  // 2つの色を定義
  vec3 colorA = vec3(0.1, 0.2, 0.8); // 深い青
  vec3 colorB = vec3(0.9, 0.5, 0.2); // 暖かいオレンジ

  // 時間によって変化する割合を計算
  float ratio = sin(st.x * 5.0 + u_time) * 0.5 + 0.5;

  // 2つの色を mix() で混ぜ合わせる
  vec3 mixedColor = mix(colorA, colorB, ratio);

  gl_FragColor = vec4(mixedColor, 1.0);
}

mix() 関数やノイズ関数、あるいはフラクタル図形を描き出すための反復計算。これらを組み合わせることで、私たちは色彩の錬金術師となります。単純な色のブレンドから、複雑な地形や銀河の生成まで。シェーダーは、ピクセルという原子を操作し、無限の色彩宇宙を創造するための、現代の賢者の石なのです。

p5.jsとの対話:シェーダーをキャンバスに招き入れる

では、この強力なGLSLシェーダーを、私たちのp5.jsスケッチに招き入れるには、具体的にどうすればよいのでしょうか。幸いにも、p5.jsは WebGL モードを通じて、この対話のための洗練されたインターフェースを提供してくれています。

プロセスはいくつかのステップに分かれます。

  1. WebGLモードの有効化: setup() 関数内で createCanvas(w, h, WEBGL); を呼び出し、p5.jsのレンダリングエンジンをWebGLに切り替えます。
  2. シェーダーの読み込み: preload() 関数内で loadShader() を使い、GLSLで記述された頂点シェーダーとフラグメントシェーダーのファイルを読み込みます。
  3. シェーダーの適用: draw() 関数内で shader() を呼び出し、読み込んだシェーダーをアクティブにします。
  4. uniform変数の送信: myShader.setUniform('uniform名', 値); という形式で、p5.js側から時間やマウス座標などのデータをシェーダーに送ります。
  5. 描画: シェーダーを適用したい図形を描画します。画面全体に適用する場合、rect(0, 0, width, height) を描くのが一般的です。
// p5.js側のスケッチ (sketch.js)
let myShader;

function preload() {
  // シェーダーファイルを読み込む(.vertは頂点シェーダー、.fragはフラグメントシェーダー)
  myShader = loadShader('shader.vert', 'shader.frag');
}

function setup() {
  // WebGLモードでキャンバスを作成
  createCanvas(windowWidth, windowHeight, WEBGL);
  noStroke();
}

function draw() {
  // このシェーダーを適用する
  shader(myShader);

  // uniform変数をシェーダーに送信
  myShader.setUniform('u_resolution', [width, height]);
  myShader.setUniform('u_time', millis() / 1000.0);
  myShader.setUniform('u_mouse', [mouseX, map(mouseY, 0, height, height, 0)]);

  // 画面全体を覆う矩形を描画することで、すべてのピクセルでシェーダーが実行される
  rect(0, 0, width, height);
}

このコードにおいて、p5.jsはオーケストラの指揮者として振る舞います。setUniform() を通じて、時間やマウスの動きといった指示をGPUという巨大なオーケストラに送る。すると、何十万というピクセル(演奏者)たちが、その指示に従って一斉に音(色)を奏でるのです。p5.jsの親しみやすさと、シェーダーの圧倒的なパワーが交わる、創造的な対話がここに始まります。

結び:コードの向こうに広がる光の宇宙

シェーダーを学ぶ旅は、単に新しい描画技術を習得すること以上の意味を持ちます。それは、描画の最小単位であるピクセルと直接向き合い、光、色、そして時間が織りなす物理法則を、自らの手でコードとして再構築していく内省的なプロセスです。これまで fill()stroke() といった便利な関数に隠されていた、描画の根源的な仕組みに触れる体験でもあります。

p5.jsが提供するWebGLモードは、この深遠なシェーダーの世界への、優しく開かれた扉です。最初から複雑なものを目指す必要はありません。まずは黒い画面に、ほんの少し色を灯すところから始めてみてください。uniform変数を一つ追加し、その値をマウスで操作してみる。すると、ピクセルたちがあなたの指先に呼応して、ささやくように色を変えるでしょう。

その小さな共鳴に耳を澄ませるとき、私たちはコードの向こう側に、自分だけの光の宇宙が広がり始めているのを感じるはずです。そこは、計算によって編まれた、静かで、無限の可能性を秘めた場所なのです。

関連記事