【連載】#1 twigl.app で始める GLSL クリエイティブコーディング! まずは GLSL の基本を理解しよう

doxas : 2020-05-29 02:41:46

まずは気軽に始めてみよう

今回から、いつもとは少し趣向を変えて GLSL のチュートリアル記事を連載形式で書いていこうかなと思います。

きっかけは、最近にわかに(ごく限られた界隈で)話題になっていた #つぶやきGLSL というハッシュタグつきの一連のツイートです。

このハッシュタグが付けられたツイートは、GLSL のソースコードを1つのツイートに収まる長さで記述して、そのキャプチャ画像(主に GIF アニメーション)と一緒にツイートするというもので、日本国内だけでなく、海外の GLSL を嗜む人々によってたくさんの GLSL 作品が投稿されました。

一部の界隈ですごく話題になったということもあり、これら一連のツイートで初めて GLSL の存在に触れたひとの中には、今までは GLSL を知らなかったという人も多いのかなと想像しています。

本連載では、そもそも GLSL とはなんぞやという極めて基本的な部分から、#つぶやきGLSL のツイートに記載されているソースコードの読み解き方、また自分でもコードを書くためのヒントなどを掲載していけたらと思います。

そもそも GLSL とはなんなのか

まずはじめに、そもそも GLSL とは何かという部分を簡単におさらいしておきましょう。

GLSL は、OpenGL や WebGL といったグラフィックス API で用いられる シェーダを記述するための言語 です。プログラミング言語の一種ですが、あくまでも「シェーダを記述するための言語」であるため、一般にプログラミング言語と呼ばれるものと比較するとその用途は極めて限定的です。

シェーダには、頂点シェーダやフラグメントシェーダ、ジオメトリシェーダやコンピュートシェーダなど、様々な種類があるのですが…… これらのシェーダに共通する特徴として、それらが「GPU 上で動作する」ということが、JavaScript などの一般的なプログラムとは大きく異なる部分です。

GPU は「グラフィックスを描画することに代表される汎用計算処理」に優れた性能を発揮するので、JavaScript と Canvas2D を使った場合などと比較すると、圧倒的なパフォーマンスを叩き出すことができます。その代表的な例が 3DCG の描画です。3DCG は現代ではすっかり珍しいものではなくなりましたが、2D の描画処理に比較すると大抵は計算量が多くなりますので、GPU のような高速な演算装置を利用できる方が有利になるわけですね。

GPU の力で高速に美しいグラフィックスを描画する。

さて、GLSL を用いればどうやら「シェーダと呼ばれるプログラム」が記述できるらしい、ということがわかりました。

実際のところ、シェーダというのはそれ自体が1つの小さなプログラムです。関数のようなもの、と考えてもいいかもしれません。

たとえば、画面上に無数に存在するピクセルに、これから1つ1つ順番に色を塗っていかなくてはならない場面を思い浮かべてみましょう。このとき、画面上の色は、誰かが作ってくれた謎のプログラムで指定できるようになっていると仮定してみます。たとえば以下のように。(注:以下の例は GLSL ではありません、架空の言語です)

pixelColor[0, 0] = rgba(255, 255, 255, 1.0);
pixelColor[1, 0] = rgba(255, 255, 255, 1.0);
pixelColor[2, 0] = rgba(255, 255, 255, 1.0);
pixelColor[3, 0] = rgba(255, 255, 255, 1.0);
...以下続く

pixelColor[511, 511] = rgba(255, 255, 255, 1.0);

これを見ると、なんとなく pixelColor というのがピクセルの色を表す変数で、それに続く括弧部分で、ピクセルの座標を示しているのかな? と想像できると思います。

また rgba() という関数には RGBA でパラメータとなる引数を与え、なんとなく…… 255 が RGB それぞれに指定されていると白が出力されそうな感じがするんじゃないでしょうか。

さらに、一番最後の行が [511, 511] で終わっているので、0 から始まり 511 で終わる縦横 512px 四方の領域に対して白を出力してるんじゃないかという想像ができます。

先程の例は GLSL ではありませんが、GLSL がやることの一部は、実際こんな感じです。

グラフィックスを出力すべき対象に対して、地道に 1px ずつ出力する色を決めていくわけですね。

ただし、ここで衝撃の事実があるんですけども、GLSL では、先程の例で言うと以下のようなかんじで動作します。

function GLSL(){
    // なんらかの GLSL のコード
}

pixelColor[0, 0] = GLSL(); // ← GLSL() はまったく同じ処理
pixelColor[1, 0] = GLSL(); // ← GLSL() はまったく同じ処理
pixelColor[2, 0] = GLSL(); // ← GLSL() はまったく同じ処理
pixelColor[3, 0] = GLSL(); // ← GLSL() はまったく同じ処理
...以下続く

pixelColor[511, 511] = GLSL(); // ← GLSL() はまったく同じ処理

どういうこと!? って思いましたか?

たぶん、思ったひとが多いと思うんですが、GLSL って、すべてのピクセルに対して まったく同じ処理を行うことしかできません 。嘘みたいな、でもこれ、本当の話なんです。

