誰も置いていかないシェーダーはじめの一歩

この記事はTech KAYAC Advent Calendar 2019の10日目の記事です。

こんにちは! jsdo.itでCreativeCodingの世界を知り、jsdo.itに惚れて入社を決め、jsdo.itの譲渡とともに入社し、jsdo.itの終了とともに退職した有給消化期間中の浅利@kasari39)です!
今回はシェーダーの世界へ一歩踏み出してみませんか?という内容です。 *1

シェーダーコーディングの世界

作品例として手前味噌ですが、このような映像をシェーダーのみで生成できます。 f:id:kasari:20191209093023g:plain
http://glslsandbox.com/e#59292.1

f:id:kasari:20191209093214g:plain
http://glslsandbox.com/e#59293.0

これらは100行にも満たないシェーダーから生成されています。 こんな短いのにこんな豪華な見た目が出るなんて面白いですよね。

それでは一歩踏み出してみましょう!

開発環境

はじめの一歩は出来るだけ気軽に始めることが大事です。 今回はブラウザ上で開発でき、かつユーザー登録なしに始められるGLSLSandboxを使います。

GLSLSandbox

GLSL Sandbox Gallery

GLSLSandboxはブラウザ上でシェーダーを実行できる環境です。 サイトを開いて直ぐにシェーダーを書き始めることができます。shaderdo.it!

今回の目標

先に今回の目標の絵を載せておきます。

単純なグラデーションですが、これを題材にシェーダーの基礎を学びましょう。

f:id:kasari:20191209102113p:plain

はじめの一歩

さぁ初めてのシェーダーです! http://glslsandbox.com/e#56456.0

void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

f:id:kasari:20191209110712p:plain

真っ赤な画面が表示されました。この実質一行の短いコードがシェーダーの基本です。

これは画面全体を赤色で塗りつぶすシェーダーになります。

解説

f:id:kasari:20191209114340p:plain

上の画像の通り、右辺はRGBAで色を表現しています。 gl_FragColorに出力したい色を代入するとその色が画面に表示されます。

今回はRとAだけが1なので赤色になっていました。

では、ちょっと改造して別の色も出して見ましょうか。 RGBのBにあたる値を0から1に変更してみます。 f:id:kasari:20191209114439p:plain

f:id:kasari:20191209113014p:plain

赤と青が混ざって紫色になりました。

うっ、目が、、、(Unityエンジニアが死ぬ音)

ここで衝撃の事実

シェーダーは塗りつぶす画素の数だけ実行されています。

例えば、横1200、縦800の画面を塗りつぶす場合は1200×800=960000の96万回シェーダーが実行されています。

シェーダーに画面全体を塗りつぶす機能はありません。1画素ごとにシェーダーが実行されています。 先程の例では全ての画素に対して「赤色を出力しろ」というシェーダーを実行したので画面全体が赤色になっていたわけです。

動作の理解のために擬似コードを挙げます。 全画素に対してシェーダーを実行していること。シェーダーでgl_FragColorに代入された値を画面の特定の位置に出力していることを感じてください。この擬似コードではfor文でくるくるシェーダーを実行していますが、実際はGPUによるパワーで各画素のシェーダーは並列に実行されます。

for (int x=0; x<width; x++) {
    for (int y=0; y<height; y++) {
        doShader();
        draw(x, y, gl_FragColor);
    }
}

画素ごとに色を指定する

さて次は画素ごとに別々の色を指定してみましょう。 このときに利用できるのがgl_FragCoordです。

gl_FragCoordには現在処理中の画素の位置がvec2で格納されています。

ここで動作の理解のために先程の擬似コードを改良します。

for (int x=0; x<width; x++) {
    for (int y=0; y<height; y++) {
        gl_FragCoord = vec2(x, y);
        doShader();
        draw(gl_FragCoord, gl_FragColor);
    }
}

gl_FragCoordに現在処理中の画素の位置がvec2で代入されました。この値をシェーダーから参照します。

gl_FragCoordの挙動を確認する

gl_FragCoordの値によって色が変わるシェーダーを書いてみます。 http://glslsandbox.com/e#56456.1

