ギンの備忘録

デジタルアートやプログラミングやテクノロジー関連のこと。

p5.jsでのシェーダー(GLSL)入門(1)描画してみる

この記事はp5.jsは知っている、使ったことがあるけど、シェーダーとかGLSLは扱ったことがないという人に向けて書いていきます。

p5.jsを知らないという方は、調べてみて下さい。 p5.jsはクリエイティブ・コーディング用のJavaScriptライブラリで、様々なコンピュータグラフィックス描画できます。

p5.jsではWebGLに対応しているため、シェーダー言語のGLSLを用いた描画表現が可能です。

今回、この記事ではp5.js上でGLSLを扱った描画方法を紹介していきます。

GLSL自体はかなり奥が深く、筆者自身も初学者で誤った理解などあるかもしれません。ご了承下さい。

まず、「シェーダー」、「WebGL」、「GLSL」とは何かを簡単に抑えてから、p5.jsでGLSLを扱う方法を説明していきます。

用語

シェーダー

Wikipediaから引用します。

3次元コンピュータグラフィックスにおいて、シェーディング(陰影処理)を行うコンピュータプログラムのこと。「shade」とは「次第に変化させる」「陰影・グラデーションを付ける」という意味で、「shader」は頂点色やピクセル色などを次々に変化させるもの(より具体的に、狭義の意味で言えば関数)を意味する。

描画処理に関するプログラムのことですね。 オブジェクトに対して、様々な描画処理を施して表示するプログラムといったところでしょうか。

WebGL

Wikipediaから引用します。

ウェブブラウザで3次元コンピュータグラフィックスを表示させるための標準仕様。

GPU による描画処理を行い、画面表示するための仕組みだそうです。(CPUでも動く) WEB ページ上で高速な 2D・3Dの画像描画処理が可能で、 GLSL(OpenGL Shader Language) というC言語ベースのプログラム言語での記述が必要となります。

GLSL

C言語をベースとした高レベルシェーディング言語です。

GLSLでは2つのシェーダーを記述する必要があり、それらは「頂点(バーテックス)シェーダー」と「フラグメントシェーダー」と呼ばれてます。

  • 頂点シェーダー
    頂点の位置を計算を行う。 位置座標や法線ベクトルなどの頂点の属性だけを参照・変換する記述。

  • フラグメントシェーダー
    色の情報の描画処理を行う。 ピクセル単位の色の計算をする記述。

以上がざっくりとした用語解説です。 ここからp5.jsでシェーダーを扱う方法を説明していきます。 読み込むことで、シェーダーによる描画を行います。

p5.jsでシェーダーを読み込むには、loadShader()createShader()の2つの関数が用意されています。

今回は、一つのファイル内でシェーダーを利用できる方法createShader()を使います。

loadShader()は外部ファイルからロードする方法です。GLSLの記述量が多くなる場合や他と共有する時にはこちらが便利ですね。

詳しくは公式Referenceをどうぞ。

シェーダーの読み込みをするコードは下の様になります。

let theShader;

function setup() {
   createCanvas(500, 500, WEBGL);
   theShader = createShader(vs, fs);
   noStroke();
}

まず、読み込んだシェーダーを格納する変数を用意します。ここではlet theShader;とします。 変数名は好きに変えて良いです。 複数シェーダーを読み込むことも可能で、その時は読み込む数だけ変数を用意して下さい。

theShader = createShader(vs, fs);で用意した変数へシェーダを格納します。 setup()内に書くのが分かりやすいですが、実際にシェーダーを使用するまでに読み込んでおけば問題ないので、記述位置を変えても良いです。

シェーダーを試す

色を指定する

色の情報を扱うのはフラグメントシェーダーになります。

まず、キャンバス全体をシェーダーで青色にしてみたいと思います。 下のコードを実行すれば、青一色が描画されると思います。

let vs = `
  precision highp float;

  attribute vec3 aPosition;

  void main() {
     vec4 positionVec4 = vec4(aPosition, 1.0);
 
     gl_Position = positionVec4;
}
`;

