光り輝くパーティクルを作ってみよう

12月 14th, 2011

Processing Advent Calendar 2011(14日目)

光り輝くパーティクルを作ってみよう


アプレットはこちら

今回は光り輝くパーティクルを作成します。

加算合成

加算合成は、光を表現する際に重要な概念です。
これは、色を足し算することで実現できます。

加算合成は、半透明(アルファ)とは少し異なります。簡単に解説すると、半透明の色を重ねた場合は、「元の色と重ねる色をそれぞれ透明度で割り算したものを足す」という計算になります。
つまり、半透明のものをいくら重ねても、色が明るくなることはありません。(透明度で割り算されてしまうため、重ねる色よりも明るくなることができないのです。)
しかし、加算合成は足し算であるため、足せば足すほど明るく(白く)なり、輝きを表現することができるのです。

まずは、加算合成のプロトタイプとして、今回は、loadPixels()でピクセルの値を取得し、パーティクルからの距離に応じて色を足し算するコードを作成しました。
全ピクセルに対して、パーティクルからの距離を算出し、その距離と光の強さに応じて、ピクセルに色を足しています。この結果、3つのパーティクルからの光が重なる場所などで、半透明では表せない、光っぽい効果(白い部分が融合していく感じ)を出すことができます。


アプレットはこちら

final int[] LIGHT_POSITION_X = {200, 100, 300};  //  各パーティクルのX座標
final int[] LIGHT_POSITION_Y = {100, 200, 300};  //  各パーティクルのY座標
 
float lightPower;        //  光の強さ
float lightPowerAdd = 1; //  光の強さの加算値
 
float halfWidth;    //  横幅の中心
float halfHeight;   //  縦幅の中心
 
void setup() {
  size(400, 400);
  halfWidth = width / 2;
  halfHeight = height / 2;
}
 
void draw() {
  background(0);
 
  //  光の強さを変更する。範囲を超えた場合は加算値の符号を逆転させる。
  lightPower += lightPowerAdd;
  if(lightPower > 50){
    lightPower = 50;
    lightPowerAdd *= -1;
  }
  if(lightPower < 0){
    lightPower = 0;
    lightPowerAdd *= -1;
  }
 
  loadPixels();
  //  パーティクルの影響範囲のピクセルについて、色の加算を行う
  for(int i = 0; i < LIGHT_POSITION_X.length; i++){
    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {
        int pixelIndex = x + y * width;
 
        //  ピクセルから、赤・緑・青の要素を取りだす
        int r = pixels[pixelIndex] >> 16 & 0xFF;
        int g = pixels[pixelIndex] >> 8 & 0xFF;
        int b = pixels[pixelIndex] & 0xFF;
 
        //  パーティクルの中心と、ピクセルとの距離を計算する
        float dx = LIGHT_POSITION_X[i] - x;
        float dy = LIGHT_POSITION_Y[i] - y;
        float distance = sqrt(dx * dx + dy * dy);  //  三平方の定理
 
        //  0除算の回避
        if(distance < 1){
          distance = 1;
        }
 
        //  光の強さを距離で割る
        r += (255 * lightPower) / distance;
        g += (255 * lightPower) / distance;
        b += (255 * lightPower) / distance;
 
        //  ピクセルの色を変更する
        pixels[pixelIndex] = color(r, g, b);
      }
    }
  }
  updatePixels();
}

光のパーティクル


アプレットはこちら

それでは、実際に光り輝くパーティクルを複数作り、マウスで動かせるようにします。

位置や速度などを処理しやすくするために、パーティクルはクラス化しておきます。

draw()では、それぞれのパーティクルについて、計算範囲(BORDERの長さ)となる矩形を決めて、その範囲内のピクセルについて、色の処理を行うことにしました。これは、全ピクセルをチェックしてしまうと、処理速度に影響が出てしまうからです。
この、色の処理では、現在のピクセルの値(baseRedなど)を、「そのピクセルとパーティクルの中心との距離」で割った値をピクセルに足すという作業を行っています。
これによって、「パーティクルに近いピクセルほど、色が加算されて明るくなり、遠いピクセルはそのまま」という表現を行っています。

また、毎回、各パーティクルが持つ速度について、マウス座標の方に向けることで、追従を表現しています。DECEL_RATIOは現在の速度の減速率です。これによって、前の速度を維持(慣性の様な動き)を残しつつ、マウスの方向に向かってくるという動きを行わせています。

なお、色の計算を行う箇所では計算速度を稼ぐために、int型で処理したり、前述したとおり計算するピクセル範囲を指定したり、距離を求めるのに平方根を取らないなどの無駄な計算を極力省いたりしています。
それでもPCのスペックによっては処理がもたつく場合があります。この場合は、MAX_PARTICLEを減らすなどしてみてください。

final int MAX_PARTICLE = 15;  //  パーティクルの個数
Particle[] p = new Particle[MAX_PARTICLE];
 
final int LIGHT_FORCE_RATIO = 5;  //  輝き具体の抑制値
final int LIGHT_DISTANCE= 75 * 75;  //  光が届く距離
final int BORDER = 75;  //  光の計算する矩形範囲(高速化のため)
 
