3
4

p5.js を使った驚くほど短いシェーダーのコード。GPUを直接操作できる技術。

Last updated at Posted at 2024-09-20

ショートストーリー: 「驚くほど短いコードの魔法」

東京の喧騒が少し落ち着いた夕方、プログラマーの悠斗(ゆうと)はコーヒーを片手に、自宅のデスクに向かっていた。彼は新しいウェブアプリの開発に夢中で、特に最近興味を持っていたのはシェーダーという特殊なプログラミング技術だった。映像に豊かな表現力を加えられるこの技術、そしてGPUを直接操作できるこの技術を、どうにかして簡単に扱えないかと試行錯誤していた。

「シェーダーって、すごく難しいんだよな…もっとシンプルにできたらいいのに。」

image.png

悠斗はデスクトップに散らばった複雑なシェーダーコードを見ながら溜息をついた。そのとき、彼の頭に浮かんだのは、最近触り始めたp5.jsというJavaScriptのライブラリだった。

「p5.jsなら、もっとシンプルに書けるかも?」

試しに、彼はエディタを開き、p5.jsを使ったシェーダーのコードを書き始めた。あまり期待はしていなかったが、驚くべきことに、ほんの数行で動き始める美しいグラフィックスが画面に広がった。


function setup() {
  createCanvas(400, 400, WEBGL);
  noStroke();
}

function draw() {
  background(0);
  rotateY(millis() / 1000);
  sphere(150);
}

たったこれだけのコードで、3Dの球体がゆっくりと回転しながら美しく描画されている。悠斗は驚いた。これまで何日もかけて複雑なコードを調整していたシェーダーが、p5.jsではまるで魔法のように短いコードで動いていた。

「信じられない…こんなに簡単にできるなんて。」

スクリーンショット 2024-09-20 153037.png

彼は興奮し、さらなる実験を始めた。p5.jsでシェーダーコードを呼び出し、色彩や形状を変化させる。ほんの少しのコードを加えるだけで、次々と美しいビジュアルが生成されていった。


let myShader;

function setup() {
  createCanvas(400, 400, WEBGL);
  noStroke();
  let vertexShader = `
    attribute vec3 aPosition;
    void main() {
      gl_Position = vec4(aPosition, 1.0);
    }
  `;
  let fragmentShader = `
    precision mediump float;
    void main() {
      gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
    }
  `;
  myShader = createShader(vertexShader, fragmentShader);
}

function draw() {
  shader(myShader);
  rotateY(millis() / 1000);
  sphere(150);
}

悠斗の画面には、青く輝く回転する球体が映し出されていた。複雑に思えていたシェーダーが、p5.jsの力でシンプルに、そして直感的に実装できたのだ。

「p5.jsって、本当にすごい。シェーダーがこんなに短いコードで書けるなんて…。」

スクリーンショット 2024-09-20 152519.png

悠斗はその瞬間、自分が新しい何かを手に入れたことを感じた。プログラマーとして、難解な技術を簡単に扱える力を。そしてそれは、東京という巨大な街の一角で、小さなデスクに座る一人のプログラマーの新たな冒険の始まりだった。

その夜、彼は新しいアイデアが次々と湧いてきて、夢中でキーボードを叩き続けた。

p5.jsとシェーダを使ってカラフルなアニメーションを作成するためのシンプルなサンプルコードを紹介します。この例では、カラフルな波模様がアニメーションとして表示され、時間とマウスの位置に応じて色や形が変化します。

スクリーンショット 2024-09-20 152519.png

