Processing2.0の新機能。シェーダを使って高速に模様を描く。 (シェーダ その2)

12月 25th, 2012

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

Processing2.0の新機能。シェーダを使って高速に模様を描く。 (シェーダ その2)

Merry Christmas!!
本日はクリスマス。AdventCalendarもいよいよ大詰めになってまいりました。最後なので、派手なものを作ります!

本日もProcessing2.0の新機能、PShaderでシェーダを取り扱います。
昨日の記事 シェーダその1も併せてご覧ください。

シェーダのメリット

シェーダは「高速に処理を行うことができる」というメリットがあります。
Processingで作品を作った場合、しばしば処理の関係で、満足な実行速度がでないことがあります。しかし、シェーダを使えば、そのような問題を解決することができるのです。

今回は、フラグメントシェーダでピクセル操作を行い、なめらかに動く模様を作成します。Processingのみではカクカクしたアニメーションになってしまうものも、フラグメントシェーダを用いると高速に動作させることができます。

線を引こう

まずは、フラグメントシェーダを使って、線を引いてみましょう。
フラグメントシェーダはピクセルを扱うので、「どの点に色を塗るか」を決めることで線を引くことができます。

昨日の復習になりますが、gl_FragCoordに座標が入ってきます。
「y座標が100の場合には、そのピクセルを白く塗る」としてみましょう。int()で囲っておくことをお忘れなく。

PShader sd;  //  シェーダ
 
void setup() {
  size(600, 600, P2D);
 
  sd = loadShader("FragmentShader.glsl");
}
 
void draw() {
  shader(sd);
 
  rect(0, 0, width, height);
}

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

void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  //  Y座標が100の場合のみ白くする。
  if(int(gl_FragCoord.y) == 100)
  {
    col = vec4(1, 1, 1, 1);
  }
  gl_FragColor = vec4(col);
}

フラグメントシェーダでは、Y座標の向きがProcessingとは逆になるので注意してください。左下が原点(0,0)になっています。

ネオン化しよう

線を引くだけでは見栄えがしないので、ネオン化してみます。
ネオン化は、基準となる位置(下図で言うと”E”の行)からの距離を調べ、その距離に応じて明るさを変えることで実現できます。

Processingのコードは変わりありません。シェーダは下記のようになります。

const float NEON_WIDTH = 50.0;  // 基準点からネオンが有効な距離
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  //  基準の位置を決定
  float h = 100;
 
  //  cは、座標が基準位置ぴったりであれば1.0。そこからNEON_WIDTHの範囲内であれば、距離に応じて1.0~0.0となる。
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  //  結果が0より大きければ、色を加算する。
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    vec3 rc = vec3(c, c, c);
    col += vec4(rc, 1);
  }
  gl_FragColor = vec4(col);
}

「abs(gl_FragCoord.y -h)」で、基準位置とピクセルとの距離を計算しています。
これを、NEON_WIDTHで割ることで、離れぐらいの割合を求めています。
さらに、1.0からその結果を引くことで、基準位置ぴったりの座標なら1.0。そこからNEON_WIDTHまでが0.0、その外はマイナスとなります。
ですので、結果が0.0より大きい場合のみ、色をつければよいわけです。

単純に明るさを変えるより、べき乗した方が見栄えが良くなるため(基準に近づくにつれて急に明るくなり、それ以外はぼやっとさせることができる)、今回は結果を3乗しています。

波にしよう

さて、単純な直線では味気ないので、波にします。
波には三角関数の正弦波を用います。おなじみsin()です。

また、Processingからカウンターを与えて、波を動かしてみます。

PShader sd;  //  シェーダ
int times = 0;  //  カウンター
 
void setup() {
  size(600, 600, P2D);
 
  sd = loadShader("FragmentShader.glsl");
}
 
void draw() {
  //  各パラメータをシェーダに渡す
  sd.set("times", times);
  shader(sd);
 
  times += 3;
 
  rect(0, 0, width, height);
}

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

uniform int times;  //  Processingから渡ってきたカウンター
const float NEON_WIDTH = 50.0;
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  //  正弦波(sin)を使って基準点を決める。
  float h = sin(radians(gl_FragCoord.x + times));
  h *= 25.0;
  h += 100;
 
  //  cは、座標が波形の位置ぴったりであれば1.0。そこからNEON_WIDTHの範囲内であれば、距離に応じて1.0~0.0となる。
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  //  結果が0より大きければ、色を加算する。
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    vec3 rc = vec3(c, c, c);
    col += vec4(rc, 1);
  }
  gl_FragColor = vec4(col);
}

