Top / 4K Procedural Gfx Monitor / 4KBプロシージャルGFX入門講座 4時限目

4KBプロシージャルGFX入門講座 4時限目

ピクセルを定義するグラフィックス

ここまでの講座を読み進めてきたのであれば、シェーダを用いた4KBプロシージャルGFXは
ピクセルの定義を書くことで、グラフィックス全体を定義できることがわかったはずだ。
このような定義の仕方は今までにあまり無いような考え方かもしれないけど、このように考えることができる
グラフィックスは多く存在する。
たとえば、画面全体で再帰的に同じ構造を繰り返す、マンデルブロ集合やジュリア集合など。
レイトレーシングなんかも、もともとピクセルごとに独立している処理なので定義することは容易だ。
これらは簡単に思いつく例だが、ほとんどの汎用的なものに適用することができる。
(3次元目の円と四角を描く例なんか結構汎用的だと思う)

レイトレーシングしてみる

最近4KBが熱いのはみんなレイトレーシングしてるからだと思う。ほとんどのスゴイと思う4KBは
みなこぞってレイトレーシングでシーンを実装している。一見難しそうだけど、実はそうでもない。
せっかくなのでこの講座で扱ってみることにしよう。

まず、レイトレーシングについてだけど、超簡単に説明すると以下のようになる。
視点からレイ(光線)をとばして、オブジェクトと交差する点をみつけ、ヒットする時は
スクリーンに絵がうつるというわけだ。まぁ基本概念を詳しく知りたいときは専門書を参考にしてくれ。
ここではまぁ、半直線とオブジェクトが交差するときに絵が出るのがレイトレだとおもっておけばいいでしょう。

GPUレイトレを実装してみる

レイを生成する

とりあえず最初のステップはレイトレースするためのレイを作る必要がある。
ここまでの講座をみてきた方はもう大方の予想はつくだろうが、VertexProgramで生成した値を用いる。
以下のようなプログラムになる。
Zが負の方向のベクトルを生成した。XとYについてはアスペクト比を考慮したもの。
FragmentProgramではベクトルを正規化して表示しただけ。

-- vs.glsl --
// VertexProgram
// This program is 16:9 ratio

varying vec3 org,dir;
void main()
{
    gl_Position = gl_Vertex;
    org = vec3(0,0,0);
    dir = normalize(vec3(gl_Vertex.x * 1.66667, gl_Vertex.y, -1.0));
}
-- fs.glsl --
// FragmentProgram

varying vec3 org,dir;

void main()
{
    gl_FragColor = vec4(normalize(dir),1);
}

まぁこんなかんじになる。

交差判定する

レイを作り出したので、今度はそのレイとオブジェクトの交差判定を行う。
まず手始めに球との交差判定を行う。プログラムは以下のとおり。(VertexProgramは変わらない)
球は(0,0,-2)の位置に半径0.5としました。
球の交差判定の解説についての詳しい説明はここでは避けますが、
sphere_intersectで判定しています。ヒットしてたらtrueを返します。
ヒットしたら白色、ヒットしなければ黒を表示しています。

-- fs.glsl --
// FragmentProgram
varying vec3 org,dir;

struct Ray
{
    vec3 org;
    vec3 dir;
};
struct Sphere
{
    vec3 c;
    float r;
};

bool shpere_intersect(Sphere s,  Ray ray)
{
    vec3 rs = ray.org - s.c;
    float B = dot(rs, ray.dir);
    float C = dot(rs, rs) - (s.r * s.r);
    float D = B * B - C;

    if (D > 0.0)
    {
        float t = -B - sqrt(D);
        if ( t > 0.0 )
            return true;
    }
    return false;
}

void main()
{
    Sphere sphere;
    sphere.c   = vec3(0.0, 0.0, -2.0);
    sphere.r   = 0.5;
    
    Ray r;
    r.org = org;
    r.dir = normalize(dir);
    
    vec4 col = vec4(0,0,0,1);
    bool hit = shpere_intersect(sphere, r);
    if (hit)
        col = vec4(1,1,1,1);
        
    gl_FragColor = col;
}

まぁここまではさっき超簡単に説明したレイトレのイメージどおりですな。
球とレイの交差判定関数は初めての人には難しいかもしれませんが、
それ以外はプログラムも簡単でしょう。

