はじめに
WebGL Advent Calendar 2018 の16日目の記事です。
keimと申します。一年くらい前までは shadertoy や GLSLfan など Shader Art 界隈で遊んでおり、去年のアドベントカレンダーでは「レイマーチングでガラスレンダリング」の記事を書いたり、今年は縁あって GLSL Tech Night 2018 で スピーカーとして登壇させていただいたりしました。
ここ数か月は主に three.js で遊んでおり、ちょうど最近作っていたものが出来上がりつつあったので、この場を借りて公開させていただこうと思います。
Structure Synth
2007年ころ登場したStructure Synthというアプリをご存知でしょうか?
複雑な幾何学立体を非常に簡潔なスクリプトで生成することができるソフトです。Frickrグループには Structure Synth で生成した立体のレンダリング結果が大量に置かれており、どんな事ができるか何となく解ると思います。
開発者は、フラクタル界隈ではかなり有名なSyntopia blogというブログを運営されているMikael H Christensen氏で、2010年まで開発され、現在は更新を停止しています。
2011年からは Fragment Shader によるレイトレースを利用したFragmentariumの開発に移行しており、こちらはポリゴンの制約を受けない本格的なフラクタル立体を描く事ができます。
GPUの性能が向上し Shader レイトレースのノウハウもだいぶ蓄積されてきた現在、フラクタル形状を生成するのにポリゴンベースのStructure Synth はあまり良い選択肢とは言えません。しかし、ポリゴンは汎用的な技術のため一般的な3D描写との相性が良く、オブジェクト数が制限されたフラクタルの独特の形状は他に類のない魅力があり、いまだにコアなファンの支持を受けています。
SolidML
この Structure Synth のスクリプトである Eisen Script を使って three.js の BufferGeometry を生成してみたいと思い、今年10月くらいから実装を開始しました。
11月下旬には一通りの実装を終えたのですが、得意なMMLと似ていると感じ、自分が書きやすいようにシンタックスシュガーを適当に入れていくうちに別言語の様相を呈してきました。
そこで、それまで StructureSynth.js という名前だったものを SolidML と改名してリリースすることにしました。
ここでは SolidML の簡単な利用方法を紹介します。まだ作り途中のため、ドキュメント整備まで手が回っていませんが、徐々に準備していきたいと考えています。
環境
ブラウザで https://keim.github.io/SolidML/demo/index.html にアクセスして左のテキストエリアにスクリプトを打って Build ボタンを押せば生成することができます。ビルドに成功すると url query にスクリプトの平文が含まれるので、このようにリンクでスクリプトをシェアすることができます。
API
https://keim.github.io/SolidML/jsdoc/index.html に一通り書いてありますが、
- Github レポジトリ の src/SolidML.js と src/SolidMLBufferGeometry.js を呼び込む。
-
SolidML.BufferGeometry のコンストラクタに Eisen Script 文字列を渡して、インスタンスを作成する。
これでthree.js 標準の BufferGeometry と同じ感覚で利用することができます。
// コンストラクタに Solid ML Script を渡す
const geometry = new SolidML.BufferGeometry( "20{rx18y2s0.90}R#R@37{tube:6{x1rz10}R}" );
// 頂点カラーを有効にする
const material = new THREE.MeshPhysicalMaterial( { vertexColors: THREE.VertexColors } );
// Mesh 生成
const mesh = new Mesh( geometry, material );
構文
基本的には Eisen Script にシンタックスシュガーや追加コマンドを加えたものなので、Eisen Script はそのままでも動きます(Structure Synth の解説ページ)。ここでは SolidML の記法をベースに解説したいと思います。
(本家との比較は、README.mdを参照ください。)
1.プリミティブを生成する
box
box、shpere、line、grid、cylinder、cone、torus、tetra、octa、dodeca、icosa、triangle、mesh、tube、cmesh、ctube の予約語を書くとそれぞれに対応した大きさ1の形状を置くことができます。
line、cylinder、cone、torus など軸が存在する形状はx軸が基準となります。
shpere、line、cylinder、coneは、shpere:20とコロンの後に正数を指定する事でポリゴン分割数を指定することができます。line、gridはコロン後の数字で太さのパーセンテージを指定できます。grid:2で全体の2%の太さのグリッド形状を置きます。また、torus[20,8,32]と記述すると「直径の20%の太さで、断面方向分割数=8、経分割数=32」のトーラス形状を置きます。