let fs = `
  precision highp float;

  void main() {
    vec3 color = vec3(0.0, 0.0, 1.0);

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

let theShader;

function setup() {
  createCanvas(500, 500, WEBGL);
  theShader = createShader(vs, fs);
  noStroke();

  shader(theShader);
  quad(-1, -1, -1, 1, 1, 1, 1, -1);
  resetShader();
}

実行すると下の様な、青一色の画面が表示されるはずです。

f:id:gin_graphic:20201130213236p:plain:w250

setup()内、下の部分でシェーダーを適用した描画が行われます。 shader()で以降にシェーダーを適用し、resetShader()することで通常のp5.jsの描画に戻します。

resetShader()は必須ではないですが、シェーダーの適用範囲が明確になるので、この記事では書いておきます。

  shader(theShader);
  quad(-1, -1, -1, 1, 1, 1, 1, -1);
  resetShader();

今回はquad()にシェーダーを適用することで、キャンバス全体をシェーダーで描画できるようにしています。 シェーダーを適用するオブジェクトは変えることもできます。それは別の記事で書きたいと思ってます。

頂点シェーダーvsは、この記事ではあまり説明しません。 GLSL側で受け取った頂点を何もせずに表示する処理になっています。

フラグメントシェーダーfsで、色情報を扱います。gl_FragColorという変数にRGBAの4要素を渡すことで、色が指定されます。

ここでは青色を表示しており、書下すとgl_FragColor=vec4(0.0,0.0,1.0,1.0)となります。GLSLで色情報は0.0〜1.0として扱うので注意が必要ですね。

グラデーション

色をキャンバス内の位置で変えて描画してみます。 x方向で緑、y方向で青を変化させていきます。

let vs = `
  precision highp float;

  attribute vec3 aPosition;

  void main() {
     vec4 positionVec4 = vec4(aPosition, 1.0);

     gl_Position = positionVec4;
}
`;

let fs = `
  precision highp float;

  uniform vec2 resolution;

  void main() {
    float r = 0.0;
    float g = gl_FragCoord.x / resolution.x;
    float b = gl_FragCoord.y / resolution.y;

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

let theShader;

function setup() {
   createCanvas(500, 500, WEBGL);

   theShader = createShader(vs, fs);
   noStroke();

  shader(theShader);
   theShader.setUniform('resolution', [width, height]);
  quad(-1, -1, -1, 1, 1, 1, 1, -1);
  resetShader();
}

下の様な青と緑のグラデーションが表示できます。

f:id:gin_graphic:20201130213241p:plain:w250

フラグメントシェーダー内でgl_FragCoordを使っています。 これは描画領域内のピクセル座標を示す変数です。

対象ピクセルgl_FragCoord.xがx座標、gl_FragCoord.yがy座標となります。

描画領域のサイズをuniform vec2 resolution;として宣言しています。 uniform型はp5.js側でも扱える変数となります。

theShader.setUniform('resolution', [width, height]);でp5.jsからGLSLの変数に値を代入しています。 キャンバス全体を描画領域とするので、resolution[width, height]を代入しておきます。

フラグメントシェーダーでfloat g = gl_FragCoord.x / resolution.xとすることで、キャンバスのx方向の座標で緑を変化させています。 ピクセル座標を描画領域で割ることで、0.0〜1.0の色情報へ割り当ています。

y方向も同様に処理して、青を割り当ててます。

指定座標で色を変える

ある座標の色だけを変えてみます。 1/10より小さい範囲のみ色を着けてみます。

let vs = `
  precision highp float;

  attribute vec3 aPosition;

  void main() {
     vec4 positionVec4 = vec4(aPosition, 1.0);

     gl_Position = positionVec4;
}
`;

let fs = `
  precision highp float;

  uniform vec2 resolution;

  void main() {
    vec3 color = vec3(0.0);

    if(gl_FragCoord.x < resolution.x/10.0){
      color.g = 1.0;
    }
    else if(gl_FragCoord.y < resolution.y/10.0){
      color.b = 1.0;
    }

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

let theShader;

function setup() {
   createCanvas(500, 500, WEBGL);

   theShader = createShader(vs, fs);
   noStroke();

  shader(theShader);
   theShader.setUniform('resolution', [width, height]);
  quad(-1, -1, -1, 1, 1, 1, 1, -1);
  resetShader();
}

下の様な、ある範囲のみ色着けた表示になります。

f:id:gin_graphic:20201130213249p:plain:w250

グラデーションのコードと大きく変わっていませんが、gl_FragCoordの意味合いがわかりやすいのではないでしょうか。

if文で条件gl_FragCoord.x < resolution.x/10.0の下で色情報を指定します。 キャンバスのx方向1/10の領域で緑、 キャンバスのy方向1/10の領域で青を表示しています。

発光したような描画

発光したような線を描いていきます。

let vs = `
  precision highp float;

  attribute vec3 aPosition;

  void main() {
     vec4 positionVec4 = vec4(aPosition, 1.0);

     gl_Position = positionVec4;
}
`;

let fs = `
  precision highp float;

  uniform vec2 resolution;

  const float freq = 20.0;

  void main() {
    vec3 color;

    vec2 c = gl_FragCoord.xy / resolution;
    c = c * 2.0 - 1.0;
    color += vec3(pow(1.0 - abs(c.y), 64.0) * 2.0);
    color *= vec3(0.2, 0.5, 0.9) ;

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

let theShader;

function setup() {
   createCanvas(500, 500, WEBGL);

   theShader = createShader(vs, fs);
   noStroke();

  shader(theShader);
   theShader.setUniform('resolution', [width, height]);
  quad(-1, -1, -1, 1, 1, 1, 1, -1);
  resetShader();
}

下の様な発光表現になります。

f:id:gin_graphic:20201130223637p:plain:w250

フラグメントシェーダーで、はじめに座標を-1.0〜1.0に変換しています。

    vec2 c = gl_FragCoord.xy / resolution;
    c = c * 2.0 - 1.0;

その後、そのy座標の絶対値をつかって、y座標の中心付近が明るい(1.0に近い)、端に行くと暗い(0.0に近い)色になるように演算しています。

    color += vec3(pow(1.0 - abs(c.y), 64.0) * 2.0);

まとめ

p5.jsからシェーダーを呼び出し、GLSLを使って色情報を扱うことができました。 p5.jsとシェーダーを用いれば表現の幅がより一層広がると思います。 みなさんの作品作りに少しでも貢献できていれば幸いです。

参考文献

itp-xstory.github.io

webglfundamentals.org

qiita.com

github.com

p5.jsでのシェーダー(GLSL)入門

p5.jsでのシェーダー(GLSL)入門(1)描画してみる - ギンの備忘録 Gin's Memorandum

p5.jsでのシェーダー(GLSL)入門(2)図形にシェーダーを適用 - ギンの備忘録 Gin's Memorandum

p5.jsでのシェーダー(GLSL)入門(3)シェーダーの描画を動かす - ギンの備忘録 Gin's Memorandum

p5.jsでのシェーダー(GLSL)入門(4)図形を変形する - ギンの備忘録 Gin's Memorandum