LoginSignup
41
37

More than 5 years have passed since last update.

GLSLだけで図形を描く

Posted at

例によって @h_doxas さんの記事を参考にさせて頂いています。

GLSLで座標を元に計算を行うことによって図形を描くことができます。
実際に動いているものはこちら

コード

まずはmain関数です。

main
void main( void ) {
    vec3 destColor = white;
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

    // positionが円の中に入っているか?
    if (inCircle (position, mouse, 0.8)) {
        destColor *= red;
    }

    // positionが正方形の中に入っているか?
    if (inRect(position, vec2( 0.5, -0.5), 0.25)) {
        destColor *= blue;
    }

    // positionが楕円の中に入っているか?
    if (inEllipse(position, vec2(-0.5, -0.5), vec2(1.0, 1.0), 0.2)) {
        destColor *= green;
    }

    // 最終的に得られた色を出力
    gl_FragColor = vec4(destColor, 1.0);
}

positionは、gl_FragCoord.xyの値(gl_FragCoordには画面のピクセル数がそのまま渡ってきます)を-1.0〜1.0の間に収まるよう正規化を行っています。
(GLSLは基本的に-1.0〜1.0の間で情報をやりとりするためです)

そのあとのif文は、該当の図形内にpositionが入っているかを判断します。

図形を「描く」というと、「中心はどこで、どのくらいのサイズの、どんな色の図形か」というふうに考えますが、GLSLのいわゆるフラグメントシェーダでは、ラスタライズされた各ピクセルの情報(位置や色など)のみが渡され、 そのピクセルは何色になるべきか という考え方でプログラムを書く必要があります。

なので、「該当のピクセルがなにがしかの図形の中に入っているか」をチェックしているわけです。
逆を返せば、この判定ロジックを様々な図形用に作成し、ピクセルをチェックすることができれば色々な図形を描くことができるというわけです。

円を描く

さて、ひとつ目は円を描くロジックです。
第一引数は判定する位置、第二引数はそのオフセットです。
第三引数のサイズはそのままの意味ですね。

check-circle
bool inCircle(vec2 position, vec2 offset, float size) {
    float len = length(position - offset);
    if (len < size) {
        return true;
    }
    return false;
}

最初のところでlength(position - offset)を実行しています。
GLSLでは画面の中心が(0, 0)となるので、仮にオフセットがなければ中心位置からの距離(length)を判定することになります。
つまり、オフセットはこの判定する中心位置を移動するパラメータ、というわけですね。
正円の場合は判定がとてもシンプルなので分かりやすいのではないでしょうか。

正方形を描く

さて、次は正方形です。
引数は円の部分とまったく同じです。

draw-square
bool inRect(vec2 position, vec2 offset, float size) {
    vec2 q = (position - offset) / size;
    if (abs(q.x) < 1.0 && abs(q.y) < 1.0) {
        return true;
    }
    return false;
}

position - offsetについては円のところで書いたとおりです。
そしてそれをさらにsizeで割っています。
なにをしているのでしょうか。

言葉で説明するなら、「計算中の座標は正方形の中に入っているか?」という判断の座標を、size分小さくする、です。
つまり、sizeの値が大きければ大きいほど除算後の値は小さくなり、結果、判断の条件であるx < 1.0 && y < 1.0の部分の条件に合いやすくなるわけです。

要は正方形が大きくなる、というわけですね。

ちなみに条件式は絶対値で判定をしています。これは円を想像してもらうと分かりますが、「中心からどれくらい離れているか」を判断するためです。
そして円を描くところでも書いた通り、直交座標で画面の中心が(0, 0)のため、上下左右に判定を行うために絶対値を取っている、というわけです。

楕円を描く

次は楕円です。
これまた引数はほぼ同じで、違う点はpropがある点です。
使い方を見てみると、propで除算しているのが分かります。

draw-ellipse
bool inEllipse(vec2 position, vec2 offset, vec2 prop, float size) {
    vec2 q = (position - offset) / prop;
    if (length(q) < size) {
        return true;
    }
    return false;
}