GLSLには、組み込み関数として様々なものがサポートされています。(そして、ほとんどはProcessingと同じ使い方ができます。)

おなじみsin()・cos()の三角関数や、radians()・degrees()といった角度変換。
先ほども使いましたが、pow()やabs()といった数学関数。
floor()やmax()・min()などもサポートされています。
ベクトル操作関数のlength()やdot()・cross()なども実装されています。
詳しくは公式リファレンスページへ。

余談ですが、Processing2.0bにおいて「time」という名前をフラグメントシェーダに渡したところうまく動かなかったので、「times」という名前にしています。2.06bではうまく動いたのですが。。。「time」は内部で使用しているのかもしれませんね。

複雑な波にしてみよう

波を重ね合わせてみます。基準点hを、sin()とcos()を足したものにすることで、複雑な波にしています。

下記は、2つの波を重ね合わせてみたものを、フラグメントシェーダを用いた場合と、Processingのみで記述した場合の動画です。

フラグメントシェーダを用いた場合はフレームレート60が出ておりますが、Processingのみの場合は5,6フレームしか出ていないことが分かります。

シェーダを用いた場合

WaveForP5WithShader from @p5info on Vimeo.

Processingのみの場合

WaveForP5 from @p5info on Vimeo.

600×600ピクセルのピクセル処理はCPUに相当な負荷をかけますが、フラグメントシェーダなら、この程度は余裕で行うことができるのです。

下記が使用したフラグメントシェーダです。

uniform int times;
const float NEON_WIDTH = 50.0;
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  float h = cos(radians(gl_FragCoord.x + times)) + sin(radians(gl_FragCoord.x * 1.7 - times));
  h *= 25.0;
  h += 100;
 
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    vec3 rc = vec3(c, c, c);
    col += vec4(rc, 1);
  }
  gl_FragColor = vec4(col);
}

Processingのみで記述したものは下記です。

int time = 0;
 
void setup() {
  size(600, 600, P2D);
 
  textSize(24);
}
 
void draw() {
  background(0);
  time++;
 
  //  画面全体をpixels操作で描画する。
  loadPixels();
  for(int y = 0; y < height; y++){
    for(int x = 0; x < width; x++){
      float resultColor = 0;
      float h = sin(radians(x + time)) + cos(radians((x - time) * 1.4));
      h = h * 50.0 + 100;
      float near = abs(y - h) / 30.0;
      float c = 1.0 - near;
      if(c >= 0.0);
      {
        c = pow(c, 3.0);
        resultColor += c;
      }
      resultColor *= 255;
      pixels[y * width + x] = color(resultColor, resultColor, resultColor, 255);
    }
  }
  updatePixels();
 
  text(frameRate, 20, 20);
}

複数の波を配置してみよう

今度は波を複数配置してみましょう。

GLSLでは、関数を書くこともできます。今回は1つのピクセルに対し、基準点を10個取り、それぞれからの距離に応じて色をつけ、それを合算させています。

また、いろいろな波が出現するよう、波に使用するパラメータをランダムで生成し、フラグメントシェーダに送っています。

PShader sd;  //  シェーダ
int times = 0;
 
//  波形のパラメータ
float r1 = 0;
float r2 = 0;
float r3 = 0;
 
void setup() {
  size(600, 600, P2D);
 
  sd = loadShader("FragmentShader.glsl");
 
  sd.set("size", width, height);
  generate();
}
 
void draw() {
  //  各パラメータをシェーダに渡す
  sd.set("times", times);
  sd.set("r1", r1);
  sd.set("r2", r2);
  sd.set("r3", r3);
  shader(sd);
 
  times += 3;
 
  rect(0, 0, width, height);
}
 
//  パラメータを変更する関数
void generate(){
  r1 = random(4) - 2;
  r2 = random(4) - 2;
  r3 = random(4) - 2;
}
 
void mousePressed(){
  generate();
}

フラグメントシェーダは下記のようになります。色を計算する部分を関数にし、for文で基準をずらしながら10回測定しています。

uniform ivec2 size;
uniform int times;
uniform float r1;
uniform float r2;
uniform float r3;
 
const float NEON_WIDTH = 50.0;
 