色情報とか球とか増やしてみる。

球の構造体に色情報を付加して、球との交差時に色情報を取得するようにする。
交差判定とかは後々使いやすいようにまとめてみる。まぁプログラムを見たほうが早いな。
判定したときの情報を得るためにIntersection構造体を追加した。
ヒットした距離t, ヒットしたかどうかのhit情報, ヒットしたオブジェクトの色情報col
Intersect関数が終われば、tが一番小さいオブジェクトの色情報が i.col にはいっていることになる。
GLSLでかくと、C言語ライクな書き方が必要になるので、情報は構造体にまとめておいたほうが便利だ。

// FragmentProgram
varying vec3 org,dir;

struct Ray
{
    vec3 org;
    vec3 dir;
};
struct Sphere
{
    vec3 c;
    float r;
    vec3 col;
};

struct Intersection
{
    float t;
    int hit;
    vec3 col;
};

void shpere_intersect(Sphere s,  Ray ray, inout Intersection isect)
{
    // rs = ray.org - sphere.c
    vec3 rs = ray.org - s.c;
    float B = dot(rs, ray.dir);
    float C = dot(rs, rs) - (s.r * s.r);
    float D = B * B - C;

    if (D > 0.0)
    {
        float t = -B - sqrt(D);
        if ( (t > 0.0) && (t < isect.t) )
        {
            isect.t = t;
            isect.hit = 1;
        }
    }
}

Sphere sphere[3];
void Intersect(Ray r, inout Intersection i)
{
    for (int c = 0; c < 3; c++)
    {
        shpere_intersect(sphere[c], r, i);
    }
}

void main()
{
    sphere[0].c   = vec3(-2.0, 0.0, -3.5);
    sphere[0].r   = 0.5;
    sphere[0].col = vec3(1,0.3,0.3);
    sphere[1].c   = vec3(-0.5, 0.0, -3.0);
    sphere[1].r   = 0.5;
    sphere[1].col = vec3(0.3,1,0.3);
    sphere[2].c   = vec3(1.0, 0.0, -2.2);
    sphere[2].r   = 0.5;
    sphere[2].col = vec3(0.3,0.3,1);
    
    Ray r;
    r.org = org;
    r.dir = normalize(dir);
    vec4 col = vec4(0,0,0,1);
    
    Intersection i;
    i.hit = 0;
    i.t = 1.0e+30;
    i.col = vec3(0, 0, 0);
            
    Intersect(r, i);
    
    gl_FragColor = vec4(i.col,1);
}

まぁこんな感じで表示される。じゃんじゃんいこう!

チェック柄の床を追加

なんか球だけだとさびしいので床を追加してみる。単色だと面白くないので、チェック柄にしてみる。
判定の枠組みとしては球と同じ。Intersect関数に判定を追加した。今後オブジェクトを追加するときは
ここに追加していけばよいことがわかる。
ここでも平面とレイの交差判定については詳しくは説明しない。
plane_intersect でレイと床との交点 vec3 p を求め、その位置情報の値を使ってチェック柄をつくる。
mod関数をつかい、xとz座標の少数部分で場合わけを行っている。
判定したら、球と同様にIntersection構造体のcolに代入している。

// FragmentProgram

const int raytraceDepth = 8;

varying vec3 org,dir;

struct Ray
{
    vec3 org;
    vec3 dir;
};
struct Sphere
{
    vec3 c;
    float r;
    vec3 col;
};
struct Plane
{
    vec3 p;
    vec3 n;
    vec3 col;
};

struct Intersection
{
    float t;
    int hit;
    vec3 col;
};

void shpere_intersect(Sphere s,  Ray ray, inout Intersection isect)
{
    (変わりません)
}

void plane_intersect(Plane pl, Ray ray, inout Intersection isect)
{
    // d = -(p . n)
    // t = -(ray.org . n + d) / (ray.dir . n)
    float d = -dot(pl.p, pl.n);
    float v = dot(ray.dir, pl.n);

    if (abs(v) < 1.0e-6)
        return; // the plane is parallel to the ray.

    float t = -(dot(ray.org, pl.n) + d) / v;

    if ( (t > 0.0) && (t < isect.t) )
    {
        isect.hit = 1;
        isect.t   = t;
        
        vec3 p = vec3(ray.org.x + t * ray.dir.x,
              ray.org.y + t * ray.dir.y,
              ray.org.z + t * ray.dir.z);

        float offset = 0.2;
        vec3 dp = p + offset;
        if ((mod(dp.x, 1.0) > 0.5 && mod(dp.z, 1.0) > 0.5)
        ||  (mod(dp.x, 1.0) < 0.5 && mod(dp.z, 1.0) < 0.5))
            isect.col = pl.col;
        else
            isect.col = pl.col * 0.5;
    }
}