float baseRed, baseGreen, baseBlue;  //  光の色
float baseRedAdd, baseGreenAdd, baseBlueAdd;  //  光の色の加算量
final float RED_ADD = 1.2;    //  赤色の加算値
final float GREEN_ADD = 1.7;  //  緑色の加算値
final float BLUE_ADD = 2.3;   //  青色の加算値
 
void setup() {
  size(400, 400);
 
  //  パーティクルの初期化
  for (int i = 0; i < MAX_PARTICLE; i++) {
    p[i] = new Particle();
  }
 
  //  光の色の初期化
  baseRed = 0;
  baseRedAdd = RED_ADD;
 
  baseGreen = 0;
  baseGreenAdd = GREEN_ADD;
 
  baseBlue = 0;
  baseBlueAdd = BLUE_ADD;
}
 
void draw() {
  background(0);
 
  //  光の色を変更
  baseRed += baseRedAdd;
  baseGreen += baseGreenAdd;
  baseBlue += baseBlueAdd;
 
  //  色が範囲外になった場合は、色の加算値を逆転させる
  colorOutCheck();
 
  //  パーティクルの移動
  for (int pid = 0; pid < MAX_PARTICLE; pid++) {
    p[pid].move(mouseX, mouseY);
  }
 
  //  各ピクセルの色の計算
  int tRed = (int)baseRed;
  int tGreen = (int)baseGreen;
  int tBlue = (int)baseBlue;
 
  //  綺麗に光を出すために二乗する
  tRed *= tRed;
  tGreen *= tGreen;
  tBlue *= tBlue;
 
  //  各パーティクルの周囲のピクセルの色について、加算を行う
  loadPixels();
  for (int pid = 0; pid < MAX_PARTICLE; pid++) {
 
    //  パーティクルの計算影響範囲
    int left = max(0, p[pid].x - BORDER);
    int right = min(width, p[pid].x + BORDER);
    int top = max(0, p[pid].y - BORDER);
    int bottom = min(height, p[pid].y + BORDER);
 
    //  パーティクルの影響範囲のピクセルについて、色の加算を行う
    for (int y = top; y < bottom; y++) {
      for (int x = left; x < right; x++) {
        int pixelIndex = x + y * width;
 
        //  ピクセルから、赤・緑・青の要素を取りだす
        int r = pixels[pixelIndex] >> 16 & 0xFF;
        int g = pixels[pixelIndex] >> 8 & 0xFF;
        int b = pixels[pixelIndex] & 0xFF;
 
        //  パーティクルとピクセルとの距離を計算する
        int dx = x - p[pid].x;
        int dy = y - p[pid].y;
        int distance = (dx * dx) + (dy * dy);  //  三平方の定理だが、高速化のため、sqrt()はしない。
 
        //  ピクセルとパーティクルの距離が一定以内であれば、色の加算を行う
        if (distance < LIGHT_DISTANCE) {
          int fixFistance = distance * LIGHT_FORCE_RATIO;
          //  0除算の回避
          if (fixFistance == 0) {
            fixFistance = 1;
          }   
          r = r + tRed / fixFistance;
          g = g + tGreen / fixFistance;
          b = b + tBlue / fixFistance;
        }
 
        //  ピクセルの色を変更する
        pixels[x + y * width] = color(r, g, b);
      }
    }
  }
  updatePixels();
}
 
//  マウスクリック時に、各パーティクルをランダムな方向に飛ばす
void mousePressed() {
  for (int pid = 0; pid < MAX_PARTICLE; pid++) {
    p[pid].explode();
  }
}
 
//  色の値が範囲外に変化した場合は符号を変える
void colorOutCheck() {
  if (baseRed < 10) {
    baseRed = 10;
    baseRedAdd *= -1;
  }
  else if (baseRed > 255) {
    baseRed = 255;
    baseRedAdd *= -1;
  }
 
  if (baseGreen < 10) {
    baseGreen = 10;
    baseGreenAdd *= -1;
  }
  else if (baseGreen > 255) {
    baseGreen = 255;
    baseGreenAdd *= -1;
  }
 
  if (baseBlue < 10) {
    baseBlue = 10;
    baseBlueAdd *= -1;
  }
  else if (baseBlue > 255) {
    baseBlue = 255;
    baseBlueAdd *= -1;
  }
}
 
//  パーティクルクラス
class Particle {
  int x, y;        //  位置
  float vx, vy;    //  速度
  float slowLevel; //  座標追従遅延レベル
  final float DECEL_RATIO = 0.95;  //  減速率
 
  Particle() {
    x = (int)random(width);
    y = (int)random(height);
    slowLevel = random(100) + 5;
  }
 
  //  移動
  void move(float targetX, float targetY) {
 
    //  ターゲットに向かって動こうとする
    vx = vx * DECEL_RATIO + (targetX - x) / slowLevel;
    vy = vy * DECEL_RATIO + (targetY - y) / slowLevel;
 
    //  座標を移動
    x = (int)(x + vx);
    y = (int)(y + vy);
  }
 
  //  適当な方向に飛び散る
  void explode() {
    vx = random(100) - 50;
    vy = random(100) - 50;
    slowLevel = random(100) + 5;
  }
}

Tags: , ,
Posted in Processing Advent Calendar2011 | No Comments »

Comments

Leave a Reply

 Comment Form