0.このテキストの目的
本記事では、Scott Drave氏のFlameアルゴリズムについて、日本語でごく簡単な解説をしつつopenFrameworksとGLSLの実装例を示します。当該アルゴリズムは、動画などで広く流布しています。出力が非常に美しく多彩で、こんなにも耽美な絵画群がいかにして1つのプログラムで生成できるのか解読したいと思ったのがきっかけで、作者の論稿を読み始めました。
アルゴリズム自体は、その出力結果からは想像できないほど驚くほどシンプルでです。このアルゴリズムを発見し、OpenSourceで公開してくれたScott Drave氏に敬意をこめながら、コンピュータによる美しい画作りをしたいと思う方の少しでも参考になればと思い本記事を書きました。
注意:数学的な記述については専門的知識を持ち合わせておらず、間違い等などあればご指摘いただけると幸いです。
目次
1. Scott Drave氏の功績 – Electric Sheep と Flame
2. 反復関数系
3. Flame アルゴリズム
4. openFrameworks + GLSL での実装
1.Scott Drave氏の功績 – Electric Sheep と Flame
Electric SheepとはScott Dravesを中心として1999年に制作されたソフトウェアと、それを運用するアートプロジェクトです。当該ソフトウェアはフラクタル図形のアニメーションを描画するスクリーンセーバとし機能します。アニメーションは、高性能な中央サーバーがレンダリングし、クライアントコンピュータはインターネット経由でストリーミングするのみです。
生成される図像は、作者が Flame と呼ぶシンプルなアルゴリズムによるものです。これは1992年にOpenSourceとして公開( 現在もGitHubに存在 )、1993年にPrix Ars Electronica Honorary Mentionedを獲得しています。Flameはその美しさそ結果の多様さからを人気を博し、スタンドアロンのアプリケーションからAdobe After Effects や Adobe Photoshop のといったポピュラーなソフトで動作するプラグイン等が存在します。
Flameはその特質上、とりうるパラメータが無数にあります。Electric Sheepではできあがった絵(つまりFlameのとったパラメータ)を、ユーザーグループ間で共有し、気に入ったものに投票することができます。その結果を重み付けとした遺伝的アルゴリズムによって逐次あらたなパラメータを生みだし、絵を生成します。つまり、人間の好みを遺伝的アルゴリズムで学習させ、より美しい絵を永遠に生成していくということを意味します。この一連のプロセスは、人間と機械の協調によって美を追求する営みであり、人間が美しさを測る「審美」を再考する上でも興味深い試みです。
Scott Drave氏の功績として特筆すべきは
1) 美を目的とした、理解しやすいアルゴリズムを発見したこと
2) 発見したアルゴリズムを1992年という早いの段階でOpen Sourceとしたこと
3) 人とマシンの協調による美学の発展を志向したこと
です。アートが「美」を追求するものでなくなった今、近未来の美しさを考える美学の発展を考えるにあたって大きなマイルストンのように思います。
こうした美に着目した作家は木本圭子、William Latham等挙げられますが、また別の機会にまとめたいと思います。
2.反復関数系
Flameは反復関数系(IFS: Iterated Function System)に属します。乱数を入力値として受け取り、再帰計算を経て座標変換します。再帰計算によって乱数の入力が、特定の系、集合による形をなすことがあります。例えば以下はその一例です。
シェルピンスキーのギャスケット
33.33..%:
33.33..%:
33.33..%:
シダ
1%:
7%:
7%:
85%:
Wikipediaよりパラメータ参照
上記の例はともに
という座標変換に帰着させることができます。
上記のa,b,c,d,e,fの6つのパラメータが、一点の確率で特定の値に決定され、再帰計算されることで、時として美しい形をなす事があるのです。逆に、秩序のないつまらない絵になるときもあります。不思議です。
3.Flame アルゴリズム
Flameアルゴリズムは http://flam3.com/flame.pdf に詳述されていますが、概観すると以下のようになります。
1)2次元ベクトルを入力し、座標変化をした上で2次元ベクトル出力する関数を、再帰的に計算する
2)この関数(座標変換処理)は、「アフィン変換」部と「バリエーション」部に別れ、それぞれのパラメータを確率にしたがって決定し実行する
・アフィン変換はパラメータを6つ、バリエーションは実質、無数のパラメータをとる
・このアフィン+バリエーションの関数のとるパラメータを複数用意し、それぞれに出現率をわりあてる。
・一回の再帰ループ(イテレーション)の中で、出現率に従って1つのアフィン+バリエーションの処理を実行する
アフィン変換部
上述した反復関数系(カオスゲーム)の通りです。これだけでも実に多様な系を生成することができます。
バリエーション部
それぞれの下位関数は、たとえば魚眼系の変換、天球系の変換など、上記の論稿では40種類以上定義されています。実質、オリジナルを定義することもできます。実装する際は、描画パフォーマンスと出力の好みによって決定します。
さらに、上記論稿では、点のカラーリング、パラメータ変更時のモーフィングについても言及していますが、このあたりの詳細は原文を参考ください。
4.openFrameworks + GLSL での実装
ここでは上記原理をGPUによって並列計算させることを念頭に、openFrameworksとGLSLでの記述をします。
main.cpp
下準備として、GLSL1.5を利用するために、OpenGL APIを3.2にします。
GLSL1.5を使う理由は、グローバル変数に in,out 修飾子が使え、openFrameworksが提供する変数(in vec4 positionなど)を使えるからです。(というかこれを推奨とofBookに記述がありました。)
1 2 3 4 5 6 7 8 9 10 11 12 |
#include "ofMain.h" #include "ofApp.h" int main( ){ ofGLWindowSettings settings; settings.setGLVersion(3, 2); settings.width = 1024; settings.height = 768; ofCreateWindow(settings); ofRunApp(new ofApp()); } |
ofApp.h / ofApp.cpp
openFrameworksのメインの記述は、指定の頂点数をもった Vertex Buffer Object (VBO) を用意し、そのオブジェクトに対して、Shader Material を適用します。オブジェクトにShader Materialを適用することで、頂点座標の計算、描画時の色計算をGLSLで記述することができます。計算内容やGPU性能によりますが頂点数100万くらいでも60fpsが十分でると思います。
描画のときに必要な変数を用意し、Uniform変数としてShaderにわたるようにします。
重要なのは、アフィン変換部、バリエーション部で使うパラメータです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// ofApp.h内で変数宣言 ofVboMesh mesh; ofShader shader; float uWeight[2]; float uAffineParams[18]; float uVariationParams[15]; float uColorParams[9]; ofVec2f uScale; ofVec2f uShift; float uSeed; // ofApp.cpp void ofApp::draw(){ shader.begin(); shader.setUniform1fv("uWeight", uWeight, 2); // uniform float[2] shader.setUniform1fv("uAffineParams", uAffineParams, 18); // uniform float[18] shader.setUniform1fv("uVariationParams", uVariationParams, 15); // uniform float[15] shader.setUniform1fv("uColorParams", uColorParams, 9); // uniform float[9] shader.setUniform2f("uScale", uScale.x, uScale.y); // uniform vec2 shader.setUniform2f("uShift", uShift.x, uShift.y); // uniform vec2 shader.setUniform1i("uNumColor", 3); // uniform int shader.setUniform1f("uSeed", ofGetElapsedTimef()); // uniform float mesh.draw(); // shader material が適用される shader.end(); } |
Vertex Shader
頂点計算は、Vertex Shaderで行います。Joel Castellanos氏によるWebGLでの実装例を参考にし、コードとして分かりやすくなるよう変数名など改変をくわえつつ、バリエーション部分は私の好きなようにカスタマイズしました。一般的にメモリ確保をする変数宣言は少なくすると良いと考えられますが、このコードは結構変数宣言をしてるのでチューニングの余地あると思います…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
#version 150 precision mediump float; // from oF default params uniform mat4 modelViewProjectionMatrix; in vec4 position; out vec4 color; // uniform data uniform float uWeight[2]; uniform float uAffineParams[18]; uniform float uVariationParams[15]; uniform float uColorParams[9]; uniform vec2 uScale; uniform vec2 uShift; uniform int uNumColor; uniform float uSeed; // random func float random(inout vec2 randomVec2) { float r = fract(sin(dot(randomVec2, vec2(12.9898, 78.233))) * 43758.5453); randomVec2[0] = randomVec2[1]; randomVec2[1] = r; return r; } void affine(inout vec2 p, in float a, in float b, in float c, in float d, in float e, in float f) { float x = p.x; float y = p.y; p.x = a*x + b*y + c; p.y = d*x + e*y + f; } // linear vec2 var1(in vec2 p) { vec2 _p = vec2(p.xy); return _p; } // spherical vec2 var2(in vec2 p, in float r2) { vec2 _p = vec2(p.xy); _p = _p / r2; return _p; } // Fisheye vec2 var3(in vec2 p, in float r2) { vec2 _p = vec2(p.xy); float r = sqrt(r2); _p = _p * (2.0 / (r + 1.0)); return _p; } // tangent vec2 var4(in vec2 p) { vec2 _p = vec2(sin(p.x)/cos(p.y), tan(p.y)); return _p; } // bubble vec2 var5(in vec2 p, in float r2) { vec2 _p = vec2(p.xy); _p = _p * (4.0 / (r2 + 4.0)); return _p; } void variation(inout vec2 p, in float v1, in float v2, in float v3, in float v4, in float v5) { float r2 = p.x * p.x + p.y * p.y + 0.00001; p = var1(p) * v1 + var2(p, r2) * v2 + var3(p, r2) * v3 + var4(p) * v4 + var5(p, r2) * v5; } void main() { float weight2 = uWeight[0] + uWeight[1]; if (weight2 > 0.9999) weight2 = 1.0001; vec3 count = vec3(0.0); int firstColorIteration = 9 - uNumColor; vec2 p2 = vec2(position.xy); vec2 randomVec2 = vec2(p2.x * uSeed, p2.y * fract(uSeed * 100.0)); float randomN; for (int i = 0; i < 21; i++) { randomN = random(randomVec2); if (randomN < uWeight[0]) { affine(p2, uAffineParams[0], uAffineParams[1], uAffineParams[2], uAffineParams[3], uAffineParams[4], uAffineParams[5]); variation(p2, uVariationParams[0], uVariationParams[1], uVariationParams[2], uVariationParams[3], uVariationParams[4]); if (i >= firstColorIteration) count[0]++; } else if (randomN < weight2) { affine(p2, uAffineParams[6], uAffineParams[7], uAffineParams[8], uAffineParams[9], uAffineParams[10], uAffineParams[11]); variation(p2, uVariationParams[5], uVariationParams[6], uVariationParams[7], uVariationParams[8], uVariationParams[9]); if (i >= firstColorIteration) count[1]++; } else { affine(p2, uAffineParams[12], uAffineParams[13], uAffineParams[14], uAffineParams[15], uAffineParams[16], uAffineParams[17]); variation(p2, uVariationParams[10], uVariationParams[11], uVariationParams[12], uVariationParams[13], uVariationParams[14]); if (i >= firstColorIteration) count[2]++; } } p2.x = (p2.x * uScale.x) + uShift.x; p2.y = (p2.y * uScale.y) + uShift.y; // define color vec3 colorWeight = vec3(0.0); colorWeight[0] = min(1.0 / (uWeight[0] + 0.00001), 3.0); colorWeight[1] = min(1.0 / (uWeight[1] + 0.00001), 3.0); colorWeight[2] = min(1.0 / (1.00001 - weight2), 3.0); colorWeight = count * colorWeight; colorWeight = (colorWeight/(colorWeight[0]+colorWeight[1]+colorWeight[2])) * 1.5; color.r = uColorParams[0] * colorWeight[0] + uColorParams[3] * colorWeight[1] + uColorParams[6] * colorWeight[2]; color.g = uColorParams[1] * colorWeight[0] + uColorParams[4] * colorWeight[1] + uColorParams[7] * colorWeight[2]; color.b = uColorParams[2] * colorWeight[0] + uColorParams[5] * colorWeight[1] + uColorParams[8] * colorWeight[2]; color.a = 0.8; gl_Position = modelViewProjectionMatrix * vec4(p2.x, p2.y, 0, 1.0); } |
Fragment Shader
Fragment Shader 側では、Vertex Shader側で計算した結果を出力するのみです。GLSLはバージョンが違うことで出力の際の記法や、使える機能にかなり差異があるので、ご注意ください。openFrameworksはデフォルトはGLSL1.2になります。(この場合、varyingの宣言ができたりします。gl_FragColorで色出力したりします。)
1 2 3 4 5 6 7 8 9 |
#version 150 precision mediump float; in vec4 color; out vec4 outputColor; void main() { outputColor = color; } |