カラフルなアニメーションのサンプルコード


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js + シェーダーによるカラフルなアニメーション</title>
  <!-- p5.jsライブラリのインポート -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
  <main>
    <h1>カラフルな波模様のアニメーション</h1>
    <p>時間とマウスの位置に応じて色が変わるカラフルなアニメーション。</p>
  </main>

  <script>
    let myShader;

    function setup() {
      // WEBGLモードでキャンバスを作成
      createCanvas(400, 400, WEBGL);
      noStroke();  // 描画に輪郭線をつけない

      // 頂点シェーダー
      let vertexShader = `
        attribute vec3 aPosition;
        uniform mat4 uProjectionMatrix;
        uniform mat4 uModelViewMatrix;
        uniform float uTime;
        varying vec2 vTexCoord;

        void main() {
          vTexCoord = aPosition.xy;
          vec4 position = vec4(aPosition, 1.0);
          gl_Position = uProjectionMatrix * uModelViewMatrix * position;
        }
      `;

      // フラグメントシェーダー
      let fragmentShader = `
        precision mediump float;
        uniform float uTime;
        uniform vec2 uResolution;
        varying vec2 vTexCoord;

        void main() {
          // マウス位置と解像度に基づいてカラーを生成
          vec2 st = gl_FragCoord.xy / uResolution.xy;
          vec3 color = vec3(0.0);

          // 時間と位置に基づいて波模様を生成
          float wave = sin(10.0 * (st.x + uTime)) + sin(10.0 * (st.y + uTime));
          color.r = 0.5 + 0.5 * sin(uTime + st.x * 5.0);
          color.g = 0.5 + 0.5 * cos(uTime + st.y * 5.0);
          color.b = 0.5 + 0.5 * sin(uTime + wave * 2.0);

          gl_FragColor = vec4(color, 1.0);
        }
      `;

      // シェーダーを作成
      myShader = createShader(vertexShader, fragmentShader);

      // シェーダーを適用
      shader(myShader);
    }

    function draw() {
      // シェーダーに時間と解像度の情報を渡す
      myShader.setUniform('uTime', millis() / 1000.0);
      myShader.setUniform('uResolution', [width, height]);

      // 平面を描画(フルスクリーン)
      rect(-width / 2, -height / 2, width, height);
    }
  </script>
</body>
</html>

解説:
頂点シェーダー:

ここではシンプルに頂点の位置を指定しているだけで、aPosition(各頂点の位置)を利用して頂点を描画します。
フラグメントシェーダー:

uTimeは時間の変数で、アニメーション効果を出すために使います。
uResolutionは画面の解像度です。これを使って、描画する位置に応じた色の変化を作ります。
波模様を生成し、時間と共に変化するカラフルな効果を生み出しています。赤、緑、青の各成分が時間や座標に応じて変化します。
シェーダーの適用:

createShader()で頂点シェーダーとフラグメントシェーダーを作成し、shader()関数で適用しています。
rect()関数でフルスクリーンの四角形を描画しており、画面全体にカラフルなアニメーションを表示します。
このコードを実行すると:
カラフルな波模様が時間とともに変化し、マウスの位置に応じて色の変化が見られます。

p5.jsを使って、ワイヤーフレームのオブジェクトを描画するサンプルコードを紹介します。ここでは3Dオブジェクト(球体)のワイヤーフレームを回転させるアニメーションを作成します。

スクリーンショット 2024-09-20 152531.png

ワイヤーフレームのサンプルコード


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.jsによるワイヤーフレームの球体アニメーション</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
  <main>
    <h1>ワイヤーフレームの球体アニメーション</h1>
  </main>

  <script>
    function setup() {
      // WEBGLモードでキャンバスを作成
      createCanvas(400, 400, WEBGL);
      // ストローク(輪郭線)を有効にして、ワイヤーフレーム表示を可能にする
      noFill();  // 中身は塗りつぶさない
      stroke(255);  // ワイヤーフレームの色を白に設定
    }

    function draw() {
      background(0);  // 背景を黒に設定

      // Y軸回りの回転アニメーション
      rotateY(millis() / 1000);

      // 球体を描画(ワイヤーフレーム表示)
      sphere(150);  // 半径150の球体
    }
  </script>
</body>
</html>

解説:
noFill():