GLSL を使ってシェーダを記述するとき、すくなくとも #つぶやきGLSL のような実装の場合、すべてのピクセル上でまったく同じ GLSL のコードが一様に実行されます。端から端まで、例外なくすべてのピクセルで 完全に同じコードによる処理 が行われるのです。

でも仮に本当にそうだとしたら、GLSL がどんな色を出力するにしても、画面が1つの色でベタ塗りされてしまって、とてもじゃないけどキレイなグラフィックスなんて描けるわけがない! と思ってしまいますよね。

そのあたりの疑問を解決しながら、そろそろ実際に、GLSL のコードを書き始めてみましょう。

twigl を使っていざ実践

まずは、以下のリンクを踏んで twigl.app を開きます。

twigl.app (整形したコード入力済み)

コードがあらかじめ入力された状態になった、黒い背景のエディタのようなものが出てきたと思います。

そして、そのエディタ内には以下のようにコードが記載されていると思います。

precision highp float;
uniform vec2 resolution;
uniform vec2 mouse;
uniform float time;
void main(){
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

普段 JavaScript を書いている人には、おそらくあまり馴染みのない……謎のキーワードがたくさん出てきているかと思いますが、ここでまず注目したいのは最後の方に定義されている main という関数と、その中に記載された gl_FragColor です。

GLSL には、もともと言語仕様上定義されている ビルトインの変数 がいくつかあります。それらのビルトイン変数には gl_ の接頭辞が付いていて、ここで登場している gl_FragColor もそのうちの1つです。

main 関数は、GLSL という小さなプログラムの本体であり、この関数が すべてのピクセルで漏れなく実行される処理そのもの を表しています。また gl_FragColor該当するピクセルに出力する色 です。

つまり、先程のコードにあった gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0) という記述は「対象となったピクセルにどんな色を出力すればいいか」を GLSL というプログラムによって指定しているんですね。

GLSL では、色の表現は4つの値を組み合わせ、RGBA のそれぞれに対して 0.0 ~ 1.0 の範囲で指定します。ですから、おそらくリンクを開いた先の twigl.app では、エディタの上部にあるレンダリング領域(スクリーン)が真っ白になっていたはずです。

試しに、出力される vec4(1.0, 1.0, 1.0, 1.0) の部分を、適当に数値を変えてみるとそれが RGBA の指定であることがわかりやすいと思います。

出力する vec4 を vec4(0.0, 0.5, 1.0, 1.0) に変更した例。

さて gl_FragColor が最終的に画面に出力される色を表しており、色の指定は vec4() を使って RGBA で指定すればよいことがわかりましたが……

そもそも、これではスクリーン全体のベタ塗りしかできませんよね。

もう少しだけ、いくつかキーワードを先に理解してから、ベタ塗り以外の色の出力ができる状態まで一気に持っていってしまいましょう。

まず、先程も出てきた vec4() のような書き方をするコード。これは一種のコンストラクタのようなものです。JavaScript で言えば const arr = new Array() みたいに、何かしらのデータ構造を new するのと同じ意味だと考えるとよいでしょう。

GLSL には、データ型として「浮動小数点を扱う float 型」や、「ベクトルを扱うことができる vec2vec4 型」などがあり、浮動小数点の場合は数値を直接記述するだけでいいのですが、ベクトルの場合は先程の例のようにコンストラクタっぽく書いてやる必要があります。

たとえば、いくつか変数を宣言するときは以下のような感じで書きます。

float x = 0.0; // 浮動小数点はただ宣言して代入するだけ
float y = 0.0;
float z = 0.0;
vec2 v2 = vec2(x, y); // vec2 以降はコンストラクタ的にデータ構造を作る
vec3 v3 = vec3(x, y, z);
vec4 v4 = vec4(0.0, 1.0, 2.0, 3.0);

その他に、ちょっと変則的な例として、以下のようなこともできます。

最初はちょっと、戸惑うかもしれないけど……

// 引数が1つしか指定されていないなら vec2(1.0, 1.0) のようにすべての要素に入る
vec2 v2 = vec2(1.0);
// 引数が合計で正しい要素数になってさえいればエラーにはならない
vec3 v3 = vec3(v2, 3.0);
vec4 v4 = vec4(0.0, v3);
// xyzw のような記号を使って要素を表現することができる(スウィズル演算子)
vec4 w = vec4(v4.xyz, 1.0);
vec4 x = w.xyzw;
// スウィズル演算子は重複や順序変更も自由に行ってよい
vec4 y = w.zzxx;

かなり柔軟に、ベクトル系の変数を使ってデータ構造を定義することができるのがわかると思います。

GLSL はもともとベクトルや行列といった線型代数の計算を多く扱うので、このように様々な記述方法が用意されているのだと思います。

ちなみに gl_FragColor のデータ型は vec4 型です。

意味は先程も触れたとおりですが、最終的に出力される RGBA の各要素の値になります。色の範囲は 0.0 ~ 1.0 の範囲で指定する必要があるため、仮に vec4(1.0) とした場合は白に、 vec4(0.0) とした場合は(アルファが 0.0 の)黒が出力されます。

