ギンの備忘録

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

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

モチベーションが下がりつつある

自分の性格的にわかっているが、段々クリエイティブコーディングに対してモチベーションが下がりつつある。 理由はわかっている。 別の趣味に興味が移っている。(カメラ熱が出てる)

趣味の中でもローテーションみたいなものがあるんですよね。 3,4個の趣味を行ったり来たりする性格なのです。

また時間が経てばクリエイティブコーディングに戻ってくると思うが、継続することも大切。 ここで止めるのは簡単だがもったいない。 なんとか続けていくために、今一度モチベーションを上げていこう。

やり方というか、切り口を変えればモチベーションが上がるのかな。 色とかデザインに関することを調べていこうかな。 アート的な要素からアプローチして、自分の興味をひいてみようか。

自作PC構成メモ(2020)

9月中頃にデスクトップPCがなくなりました。

gin-graphic.hatenablog.com

そんなわけでデスクトップPCを組み替えました。
グラフィックボードとSSD,HDDは今まで使っていたものをそのまま流用。
新規購入したのはCPU、マザーボード、メモリ、電源、ケースです。
購入金額は約6.5万円。 基本構成のものをほぼほぼ新規で組み上げました。
そして初のAMD構成。

以下が今回の自作で組み上げたPCの構成です。記録として残すためにメモ。

  • CPU: AMD Ryzen 5 3600 BOX
  • マザーボード: MSI B450 GAMING PLUS MAX
  • メモリ: G.SKILL F4-3600C19D-16GSXWB (DDR4 PC4-28800 8GB 2枚組)
  • 電源: Antec NE750 GOLD (750W)
  • ケース: Antec ATX対応 P101 Silent

(↓流用)

  • グラフィックボード: Palit GeForce GTX 1050Ti 4GB STORMX
  • SSD: 128GB * 1
  • HDD: 2TB * 1 + 3TB * 1
  • OS: Ubuntu 20.04 + Windouw 8.1

電源がオーバースペック過ぎました。
構成考えたときにあまり考慮できてなかった。(勢いで選んでしまった)
ゲームとかをするわけではないので、この構成で満足しています。
いずれグラボはもっとスペック高いものにしてみたいです。


自作PCを新しくして1ヶ月程が経過しました。

PCの使用目的が

  • WEB閲覧
  • 動画鑑賞
  • プログラミング
  • 写真編集

という前提での感想を書いていきます。

結果から言うと大満足です。
CPUはパワーがあるので、写真編集でRaw現像する際に処理時間が格段に短くなりました。
大量のRaw現像を一括で行っても、現像時間で苦になる程は待たなくて良いです。

動画の再生はグラフィックボードにも依存する部分があるのですが、4K動画でも問題なく再生できますね。
4Kモニタを使ってないので、その恩恵は受けられませんが、試しに再生したらモニタを4K対応に変えたくなってしまいました。

プログラミングはグラフィカルなProcessingやp5.jsでの描写もなんなくこなしてくれます。

AMD Ryzen 5 3600 は評判高かったので期待して購入したのですが、評判通りの良いスペックで私の使い方では何をするにも問題なくこなしてくれます。
今回買い替えたパーツは6.5万円でしたが、コスパで考えても大満足です。

p5.jsのP2DとWebGLの透過度による描写の違い

p5.jsのP2DとWebGLの透過度によって描写が異なっている様な気がします。
ブラウザ上では違いが分からないのですが、画像として保存した時に差異が顕在化しました。 比較のために半透明の四角形を明度、彩度を変えて描写したものを見てみましょう。
保存形式はpngとしています。

P2Dによる描写

f:id:gin_graphic:20201023073016p:plain:w250 f:id:gin_graphic:20201023073155p:plain:w250

WebGLによる描写

f:id:gin_graphic:20201023073026p:plain:w250 f:id:gin_graphic:20201023073211p:plain:w250

何が起きているのだろう

おそらくですが、WebGLモードで保存した画像に何かありそうな気がしますね。
背景を黒の描写を見ると分かりやすいですが、WebGLモードの方は保存した画像が半透明なんですね。
WebGLの方は保存した画像にアルファ値が残っているというか、有効になっているということですかね。
P2Dの方は保存した画像自体にはアルファ値が残っていないのかな。