ワイヤーフレーム表示にするために、オブジェクトの中身を塗りつぶさない設定です。
stroke(255):

球体の輪郭線(ワイヤーフレーム)の色を白に設定します。255はRGBカラーで白を表します。
rotateY():

millis()関数を使って、経過時間に基づいたY軸回転を行います。これにより球体がゆっくりと回転します。
sphere():

半径150の球体を描画します。この球体はワイヤーフレーム表示になります。
このコードを実行すると:
黒い背景に白いワイヤーフレームの球体が描画され、球体がY軸を中心にゆっくりと回転するアニメーションが表示されます。

p5.jsでシェーダーを使用して形状を変化させるサンプルコードを紹介します。この例では、時間に応じて球体の形状が変化します。具体的には、サイン波を使って球体の表面を変形させます。

スクリーンショット 2024-09-20 152726.png

シェーダーコードを使った形状変化のサンプル


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js + シェーダーによる形状変化アニメーション</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
  <main>
    <h1>シェーダーによる形状変化のアニメーション</h1>
  </main>

  <script>
    let myShader;

    function setup() {
      createCanvas(400, 400, WEBGL);
      noStroke();

      // 頂点シェーダー
      const vertexShader = `
        attribute vec3 aPosition;
        uniform float uTime;
        varying vec3 vPosition;

        void main() {
          float offset = sin(aPosition.x * 5.0 + uTime * 2.0) * 0.2; // サイン波で変形
          vec3 pos = vec3(aPosition.x, aPosition.y + offset, aPosition.z);
          vPosition = pos;
          gl_Position = vec4(pos, 1.0);
        }
      `;

      // フラグメントシェーダー
      const fragmentShader = `
        precision mediump float;
        varying vec3 vPosition;

        void main() {
          // 簡単なグラデーション色を設定
          float colorValue = vPosition.y * 0.5 + 0.5; // Y位置に基づく色
          gl_FragColor = vec4(colorValue, 0.5, 1.0 - colorValue, 1.0); // RGBA
        }
      `;

      myShader = createShader(vertexShader, fragmentShader);
    }

    function draw() {
      background(0);
      shader(myShader); // シェーダーを適用

      myShader.setUniform('uTime', millis() / 1000.0); // 時間をシェーダーに渡す

      // 球体を描画
      sphere(150);
    }
  </script>
</body>
</html>

解説:
頂点シェーダー:

uTimeを使用して、サイン波によって球体のY座標を変化させます。これにより、上下に波打つような変形が実現されます。
フラグメントシェーダー:

Y位置に基づいて色を設定します。Y座標の値を使って色のグラデーションを作成しています。
シェーダーの適用:

createShader()でシェーダーを作成し、shader()関数で適用します。
setUniform():

uTimeに経過時間を渡すことで、アニメーションを実現しています。
このコードを実行すると:
球体が上下に波打つように変形し、色もY座標に応じて変化します。時間の経過に伴って形状と色がダイナミックに変わるアニメーションが楽しめます。

別のシェーダーコードのサンプル。色彩の変化と波の動きを持つ球体のアニメーションを作成します。

スクリーンショット 2024-09-20 153037.png

