ジュリア集合
今回のサンプルの実行結果
美しいフラクタル
前回はフラクタル図形としても、またコンピューターグラフィックスの世界でも非常にポピュラーなマンデルブロ集合を GLSL のみでレンダリングしてみました。
実際に漸化式と呼ばれる数式を読み解きつつ解説してみましたが、少々難しかったかもしれませんね。
プログラミングを学ぶ上で大切なのは感動体験だと思います。単純な演算処理の繰り返しだけで、驚くほど緻密な模様が描き出されるこの感動を、ぜひ多くの人に味わってもらえたらと思います。
さて、今回ですがジュリア集合について見ていきます。
ジュリア集合は、マンデルブロ集合と非常に近い概念で描かれるフラクタル図形です。実際には、マンデルブロ集合のほうが後から確立したもので、ジュリア集合のほうが先輩なんだそうです。
レンダリングする上でのプロセスや、利用する漸化式も非常に似通っています。しかし全体的にジュリア集合のほうが複雑で美しい描画結果を比較的簡単に生成できるような気がします。マンデルブロ集合の場合には、細部を割と拡大してやらないと美しい模様を見ることが難しいです。一方、ジュリア集合のほうは拡大率をそれほど変えなくても美しい図形を見ることが可能です。
どのあたりのパラメータを調整すればいいのかなど踏まえつつ、見ていきましょう。
ジュリア集合の漸化式
さて、まずは確認の意味も含めてマンデルブロ集合の漸化式を再度見てみましょう。
マンデルブロ集合の漸化式
Zn+1 = Zn2 + C
なおかつ
Z0 = 0
マンデルブロ集合では、漸化式の C を様々に変化させながら計算を行うのでしたね。
そして、ループしつつ繰り返し計算していく上で、発散するかどうかを判断基準としてマンデルブロ集合に属するかどうかを判断していました。
ジュリア集合では、マンデルブロ集合でいう C を固定してしまいます。前回のマンデルブロ集合のサンプルの場合は C に処理対象ピクセルの座標を代入して処理していましたが、今回はここに固定値をあらかじめ入れてしまうわけですね。
さらに、マンデルブロ集合では Z0 は 0 である必要がありましたが、これに処理対象フラグメントの座標を渡して処理します。
つまり、マンデルブロ集合では C に入っていた値を、そっくりそのまま Z0 に入れてしまうわけですね。
なんとなく概念がつかめたところで、シェーダのソースを見てみてください。
ジュリア集合を描くシェーダのソース
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
vec3 hsv(float h, float s, float v){
vec4 t = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(vec3(h) + t.xyz) * 6.0 - vec3(t.w));
return v * mix(vec3(t.x), clamp(p - vec3(t.x), 0.0, 1.0), s);
}
void main(void){
vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
int j = 0;
vec2 x = vec2(-0.345, 0.654);
vec2 y = vec2(time * 0.005, 0.0);
vec2 z = p;
for(int i = 0; i < 360; i++){
j++;
if(length(z) > 2.0){break;}
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + x + y;
}
float h = abs(mod(time * 15.0 - float(j), 360.0) / 360.0);;
vec3 rgb = hsv(h, 1.0, 1.0);
gl_FragColor = vec4(rgb, 1.0);
}
今回のサンプルも、レンダリング結果の着色には前回同様 HSV 変換関数を使っています。
また、全体的なシェーダ内の流れも、前回のサンプルと非常に似ています。
前回のサンプルでは vec2
型の変数 x
には、処理対象フラグメントの座標位置、すなわち変数 p
を元にした値が入るようになっていました。これが漸化式でいう C の部分に相当します。
今回はここを固定値にしますので、任意の vec2
型の値を仕込んでおきます。
漸化式の C に与える初期値
vec2 x = vec2(-0.345, 0.654);
ちなみに、ここで変数 x
に入る値を様々に変更することで、描き出されるジュリア集合の様相は一辺します。レンダリング結果は非常にめまぐるしく変化するので、uniform 変数 time
を使ってアニメーションするようにしておくと、様々なレンダリング結果を見ることができて楽しいと思います。
また先ほども書いたように、Z0 に与える初期値は処理対象フラグメントの座標を表す変数 p
をそのまま代入しています。もしレンダリング結果を拡大したり縮小したりといったことがしたければ、この変数 p
の代入を行っている部分にひと手間加えればいいでしょう。
シェーダ内のループでは、マンデルブロ集合と同様に 2.0 という値を基準として発散を判断しつつ、ループ回数をカウントします。このカウント数が出力される色の指標として使われます。
ちょっとわかりにくい部分もあると思いますので、マンデルブロ集合とジュリア集合、双方のシェーダ内コードを見比べてみましょう。
マンデルブロ集合の場合
int j = 0;
vec2 x = p + vec2(-0.5, 0.0);
float y = 1.5 - mouse.x * 0.5;
vec2 z = vec2(0.0, 0.0);
for(int i = 0; i < 360; i++){
j++;
if(length(z) > 2.0){break;}
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + x * y;
}
ジュリア集合の場合
int j = 0;
vec2 x = vec2(-0.345, 0.654);
vec2 y = vec2(time * 0.005, 0.0);
vec2 z = p;
for(int i = 0; i < 360; i++){
j++;
if(length(z) > 2.0){break;}
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + x + y;
}
構造はほとんど同じですが、与えられる初期値や計算式が若干異なっていることがわかると思います。ちなみに、ジュリア集合のサンプルコードで、変数 x
に代入している初期値は適当です。深い意味はなく、なんとなくきれいに見えるポイントを手動で設定しただけです。
前回の内容が理解できていれば、それほど大きな変化もないのでおおよそ問題はないでしょう。あとは与える初期値を様々に変更して実行してみれば、そのレンダリング結果の変わりようを体験できると思います。
まとめ
マンデルブロ集合の解説では、漸化式の概念などを説明する必要があったため、ちょっと難解なテキストだったように思います。その分、もし前回の内容が理解できてさえいれば、今回のジュリア集合についてはそれほど難儀せずに済むでしょう。
マンデルブロ集合と比較すると、ジュリア集合のほうが見栄えのする図柄を演出しやすいと思います。図形が点対称として展開される点から言っても扱いやすいと思います。GLSL だけで様々なものをレンダリングする際、手軽に模様をつけるには便利な概念だと思います。
問題はリアルタイムで処理するとなると若干重すぎるというところでしょうか。しかしこのあたりの問題は、ハードウェアの進化とともに些細な問題になっていくのかもしれません。
サンプルはいつものように以下のリンクから。