これは、円と正方形の描き方の合わせ技です。
まず、if文の中身を見るとlengthを取ってsizeと比較しています。
これは円を描くときにやっていた部分ですね。
円は基本的に中心と半径が分かれば判定が行えます。

ただ今回、lengthを取っているのは直前で計算した値です。
式を見ると正方形を描くときにやっていた処理に似ていますね。

違いとしては、正方形のときはfloatだったものがvec2になっている点です。
正方形の場合は縦横ともに同じ数字でよかったものが、楕円の場合は縦と横を別に計算する必要があるためです。
(なのでpropを例えばvec2(0.5, 0.5)とすると0.5のサイズの正円になります)

余談

最初、なぜ「除算なのか」と思うかもしれません。
サイズを大きくしたいのに割ったらもっと小さくなっちゃうじゃないか、と。
しかし、円を描くところでも触れましたが、「どういうサイズで描くか」ではなく、「該当のピクセルは(基本的に)中心に置かれた図形内に入っているか」を判断しているため、判断するピクセル位置が大きくなる=遠くになると、判定の外にどんどん離れていくことになるわけです。

閑話休題

さて、実際にサンプルとして書いたコード全文を以下に載せます。
これを実行すると以下のようになります。

cap.png

(ちなみにglsl sandboxという、Three.jsを作っている@mrdoob氏が作ったサイトで作成しました)

上記サンプルはさらに、マウス位置に応じて円が動くようになっています。
そして図形が重なったところは乗算しているので色が変わるようになっています。

#ifdef GL_ES
precision mediump float;
#endif

uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;

const vec3 white = vec3(1.0, 1.0, 1.0);
const vec3 red   = vec3(1.0, 0.0, 0.0);
const vec3 green = vec3(0.0, 1.0, 0.0);
const vec3 blue  = vec3(0.0, 0.0, 1.0);

bool inCircle(vec2 position, vec2 offset, float size) {
    float len = length(position - offset);
    if (len < size) {
        return true;
    }
    return false;
}

bool inRect(vec2 position, vec2 offset, float size) {
    vec2 q = (position - offset) / size;
    if (abs(q.x) < 1.0 && abs(q.y) < 1.0) {
        return true;
    }
    return false;
}

bool inEllipse(vec2 position, vec2 offset, vec2 prop, float size) {
    vec2 q = (position - offset) / prop;
    if (length(q) < size) {
        return true;
    }
    return false;
}

void main( void ) {
    vec3 destColor = white;
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

    if (inCircle (position, mouse, 0.8)) {
        destColor *= red;
    }

    if (inRect(position, vec2( 0.5, -0.5), 0.25)) {
        destColor *= blue;
    }
    if (inEllipse(position, vec2(-0.5, -0.5), vec2(1.0, 1.0), 0.2)) {
        destColor *= green;
    }
    gl_FragColor = vec4(destColor, 1.0);
}

これをどう使う?

さて、GLSLを使って図形を描くことができることが分かりました。
が、実際、WebGLを使ってコンテンツを作る際、GLSLはライティングやシャドウなどを計算するために使われます。
こうした図形をどこで使うのでしょうか。

Filtersで試せる!

cap.png

半分宣伝になっちゃいますが、先日弊社がリリースしたFiltersというアプリがあります。

これはiPhoneのカメラフィルターアプリですが、実はフィルター部分はGLSLで書かれています。
(というか、だいたいの場合はGLSLで書かれていると思います)

そう、まさに今回取り上げたGLSLの図形が使えるわけです。

今回試しに、図形とモザイクを合わせたピンポイントモザイクフィルターを作ってみました↓
ピンポイントモザイクフィルター

動画中のモザイクのサイズ変更や位置変更は、 Filtersのフィルターはピンチイン・アウトや、タップによる位置の取得ができる のでそれを利用しています。
これらを利用することで、例えばモザイクが徐々に晴れる、みたいな面白動画向けのフィルターなんかも作れるからオススメですw

さらに、他の人が作った面白フィルターがforkできるので、イチから作らなくても「こういうのあったらいいのになー」というのも実現できるかもしれませんよ!
なによりも人のコードを見ることができるのでGLSLの勉強にも持って来いですw

AppStoreへ

41
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
37