シェーダーを使った色彩変化のサンプルコード


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>p5.js + カラフルな波動球体アニメーション</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
  <main>
    <h1>シェーダーによるカラフルな波動球体のアニメーション</h1>
  </main>

  <script>
    let myShader;

    function setup() {
      createCanvas(400, 400, WEBGL);
      noStroke();

      // 頂点シェーダー
      const vertexShader = `
        attribute vec3 aPosition;
        uniform float uTime;
        varying vec3 vPosition;

        void main() {
          float wave = sin(aPosition.x * 5.0 + uTime * 3.0) * 0.1; // 波の動き
          vec3 pos = aPosition + vec3(0.0, wave, 0.0); // Y軸方向に波を加える
          vPosition = pos;
          gl_Position = vec4(pos, 1.0);
        }
      `;

      // フラグメントシェーダー
      const fragmentShader = `
        precision mediump float;
        varying vec3 vPosition;

        void main() {
          // 色をY位置に基づいて変化させる
          float r = 0.5 + 0.5 * sin(vPosition.y * 5.0);
          float g = 0.5 + 0.5 * sin(vPosition.y * 5.0 + 2.0);
          float b = 0.5 + 0.5 * sin(vPosition.y * 5.0 + 4.0);
          gl_FragColor = vec4(r, g, b, 1.0); // RGBA
        }
      `;

      myShader = createShader(vertexShader, fragmentShader);
    }

    function draw() {
      background(0);
      
      myShader.setUniform('uTime', millis() / 1000.0); // 時間をシェーダーに渡す

      // シェーダーを適用して球体を描画
      shader(myShader);
      sphere(150); // 半径150の球体を描画
    }
  </script>
</body>
</html>

解説:
シェーダーの設定:

頂点シェーダー: Y軸方向にサイン波を追加して球体の形状を変化させます。uTimeに基づいて波の動きがアニメーションします。
フラグメントシェーダー: Y位置に基づいて色を変化させ、赤、緑、青の値をサイン波で設定しています。
draw関数:

背景を黒に設定し、シェーダーを適用して球体を描画します。uTimeを使ってアニメーションを実現しています。

参考。

基本的には p5.js の方が Three.js と比べて短いコードで同様の表現が可能です。これにはいくつかの理由があります:

シンプルなAPI:
p5.jsは、芸術的表現やクリエイティブコーディングに特化して設計されており、3D描画の基本的な要素(例えば、図形や色の変化など)を簡単に扱えるような関数が用意されています。setup()やdraw()のような基本関数を使うだけで、自動的にフレームを描画するループが組み込まれています。

高レベルの抽象化:
p5.jsは3D描画の処理をWebGLベースで行いますが、内部処理を抽象化していて、開発者は詳細を意識せずに3D表現ができます。例えば、sphere()やrotateX()、rotateY()といったシンプルな関数を呼び出すだけで、3Dの図形描画や回転が行えるようになっています。

Three.jsは強力な機能が多い:
Three.jsはより複雑な3Dシーンの構築に強力で、例えば光源や影、物理シミュレーション、カメラの高度な制御など、より細かい制御が可能です。その分、設定や処理のコードが長くなる傾向があります。Three.jsでは、ジオメトリ、マテリアル、シーン、カメラなどを細かく定義する必要があります。

比較の例:
Three.jsでは、scene、camera、rendererなどを手動で設定し、パーティクルの位置や色の属性をBufferGeometryを使って定義する必要があります。
p5.jsでは、createCanvas()、rotateX()、sphere()など、簡潔な関数呼び出しで同様の結果が得られます。
したがって、簡単な3D表現やアニメーションを作成する場合は、p5.jsの方がシンプルで短いコードで済むことが多いです。ただし、より複雑な3Dシーンや高性能なレンダリングを必要とする場合、Three.jsの方が適しています。

p5.jsは、非常に多くの関数を提供しており、2Dや3Dのグラフィックス、インタラクティブなアート、アニメーション、データビジュアライゼーションなど、幅広い用途で使うことができます。ここでは、p5.jsで利用できる主要な関数のカテゴリを紹介し、それぞれの代表的な関数をリストします。また、Perlinノイズの関数もありますので、それについても詳しく説明します。

主なp5.jsの関数カテゴリ
描画(Drawing)

line(x1, y1, x2, y2) - 線を描画
rect(x, y, w, h) - 矩形を描画
ellipse(x, y, w, h) - 楕円を描画
background(r, g, b) - 背景を設定
fill(r, g, b) - 塗りつぶしの色を設定
stroke(r, g, b) - 線の色を設定
noStroke() - 線を非表示に
beginShape() / endShape() - カスタムシェイプの描画