vec4 drawLine(vec4 col, float off, float m, float offsetY){
  float tempTime = times * r3 * m;
 
  float h = cos(radians(gl_FragCoord.x * r1 + tempTime + off)) + sin(radians(gl_FragCoord.x * r2 - tempTime));
  h *= 25.0;  
  h += offsetY;
 
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    vec3 rc = vec3(c, c, c);
    col += vec4(rc, 1);
  }
 
  return col;
}
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  //  ピクセルと10個の波の位置関係を調べる。
  for(int i = 0; i < 10; i++)
  {
    col = drawLine(col, i, i / 2.0, size.y / 10 * i);
  }
 
  gl_FragColor = vec4(col);
}

インタラクティブにしてみよう

フラグメントシェーダにマウスの座標を送れば、インタラクティブな作品にすることができます。

使いたい値はフラグメントシェーダに渡していきましょう。

PShader sd;  //  シェーダ
int times = 0;
 
//  波形のパラメータ
float r1 = 0;
float r2 = 0;
float r3 = 0;
 
void setup() {
  size(600, 600, P2D);
 
  sd = loadShader("FragmentShader.glsl");
 
  sd.set("size", width, height);
 
  generate();
}
 
void draw() {
  sd.set("mouseX", mouseX);
  sd.set("mouseY", mouseY);
  sd.set("times", times);
  sd.set("r1", r1);
  sd.set("r2", r2);
  sd.set("r3", r3);
  shader(sd);
 
  times += 3;
 
  rect(0, 0, width, height);
}
 
//  パラメータを変更する関数
void generate(){
  r1 = random(4) - 2;
  r2 = random(4) - 2;
  r3 = random(4) - 2;
}
 
void mousePressed(){
  generate();
}
uniform ivec2 size;
uniform int times;
uniform int mouseX;
uniform int mouseY;
uniform float r1;
uniform float r2;
uniform float r3;
 
const float NEON_WIDTH = 50.0;
 
vec4 drawLine(vec4 col, float off, float m, float offsetY){
  float tempTime = times * r3 * m;
 
  float h = cos(radians(gl_FragCoord.x * r1 + tempTime + off)) + sin(radians(gl_FragCoord.x * r2 - tempTime));
  h *= 25.0;  
  h += offsetY;
 
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    vec3 rc = vec3(c, c, c);
    col += vec4(rc, 1);
  }
 
  return col;
}
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  //  ピクセルと10個の波の位置関係を調べる。
  for(int i = 0; i < 10; i++)
  {
    col = drawLine(col, mouseX * i, i / 2.0, (size.y - mouseY) / 10 * i);
  }
 
  gl_FragColor = vec4(col);
}

 

色をつけてみよう

最後に色をつけます。

GLSLはHSV(ProcessingでいうとHSBモード)には対応しておりません。
そこで、シェーダ側にHSB→RGB変換の関数を作成し、色をつけることにしました。

HSB変換の式はWikipediaを参考にしています。

Processingのコードに変わりはありません。フラグメントシェーダは下記のようになります。

uniform ivec2 size;
uniform int times;
uniform int mouseX;
uniform int mouseY;
uniform float r1;
uniform float r2;
uniform float r3;
 
const float NEON_WIDTH = 50.0;
 
//  HSBをRGBに変換する関数。
vec3 hsb2rgb(vec3 hsb)
{
  hsb.x = mod(hsb.x, 360);
  vec3 res = vec3(0, 0, 0);
  float h = mod(floor(hsb.x / 60), 6);
  float f = (hsb.x / 60.0) - h;
  float v = hsb.z;
  float p = v * (1.0 - hsb.y);
  float q = v * (1.0 - f * hsb.y);
  float t = v * (1.0 - (1.0 - f) * hsb.y);
 
  switch((int)h)
  {
  case 0:
    res = vec3(v, t, p);
  break;
  case 1:
    res = vec3(q, v, p);
  break;
  case 2:
    res = vec3(p, v, t);
  break;
  case 3:
    res = vec3(p, q, v);
  break;
  case 4:
    res = vec3(t, p, v);
  break;
  case 5:
    res = vec3(v, p, q);
  break;
  }
  return res;
}
 
