Processingで描画スレッドと計算スレッドを分ける


例えば、昨日の球体生成するコードを発展させて、150BPMで膨張->収縮を繰り返すようにしてみる。
まずは、普通にframeCountを使ってBPMに換算する感じで。

```java:BeatSphere.pde
private static final int PLOTS_COUNT = 7500;
private static final int RADIUS = 100;

private Plot[] plots = new Plot[PLOTS_COUNT];

void setup() {
  size(300, 300, P3D);
  frameRate(60);

  for (int i = 0; i < PLOTS_COUNT; i++) {
    plots[i] = new Plot();
  }

  // プロットの色と大きさ
  stroke(0, 192, 255, 180);
  strokeWeight(4);
}

void draw() {

  if (frameCount % 100 == 0) {
    println(frameRate);
  }

  background(0);

  // 中心点を移動
  translate(width/2, height/2, 0);
  rotateY(frameCount*0.005);
  rotateZ(frameCount*0.005);

  for (Plot p : plots) {
    p.beat();
    point(p.x, p.y, p.z);
  }
}

private class Plot {

  final float a, b, c;
  float x, y, z;

  Plot() {
    // 球面上の座標をランダムで計算
    float unitZ = random(-1, 1);
    float radianT = radians(random(360));
    float sinS = sq(unitZ);
    a = sqrt(1 - sinS) * cos(radianT);
    b = sqrt(1 - sinS) * sin(radianT);
    c = unitZ;
  }

  // 60fps -> 3600fpm -> 3600fpm / 150bpm -> 24frame/beat
  void beat() {
    int state = frameCount % 24;
    int r = RADIUS;
    if (state < 6) {
      r += (state + 1) * 3;
    } else {
      r += (18 - state);
    }

    x = r * a;
    y = r * b;
    z = r * c;
  }
}
```

Plotクラスにbeatメソッドを追加した。
60fpsだから150bpmにするためには、1拍24フレーム中6フレームで膨張、残り18フレームで収縮という計算をしている。

ここで問題になるのが、PCスペックが低かったりプロットの描画などに時間がかかったりすると、フレームレートが落ちてBPMが狂ってしまう。実際、手元のPCだと、プロット数を7500ぐらいに増やすとフレームレートが56前後になってしまい、150BPMにならない。

そこで、球体半径の計算を別スレッドに切り離して、描画スレッドは計算済みの値を使用して表示する処理のみにすることで、フレームレート非依存となるように変更した。

```java:BeatSphere.pde
import java.util.concurrent.*;

private static final int PLOTS_COUNT = 7500;
private static final int RADIUS = 100;

private Plot[] plots = new Plot[PLOTS_COUNT];

void setup() {
  size(300, 300, P3D);
  frameRate(30);

  for (int i = 0; i < PLOTS_COUNT; i++) {
    plots[i] = new Plot();
  }

  // プロットの色と大きさ
  stroke(0, 192, 255, 180);
  strokeWeight(4);

  // 計算用のスレッドを開始(25msごと16回で1拍)
  // 150beat/min -> 2.5beat/sec -> 400msec/beat -> 25msec * 16times
  ScheduledExecutorService schedule = Executors.newSingleThreadScheduledExecutor();
  schedule.scheduleAtFixedRate(new Calculator(), 0, 25, TimeUnit.MILLISECONDS);
}

void draw() {
  background(0);

  // 中心点を移動
  translate(width/2, height/2, 0);
  rotateY(frameCount*0.01); // 回転速度はフレームレートに依存する
  rotateZ(frameCount*0.01);

  // 計算は別スレッドでやっているので、x,y,z座標を取り出して表示するだけ。
  for (Plot p : plots) {
    point(p.x, p.y, p.z);
  }
}

private class Plot {

  final float a, b, c;
  float x, y, z;
  int state;

  Plot() {
    // 球面上の座標をランダムで計算
    float unitZ = random(-1, 1);
    float radianT = radians(random(360));
    float sinS = sq(unitZ);
    a = sqrt(1 - sinS) * cos(radianT);
    b = sqrt(1 - sinS) * sin(radianT);
    c = unitZ;

    state = 0;
  }

  void beat() {
    int r = RADIUS;
    if (state < 4) {
      r += (state + 1) * 3;
    } else {
      r += (12 - state);
    }

    x = r * a;
    y = r * b;
    z = r * c;

    state = (state + 1) % 16;
  }
}

class Calculator implements Runnable {
  void run() {
    for (Plot p : plots) {
      p.beat();
    }
  }
}
```

元の1拍24フレームに合わせると計算スレッドの起動周期がキリが悪い感じだったので1拍16回の処理で動作させるようにした。

ScheduledExecutorServiceを使って、すべてのプロットに対して、自分のx,y,z座標を計算する処理を25msに1回呼び出す。描画スレッドは、プロットからx,y,z座標の値を使って描画するだけ。
プロットの描画中に25ms周期が被ったとしてもバックグラウンドで計算されるだけなので、次回描画時にその計算済みの値が使用される。描画でカクつきはするかもしれないけど膨張・収縮自体は150BPMのまま変わらない。という感じ。

当然だけど、計算処理の起動周期が1回の計算処理にかかる時間を超えてしまうとどうしようもないので、そこは注意が必要。