Sphere sphere[3];
Plane plane;
void Intersect(Ray r, inout Intersection i)
{
    for (int c = 0; c < 3; c++)
    {
        shpere_intersect(sphere[c], r, i);
    }
    plane_intersect(plane, r, i);
}

void main()
{
    sphere[0].c   = vec3(-2.0, 0.0, -3.5);
    sphere[0].r   = 0.5;
    sphere[0].col = vec3(1,0.3,0.3);
    sphere[1].c   = vec3(-0.5, 0.0, -3.0);
    sphere[1].r   = 0.5;
    sphere[1].col = vec3(0.3,1,0.3);
    sphere[2].c   = vec3(1.0, 0.0, -2.2);
    sphere[2].r   = 0.5;
    sphere[2].col = vec3(0.3,0.3,1);
    plane.p = vec3(0,-0.5, 0);
    plane.n = vec3(0, 1.0, 0);
    plane.col = vec3(1,1, 1);
    
    Ray r;
    r.org = org;
    r.dir = normalize(dir);
    vec4 col = vec4(0,0,0,1);
    
    Intersection i;
    i.hit = 0;
    i.t = 1.0e+30;
    i.col = vec3(0, 0, 0);
        
    Intersect(r, i);
    if (i.hit != 0)
        col.rgb = i.col;
            
    gl_FragColor = col;
}

まぁこんな感じになる。なんかそれっぽくなってきたよね。
まだのっぺりしてるけど、次からは立体にみえるように陰影をつけるような処理をかいていく。

レイの交差点情報を取得

陰影をつけるために、レイとオブジェクトの交差点の情報を取得する。
Planeのところではすでに交点をもとめたけど、球についても同様にもとめる。
さらに法線ももとめておく。球の中心から交点方向のベクトルが法線となる。
Intersection構造体に交点位置p と交点の法線情報n を追加した。

// FragmentProgram

varying vec3 org,dir;

struct Ray
{
    vec3 org;
    vec3 dir;
};
struct Sphere
{
    vec3 c;
    float r;
    vec3 col;
};
struct Plane
{
    vec3 p;
    vec3 n;
    vec3 col;
};

struct Intersection
{
    float t;
    vec3 p;     // hit point
    vec3 n;     // normal
    int hit;
    vec3 col;
};

void shpere_intersect(Sphere s,  Ray ray, inout Intersection isect)
{
    // rs = ray.org - sphere.c
    vec3 rs = ray.org - s.c;
    float B = dot(rs, ray.dir);
    float C = dot(rs, rs) - (s.r * s.r);
    float D = B * B - C;

    if (D > 0.0)
    {
        float t = -B - sqrt(D);
        if ( (t > 0.0) && (t < isect.t) )
        {
            isect.t = t;
            isect.hit = 1;

            // calculate normal.
            vec3 p = vec3(ray.org.x + ray.dir.x * t,
                          ray.org.y + ray.dir.y * t,
                          ray.org.z + ray.dir.z * t);
            vec3 n = p - s.c;
            n = normalize(n);
            isect.n = n;
            isect.p = p;
            isect.col = s.col;
        }
    }
}

void plane_intersect(Plane pl, Ray ray, inout Intersection isect)
{
    // d = -(p . n)
    // t = -(ray.org . n + d) / (ray.dir . n)
    float d = -dot(pl.p, pl.n);
    float v = dot(ray.dir, pl.n);

    if (abs(v) < 1.0e-6)
        return; // the plane is parallel to the ray.

    float t = -(dot(ray.org, pl.n) + d) / v;

    if ( (t > 0.0) && (t < isect.t) )
    {
        isect.hit = 1;
        isect.t   = t;
        isect.n   = pl.n;

        vec3 p = vec3(ray.org.x + t * ray.dir.x,
                      ray.org.y + t * ray.dir.y,
                      ray.org.z + t * ray.dir.z);
        isect.p = p;
        float offset = 0.2;
        vec3 dp = p + offset;
        if ((mod(dp.x, 1.0) > 0.5 && mod(dp.z, 1.0) > 0.5)
        ||  (mod(dp.x, 1.0) < 0.5 && mod(dp.z, 1.0) < 0.5))
            isect.col = pl.col;
        else
            isect.col = pl.col * 0.5;
    }
}