vec4 drawLine(vec4 col, float off, float m, float offsetY){
  float tempTime = times * r3 * m;
 
  float h = cos(radians(gl_FragCoord.x * r1 + tempTime + off)) + sin(radians(gl_FragCoord.x * r2 - tempTime));
  h *= 25.0;  
  h += offsetY;
 
  float t = abs(gl_FragCoord.y - h) / NEON_WIDTH;
  float c = 1.0 - t;
 
  if(c > 0.0)
  {
    c = pow(c, 3.0);
    //  HSBをRGBに変換する。
    vec3 rc = hsb2rgb(vec3(off / 10.0 + times, min(c, 1.0), min(c, 1.0)));
    col += vec4(rc, 1);
  }
 
  return col;
}
 
void main()
{
  vec4 col = vec4(0, 0, 0, 1);
 
  for(int i = 0; i < 10; i++)
  {
    col = drawLine(col, mouseX * i, i / 2.0, (size.y - mouseY) / 10 * i);
  }
 
  gl_FragColor = vec4(col);
}

ネオン化しているおかげで、各波が重なった箇所が輝くところがポイントです。
すぐに実行できない方のために動画も準備しました。

ShaderRainbowLine from @p5info on Vimeo.

このような表現が今までのProcessingでできたでしょうか?
これをProcessingで書いたとしたら、カクカクして見るに堪えないものになってしまうでしょう。
しかし、フラグメントシェーダなら、動画の通りサクサク動きます。凄い!

 

テクスチャにしてみよう

さて、今回はフラグメントシェーダ寄りの話になってしまったので、最後にProcessingとうまいこと連携します。

フラグメントシェーダで描いた絵を、Processingのテクスチャとして使用したらどうなるでしょうか。

そこで今回は、テクスチャを環状に配置することにしました。

環状に配置する、というイメージは下図の通りです。テクスチャの上部をしぼませ、下部を引き延ばして環っかにする感じです。切れ目をうまくつなげるために、上下でそれぞれのテクスチャを使用し、片方は反転させています。

テクスチャサンプル

上記テクスチャを環状に配置

PShader sd;  //  シェーダ
PGraphics pg;
 
//  波形のパラメータ
float r1 = 0;
float r2 = 0;
float r3 = 0;
int time = 0;
 
void setup() {
  size(600, 600, P2D);
 
  sd = loadShader("FragmentShader.glsl");
 
  sd.set("size", width, height);
 
  pg = createGraphics(width, height, P2D);
  textureMode(NORMAL);
  noStroke();
 
  generate();
}
 
void draw() {
  background(0);
  //  各パラメータをシェーダに渡す
  sd.set("mouseX", mouseX);
  sd.set("mouseY", mouseY);
  sd.set("times", time);
  sd.set("r1", r1);
  sd.set("r2", r2);
  sd.set("r3", r3);
 
  time += 3;
 
  //  PGraphicsをシェーダで描画する。
  pg.beginDraw();
  pg.shader(sd);
  pg.rect(0, 0, width, height);
  pg.endDraw();
 
  //  環状にvertex()を描き、そこにPGraphicsをテクスチャとして貼る。
  beginShape(TRIANGLE_FAN);
  texture(pg.get());
  vertex(300, 300, 0, 0);
  for(int i = 0; i <= 720; i += 10){
    float xx = cos(radians(i / 2));
    float yy = sin(radians(i / 2));
    vertex(xx * 300 + 300, yy * 300 + 300, abs(360 - i) / 360.0, 1);
  }
  endShape(CLOSE);
}
 
//  パラメータを変更する関数
void generate(){
  r1 = random(4) - 2;
  r2 = random(4) - 2;
  r3 = random(4) - 2;
}
 
void mousePressed(){
  generate();
}

フラグメントシェーダに変わりはありません。

これを実行すると、次の様になります。

これも、すぐに実行できない方のために動画を準備しました。

ShaderRainbowCircle from @p5info on Vimeo.

美しい模様が高速に動いています。

 

まとめ

今回のまとめとしては

  • フラグメントシェーダはピクセル処理もできる。
  • フラグメントシェーダは処理が高速である。
  • PGraphicsに対してシェーダを適用することもできる。

2回に渡ってお送りしたシェーダ特集はいかがでしたか?
フラグメントシェーダを使うことで、これまではできなかった表現を行うことができるようになります。

今まで、loadPixels()~updatePixels()で書いていたような処理であれば、大抵はフラグメントシェーダで行うことができるでしょう。

みなさんもシェーダを使って新しい作品をたくさん作ってくださいね。

最後まで読んで頂きありがとうございました!

Posted in Processing Advent Calendar2012 | No Comments »

Comments

Leave a Reply

 Comment Form