目次/第12章
くり返しが、形をつくる
recursion & self-similarity — 再帰と自己相似
- ブロッコリーやロマネスコは、横顔を回しても(04章)軸に沿わせても(10章)作れません。
- この3本の正体は同じ手順を、小さくしてくり返すことです。これを再帰といいます。
- むずかしいのは作り方ではなくどこで止めるか。止め方を外すと、粒がバラバラになるか個数が爆発します。
ここまでの章で作ってきた野菜は、どれも「1個のかたまり」でした。 横顔を回して作るか(04章)、曲がった軸に断面を並べて作るか(10章)。 ところがブロッコリー・カリフラワー・ロマネスコは、そのどちらでもありません。 これらは枝が分かれて、その先でまた枝が分かれてできた構造で、 形を1本の折れ線で言い表すことができません。
こういうものは、自分自身を呼び出す関数で作ります。 「円錐をひとつ描く。その表面に、少し小さい円錐をいくつか置く。 置いた円錐それぞれについて、また同じことをする」—— これだけで、あの複雑な形が出てきます。
ロマネスコの正体は、2行で言える
ロマネスコを「ブロッコリーの房を尖らせたもの」と考えると、絶対に似ません。 ロマネスコ.js の冒頭には、こう書いてあります。
- (a) 親の円錐の面のうえに、子を「頂点からの距離が等比数列 si = S·λi、 方位が137.5°ずつ」で置く
- (b) 子の大きさを、その距離に比例させる
この2つが揃うと自己相似になります。どこを拡大しても同じ形が出てくる、という性質です。 そして自己相似になったとたん、見る向きによって8本・13本・21本と数の変わる螺旋が 勝手に現れます。螺旋を線として描こうとしたり、子を等間隔に並べたりすると、 ただのイガグリで終わります(この螺旋の話は13章で)。
function growFractal(cfg, rng, out, base, axis, rho, H, level) { const L = cfg.levels[Math.min(level, cfg.levels.length - 1)]; // 打ち切りの判定。ここが章の本題 const leaf = (level >= cfg.maxLevel) || (L.coneK * rho * L.startFrac < cfg.minChild); ... for (let i = 0; i <= iMax; i++) { const f = L.startFrac * Math.pow(lam, i); // 頂点からの距離(等比) const th = th0 + i * 2.3999632; // 137.5° ずつ回す const cr = L.coneK * coneR(rho, ..., tt); // その場所の半径に見合う大きさ growFractal(cfg, rng, out, cb, ax, cr, cr * CL.aspect, level + 1); } }
「何階層まで」で止めてはいけない
再帰でいちばん大事なのは、止める条件です。素直に考えると「3階層まで潜る」と 決めたくなります。でもこれをやると失敗します。
頭の下側にある大きな小塔は、3階層では末端がまだ1cm近くあります。
つるつるした大きな瘤にしか見えません。逆に頂上付近の小さな小塔は、
3階層目がもう見えない大きさになっています。
実物のロマネスコは、大きい小塔ほど深く分岐していて、
末端の粒の絶対的な大きさがどこでも1〜2mmに揃っています。
だから打ち切りは階層数ではなく、minChild(cm単位の大きさ)で決めます。
実験 — どこで止めるか
「階層数で打ち切る」を入れると、末端の大きさがそろわなくなります。最後の行の「倍」を見てください。
本気で作ると1000万個になる
実験で「打ち切る粒の大きさ」を下げていくと、個数がすごい勢いで増えます。 これは計算してみると分かります。子は1階層あたり k 倍にしか小さくならないので、 4.6cmの頭から1mmの粒まで届くには、k = 0.5 でも log(0.001/0.046) / log(0.5) ≒ 5.5階層。 1階層あたり親の面を埋めるのに必要な個数を掛けると、末端は1000万個になります。 実物のロマネスコは、本当にその規模で分岐しています。
パソコンでは無理です。そこでロマネスコ.js は、末端だけ「面に粒を彫る」ことで 代用しています。ジオメトリは輪郭だけを担当し、細かい螺旋は面の凹凸に任せる。 作りたいものが大きすぎるときは、どこまでを本物の形で作り、 どこからを表面の細工で済ませるかを決めることになります。
ブロッコリーは、順番が逆
同じ再帰でも、ブロッコリーは作り方が逆向きです。 素直に「頭の縄張りを3〜4個に分けて、それぞれをまた分けて……」とやると、 房と房のあいだに穴が空きます。3個に分けたとき、隣り合う子の中心は 100°以上離れてしまうからです。
そこでブロッコリー.js は、先に房の位置を全部決めます。 頭の表面にフィボナッチ格子で均等に並べ(これも13章の話です)、 そのあとで近いものどうしを k-means でまとめ直し、 まとまりの中心へ向かって枝を伸ばす。こうすれば、 表面が埋まっていることが最初から保証されます。
子の軸を、親の面の法線のままにしないこと。
面に垂直なまま生やすと、小塔が放射状に開いてウニになります。
実物はどの小塔も頭のてっぺんを向いて寝ているので、
法線から親の軸のほうへ大きく寄せます(tilt)。
上から見て星形・ヒトデ形になったら、たいていここが原因です。
- 「階層数で打ち切る」を入れて、末端の「いちばん大きい ÷ いちばん小さい」が何倍になるか読み取る。
- 打ち切る大きさを0.8mmまで下げて、円錐の総数が何個になるか見る。
- 子の大きさの比を0.30まで下げる。親の面が見えてくる(被覆率が1を切る)ことを確かめる。
- 身のまわりで自己相似な形を3つ探す(例:シダの葉、雷、木の枝)。