Processing2.0の新機能。シェーダを使ってフィルタを作成する。 (シェーダ その1)

12月 24th, 2012

Processing Advent Calendar 2012/12/24(24日目)

Processing2.0の新機能。シェーダを使ってフィルタを作成する。

本日と明日はProcessing2.0の新機能、PShaderでシェーダを取り扱います。
本日は、シェーダの導入とフィルタの作成について。

シェーダ

Processing2.0のリファレンスに突如登場したPShader(PShader(Processing公式リファレンスページ)
これは名前の通り、Processingでシェーダを扱うためのものです。

シェーダは、グラフィックスをどのように描画するかの計算を記述したものになります。
シェーダを使用しない(つまり固定シェーダを使用する)場合は、予め決められた描画方法しかできません。

例えばProcessingで、P3Dモードでbox()とdirectionalLight()を使うと、立方体の各面の色は、その向きと光の向きに応じて、明るさが異なるようになります。これは固定シェーダの機能です。固定シェーダでは、このレベルのことしかできません。
「3Dのシーンに水晶の球体を作り、奥の風景が屈折して見える」というものは固定シェーダにない機能なので実現できません。
しかし、シェーダを扱えば、自分で描画方法を記述できるため、前述した機能を実現することができます。

ProcessingはOpenGLを使用しており、OpenGLでのシェーダは「GLSL」という構文を用いて記述します。
C言語風の構文ですが、使う構文はProcessing(というよりjava)とそこまで変わらないので、Processing Onlyな人であっても、そこまで身構える必要はないでしょう。

良く知られているシェーダとしては「頂点シェーダ」「ジオメトリシェーダ」「フラグメントシェーダ」と3種類あるのですが、 PShaderの引数を見ると「頂点シェーダ」と「フラグメントシェーダ」を使えることが分かります。
今回は、このうち「フラグメントシェーダ」を使います。

画面(各ピクセル)に表示する色を計算するのが、フラグメントシェーダの役割です。

フラグメントシェーダを使って見よう

ProcessingでPShaderを使うには下記のようにコーディングします。
PShaderを宣言し、loadShader()で読み込むフラグメントシェーダファイルを指定し、shader()でそのフラグメントシェーダをセットします。基本はこれだけです。

PShader sd;  //  シェーダ
 
void setup() {
  size(600, 600, P2D);
 
  //  フラグメントシェーダの読み込み
  sd = loadShader("FragmentShader.glsl");
}
 
void draw() {
  //  フラグメントシェーダのセット
  shader(sd);
 
  //  描画すると、フラグメントシェーダの内容で色がつく。
  rect(50, 50, width - 100, height - 100);
}

.pdeファイルと同階層にdataフォルダを作成し、そこにフラグメントシェーダファイルを配置します。上記サンプルを使うのであれば「FragmentShader.glsl」という名前にしてください。

フラグメントシェーダは下記のようにコーディングします。
シェーダはvoid main()から開始します。

void main()
{
  gl_FragColor = vec4(0, 0, 1, 1);
}

 

すると、下記のように青い四角形が描画されます。fill()を指定していないのに青くなります。

 

解説

shader()を使用しなかった場合は、下記のようになりますが、shader()を使用した場合は青くなりました。
つまり、フラグメントシェーダが青く塗るという処理をしてくれていることが分かります。

青くするという処理は、フラグメントシェーダに記述した、たった一行の処理「gl_FragColor = vec4(0, 0, 1, 1);」によるものです。

冒頭に書いた「画面(各ピクセル)に表示する色を計算するのが、フラグメントシェーダの役割」という言葉を思い出してください。
今回、Processingはrect()によって、四角形を描画することとなりました。当然、この四角形の色を塗る必要があります。
この時、塗る必要のあるピクセルに対して、どのような色にするかを決めるためにフラグメントシェーダが呼び出されます。

ピクセルをどのような色にするかは、フラグメントシェーダ側で「gl_FragColor」にセットします。
gl_FragColorは「vec4」という型で、名前の通り4つの要素を持つ型です。前からR,G,B,Aを0.0~1.0で表します。
今回はgl_FragColorにvec4(0, 0, 1, 1)をセットしたので、「ピクセルを青く塗る」という意味になります。(Processingでいうところのcolor(0,0,255,255)に値します。)
これにより、色を塗る必要があるピクセルは、全て青くなっているのです。

フィルタとしてシェーダを使用する

Processingのリファレンスを見ると、filter()の引数にPShaderを取ることができるようになっています。フラグメントシェーダを使えば、独自のフィルタを作ることができるというわけです。

PShaderをfilter()の引数に取ることで、画面全体に対して、フラグメントシェーダの処理を行うことができます。

PShader sd;  //  フラグメントシェーダ
 
void setup() {
  size(600, 600, P2D);
 
  //  フラグメントシェーダの読み込み
  sd = loadShader("FragmentShader.glsl");
}
 
void draw() {
  filter(sd); // フラグメントシェーダをフィルタとして適用。
}

フラグメントシェーダは最初にtextureSamplerの宣言を一行追加して、下記のようになります。フィルタとして使用する場合はこの一行が必要です。

uniform sampler2D textureSampler;  //  キャンバスとフラグメントシェーダを繋ぐ。
void main()
{
  gl_FragColor = vec4(0, 0, 1, 1);
}

実行すると画面全体が青くなります。

フィルタによって、画面全体が青く塗られました。

フラグメントシェーダをフィルタとして使いピクセルを操作する

まず、先ほどのサンプルで追加した「uniform sampler2D textureSampler;」について解説をいたします。

「uniform」というのはProcessingからフラグメントシェーダに対して与える変数です(フラグメントシェーダに対しての引数に近いです)。
Processingからは、textureSamplerという名前で、キャンバス全体が送られているようなので、textureSamplerの内容を使って、gl_FragColorに色をセットすれば、フィルタとして機能させることができます。

uniformで宣言すれば、ユーザーが自由にProcessingからフラグメントシェーダに対して値を送ることができます。(ただし、送れる数には上限があるようです。著者のPCでは256個の値を送ることができ、それ以上だとエラーとなりました。上限数はグラフィックボードにもよると思います。)

textureSamplerはテクスチャ座標でアクセスするため、画面のサイズが必要となります。そこで”size”という名前で、Processingからフラグメントシェーダに画面サイズを渡しています。渡すにはPShader.set()を使用します。

PShader sd;  //  フラグメントシェーダ
 
PImage img;
 
void setup() {
  size(640, 480, P2D);
 
  sd = loadShader("FragmentShader.glsl");
 
  //  フラグメントシェーダに画面サイズを渡す
  sd.set("size", width, height);
 
  img = loadImage("picture.jpg");  
}
 
void draw() {
  //  画像を描画  
  image(img, 0, 0);
 
  filter(sd);
}

フラグメントシェーダでは、ピクセルの色を、その赤・緑・青要素を平均したものとすることで、簡易グレースケールのフィルタにしています。

uniform sampler2D textureSampler; //  キャンバスの内容
uniform ivec2 size;  //  キャンバスのサイズ
 
//  簡易グレースケールフィルタ
void main()
{
  //  ピクセルの色を取得。テクスチャ座標なので、座標/画面サイズで位置を定める。
  vec4 color = texture2D(textureSampler, gl_FragCoord / size);
 
  //  取得したピクセルのR,G,B要素の平均を取る。
  float averageColor = (color.r + color.g + color.b) / 3;
 
  //  ピクセルの色を計算した平均色にする。
  gl_FragColor = vec4(averageColor, averageColor, averageColor, 1);
}

元の画像

フィルタ後の画像

texture2D()を使用すると、第一引数に指定したテクスチャの、第二引数の座標の色を取得することができます。

今回、第一引数にはtextureSampler(これはProcessingのキャンバスですね。フィルタが適用される前までは、キャンバスにはimage(img, 0, 0);によって、カラーの画像が描画されています)。第二引数では、テクスチャ座標を取得する必要があるので、gl_FragCoordと画面サイズを割ることで、テクスチャ座標を計算しています。

「gl_FragCoord」は、その時処理している座標が入っています。(ProcessingのmouseXのように、勝手に値が入っている変数と思うとよいでしょう。)

sizeは、Processingから、PShader.set()によって送った値です。int型の値2つを送ったので、フラグメントシェーダはivec2という型で受けています。ivec2はint型を格納するvec2になります。(float型であれば、vec2で受けれます。また、bool型であれば、bvec2で受けれます。)
シェーダは、変数の型がProcessingよりも厳密なので注意が必要です。

フラグメントシェーダでは、vec同士の計算は、一気に行うことができます。
今回の

gl_FragCoord / size

gl_FragCoord.x =  gl_FragCoord.x / size.x;
gl_FragCoord.y =  gl_FragCoord.y / size.y;

と同じ意味です。便利ですね。

ちなみに、フラグメントシェーダを使わずにProcessingのみで同じ処理を書くならば、下記のようになります。
ピクセルの操作をフラグメントシェーダで行っているという理解の助けになれば幸いです。

 
PImage img;
 
void setup() {
  size(640, 480, P2D);
 
  img = loadImage("picture.jpg");  
}
 
void draw() {
  //  画像を描画  
  image(img, 0, 0);
 
  loadPixels();
  for(int y = 0; y < height; y++){
    for(int x = 0; x < width ;x++){
      color c = pixels[y * width + x];
      float averageColor = (red(c) + green(c) + blue(c)) / 3;
      pixels[y * width + x] = color(averageColor, averageColor, averageColor, 255);  
    }
  }
  updatePixels();
}

モザイクフィルタを作成する

それでは最後にモザイクフィルタを作成します。

モザイクは、下記のように、ある一定の範囲を同じセルの色にすることがで実現できます。
(図中の数字は「色」と思ってください。違う数であれば違う色。同じ数字であれば同じ色という意味です)

これをフラグメントシェーダで記述することで、モザイクフィルタを作成することができます。

PShader sd;  //  フラグメントシェーダ
 
PImage img;  //  画像イメージ
 
void setup() {
  size(640, 480, P2D);
 
  //  フラグメントシェーダを読み込む
  sd = loadShader("FragmentShader.glsl");
 
  //  フラグメントシェーダに画面サイズを渡す
  sd.set("size", width, height);
 
  img = loadImage("picture.jpg");
}
 
void draw() {
  //  画像を描画  
  image(img, 0, 0);
 
  //  フィルタを適用
  filter(sd);
}

フラグメントシェーダは下記のようになります。

uniform sampler2D textureSampler;  //  キャンバスの内容
uniform ivec2 size;  //  キャンバスのサイズ
 
//  モザイクフィルタ
void main()
{
  //  モザイクとして使用する色を決定
  vec2 target;
  target.x = int(gl_FragCoord.x / 6) * 6;
  target.y = int(gl_FragCoord.y / 6) * 6;
 
  vec4 color = texture2D(textureSampler, target / size);
 
  gl_FragColor = color;
}

「int(● / 6) * 6」で、上図で言う左上のピクセルを表しています。

これで、6倍モザイクのフィルタが完成しました。

モザイクのレベルを可変にする。

先ほどのモザイクは6倍モザイク固定でした。
Processingからフラグメントシェーダに対して値を送ることで、モザイクのレベルを可変にするには、下記のようにします。

PShader sd;  //  フラグメントシェーダ
 
PImage img;  //  画像イメージ
int level = 1;  //  モザイクレベル
 
void setup() {
  size(640, 480, P2D);
 
  //  フラグメントシェーダを読み込む
  sd = loadShader("FragmentShader.glsl");
 
  //  フラグメントシェーダに画面サイズを渡す
  sd.set("size", float(width), float(height));
 
  img = loadImage("picture.jpg");
}
 
void draw() {
  //  画像を描画  
  image(img, 0, 0);
 
  //  モザイクレベルの計算
  level = mouseX / 10;
  level = level + 1;      //  レベルは1から始めるので+1
  sd.set("level", level); //  フラグメントシェーダにモザイクレベルを渡す
 
  filter(sd);
}

フラグメントシェーダは下記になります。uniformをつけて宣言をすると、Processingから値を受け取ることができることを思い出してください。
Processingで「sd.set()」したlevelをフラグメントシェーダで受け取り、「6」固定だった所に埋めることで、モザイクレベルを可変にしています。
これで、マウスのX座標に応じて、モザイクのレベルが変わってゆきます。

uniform sampler2D textureSampler;  //  キャンバスの内容
 
uniform ivec2 size;  //  キャンバスのサイズ
uniform int level;  //  モザイクレベル
 
//  モザイクフィルタ
void main()
{
  //  モザイクとして使用する色を決定
  vec2 target;
  target.x = int(gl_FragCoord.x / level) * level;
  target.y = int(gl_FragCoord.y / level) * level;
 
  vec4 color = texture2D(textureSampler, target / size);
 
  gl_FragColor = color;
}

まとめ

今回のまとめとしては、

  • PShaderでフラグメントシェーダを扱うことができる。
  • フラグメントシェーダでは、FragCoordに、処理するピクセルの座標が入ってくる。
  • フラグメントシェーダでは、FragColorに対して、ピクセルの色を設定する。
  • textureSamplerとtexture2Dで、キャンバスの内容をフラグメントシェーダで扱える。

です。
これで、オリジナルのフィルタ作ることができるようになりましたね!

明日は、引き続きフラグメントシェーダを使って、高速に模様を描いてみようと思います。お楽しみに!

Posted in Processing Advent Calendar2012 | No Comments »

Comments

Leave a Reply

 Comment Form