やさいのつくりかた.

目次/第12章

12Recursion

くり返しが、形をつくる

recursion & self-similarity — 再帰と自己相似

この章の3行まとめ
  • ブロッコリーやロマネスコは、横顔を回しても(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);
    }
}
ロマネスコ.js の中心。自分自身を呼んでいるのが分かる

「何階層まで」で止めてはいけない

再帰でいちばん大事なのは、止める条件です。素直に考えると「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 は、末端だけ「面に粒を彫る」ことで 代用しています。ジオメトリは輪郭だけを担当し、細かい螺旋は面の凹凸に任せる。 作りたいものが大きすぎるときは、どこまでを本物の形で作り、 どこからを表面の細工で済ませるかを決めることになります。

被覆率という物差し。 親の面が子で埋まっているかどうかは、式で確かめられます。 親の側面積を子の底面積で割った被覆率 = k²·ρ /( S·(1−λ²) )。 これが1を切ると親の面がそのまま見えて、「つるつるの円錐に粒が点在する」絵になります。 子を小さくすれば実物に近づく、と単純には言えません。 小さくすると面を埋めるのに必要な個数が増えて、すぐ破綻します。 子の大きさと個数は、独立に決められません。

ブロッコリーは、順番が逆

同じ再帰でも、ブロッコリーは作り方が逆向きです。 素直に「頭の縄張りを3〜4個に分けて、それぞれをまた分けて……」とやると、 房と房のあいだに穴が空きます。3個に分けたとき、隣り合う子の中心は 100°以上離れてしまうからです。

そこでブロッコリー.js は、先に房の位置を全部決めます。 頭の表面にフィボナッチ格子で均等に並べ(これも13章の話です)、 そのあとで近いものどうしを k-means でまとめ直し、 まとまりの中心へ向かって枝を伸ばす。こうすれば、 表面が埋まっていることが最初から保証されます。

つまずきポイント

子の軸を、親の面の法線のままにしないこと。 面に垂直なまま生やすと、小塔が放射状に開いてウニになります。 実物はどの小塔も頭のてっぺんを向いて寝ているので、 法線から親の軸のほうへ大きく寄せます(tilt)。 上から見て星形・ヒトデ形になったら、たいていここが原因です。

やってみよう
  1. 「階層数で打ち切る」を入れて、末端の「いちばん大きい ÷ いちばん小さい」が何倍になるか読み取る。
  2. 打ち切る大きさを0.8mmまで下げて、円錐の総数が何個になるか見る。
  3. 子の大きさの比を0.30まで下げる。親の面が見えてくる(被覆率が1を切る)ことを確かめる。
  4. 身のまわりで自己相似な形を3つ探す(例:シダの葉、雷、木の枝)。