詳細考察

保存された画像を解析していきます。
今回はLinux環境でコマンドidentify -verbose hoge.pngにて画像情報を比較していきます。

画像情報から差分が見えたので、わかりやすい部分を以下に抜粋します。

  • P2D保存PNG画像

Channel depth:
Red: 8-bit
Green: 8-bit
Blue: 8-bit
Alpha: 1-bit
Channel statistics:
...
Alpha:
min: 255 (1)
max: 255 (1)
mean: 255 (1)

Channel depth:
Red: 8-bit
Green: 8-bit
Blue: 8-bit
Alpha: 8-bit
Channel statistics:
...
Alpha:
min: 191 (0.74902)
max: 255 (1)
mean: 233.487 (0.915635)

アルファ値の深度が異なっています。 P2Dでは1bit、WebGLでは8bitです。
P2Dではアルファ値は無効な状態(どのピクセルも透明でない)で使われていないですが、 WebGLではアルファ値も描写した通りに保存されています。

保存形式の差がP2DとWebGLではあるようで、 それよって描写が異なる様に見えたということでした。 jpegで保存した場合にどうなるのかなど調べきれていない部分はありますが、 個人的にWebGLの透明度が残ることがわからず悩まされました。 原因を突き止めることができたのでよかったです。

最後に、今回使ったソースコードを以下に記載しておきます。

P2D描写でのソースコード

let bg=100;
let nx=7;
let ny=5;
let d=50;

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

   colorMode(HSB);
   rectMode(CENTER);
   background(bg);

   strokeWeight(1);
   stroke(0,0,50,0.5);

   for(let i=0;i<nx;i++){
      for(let j=0;j<ny;j++){
         let col = color(360/(nx-1)*i ,(i==0)?0:100 , 100/ny*j, 0.5) ;
         let x = (i+0.5)*width/nx;
         let y = (j+0.5)*height/ny;
         push();
         fill(col);
         translate(x, y);
         rect(0, 0, d);
         pop();
      }
   }
}

function keyPressed(){
   //save PNG
   save("img_"+month()+day()+hour()+minute()+second()+".png");
}

WebGL描写でのソースコード

let bg=100;
let nx=7;
let ny=5;
let d=50;

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

   colorMode(HSB);
   rectMode(CENTER);
   background(bg);
   translate(-width/2, -height/2);

   strokeWeight(1);
   stroke(0,0,50,0.5);

   for(let i=0;i<nx;i++){
      for(let j=0;j<ny;j++){
         let col = color(360/(nx-1)*i ,(i==0)?0:100 , 100/ny*j, 0.5) ;
         let x = (i+0.5)*width/nx;
         let y = (j+0.5)*height/ny;
         push();
         fill(col);
         translate(x, y);
         rect(0, 0, d);
         pop();
      }
   }
}

function keyPressed(){
   //save PNG
   save("img_"+month()+day()+hour()+minute()+second()+".png");
}

「岸辺露伴は動かない」がドラマ化らしいのでp5.jsで作品をつくる

露伴先生のイメージをp5.jsで形にしました。

ヘアバンドとピアス。

f:id:gin_graphic:20201015155028j:plain

ジョジョの奇妙な冒険第4部のスピンオフである、岸辺露伴は動かないがドラマ化されるみたいですね。

ドラマは12月28~30日午後10時に3夜連続で放送ということで、年末楽しみです。

ドラマ「岸辺露伴は動かない」では、第1話で人気エピソード「富豪村」、第2話で「くしゃがら」、第3話で「D.N.A」を実写化するということです。

今年の年末は家に居ることが多くなるでしょうし、是非チェックして欲しいですね。

https://news.yahoo.co.jp/articles/e8f94318ba23c22c507ddcd2bd63e4aad1d94690

露伴先生は自分の哲学を持った変人漫画家でスタンド使い

割と好きです。いや、かなり好きです。

どこまで露伴先生を作り出せるのかな。 別人として話だけなぞるとかはして欲しくないかな。

ジョジョの実写化なんて賛否両論になるのは目に見えてますが、どうなりますかね。

正確にはジョジョではなく岸辺露伴ですが、岸辺露伴というキャラクターをどこまで表現できるかにかかってますね。

良い作品となることを期待して待ちたいところです。