Processing中毒者の嘔吐物

ProcessingやらopenFrameworksやら色んなプログラミングについて吐きます

Processingで発光表現

Processing Advent Calendar 2015 : ATNDの参加記事です。

長い間悩み続けていたProcessingでの発光表現にひとまず終止符が打たれたので記事にします。
終止符を打ってくれたのが以下の記事。qiita.com
まじでありがたい。結局発光表現には画像を使うしかないんだけど、その画像をProcessingで作る方法と3D空間に配置する方法が書いてある。上の記事内にあるプログラムを解説しやすいように変更したものを掲載させていただきます。次のプログラムを実行してみてください。マウスの位置に発光する球体が描画されます。

PImage img; // 光る球体の画像

void setup() {
  size(500, 500, P2D);
  imageMode(CENTER);
  // 加算合成
  blendMode(ADD);
  noCursor();
  
  // 画像の生成
  img = createLight(random(0.5, 0.8), random(0.5, 0.8), random(0.5, 0.8));
}

// 光る球体の画像を生成する関数
PImage createLight(float rPower, float gPower, float bPower) {
  int side = 200; // 1辺の大きさ
  float center = side / 2.0; // 中心座標
  
  // 画像を生成
  PImage img = createImage(side, side, RGB);
  
  // 画像の一つ一つのピクセルの色を設定する
  for (int y = 0; y < side; y++) {
    for (int x = 0; x < side; x++) {
      //float distance = sqrt(sq(center - x) + sq(center - y));
      float distance = (sq(center - x) + sq(center - y)) / 50.0;
      int r = int( (255 * rPower) / distance );
      int g = int( (255 * gPower) / distance );
      int b = int( (255 * bPower) / distance );
      img.pixels[x + y * side] = color(r, g, b);
    }
  }
  return img;
}

void draw() {
  background(0, 15, 30);
  
  // 画像を描画
  image(img, mouseX, mouseY);
}

void mousePressed() {
  img = createLight(random(0.5, 0.8), random(0.5, 0.8), random(0.5, 0.8));
}

createLight()関数で発光球体の画像を作ります。加算合成にしないと画像が透過されないので注意です。

createLight()関数の中身を詳しく解説します。createLight()の3つの引数はRGBのそれぞれの色成分の強さを0〜1の値で表します。createImage()で幅と高さが200pxの画像を作り、2重for文でピクセルの一つ一つに色を設定していきます。その際、色を設定しようとしている座標と中心座標との距離distance(正確には距離ではない)を計算し、distanceで色成分を割ることで中心から遠い座標にあるピクセルほど暗い色になり、発光表現が作れます。正確な距離を計算する場合は距離の差の2乗のルートを計算するのですが(コメントアウト部分)、それだと色の変化が単調になり発光表現がうまくいきません。ルートの代わりに数値で割るようにするとdistanceは2次関数になり発光表現がいい感じになります。ここらへんはdistanceを実際にプロットしてみるとわかりやすいので、distanceのグラフを作るコードも載せておきます。

size(500, 500);
background(0);

float cx = width/2;
float cy = height/2;

noFill();
stroke(255);
strokeWeight(4);

beginShape();
for (int x = 0; x < width; x++) {
  //float dist = sqrt(sq(cx - x) + 0.0);
  float dist = (sq(cx - x) + 0.0) / 50.0;
  vertex(x, height-dist);
}
endShape();

発光球体の画像の作り方がわかったところでこんな感じに3D空間に描画してみます↓
f:id:P5Aholic:20151209225802j:plain
元にしてるプログラムは以下の記事で解説しています。p5aholic.hatenablog.com
ソース↓

PImage img; // 光る球体の画像

float velocity = 0;
float acceleration = 0.05;

void setup() {
  fullScreen(P3D);
  // zテストを無効化
  hint(DISABLE_DEPTH_TEST);
  // 加算合成
  blendMode(ADD);
  imageMode(CENTER);
  // 画像の生成
  img = createLight(random(0.5, 0.8), random(0.5, 0.8), random(0.5, 0.8));
}

// 光る球体の画像を生成する関数
PImage createLight(float rPower, float gPower, float bPower) {
  int side = 200; // 1辺の大きさ
  float center = side / 2.0; // 中心座標

  // 画像を生成
  PImage img = createImage(side, side, RGB);

  // 画像の一つ一つのピクセルの色を設定する
  for (int y = 0; y < side; y++) {
    for (int x = 0; x < side; x++) {
      float distance = (sq(center - x) + sq(center - y)) / 50.0;
      int r = int( (255 * rPower) / distance );
      int g = int( (255 * gPower) / distance );
      int b = int( (255 * bPower) / distance );
      img.pixels[x + y * side] = color(r, g, b);
    }
  }
  return img;
}

void draw() {
  background(0, 15, 30);
  //fill(255);
  //text(frameRate, 50, 50);
  translate(width/2, height/2, 0);
  rotateX(frameCount*0.01);
  rotateY(frameCount*0.01);

  float lastX = 0, lastY = 0, lastZ = 0;
  float radius = 280;
  float s = 0, t = 0;

  while (s <= 180) {
    float radianS = radians(s);
    float radianT = radians(t);
    float x = radius * sin(radianS) * cos(radianT);
    float y = radius * sin(radianS) * sin(radianT);
    float z = radius * cos(radianS);

    stroke(128);
    if (lastX != 0) {
      strokeWeight(1);
      line(x, y, z, lastX, lastY, lastZ);
    }

    pushMatrix();
    // 画像の座標へ原点を移動
    translate(x, y, z);
    // 画像の向きを元に戻す
    rotateY(-frameCount*0.01);
    rotateX(-frameCount*0.01);
    // 画像を描画
    image(img, 0, 0);
    popMatrix();

    lastX = x;
    lastY = y;
    lastZ = z;

    s++;
    t += velocity;
  }
  velocity += acceleration;
}

void mousePressed() {
  img = createLight(random(0.5, 0.8), random(0.5, 0.8), random(0.5, 0.8));
}

発光球体の画像を3D空間に配置するときはhint(DISABLE_DEPTH_TEST)でzテストを無効化しておく必要があります。zテストとは簡単に言うと3D空間に描画される図形の重なりを調べ、手前にある図形によって覆い隠されている奥の図形の描画を省くことで処理速度を上げる仕組みのことです。今回は画像を透過させて奥にある画像も描画させたいのでzテストを無効化させています。

3D空間に発光球体画像を描画する場合は常に画像が正面を向いた状態でなければいけません。今回の場合はrotateX()とrotateY()で座標全体が回転しているので、画像を描画するときは画像の向きを正面に戻す必要があります。これは単純に同じ分だけ逆回転させればいいのですが、rotateX()→rotateY()の順で回転させた場合はrotateY()→rotateX()の順番で戻すことに注意が必要です。

ちなみに明日のProcessingアドベントカレンダーも僕が担当なんすよ。ちょっと前に作った軽めのスケッチを公開します。