Sphere sphere[3];
Plane plane;
void Intersect(Ray r, inout Intersection i)
{
    for (int c = 0; c < 3; c++)
    {
        shpere_intersect(sphere[c], r, i);
    }
    plane_intersect(plane, r, i);
}

void main()
{
    sphere[0].c   = vec3(-2.0, 0.0, -3.5);
    sphere[0].r   = 0.5;
    sphere[0].col = vec3(1,0.3,0.3);
    sphere[1].c   = vec3(-0.5, 0.0, -3.0);
    sphere[1].r   = 0.5;
    sphere[1].col = vec3(0.3,1,0.3);
    sphere[2].c   = vec3(1.0, 0.0, -2.2);
    sphere[2].r   = 0.5;
    sphere[2].col = vec3(0.3,0.3,1);
    plane.p = vec3(0,-0.5, 0);
    plane.n = vec3(0, 1.0, 0);
    plane.col = vec3(1,1, 1);
    
    Ray r;
    r.org = org;
    r.dir = normalize(dir);
    vec4 col = vec4(0,0,0,1);
    
    Intersection i;
    i.hit = 0;
    i.t = 1.0e+30;
    i.n = i.p = i.col = vec3(0, 0, 0);
        
    Intersect(r, i);
    if (i.hit != 0)
    {
#if 1
        // display normal
        col.rgb = i.n;
#else
        // display position
        col.rgb = i.p;
        col.b = 0.5;
#endif      
    }
    gl_FragColor = col;
}

法線を表示すると左側のような感じになる。
わかりにくいけど、位置情報を表示すると右側のようになる。
ここまでくればしめたものだ。

陰影をつける

さて、法線情報も取得できたので、シェーディングしてみよう。ほとんどプログラムの大枠は完成しているので
あとは肝心の処理を書くだけだ。Intersect関数後の処理だけを書く。
ライトは(1,1,1)の手前右上方向からの平行光源とする。
シェーディング方法はN・Lだ。まぁシェーダ書いたことあるひとならごく普通の処理ですな。

Intersect(r, i);
if (i.hit != 0)
{
    vec3 lightDir = normalize(vec3(1,1,1));
    col.rgb = max(0.0, dot(i.n, lightDir)) * i.col;// カラーと乗算
}

まだあんまりレイトレぽくないけど、なんか結構ちゃんとしてきたよね。

影をつける

陰影をつけただけだと、なんか接地感がたらないよね。というわけで次は影をつけてみる。
いわゆるラスタライズ方式だと影をつけるのって結構しんどいけど、レイトレーシングなら簡単だぜ。
影をつけるには、視点からのレイとオブジェクトの交点からライトの方向に向けてレイをとばし、
交点とライトの間に何もなければ、交点にライトがあたっているはず。
交点とライトの間に何かあれば、交点は影になるはず。
というわけで、交点からライトの方向にむかってレイを1回飛ばしてやればよい。
レイヒットすればhit = 1なので1.0からhitを引くことで、あたっていれば0、あたっていなければ1となる。
そのまま色情報に乗算してやればいいよね。
レイを飛ばすときに交点の値をそのまま使うと、精度の問題で交点にもう一度当たってしまうので
法線方向に少しずらして交点自身に当たらないようにしている。

Intersect(r, i);
if (i.hit != 0)
{
    vec3 lightDir = normalize(vec3(1,1,1));
        
    float eps  = 0.0001;
    Ray rsh;
    rsh.org = i.p + i.n * eps;// 法線方向にすこしずらす
    rsh.dir = lightDir;
    Intersection ish;
    ish.hit = 0;
    ish.t = 1.0e+30;
    ish.n = ish.p = ish.col = vec3(0, 0, 0);
    Intersect(rsh, ish);
        
    float shadow = 1.0 - ish.hit;
    col.rgb = max(0.0, dot(i.n, lightDir)) * i.col * shadow;
}

