🤩

p5.jsとGLSLを使ったポストエフェクトが楽しすぎる!

2023/12/04に公開

この記事はProcessing Advent Calendar 2023 - Adventarの4日目の記事です。

最近p5.js上でGLSL(フラグメントシェーダー)を使ったポストエフェクトにハマっているのでそちらをサクッと扱うための方法といくつかの実装パターンを紹介します。
私自身人に説明できるだけのシェーダー理解ができておらず、GLSL上の細かい用語をかなり省いており、GLSLがなんとなくわかっている方向けの記事になっています。そのため、

p5.js上でGLSLを使う魅力

GLSLおよびフラグメントシェーダーを使うためだけであればわざわざp5.jsを使う必要はありません。例えばshadertoyGLSL Sandboxのようなweb上で実行可能なGLSLエディタが存在しますし、WEBGLに特化したPixiJSやThree.jsといったライブラリでも勿論GLSLを書くことができます。

それなのになぜわざわざp5.js上でGLSLを用いるかの魅力について紹介します。

図形とシェーダーのエフェクトを分割できる

例えばGLSL Sandboxでは図形もシェーダー上に書く必要があります。私のようなGLSL初心者にはどこがエフェクト的なコードなのか区別をつけるのも大変です。しかし、p5.jsではcanvas上に図形を描いてからポストエフェクトのようにシェーダーをかけることができるため、どこまでがシェーダーの表現なのかわかりやすく整理することができます。
(個人的にはphotoshopのフィルターを図形にかけるノリで書いています)

また、シェーダーとして一部のエフェクトを分割することで、p5.js上では図形を描くことに集中することができコードが汚れないというメリットもあります。(エフェクトが気に入らなければGLSLごとはぎ取れば素の状態に戻せる)

p5.js自体がコード量を少なくかけるのでわかりやすい

p5.jsはThree.jsやPixiJSといったグラフィカルな表現ができるライブラリに比べてGLSLを使うための設定含め元々のコード量を圧倒的に少なく記述することができます。そのため、お手軽にGLSLを試すことが可能になります。

p5.js上でさくっとポストエフェクトをするための下準備

シェーダーを使うにはcanvasをWEBGLモードで書く必要があります。
今回は描画されたcanvasのピクセル情報にシェーダーをかけたいので、createGraphics関数で設定したグラフィックスバッファオブジェクトに図形を描いて、そこにシェーダーをかけていくやり方で行っていきます。
今回紹介するコードは以下になります。
https://editor.p5js.org/Karakure178/sketches/-IKnoVhmB

ちょっとだけ中身を紹介していきます。

let pg;
let theShader1;

function setup() {
  createCanvas(800, 800,WEBGL);
  pg = createGraphics(width, height);
  theShader1 = createShader(shader.vs, shader.fs);
}

シェーダーをかけるpgオブジェクトを定義し、今回使用するシェーダーをtheShader1に定義しています。

シェーダーの基本設定をしていきます。

function draw() {
  //WEBGLは真ん中基準だがcreageGraphicsは左上基準なので合わせるために設定している
  background(220);
  translate(-width / 2,-height / 2);
  
  pg.push();
  pg.translate(width/2,height/2);
  pg.circle(0,0,200);
  pg.pop();
  
  // シェーダーの設定
  shader(theShader1);
  theShader1.setUniform(`u_tex`, pg);
  theShader1.setUniform(`u_time`, frameCount / 35);
  theShader1.setUniform('u_resolution', [pg.width, pg.height]);
  
  // シェーダー適用したイメージを描画
  image(pg,0,0);
}

const shader1 = {
  vs: `
  precision highp float;
  precision highp int;

  attribute vec3 aPosition;
  attribute vec2 aTexCoord;

  varying vec2 vTexCoord;

  uniform mat4 uProjectionMatrix;
  uniform mat4 uModelViewMatrix;

  void main() {
    vec4 positionVec4 = vec4(aPosition, 1.0);
    gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
    vTexCoord = aTexCoord;
  }
`,
  fs: `
  precision highp float;
  precision highp int;

  varying vec2 vTexCoord;

  uniform sampler2D u_tex;
  uniform float u_time;
  uniform vec2 u_resolution;

  float PI = 3.14159265358979;

  void main() {
    vec2 uv = vTexCoord;
    vec4 tex = texture2D(u_tex, uv);

    gl_FragColor = tex;
  }
`,
};

