独楽回しeddyと申します。
今回Processing Advent Calendar 2020の23日目を担当させていただきます。
テーマはPGraphicsとPShaderです。これら2つの使い方とそれによって何が可能になるかについて備忘録的な意味も込めて今回記事にします。
Processing
https://processing.org
Processingとは、Ben Fry氏とCasey Reas氏らによって作られたJavaをベースにして単純化して描きやすくして、グラフィック面に特化した言語です。Windows、Mac、Linuxで動作する上使うために必要な準備も非常に簡単です。
今回のテーマについて
今回のテーマは下記の2点になります。
- PGraphics
- PShader
両方ともProcessingで標準搭載されているクラスです。
それぞれについてどのようなものかをまず簡単に説明します。
PGraphics
公式の解説のURL : https://processing.org/reference/PGraphics.html
普通Processingは以下のコードのようにsizeで画面の大きさを指定し、backgroundなどで背景色を決め、ellipseなどで図形を描画して...という感じで書かれると思います。
// 普通のProcessingで描画する方法
void setup(){
size(360, 360);
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
fill(220, 100, 100, 100);
stroke(120, 100, 100, 100);
ellipseMode(CENTER);
ellipse(width/2, height/2, 100, 100);
}
中央に円が描かれる、シンプルなスケッチです。作った画面の中に描画内容が表示されております。
それに対してPGraphicsは作った画面の中に「描画領域」を新たに作って、そこに描画内容を記載することで表示させるイメージです。描画内容の画像化してから表示するイメージと言っても良いかもしれません。こちらに関しましても実際にコードと描画画面を例示します。
// PGraphicsで描画する方法
PGraphics graphics;
void setup(){
size(360, 360);
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
graphics = createGraphics(180, 180); // 180×180の描画領域を別で作成
graphics.beginDraw(); // 作ったPGraphicsに対しての描画内容の記述を開始する合図
graphics.clear(); // graphicsに対して初期化
graphics.colorMode(HSB, 360, 100, 100, 100); // 色をHSBで表現するモードに
graphics.fill(0, 0, 0, 100); // 1枚目の画像の後ろ背景用の四角形(colorModeの関係で0, 0, 0で黒)
//graphics.fill(0, 0, 100, 100); // 2枚目の画像の後ろ背景用の四角形(colorModeの関係で0, 0, 100で白)
graphics.noStroke();
graphics.rectMode(CENTER); // 四角形の描画の際に指定座標は中心位置にする
graphics.rect(graphics.width/2, graphics.height/2, width, height); // width, heightで作成したPGraphicsの縦横の長さが取れる
graphics.fill(220, 100, 100, 100); // 円の塗り潰し色
graphics.stroke(120, 100, 100, 100); // 円の枠線の色
graphics.ellipseMode(CENTER); // 円の描画の際に指定座標は中心位置にする
graphics.ellipse(graphics.width/2, graphics.height/2, 100, 100);
graphics.endDraw(); // 作ったPGraphicsに対しての描画内容の記述を終了する合図
imageMode(CENTER);
image(graphics, width/2, height/2); // 作ったPGraphicsを実際に表示する。
}
PGraphicsを使う場合は最初にまずPGraphicsの変数を宣言します。そしてcreateGraphicsを描画領域の横の長さと縦の長さを引数とする事で描画領域の生成を行います。作られた描画領域内に表示する内容を記述する際はbeginDraw()
とendDraw()
を呼ぶのが必須で、その間に記述します。また、普通のProcessing上での描画と異なり、新たに作成した描画領域に対して描画を行うため、全てに対してPGraphicsから呼ぶようにしなければなりませんし、新たにセットアップ(背景色、色範囲の設定、円描画の際の位置基準など)する必要もあります。(コード内のgraphics.~ の部分です)
そして最後に描画領域を表示する部分としてimage(...)
を呼びます。これがないと実際に表示されません。最初に「描画内容の画像化してから表示するイメージ」と書いたのはこの部分の影響もあります。
そして上記コードを実行すると下記画面になります。
普通に書いた場合と比較して全く同じ出力になり、この時点では違いは見受けられません。違いが分かるように、背景色を変えてみます。
上のコードの「1名目の画像」と書かれている部分をコメントアウトし、「2名目の画像」と書かれている部分のコメントアウトを削除します。すると下記画面になります。
四角形の描画の際長さは主の描画画面の縦横の長さと同一(sizeの引数に書いた360)になっており、PGraphicsで作成した描画領域よりも大きい値となっておりますが、実際はPGraphicsで作られた描画領域部分のみが背景が白になっております。あくまでPGraphics内のみに描画結果が反映されているという事がこの点からも分かります。
PGraphicsはこのような感じで描画領域を別で作ってその中に表示する事ができます。
PShader
公式の解説のURL : https://processing.org/reference/PShader.html
PShaderはProcessing上でシェーダーを扱えるようにするためのものです。使い方としましては、下記のようになります。
- シェーダーファイルを用意し、作成したスケッチのdataフォルダに格納する。
- PShaderを変数宣言する。
- loadShaderでシェーダーファイルを読み込む。
- シェーダー適用開始のための関数を呼ぶ。
正直これだけだと情報が不足し過ぎていると思われますので、ここからもう少し説明を加えます。
そもそもシェーダーって何?
そもそもシェーダーって何だろう?って思う方もいらっしゃると思われるため、簡単にはなりますが説明しようと思います。
シェーダーは3次元物体に対して陰影や物体の質感を表現するために使用されるプログラムです。
GPUというグラフィックスの処理に特化した演算装置を用いた並列計算を用いている点が特徴で、これによって同時並行で大量に処理を行う事が出来ます。ピクセル単位の計算が要求される場合だと計算量は膨大になりますが、それと相性が良いのです。
色々難しい知識がたくさん出てくる上、自分自身も説明できる自信がないためここではこれ以上深くは解説しません。
シェーダーには物体の頂点に関する計算を行うバーテックスシェーダー、色に関する計算を行うフラグメントシェーダーの大きく2つに分かれております。
今回はバーテックスシェーダーに関しては扱わず、フラグメントシェーダーのみ扱って上のスケッチに対して変化を加えてみます。
準備
シェーダーを使う準備に取り掛かる前に
それではシェーダーを扱う準備を上に書いた使い方に沿って準備をしていくのですが、その前に動画として動くものにするためにPGraphicsの描画部分をdraw
に移します。
PGraphics graphics;
void setup(){
size(360, 360, P2D); // P2Dで
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
graphics = createGraphics(180, 180, P2D); //
}
void draw(){
background(0);
graphics.beginDraw();
graphics.clear();
graphics.colorMode(HSB, 360, 100, 100, 100);
graphics.shader(shader);
graphics.fill(0, 0, 100, 100);
graphics.noStroke();
graphics.rectMode(CENTER);
graphics.rect(graphics.width/2, graphics.height/2, graphics.width, graphics.height);
graphics.fill(220, 100, 100, 100);
graphics.stroke(120, 100, 100, 100);
graphics.ellipseMode(CENTER);
graphics.ellipse(graphics.width/2, graphics.height/2, 100, 100);
graphics.resetShader();
graphics.endDraw();
imageMode(CENTER);
image(graphics, width/2, height/2);
}
drawに移動した後のスケッチの中身は上記になります。beginDraw
からendDraw
、image
で表示する部分までをdrawに移す以外は特に変更はございません。Processingのsetupは1回のみ通る場所であるため、動画を作る場合だとそれのみで対応は出来ません。逆にdrawはループされる部分であるためここに書けば値が途中で変化した際に描画結果がリアルタイムで変化するようになり、動画に対応出来るようになります。
そしてもう一つ変更点があります。sizeとcreateGraphicsの3番目の引数にP2D
と追加されております。これは描画モードを指定する意図で書かれております。これがなければシェーダーファイルの読み込みに失敗して使えないため必須の変更となります。出力結果はPGraphicsの最後の出力結果と同じであるため割愛します。以上で準備の前の準備は完了です。
1. シェーダーファイルを用意し、作成したスケッチのdatasフォルダに格納する。
まずはシェーダーファイル(今回はフラグメントシェーダーのみ)を用意いたします。ファイル名は今回はfragment.fragとしますが、拡張子が.frag
であれば名前は基本的になんでも構いません。
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 vertColor; // 受け取った頂点にある色情報
void main() {
gl_FragColor = vertColor; // 色情報をそのまま出力する
}
上記が今回用意するfragment.fragのファイルの中身です。頂点にある色情報を受け取って、それをそのまま出力するという内容になっております。つまりは何も変化させないシェーダーです。
作成した後それをProcessingのスケッチにdataフォルダを作って格納します。(下図参考)
Processingの画面の上にファイルをドラッグ&ドロップする事で自動的に行ってくれるため確実だと思います。
2. PShaderを変数宣言する。
新たにPShaderを型とする変数定義をします。下記の感じです。(setup内とdraw内は省略します)
PGraphics graphics;
PShader shader; // 新規追記部分
void setup(){
...
}
void draw(){
...
}
3. loadShaderでシェーダーファイルを読み込む。
次にsetupの中でloadShaderを使ってfragment.fragを読み込みます。(drawは省略してます)
PGraphics graphics;
PShader shader;
void setup(){
size(360, 360, P2D); // P2Dで
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
graphics = createGraphics(180, 180, P2D);
shader = loadShader("fragment.frag"); // 新規追記部分: シェーダーファイルを読み込む
}
4. シェーダーを適用するのための関数を呼ぶ。
シェーダーを適用するための関数を呼びます。方法は少なくとも2つあります。
- PGraphicsの関数としてshaderがあるのでそれを呼ぶ。
- imageで描画後にfilterを呼ぶ。
今回は1で行います。
以下のようにdraw関数内を書き加えます。
PGraphics graphics;
PShader shader;
void setup(){
size(360, 360, P2D); // P2Dモード指定
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
graphics = createGraphics(180, 180, P2D); // P2Dモード指定
shader = loadShader("fragment.frag"); // シェーダーファイルを読み込む
}
void draw(){
background(0);
graphics.beginDraw();
graphics.clear();
graphics.colorMode(HSB, 360, 100, 100, 100);
graphics.shader(shader); // 新規追記部分 : シェーダー適用を開始
graphics.fill(0, 0, 100, 100);
graphics.noStroke();
graphics.rectMode(CENTER);
graphics.rect(graphics.width/2, graphics.height/2, graphics.width, graphics.height);
graphics.fill(220, 100, 100, 100);
graphics.stroke(120, 100, 100, 100);
graphics.ellipseMode(CENTER);
graphics.ellipse(graphics.width/2, graphics.height/2, 100, 100);
graphics.resetShader(); // 新規追記部分 : シェーダー適用を終了
graphics.endDraw();
imageMode(CENTER);
image(graphics, width/2, height/2);
}
draw関数内でコメントとして「新規追記部分」と書いたところが今回で追記した部分です。shader(PShader)
でシェーダーの開始を宣言し、以降で書かれる内容に対してシェーダーが適用されます。終了する際はresetShader()
を呼びます。(ただresetShaderは途中からシェーダー適用なしで描画したいときは必要ですが、今回はそういうのはないため呼ばなくても問題はないです)
その出力結果は下図になります。
PGraphicsの説明の際の最終出力からは何も変わっていない結果が出力されます。ここではそれが正解です。何故ならシェーダー側で何も変化を加えていないからです。
ここから少しシェーダーに手を加えてみましょう。
変更1
PShaderでは、必要あればProcessingのスケッチ側からシェーダー側に送信する値を下記に記すset関数を用いて設定する事が可能です。
set("シェーダーファイル内での変数名",セットする変数(最大4つまで));
それではProcessingのスケッチ側からシェーダー側に送る情報を設定してみましょう。
今回は以下の情報をシェーダー側に送信するようにしようと思います。
- 作成したPGraphicsの縦横の長さ
- 経過時間。
1のPGraphicsの縦横の長さはスケッチを起動してから変わる事はないので、setup内で書きます。上に記した通り、set関数は最初にシェーダー側で扱う際の変数名を文字列型で記述し、そこから最大4つまで値を設定できます。PGraphicsの縦横の長さはそれぞれPGraphicsのwidth、heightで取得する事が可能です。
2の経過時間はスケッチを起動してから絶えずに変わっていくものであるため、draw内で書きます。こうする事でシェーダー側にも経過時間を送る事ができます。Processingの変更後のスケッチを下記に示します。
PGraphics graphics;
PShader shader;
void setup(){
size(360, 360, P2D); // P2Dモード指定
background(0);
colorMode(HSB, 360, 100, 100, 100);
frameRate(60);
graphics = createGraphics(180, 180, P2D); // P2Dモード指定
shader = loadShader("fragment.frag"); // シェーダーファイルを読み込む
shader.set("resolution", graphics.width, graphics.height); // PGraphicsの縦横の長さを送る。(シェーダー側ではresolutionという変数名で取得できる)
}
void draw(){
background(0);
shader.set("time", millis()/1000.0f); // 経過時間を送る。(シェーダー側ではtimeという変数名で取得できる)
graphics.beginDraw();
graphics.clear();
graphics.colorMode(HSB, 360, 100, 100, 100);
graphics.shader(shader); // 新規追記部分 : シェーダー適用を開始
graphics.fill(0, 0, 100, 100);
graphics.noStroke();
graphics.rectMode(CENTER);
graphics.rect(graphics.width/2, graphics.height/2, graphics.width, graphics.height);
graphics.fill(220, 100, 100, 100);
graphics.stroke(120, 100, 100, 100);
graphics.ellipseMode(CENTER);
graphics.ellipse(graphics.width/2, graphics.height/2, 100, 100);
graphics.resetShader(); // 新規追記部分 : シェーダー適用を終了
graphics.endDraw();
imageMode(CENTER);
image(graphics, width/2, height/2);
}
次にシェーダーファイルに手を加えます。先ほどProcessing側でsetした値(シェーダー側に送った値)を実際にシェーダーで扱うために以下のように書きます。
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 resolution; //Processing側から送られてきた情報1: PGraphicsの縦横の長さ
uniform float time; //Processing側から送られてきた情報2: 経過時間
varying vec4 vertColor;
void main() {
gl_FragColor = vertColor;
}
まず見ていただきたいのは変数名です。変数名は必ずProcessing側でsetしたときに第1引数で記載した文字列と同一
でなければなりません。
そして次に注目していただきたいのは変数宣言前のuniform
です。uniform
はシェーダー側に送られる値に対してつける接頭辞的なもので、これもなければ正しく動作しません。
またresolutionについてですが、ProcessingではPGraphicsの縦の長さ、PGraphicsの横の長さと2つ値を設定しております。そのためシェーダー側で扱う際の型はvec2
となっております。2つの情報が1つの変数に入っているイメージです。
実行すると下記になります。
まだ特に何も起きません。送ってもらった値に対して特に何も行っていないからです。
ここから更にもう一段手を加えます。
変更2
折角送信してもらった値を何も使わないのはもったいないので、それを利用した例として波紋を作ってみたいと思います。
最初に変更後のシェーダーファイルの中身を記載します。
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 resolution;
uniform float time;
varying vec4 vertColor;
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y); // 画面の正規化
float alpha = step(sin(length(uv) * 20.0 + time), 0.5); // 波紋計算部分
gl_FragColor = vec4(vec3(1.0 - alpha), 1.0); // 色を出力
}
上記は波紋を作るフラグメントシェーダーです。
まず最初に出てくる1行目が何者かという話ですが、ここでは図を使って説明します。
流れとしては上図の1~4の順番になります。
- 最初は今回の場合ですと縦横の長さが180×180のPGraphicsとなり、左下が原点(0, 0)となっております。
gl_FragCoord.xy
はピクセルの位置を示す今回扱っているシェーダー言語に標準で使用できる変数で、今回の場合ですとx,y共に0~180の値を取っております。中心は(90, 90)、右上が(180, 180)をとなります。 -
gl_FragCoord.xy
に対して2.0倍を行います。そうする事で取りうる値は0~360になります。中心の値の大きさ、右上の値の大きさも2倍になっております。 - 2倍した
gl_FragCoord.xy
に対してresolutionの値を引きます。今回のresolutionはx,y共に180(PGraphicsの縦の長さ、横の長さ)であり、そうする事で範囲は0~360から-180~180になり、中心が原点(0, 0)になります。gl_FragCoord.xy
はピクセルの位置を示しますが、実際に最大値が幾つなのか、範囲はどこまでなのかに関してはシェーダー側だけで分かる情報ではないため、Processing側からPGraphicsの縦横の長さを送ってもらう事で対応が出来る様になります。 - 最後にPGraphicsの縦横の長さの小さい方の値で割ります。そうする事でx,y共に範囲が-1~1、中心が原点(0,0)を取るようになり綺麗に正規化されます。
基本的に最後に出力する値は0.0~1.0
の範囲であるため、このようにして正規化する事でフラグメントシェーダーを扱う上では何かと都合が良いのです。
-
length
は今回扱っているシェーダー言語に標準で使用できる関数で、原点からの長さを計算します。その結果のみを表示させたのが1枚目になります。見ての通り中央に近い場所ほど黒く、離れていくほど色が薄れて最終的には白いままの部分も出来ております。 - 次に
step(length(uv), 0.5);
のみを実行した場合の表示結果です。step関数は2番目に指定した値より小さい場合は1、大きい場合は0として計算する関数です。1つ目の画像と2つ目の画像を見比べますと中央に近い部分(0.0 ~ 0.5)に関しては1、それ以外が0として扱われており、その証拠として中央が白く(RGBが全て1.0)塗り潰されており、それ以外が黒く(RGBが全て0.0)塗り潰されております。 - そして
length(uv)
をsin関数の値とし、周期パターンを多くするためにlength(uv)
を20倍します。そうする事で3枚目のような波紋を表示させる事が出来ます。 - 最後にsin関数の中に経過時間をプラスします。そうする事で波紋が収束するようなアニメーションを再生が出来ます。
このようにフラグメントシェーダーを変更して再度実行してみましょう。
すると下記のようなgifになります。
PGraphicsの領域が白黒の波紋になりましたね。このように色情報を数学的に操作するだけで様々な動画を作ることも可能です。
ただ、これでは元画像の影も形もない状態なので元画像を活かすようにしたいと思います。
変更3
ここで変更する内容はそこまで大きくはありません。
先ほど書いた波紋のシェーダーをあくまで透明度のみに適用させ、それ以外のRGBは全て元の頂点の色情報を載せれば終わりです。
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 resolution;
uniform float time;
varying vec4 vertColor;
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y); // 画面の正規化
float alpha = step(sin(length(uv) * 20.0 + time), 0.5); // 波紋計算部分
gl_FragColor = vec4(vertColor.xyz, alpha); // 色を出力
}
元の画像が波紋部分が欠けているような外見になりました。
ちょっとこれだと分かりづらいと思われるため、Processing側で主の描画の背景カラーがフレーム数で変化するように下記の通りに変えました。
void draw(){
//background(0); // コメントアウト
background(frameCount%360, 100, 100, 100); // 時間で背景色が変化する
...
}
波紋部分が欠けているような外見がより顕著に分かると思います。
このようにする事でPGraphicsとPShaderを組み合わせることで上記のように面白い見た目のものを作る事ができます。
まとめ
今回はPGraphicsとPShaderの大まかな使い方と活用例に関しまして説明致しました。
正直シェーダーに関しまして非常に難しく、自分も正直理解が正しく行えているかは怪しい部分が多々あります。
しかしこの部分を使えるようになる、或いは術を知るだけでも表現の幅広さが変わるのではないかなと思いますし、何より面白いかなと思い備忘録的な意味も込めて今回記事とさせていただきました。
誰かにとってこの記事が少しでも参考になるときがくれば良いなと思います。
ここまで読んで下さりありがとうございました。
参考文献
特にシェーダー周りの説明は難しい点が多いため、ここでは参考になる資料を記載しておきます。
##FMS_CAT氏のProcessing Community Days 2019のワークショップの資料
slide : https://docs.google.com/presentation/d/12y-hHiKzF8Qn4lzI_eRjd1fKN2-dyW6UGsQ-igbHFDo/edit#slide=id.p
gitHub : https://github.com/FMS-Cat/20190202-pcd-workshop
Processingとシェーダーの扱い方についても記載してあります。しかもポストエフェクトというシェーダーを使ったエフェクトについてや、バーテックスシェーダーを使ったパフォーマンスの改善まで幅広いです。
aadebdeb氏のProcessing Community Days 2020のワークショップの資料
gitHub : https://github.com/aadebdeb/PCD_Tokyo_2020_Workshop
シェーダーについてやシェーダーを使ったアートについての解説がしっかりされております。
h_doxas氏によるWebGL、シェーダー関連の開発支援サイト
https://wgld.org
今回の記事で使用したglslについて文法等で詳しい解説が書かれており、興味を持ったら見てみると良いと思います。
The Book of Shaders
https://thebookofshaders.com/?lan=jp
有志によって纏められているシェーダーに関する解説サイト
基礎的な部分からしっかり書かれているため、こちらもおすすめです。