というわけでこんなかんじ。なんかだんだんらしくなってきたんじゃない〜?

反射を計算する

いよいよレイトレの本領発揮〜!じゃないけど、反射を計算する。
要領はさっき影を計算したときと同じ。さっきはレイを飛ばす方向がライトの方向だったけど
今度は交点の法線情報をつかって、視点からのレイが反射する方向にレイを飛ばして、
反射した方向にオブジェクトがあるかどうかを調べる。
ややこしいから、いったん影のことは忘れよう。
反射ベクトルはreflect関数をつかって視線ベクトルと法線ベクトルから計算している。
最後にヒットしたときの色をシェーディングに追加。

Intersect(r, i);
if (i.hit != 0)
{
    Ray rsh;
    rsh.org = i.p + i.n * eps;// 法線方向にすこしずらす
    rsh.dir = reflect(r.dir, i.n);// 反射ベクトル
    Intersection ish;
    ish.hit = 0;
    ish.t = 1.0e+30;
    ish.n = ish.p = ish.col = vec3(0, 0, 0);
    Intersect(rsh, ish);
        
    col.rgb = max(0.0, dot(i.n, lightDir)) * i.col;
    col.rgb += ish.col;// 反射したオブジェクトの色を追加
}

ついに反射キター!! 反射してうつりこんでいる物体はシェーディングされてないけど
これで一応GPUレイトレ完成って感じですかね?
映りこんでいる物体もシェーディングしたいときは、反射して当たった交点情報をもとに
光源とのシェーディング処理を書けばよい。

レイトレの処理をループ化

レイトレはよく再帰関数でかかれることが多いけど、GLSLではまだ再帰関数はかけない。
そこで、ループ化して処理をかく。
ポイントは再帰関数として渡す情報を、引数のIntersectionに渡してやって関数を抜け、
何にも当たっていなければループ終了。当たっていればシェーディングを計算し、
交点情報をもとに次のRay 情報を作成、その情報を使って再び関数を呼び出し直せばよい。
影を計算する部分はcomputeLightShadow()にまとめておいた。
ここでは書かないけど、このコードはsampleのraytraceフォルダにあるfs.glslなのでそちらを参照してほしい。
(実はシェーディング処理は結構適当なので詳しい人は突っ込まないでねw)

Ray r;
r.org = org;
r.dir = normalize(dir);
vec4 col = vec4(0,0,0,1);
float eps  = 0.0001;
vec3 bcol = vec3(1,1,1);
const int raytraceDepth = 8;
for (int j = 0; j < raytraceDepth; j++)
{
    Intersection i;
    i.hit = 0;
    i.t = 1.0e+30;
    i.n = i.p = i.col = vec3(0, 0, 0);
        
    Intersect(r, i);
    if (i.hit != 0)
    {
        col.rgb += bcol * i.col * computeLightShadow(i);
        bcol *= i.col;
    }
    else
    {
        break;
    }
            
    r.org = vec3(i.p.x + eps * i.n.x,
                 i.p.y + eps * i.n.y,
                 i.p.z + eps * i.n.z);
    r.dir = reflect(r.dir, vec3(i.n.x, i.n.y, i.n.z));
}
gl_FragColor = col;

うぉぉぉ複数回反射してるぜぇぇぇ!!!
それっぽくみえてるけど、きちんと理論的にやりたい人はレイトレの専門書を参考にしてくれ!

GPUレイトレまとめ

GPUレイトレースはどうだっただろうか。言葉だけ聞くと案外難しそうに聞こえるが、
実際つくってみると結構簡単だったのではないだろうか。
何も無い状態からGPUレイトレを作るとなると、いろいろ大変だけど、4KBプロシージャルGFXの枠組みが
うまいことGPUレイトレを書きやすいような環境になってるんじゃないかと思う。

ちなみに一番最後の絵だけど、GeForce8800GTで12msでレンダリングしてる。結構早いよね。
というわけで、まだまだいろいろな要素をガンガン追加しても大丈夫ってこと。
しかもまだ4KBにはまだまだ余裕があるし〜。
みんなもコレをベースか参考にして、自分の作品をつくってくれ!!!!

というわけで、GPUレイトレ講座はここまで。次は自分の作品を実行ファイルにする方法について説明する