Log: KI

雑記ブログです。大体プログラミングのあれこれを書きます。

モーションブラーをかけよう

今回はCCC Advent Calender 2018の参加記事ということで、こんな感じのモーションブラーを簡単に実装するやり方を解説します。 また、ここではGLSLの解説は省かせていただきます。ご了承ください。

このモーションブラーは@FMS_Catさんの240x240上で公開されている作品を参考にして作成しました。

目次

概要

具体的なやり方を紹介する前に今回紹介するモーションブラーの手順をざっくりと説明します。

  1. テクスチャにモーションブラーさせたいシーンを描画。
  2. スクリーンにテクスチャを加算合成。
  3. 1, 2の手順を繰り返し、時間変数を調整してフレーム間を補間

シーンの描画後に行うことからわかる通り、この手法はポストエフェクトと呼ばれるものです。そのため、スクリーンに描画するシーンをすべて描画してからブラーをかけます。 また、ブラーをかけたいシーンでは時間変数を用意して、その時間変数の値によってアニメーションするようにします。(私は大抵 0.0-1.0 に正規化しておきます)

仕組み

1. シーンをテクスチャへ描画

まず、通常の描画時と同じような手順で、テクスチャにシーンを描画します。このとき、このテクスチャはGPUからの読み取りと書き込みが可能であれば問題ありません。 Processingでは、シーンの描画ができる(レンダーターゲットになる)テクスチャはPGraphicsを使うことで扱えるので、それを使って描画します。 詳しくは「PGraphics」で調べるなどしてみてください。描画するターゲットが変わるだけなので、そこまで理解に苦労はないと思います。

簡単に例をあげると、通常1つ目のコードになるところを2つ目にのようになるように書き直すということです。

void draw()
{
  clear();
  
  pushMatrix();

  fill(#ffffff);
  translate(width/2, height/2, 0);
  sphere(20);

  pushMatrix();
}
PGraphics target; // 初期化処理は省略

void draw()
{
  target.clear();
  target.pushMatrix();

  target.fill(#ffffff);
  target.translate(width/2, height/2, 0);
  target.sphere(20);

  target.pushMatrix();
}

上記のコードではスクリーンに何も描画されませんが、image()を使ってtargetの内容をそのままスクリーンに描画することもできます。

2. スクリーンにテクスチャを加算合成

次に、スクリーンへ先ほど描画したテクスチャを描き写しますが、その時に適当な透明度を設定して加算合成します。 加算合成自体はもともとAPI等に用意されている場合があります。(PrcessingではblendMode()を使えば加算合成モードに切り替えられます。)しかし、基本的にメッシュ単位で加算されてしまったりで少し面倒だったので今回はGLSLのフラグメントシェーダーで実装しました。 Processingのコードはこちらになります。

// 加算合成時の加算したいテクスチャのα値
uniform float m;
// Processingで決まっている、フィルターをかけられるテクスチャ
uniform sampler2D texture;
// 手順1で描画したシーンのテクスチャ
uniform sampler2D targeted;

void main()
{
  // 描画するピクセルの座標をセット
  ivec2 p = ivec2(gl_FragCoord.xy);
  // (スクリーン) + (手順1で描画したシーン) * m で加算合成
  gl_FragColor = texelFetch(texture, p, 0) + texelFetch(targeted, p, 0) * m;
}

少し補足すると、このuniformはCPUからGPUへ送った定数データで、このシェーダの処理が始まる前にセットするものです。

3. 繰り返してフレーム間を補間

最後に手順1, 2で行なった処理を繰り返します。 繰り返す回数は見栄えによりけりで調整するのがいいかと思いますが、一番重要なのはループ内で、アニメーションに使っている時間変数を1フレーム進む分より小さい値で進ませることです。 私のイメージとしては、「時間解像度を引き上げる」ような感じですが、「ループ内で∆tだけ時間を進める」というような表現もできそうです。 これにより、ループさせた回数だけフレーム間の補間ができるようになり、物体が動いた方向にぼかす(モーションブラーをかける)ことができます。

しかし、手順2においてm = 1.0として加算合成をループ回数分行なってしまうと、フレーム間の動きが少ないものが全て白とびしてしまいます。 背景が黒の場合は演出としてありかもしれませんが、これではモーションブラーにはなりません。 そこで、ループ毎にmの値を調整して、あるループ回数の時はm = 1.0、またあるループ回数の時はm = 0.01のようにすることでいい感じにブラーがかかってくれます。 今回紹介する私のコードでは、ループの最後の加算合成時はm = 0.2、ループの最初の加算合成時はm = 0.0になるようにm = cos(PI/2*i/10) * 0.2という感じで調整しています。

実装

最後に、実際に冒頭のTweetのプログラムで使っているモーションブラーのpdeファイルのコードを載せておきます。(フラグメントシェーダは手順2で載せたものが全てです。) ここで載せているコードではモーションブラーとして最低限動作に必要なもの以外省略しているので、全文読みたい方はgithub.com/Kuyuri-Iroha/pendulumにおいてあるのでご自由にどうぞ。

// Kuyuri Iroha
// 2018

PGraphics target;
PShader blurShader;
final int END_FRAME = 200;

void setup()
{
  size(500, 500, P3D);
  target = createGraphics(width, height, P3D);
  blurShader = loadShader("blur.glsl");
}

void draw()
{
  clear();
  for(int i=0; i<10; i++)
  {
    /*
    [0.0, 1.0] の間で時間変数t が動き、
    forループ内では(i / 1500)づつ増えてフレーム間の補間を行います。
    */
    float t = float(frameCount % END_FRAME) / END_FRAME + float(i) / 1500;

    target.beginDraw();

    //ここでシーンを描画

    target.endDraw();

    // uniform としてGPUへ値を渡す
    blurShader.set("rendered", target);
    blurShader.set("m", cos(PI/2*i/10)*0.2);

    // filterで簡易的にフラグメントシェーダを実行
    /*
    先ほどのフラグメントシェーダのtextureは、
    このfilter()を使用するとProcessing側がGPUに渡してくれるもの
    */
    filter(blurShader);
  }
}

後書き

今回はProcessingで簡単にできるモーションブラーの解説を行いました。 他の言語、環境でも大抵似たような手法で実装できるとは思いますが、PGraphicsのおかげでかなり簡単にできるようになっていると思います。 モーションブラーは適用するだけで描画される物質にグッと質量感が出るので、おすすめです。

また今回のような描画系の小ネタ的なものを書こうと思います。 それではまた。