冒頭部分に書かれている各種コードの意味

さて、ベクトル系の変数の宣言の仕方などがなんとなく理解できたら、続いては「冒頭に書かれているよくわからんセクション」の部分を簡単に解説しておきます。

まず、確認の意味でもう一度先程のコードを掲載します。

precision highp float;
uniform vec2 resolution;
uniform vec2 mouse;
uniform float time;
void main(){
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

まず冒頭1行目。ここでは precision キーワードを使ったコードが書かれています。

precision(プレシジョン)は直訳すると「精度」のような意味になる英単語で、ここでは「この GLSL のコードでは highp 精度で float を扱いますよ~」ということを宣言しています。このような precition 宣言は、#つぶやきGLSL が動作するプラットフォーム上では必須なので、お約束のような感じで大抵書いてあります。(書いてない場合があるとしたら、WebGL の処理を行っている JavaScript 側で補完してるはずです)

その次の行からは uniform というキーワードが連続して出てきていますね。

この uniform(ユニフォーム)は変数の種類を表すもので、uniform 修飾子が付与された変数は「CPU 側からデータを受け取るための窓口」となる変数を意味します。

どういうことかというと……たとえば、GLSL には「実行されてから何秒経ったのか?」を調べる方法はありません。このような時間の経過は、CPU 側(WebGL 前提なら JavaScript 側)で計測して、uniform 変数に詰め込んでシェーダに送ります。

同様に、たとえば画面の解像度であるとか、マウスカーソルの座標であるとか……そういうことは、GLSL の世界では知ることができないので、GLSL の内部でなにかしらの計算などに使いたい場合は CPU 側から送ってやらなくちゃいけません。

uniform vec2 resolution; // スクリーンの解像度(ピクセル)
uniform vec2 mouse;      // マウスカーソルの位置(0.0 ~ 1.0)
uniform float time;      // 経過時間(秒)

GLSL だけでは判断できないような値に関しては、CPU 側で調査したり、あるいはあらかじめ計算したりして、送ってやるわけですね。

ピクセルごとに出力される色を変える

さて、ちょっと覚えることが多くて大変ですが……

今回の記事では最後に「ピクセルごとに異なる色を出力する方法」の基本だけ覚えておきましょう。

ここまで出てきた概念のうち、重要なことを再度おさらいします。

  • GLSL はすべてのピクセルで一様に同じ処理が実行される
  • この「一様に実行される処理」が main 関数
  • gl_ の接頭辞がつく変数は何かしらの意味のあるビルトイン変数
  • gl_FragColorvec4 で出力した値が画面上の色になる
  • 色の範囲は 0.0 ~ 1.0 の範囲でそれぞれ RGBA に出力する
  • uniform 修飾子で宣言された変数には CPU から値が送られてくる

さて、これらのことを踏まえて、最後に1つ覚えておきたいのが gl_FragCoord です。

gl_FragColor と綴りが似ていますが、両者は完全な別物です。

gl_FragCoordvec4 型のビルトイン変数で、その意味は「今まさに処理しようとしているピクセルの座標」です。

GLSL で記述された処理自体は、先述のとおりすべてのピクセルに対して一様に処理されてしまうのですが、この gl_FragCoord の中身は、各ピクセルごとに異なる値が あらかじめ入った状態になっている のです。

つまり gl_FragCoordxy 要素を参照しながら処理を行うと、そのピクセル座標に特有な結果を色として出力することができるようになります。

ごく簡単な例で、確かめてみましょう。

たとえば gl_FragCoord.x というように、今処理しようとしている X 座標の値に対して、uniform 変数である resolution を使った計算を行う例が以下のようになります。

precision highp float;
uniform vec2 resolution;
uniform vec2 mouse;
uniform float time;
void main(){
  // 最終出力  = vec4( vec3(処理中の座標 X / 解像度の X)  , 1.0)
  gl_FragColor = vec4( vec3(gl_FragCoord.x / resolution.x), 1.0);
}

この結果は以下のようになります。

横方向にグラデーションが!

gl_FragCoord.x の値は、左端の時点で 0.0 で、これが画面の右側に向かって徐々に増えていきます。画面の右端は、当然と言えば当然ですが、解像度の横方向の幅に等しくなりますので gl_FragCoord.x / resolution.x を行うと、その計算結果は常に 0.0 以上 1.0 未満になるはずです。

GLSL では色を 0.0 ~ 1.0 の範囲で扱うため……

画面全体が、横方向にグラデーションしたような描画結果を得ることができるわけですね。

最後に

さて、ちょっと最初なので長くなりましたが、GLSL でシェーダを書くということの、入り口としてはこの程度の理解でまずはいいのではないでしょうか。

次回以降は、もう少し踏み込んで GLSL での表現方法を詳しく見ていきます。

また、連載の後半では、#つぶやきGLSL というハッシュタグで見られるような「極度に minify された GLSL」の書き方のコツなんかも取り上げていく予定です。

お楽しみに。

リンク:

twigl.app

share

follow us in feedly

search

search

sponsor

social