draw関数内でpgに描画する処理を入れています。今回はシンプルに円を中心に描画しています。
そのあとGLSLに渡すための変数を定義しています。
u_texがpgに含まれるピクセル情報、u_timeが時間、u_resolutionがシェーダーが描画する領域の大きさになります。
draw関数の最後にimage関数を呼ぶことでシェーダーを重ねたpgオブジェクトを描画することができます。
今回シェーダーをshader1オブジェクトに記述しました。vsプロパティには基本的なバーテックスシェーダーの記述を、fsプロパティにはフラグメントシェーダーを記述しています。
後は、fsプロパティ上にガリガリとフラグメントシェーダーの内容を記述することでポストエフェクトを表現することができます。

いくつかシェーダーのパターンを紹介

実際に書いていていくつかパターンを作成したのでそれらを紹介します。
(技術的なことはそこまで理解できていないためコードが変であればコメント頂けますと幸いです。)

シェーダーサンプル紹介

シェーダーをかさねるサンプルを紹介します。
基本的なサンプルイメージは以下になります。

コードは以下になります。
https://editor.p5js.org/Karakure178/sketches/cxM8USsOI
今回はシェーダーによってどれだけ変化したかわかりやすくするために先ほどの下準備と違って格子状に円を並べて配置しています。

魚眼レンズ表現


コードは以下になります。
https://editor.p5js.org/Karakure178/sketches/YjP0PLmvU
主要なコードは以下になります。

    float apertureHalf = 0.5 * u_aperture * (PI / 180.0);
    float maxFactor = sin(apertureHalf);

    vec2 xy = 2.0 * uv.xy - 1.0;
    float d = length(xy);

    if (d < (2.0-maxFactor)){
      d = length(xy * maxFactor);
      float z = sqrt(1.0 - d * d);
      float r = atan(d, z) / PI;
      float phi = atan(xy.y, xy.x);
      
      uv.x = r * cos(phi) + 0.5;
      uv.y = r * sin(phi) + 0.5;
    }

あらたにu_apertureをp5.js上から渡される変数としています。u_apertureは魚眼レンズのサイズを変えることができます。

ハッチング表現


コードは以下になります。
https://editor.p5js.org/Karakure178/sketches/NGeMf9Bxv

主要なコードは以下になります。

    float hatch = 10.0;// ハッチングのサイズを変えられる
    float lum = length(texture2D(u_tex, uv.xy).rgb);

    vec4 tex = texture2D(u_tex, uv);
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    bool isHatch = false;

    if (lum < 1.00){
      if (mod(gl_FragCoord.x + gl_FragCoord.y, hatch) == 0.0){
        gl_FragColor = tex;
        isHatch = true;
      }
    }

    if (lum < 0.75){
      if (mod(gl_FragCoord.x - gl_FragCoord.y, hatch) == 0.0){
          gl_FragColor = tex;
          isHatch = true;
      }
    }

    if (lum < 0.50){
      if (mod(gl_FragCoord.x + gl_FragCoord.y - 5.0, hatch) == 0.0){
          gl_FragColor = tex;
          isHatch = true;
      }
    }

    if (lum < 0.3){
      if (mod(gl_FragCoord.x - gl_FragCoord.y - 5.0, hatch) == 0.0){
          gl_FragColor = tex;
          isHatch = true;
      }
    }

    if(isHatch == false){
      gl_FragColor = vec4(u_color, 1.0);
    }

こちらはPixiJSのフィルターを参考にして書いたものです。別途u_colorをp5.jsから渡してもらい、ハッチングをかけない部分は塗りつぶししてもらうよう記述しています。

走査線表現

コードは以下になります。
https://editor.p5js.org/Karakure178/sketches/H6MBxWU-n
主要なコードは以下になります。

    // 走査線を書く
    float scanLineInterval = 1500.0; // 大きいほど幅狭く
    float scanLineSpeed = u_time * 5.0; // 走査線移動速度
    float scanLine = max(1.0, sin(uv.y * scanLineInterval + scanLineSpeed) * 1.6);

    tex.rgb *= scanLine;

scanLineIntervalの数値をいじることで線の大きさを変えることができます。

終わりに

p5.jsにGLSLでシェーダーをかさねる表現はいかがだったでしょうか?
p5.js上では図形を一切変えずに、それでも表現を追加できることは非常に魅力的だと個人的に考えています。
備忘録的な形でこちらでもゆっくりとシェーダーパターンを記録しているため興味ある方は見ていただけますと幸いです。

参考資料

Discussion