見た目は少し長くなりましたが、実装内容は単純です。見ていきましょう。*2

precision highp float;

void main() {
    vec3 col = vec3(0.0);
    if (gl_FragCoord.x > 100.0) {
        col.r = 1.0;
    }
    if (gl_FragCoord.y > 100.0) {
        col.b = 1.0;
    }
    gl_FragColor = vec4(col, 1.0);
}

まずはじめに precision highp float; という謎の記述が増えているので軽く解説します。

precision は精度修飾子といって float の精度を指定します。float の精度を頭で指定しないとシェーダーが動作しません。とりあえずは最高精度の highp を指定しておけば精度の問題で無駄にハマることはありません。

それでは処理を見ていきます。まずはじめに変数名 col で初期化された変数があります。この変数には最終的に出力する色を格納します。 次に if 文で、座標Xが100以上ならRを1 に、座標Yが100以上ならBを1にしています。

このシェーダーは、座標Xが100以上のときに赤色に、座標Yが100以上のときに青色に、座標Xが100以上かつ座標Yが100以上のときに紫色になります。

f:id:kasari:20191209121437p:plain

グラデーションをつくる

あとは色の変化を滑らかにしてあげたら目標の絵が出せそうです。

100以上かどうかできっぱり0,1を出し分けるのではなく、0から100にかけて徐々に0~1の値を取るように変更してみましょう。

http://glslsandbox.com/e#59299.0

precision highp float;

void main() {
    vec3 col = vec3(0.0);
    col.r = gl_FragCoord.x/100.0;
    col.b = gl_FragCoord.y/100.0;
    gl_FragColor = vec4(col, 1.0);
}

f:id:kasari:20191209125825p:plain

いい感じです!グラデーションができましたね。ただ目標の絵では画面右上までグラデーションが続いているのに対して、こちらは(100, 100)の位置でグラデーションが途切れてしまっています。

RとBの値が画面右上のときにちょうど1になるようにしましょう。 gl_FragCoordの取る値の範囲は (0, 0) ~ (width, height) でした。100ではなく、width と height で割るようにしたら画面右上のときにちょうど1になりそうです。

col.r = gl_FragCoord.x/width;
col.b = gl_FragCoord.y/height;

この width と height をシェーダーから参照できるようにしましょう。

完成

http://glslsandbox.com/e#59299.1

precision highp float;
uniform vec2 resolution;

void main() {
    vec3 col = vec3(0.0);
    col.r = gl_FragCoord.x/resolution.x;
    col.b = gl_FragCoord.y/resolution.y;
    gl_FragColor = vec4(col, 1.0);
}

uniform vec2 resolution; を書くと、シェーダー内で画面の解像度を参照できるようになります。 width を resolution.x、height を resolution.y に置き換えて上げれば動作します。

f:id:kasari:20191209102113p:plain

おまけ

uniform float time; を書くと、ページを開いてからの秒数を参照できるようになります。 time を使って col.g の値を時間で変化するようにしてみましょう。

http://glslsandbox.com/e#59299.2

precision highp float;
uniform vec2 resolution;
uniform float time;

void main() {
    vec3 col = vec3(0.0);
    col.r = gl_FragCoord.x/resolution.x;
    col.g = 0.5+0.5*sin(time);
    col.b = gl_FragCoord.y/resolution.y;
    gl_FragColor = vec4(col, 1.0);
}

f:id:kasari:20191209141118g:plain

きれい~

さいごに

かく言う僕も半年前まではシェーダーは意味わからんこわいものだと思っていました。 あの頃の自分に向けて、これだけ知っておけば案外シェーダーこわくないよ。と伝えられるような内容にしてみました。 この記事が誰かのはじめの一歩になれると嬉しいです。

それでは、良いシェーダーライフを!

*1:この記事ではGLSLのフラグメントシェーダーのことをシェーダーと単純に表記します

*2:step関数を使えば if 文を使わなくても実装できますが、今回は素朴な実装にしました。step関数を使うバージョンはこちら http://glslsandbox.com/e#56456.2