3D描画(3D Shapes)

box(size) - 立方体を描画
sphere(radius) - 球体を描画
rotateX(angle) / rotateY(angle) / rotateZ(angle) - 3D回転
translate(x, y, z) - 位置の移動

色(Color)

color(r, g, b) - 色を作成
lerpColor(c1, c2, amt) - 色の補間
alpha(c) - 色のアルファ値を取得

入力(Input)

mouseX / mouseY - マウスの現在位置
keyIsPressed - キーが押されているか確認
mousePressed() - マウスがクリックされた時の動作を定義
keyPressed() - キーが押された時の動作を定義

数学(Math)

sin(angle) / cos(angle) / tan(angle) - 三角関数
dist(x1, y1, x2, y2) - 2点間の距離
map(value, start1, stop1, start2, stop2) - 値の範囲を別の範囲にマッピング
random(min, max) - ランダムな数値を生成
noise(x, y, z) - パーリンノイズ(後述)
norm(value, start, stop) - 値を範囲内で正規化

時間(Time)

millis() - プログラム開始からの経過時間をミリ秒単位で取得
frameCount - フレーム数をカウント

画像(Images)

loadImage(path) - 画像をロード
image(img, x, y, w, h) - 画像を描画
tint(r, g, b) - 画像に色の効果を適用

アニメーション(Animation)

frameRate(fps) - フレームレートを設定
loop() / noLoop() - アニメーションのループを制御
delay(time) - 指定した時間だけ処理を遅延させる
フォントとテキスト(Fonts and Text)

text(str, x, y) - テキストを描画
textSize(size) - テキストのサイズを設定
loadFont(path) - フォントをロード

サウンド(Sound) (p5.sound.jsライブラリを使用する必要があります)

loadSound(path) - サウンドをロード
play() - サウンドを再生
setVolume(value) - ボリュームを設定

パーリンノイズ(Perlin Noise)
p5.jsには、パーリンノイズを生成するための組み込み関数があります。パーリンノイズは、より自然なランダムな動きを作り出すために使われ、雲、地形、波などの表現に広く利用されています。

noise(x, [y], [z])

1次元、2次元、3次元のパーリンノイズ値を返します。x、y、zに渡す座標に応じて滑らかなノイズ値が得られます。


function setup() {
  createCanvas(400, 400);
  noLoop();  // 描画を一度だけ行う
}

function draw() {
  background(255);
  noFill();
  beginShape();
  for (let x = 0; x < width; x++) {
    let y = noise(x * 0.01) * height;  // xに基づいてパーリンノイズを計算
    vertex(x, y);  // ノイズに基づいて頂点を設定
  }
  endShape();
}

この例では、noise(x * 0.01)を使用して、滑らかなノイズに基づく波線を生成しています。

noiseDetail(lod, falloff)

パーリンノイズの精度(レベルオブディテール)や減衰率を制御します。これにより、ノイズの細かさや変動の幅を調整できます。
noiseSeed(seed)

ノイズのシード値を設定します。同じシードを使うと、同じノイズパターンが再現されます。

まとめ
p5.jsは、非常に簡潔なコードで2Dや3Dのグラフィックス、アニメーション、インタラクティブなアートを作成するための強力なツールです。特に、パーリンノイズのような自然なランダム性を持つアニメーションや表現を行う際に便利な関数が揃っています。

p5.js でも シェーダー を使用することが可能です。p5.jsはWebGLベースのレンダリングをサポートしており、シェーダーコード(GLSL)を使用してカスタムなレンダリング効果を適用できます。p5.jsでは、createShader() 関数を使ってシェーダーを作成し、それをキャンバス上に適用することができます。

p5.jsには、頂点シェーダーとフラグメントシェーダー(ピクセルシェーダー)の両方をサポートする仕組みが組み込まれています。つまり、GLSLシェーダーコードをp5.jsの中で直接使えるということです。

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