script
2.変換&複製する
10{x2}sphere
予約語やルール名の前に {...}で変換方法を指定し、その前に変換回数を指定することで、指定回数分変換を繰り返しつつ、形状を置きます。上記スクリプトでは「10回 xを+2移動させつつ 球を配置する」ので、以下のような形状を生成します。
変換コマンドには、軸並行移動(x、y、z)、軸回転(rx、ry、rz)、拡大縮小(s)、3x3行列指定(m)、軸反転(fx、fy、fz)があります。本家コマンドと同じです。連続した変換は中かっこ内に逆順(行列をかける方向)で書きます。例えば「y軸中心に10度回転してx方向に2移動」は {x2ry10} と記述します。
10{x2ry10}box
変換を分けて連続で記述すると全ての組み合わせを実行します。以下の例では「x+2を5回」と「y+2を4回」で全変換をかけるため、全部で20個のcylinderが並びます。
5{x2}4{y2}cylinder
3.色操作
変換コマンドと同様に、色変換または色指定を記述することができます。本家と同様にSVG keyword names、cssと同様の16進数カラー表現、HSBによるカラー表、おぼびalpha指定が記述可能です。
{x0 red} box
{x2 navy} sphere
{x4 #ff00ff} grid
{x6 #0f0} cylinder
{x8 h60sat1b1} cone
{x10 a0.5} torus
h、sat、b、a の各コマンドはひとつ前のオブジェクト設置時の色からの変化分を記述します。色相は加算、それ以外は乗算されます。初期値は{hue:0, saturation:1, brightness:1}の赤です。
*コマンドは色のブレンディングを計算します。例えば*blue 0.1であれば、一個オブジェクトを設置するたびに0.1だけ青成分を混ぜます(本家blendコマンドのシンタックスシュガーです)。
また、彩度・明度のみのブレンドも可能です。*s0.8 0.1は、彩度0.8に向けて0.1だけ近づけます(本家では彩度明度は乗算しかできないため、目的値に向かって値を近づける方法が提供されていませんでした。)
{y0} 36{x1 h10} sphere
{y2} 36{x1 sat0.9} tetra
{y4} 36{x1 b0.9} box
{y6} 36{x1 a0.9} octa
{y8} 36{x1 *blue 0.1} dodeca
{y10sat0.01} 36{x1 *s0.8 0.1} icosa
#?コマンドを用いると、あらかじめ@cp[...](color pool コマンド)で指定した色のリストからランダムに色を採用します(本家color randomとset color pool list:のシンタックスシュガーです。)color pool は色リストのほかに @cp:h0(色相のみ乱数)、@cp:g0(グレイスケールの中から乱数)があります。無指定の場合、RGB3値とも乱数で決定します。本家のcolor poolと同等の機能実装で、仕様は開発者ブログに説明があります。
@cp[black,yellow,yellow]
10{x2#?} cone
4.メッシュ、チューブ
mesh、tube、cmesh、ctubeの各コマンドは、連続で呼び出されると変換の進む方向に向かってメッシュを作ります。meshはスケーリングが線の細さに影響しますが、tubeは一定の太さでメッシュを生成します。また、meshはフラットシェーディングによって角が現れるのに対してcmeshはスムースシェーディングのため側面が円筒状に見えます(断面は多角形です)。
これらのコマンドは、shpereと同様、コロンで区切って数字を指定することで、断面の正多角形数を指定することができます。連続しない別メッシュを作りたい場合は、ルールを分けて記述します。
100{x1ry10s0.99} mesh
5.ルール
#[rule名]{...} と記述することで、ひとかたまりの形状配置に対して名前を付けて、1つのプリミティブのように呼び出すことができます。基本的に本家のルール記述のシンタックスシュガーです。
{y4 red} BoxAndSphere
{y2 green} BoxAndSphere
{blue} BoxAndSphere
# BoxAndSphere{
5{x3}box
{x1.5}4{x3}sphere
}
同名のルールを定義すると、参照されるたびにランダムに選択されます。3つ以上でも同様です。
{y4 red} 9{x1.5} BoxOrSphere
{y2 green} 9{x1.5} BoxOrSphere
{blue} 9{x1.5} BoxOrSphere
# BoxOrSphere{
box
}
# BoxOrSphere{
sphere
}
上記ランダム選択時に、ルール名の後に@w[正数]を指定することでランダム選択に重み付けをすることができます。この例では、90%の確率で上、10%の確率で下のルールが選択されます。
9{x1.5}9{y1.5} BoxOrSpheres
# BoxOrSpheres@w9{
box
}
# BoxOrSpheres@w1{
8{z0.6}sphere
}
ランダム選択されるルールを部分的に差し替えたい場合、(...|...)と記述すると複数ルールを一度に定義できます。
@cp[red,blue,green,yellow]
9{x1.5}9{y1.5} shapes
# shapes{
{#?} (box|sphere|tetra|octa)
}
これもシンタックスシュガーの一種で、内部的にはコンパイル前に文字列処理によって以下のように展開されています(この展開結果は、コンソールにログとして出力されますので、デベロッパーツールで確認することができます。)。
@cp[red,blue,green,yellow]
9{x1.5}9{y1.5} shapes
# shapes @w1{
{#?} box
}
# shapes @w1{
{#?} sphere
}
# shapes @w1{
{#?} tetra
}
# shapes @w1{
{#?} octa
}
複数の分岐を記述した場合、全組み合わせが展開されます。沢山の分岐を含めると指数的にルール定義が増えてしまうので注意してください。それぞれの分岐に重み付けを行いたい場合は、分岐内に@wコマンドを含めます。
9{x1.5}9{y1.5} shapes
# shapes{
{white} (@w4box|@w3icosa)
{z1.2 s0.5} (@w9{black}sphere|{red}box)
}
内部的には、以下のように展開されます。
9{x1.5}9{y1.5} shapes
# shapes @w36{
{white} box
{z1.2 s0.5} {black}sphere
}
# shapes @w4{
{white} box
{z1.2 s0.5} {red}box
}
# shapes @w27{
{white} icosa
{z1.2 s0.5} {black}sphere
}
# shapes @w3{
{white} icosa
{z1.2 s0.5} {red}box
}
6.ルールの再帰呼び出し
(To Be Added)
6.システムコマンド
@1000 max depth。再帰呼び出しの最大回数
@mo10000 max object。オブジェクトの最大数
@s1234 乱数のシード値の指定。
@bg[#679,#fff,#eef] 背景の空、床、チェッカーボードの各色を指定。
@mat[10,90,30,20] 素材の metalness, roughness, clearcoat, clearcoat roughness を%で指定。
(To Be Added)
まとめ
(すいません。後半間に合わず、中途半端な解説でリリースしてしまいました。後日追記します。)
SolidML は、Struture Synth の Three.js への拡張実装です。URLをもちいて比較的簡単にシェアできますので、面白い形状ができたらSNSで拡散したり、Gist で書き捨てしたりしてくれると嬉しいです。
この技術はうまく使えば、例えばメカパーツをベースに生成することでシューティングゲームのボス生成とか、応用範囲が結構広いのではないかと考えています。もうちょっと色々展開できたらと思っています。













