ピクセルシェーダで綺麗な映像を作ろう

皆さんはじめまして!プログラマのにっしーです。
今回は、学生時代から趣味で追いかけていた「メガデモ」について、
簡単にお話させていただきたいと思います。

メガデモってご存知ですか?

メガデモ(またはデモシーン)とは、4kBや64kBといったごく限られた容量中に、
綺麗な映像とサウンドを詰め込んだ映像作品のことを言います。
調べれば様々な作品が動画で観れますし、もしかしたら有名なものは観たことがあるかもしれません。

超軽量な映像を実現するために、以下のような重要なテクニックがあります。
・とにかく無駄を省き、できるだけ圧縮する。
・描画はすべてピクセルシェーダで行う。

今回はピクセルシェーダでの描画に焦点を絞り、
簡単なデモ作成の紹介と、応用方法について検討を行いたいと思います。

オブジェクトの描画方法

ピクセルシェーダでオブジェクトのモデリングをするためには、
レイマーチング(raymarching) という方法を使用します。

レイマーチングとは、レイトレーシング法のひとつで、
レイを徐々に伸ばしていき、レイの原点から描画したいオブジェクトまでの最短距離を
distance function と呼ばれる関数を使用して求める方法です。

distance function はオブジェクトまでの最短距離をわずか数行で求めることができ、
関数の組み合わせにより様々な形状を表現することができます。

// 球
float sphere(vec3 pos){
    return length(pos) - 0.5;
}
// 立方体
float box(vec3 pos){
    return length(max(abs(pos) - 0.5,0.0));
}

実際に使ってみる

百聞は一見にしかずということで、
raymarching を使用してシェーダーを書いてみました。

uniform vec2 resolution;
uniform float time;

vec3 repetition(vec3 p, float size)
{
     return mod(p, size) - size * 0.5;
}

float distanceFunction(vec3 pos)
{
     vec3 q = repetition(pos, 8.0);
     //** sphere **//
     // return length(q) - 1.0;
     //** box **//
     // return length(max(abs(q) - vec3(1.0), 0.0));
     float period = (cos(time * 1.0) + 1.0) * 0.5;
     return length(max(abs(q) - vec3(1.0 - period), 0.0)) - period;
}

vec3 getNormal(vec3 p)
{
     const float d = 0.0001;
     return
         normalize
         (
             vec3
             (
                 distanceFunction(p + vec3(d,0.0,0.0)) - distanceFunction(p + vec3(-d,0.0,0.0)),
                 distanceFunction(p + vec3(0.0,d,0.0)) - distanceFunction(p + vec3(0.0,-d,0.0)),
                 distanceFunction(p + vec3(0.0,0.0,d)) - distanceFunction(p + vec3(0.0,0.0,-d))
             )
         );
}

void main() {
     vec2 pos = (gl_FragCoord.xy * 2.0 - resolution) / resolution.y;

     vec3 cameraPos = vec3(0.0, 0.0, time * 5.0);
     vec3 cameraDir = vec3(0.0, 0.0, -1);
     vec3 cameraUp = vec3(0.0, 1.0, 0.0);
     vec3 cameraSide = cross(cameraDir, cameraUp);

     vec3 rayDir = normalize(cameraSide * pos.x + cameraUp * pos.y + cameraDir * 2.0);

     // raymarching
     float length = 0.0, distance = 0.0;
     vec3 rayPos = cameraPos;
     for(int i = 0; i < 64; ++i){
         distance = distanceFunction(rayPos);
         length += distance;
         rayPos = cameraPos + length * rayDir;
     }

     vec3 normal = getNormal(rayPos);
     if(abs(distance) < 0.001){
         gl_FragColor = vec4(abs(cos(time * 0.5)) * 0.5, 0.0, abs(sin(time * 0.5)) * 0.5,1.0);
     }else{
         gl_FragColor = vec4(0.0);
     }
}

描画してみると↓こんな感じになります。(色味が悪い…)

こんなに簡単なコードで色々なものが描画できてしまうのですから驚きです。

ゲーム開発への応用

このように、簡単にトライアンドエラーができるので、
ゲーム開発への応用も期待が高まります。

しかし、このレイマーチングによる描画には最大の欠点があります。
それは、「とてつもなく処理が遅い」、ということです。

上記のような簡単なフラクタル程度だったらあまり体感できないかもしれませんが、
より綺麗な映像作品を作ろうとなると、CPUパワーを食い尽くします。

そのため、何らかの対策が必要となります。

これについては、今年のCEDECの講演で詳しく紹介されていました。
CEDiL にて資料が公開されています。
http://cedil.cesa.or.jp/cedil_sessions/view/1637

ざっくり説明すると、
・レイマーチングの回数を減らす
・描画するピクセル数を減らす
を行うことにより、高速化を目指しています。

また、極論ですがプリレンダリングをしてしまうのも手です。
ライトを考慮しない場所等、使用できる箇所が限られてしまいますが、
そのような箇所では最速で描画することができます。

まとめ

今はWebGLやUnityのおかげで、
メガデモも試作・公開・共有がとてもやりやすくなっています。

是非作品を作り上げて、立派なデモシーナーを目指してみてください。