この記事は、AmusementCreators AdventCalender 通称 ACAC 2日目の記事です。
担当は AmusementCreators 2年のランです。
shadertoy とは?
shadertoy というサイトがあるのですが、ざっくりいうとオンラインで shader を書いてそれを実行して描画するサービスを提供しているサイトです。
shader の toy というそのままの意味を取れば、「 shader のおもちゃ 」で、shader 以外に必要なプログラムを気にせず、気軽にshader だけを書くことができるサービスになってます。
みんなもアカウントを作成して、自分のshaderを書いて保存しよう!
そもそも shader とは?
私が shader に出会ったきっかけは、ゲームを作っている時に、「それぞれのオブジェクトにテクスチャを用意するのが面倒!!計算で画像生成したい!!!」と思ったところにあります。その時に、WebGLに出会って、「ブラウザを使うWebGLを勉強すればクロスプラットフォームな開発ができるのでは…?」となったわけです。shaderは、そのWebGLの一部の処理にあたり、WebGLを勉強する上でぶち当たった壁でもありました。
「 shaderで遊べるサイトがあるよ? 」
なるほど、そんなサイトが……そこで出会ったのが shadertoy なわけですが、ここで書くのshaderというのは、いわゆる「フラグメントシェーダ」と呼ばれるもので、頂点情報をxy座標として、その座標の色を決めるという役割を担っています。簡単に言えば、座標を入力、色を出力とするわけです。
つまるところ、ここでいうshaderは、座標を入力、色を出力とするアルゴリズムが実装されたプログラムなわけです。
マンデルブロ集合を書こう
とりあえず書いてみよう。
マンデルブロ集合は、
z_{n+1} = z^{2}_{n} + c\\
z_{0} = 0
という漸化式で表したとき、zが発散しない点の集合で表される。
ここでcを、複素平面上の座標として定義すると、その発散模様が面白くなる集合である。
単純にアルゴリズムを考えると、
- cに座標を代入
- zの漸化式を進める
- 発散したときのnを記録
- nに応じて色を出力
これを各座標ごとにやっていけばいいわけと。
実際にプログラム組むときは、nに上限を与えて、漸化式ループを止める必要がある。
実際に組んでみたものがこちら
#define PI 3.14159265359
#define speed 10.
#define dim 2.
#define limit 500
#define zoom 6.
//// choose one and comment out
//#define NORMAL
#define ZOOM
//#define MOVE_Z0
//#define MOVE_DIM
//#define JULIA
//// color variation
#define COLORFUL
//#define GRAY
//#define CYAN
//#define WHITE
vec2 cx_pow(in vec2 z, const float n)
{
float r = length(z);
float theta = atan(z.y/z.x);
if (z.x<0.) theta+=PI;
if (theta<0.) theta+=2.*PI;
theta -= PI;
return pow(r,n)*vec2(cos(n*theta),sin(n*theta));
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
float scale = .3;
vec2 offset = vec2(-.34,.0);
#ifdef NORMAL
scale = .3;
offset = vec2(-.34,.0);
#endif
#ifdef ZOOM
scale = pow(.3,zoom);
offset = vec2(-.05,.6805);
#endif
#ifdef MOVE_Z0
scale = .3;
offset = vec2(-.34,.0);
#endif
#ifdef MOVE_DIM
scale = .3;
offset = vec2(-.34,.0);
#endif
#ifdef JULIA
scale = .3;
offset = vec2(-.34,.0);
#endif
float t = iGlobalTime;
t *= speed;
vec2 c = vec2(
(fragCoord.x / iResolution.x) * 16. - 8.,
(fragCoord.y / iResolution.y) * 9. - 4.5
);
c *= scale;
c += offset;
vec3 col = vec3(0.,0.,0.);
vec2 z = vec2(0,0);
#ifdef MOVE_Z0
float zr = 1.;
vec2 zim = cx_pow(vec2(0.,1.),mod(t/100.,6.));
z = zr*zim;
#endif
#ifdef JULIA
float zr = .5;
vec2 zim = cx_pow(vec2(0.,1.),mod(t/20.,6.));
z = c;
c = zr*zim;
#endif
const int h = limit;
int border = h;
#ifdef ZOOM
border = int(mod(1.5*t,2.*float(h)));
#endif
for (int i = 0; i < h; i++) {
if(i >= border) {
break;
}
if (length(z) >= 2.) {
col = vec3(1.-1./(.001+mod(float(i),4.))+.3,1./(.001+mod(float(i),7.))+.3,1.-1./(.001+mod(float(i),3.))+.3);
#ifdef WHITE
col = vec3(1.,1.,1.);
#endif
#ifdef GRAY
col = min(1.,1./log(.1*float(i)))*vec3(1.,1.,1.);
#endif
#ifdef CYAN
col = min(1.,1./log(.1*float(i)))*vec3(0.,1.,1.);
#endif
break;
}
float n = cos(t/3.)+1.;
n = dim;
#ifdef MOVE_DIM
n = t/10.;
#endif
z = cx_pow(z,n) + c;
}
fragColor = vec4(col,1.);
}
と、まあ色を変えたり、漸化式を2乗からn乗に拡張して、zとcをひっくり返してジュリア集合にしたり、それらを#defineのコメントアウトで操作できるようにしたらコードがだらだらと長くなってしまいました。
結果だけ見ると面白い。
shadertoyにハマったとき、コード読むのにつまずいたところだけ拾っていきたいと思います。
各種使う定数
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
...
vec2 c = vec2(
(fragCoord.x / iResolution.x) * 16. - 8.,
(fragCoord.y / iResolution.y) * 9. - 4.5
);
ここで、cは使用する座標を設定しています。
vec2 fragCoord
画面上の座標をピクセル単位で取得できます。
例として、 fragCoord.x で ピクセル単位で x座標を float で取り出すことができます。
vec2 iResolution
画面の解像度を取得できます。画面を最大化したり、ブラウザのサイズを縮めたりする時に、値が変わります。
これは、fragCoordのxyのそれぞれの最大値にあたるので、fragCoordをiResolutionで割ることで、画面上の座標を[0, 1]にそれぞれ正規化することができます。
float t = iGlobalTime;
ここでは時間tを設定しています。
float iGlobalTime
フレームの経過時間を取得できます。内部的にはどうなってるのか詳しく知りませんが、アニメーションができたあとに、tを何倍か、割ったりとか、すればアニメーションスピードを調整することができます。
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
...
fragColor = vec4(col,1.);
}
ここでは、その座標の色を代入しています。
vec4 fragColor
mainImageの返り値となるのがこのfragColorです。vec4の書き方も独特で、RGBAの値を割り振るのですが、色RGBを先にvec3 colとして設定して、vec4 fragColorに代入する際にはvec4(col, 1.)を与えたりする記法があったりします。わざわざcolの中身を取り出さないあたりが簡潔で、お決まりになっているようです。
使える関数
この辺は実際よくわかってないのですが、数学の関数は大体使えるのでやってみてください。色の決定に結構使ってるmod関数ですが、これは実数範囲まで拡張されており、余りが実数ででるようになっています。整数関数の扱いは若干違うと思いますが、floorやceilもあるので、それらを使ってうまいことやりましょう。
(何にも気にしないでlog使ったりしてますが、未定義範囲は0.が返ってくるようです。)
まとめ
とまあ、あれやこれやするとこんな感じにマンデルブロが映るわけです。
実際に動く場面を見た方が面白いと思うので、こちらに飛ぶなり、自分で実装するなりして楽しんでください。
AmusementCreators AdventCalender 2日目 ランの記事でした。