木の枝ぶりは、芽どうしの光の取り合いで決まる

木は芽をたくさん作るが、その多くは長く残る枝にならない。どの芽を伸ばすかを、芽ごとの日当たりと幹から配られる養分だけで決めても、隣を避ける冠も切り口の下の脇芽も現れる。

記事

木の枝ぶりは、芽どうしの光の取り合いで決まる

木は芽をたくさん作るが、その多くは長く残る枝にならない。どの芽を伸ばすかを、芽ごとの日当たりと幹から配られる養分だけで決めても、隣を避ける冠も切り口の下の脇芽も現れる。

作成日 2026年11月4日・更新日 2026年11月4日・執筆・監修 佐々木 航

現象

芽の多くは、枝にならずに終わる

冬に葉を落とした木を見上げると、どの枝先にも芽が並んでいる。春になると、そのうちのいくつかが伸びて新しい枝になる。けれども温帯の木や低木は、芽をたくさん作るわりに、その多くを長く残る枝にしない1。眠ったまま終わる芽もあれば、伸びても数年で枯れ落ちる枝もある。

芽の並び方そのものは、木の種類によってそれほど違わない。植物学者のサックスとノボプランスキーは、枝のもとになる芽の配置が、オークでも柱サボテンでもヤシでもよく似ていると指摘している1。それでも、野原に1本で立つ木と林の中の木とでは、同じ種類でも枝ぶりがまるで変わる。

では、どの芽が伸び、どの枝が残るかは何で決まるのか。カナダのカルガリー大学のPalubickiらは2009年に、芽と枝が光や空間を取り合い、その取り合いを木の中の信号が調整する、という考えだけで木を育てるモデルを示した1。設計図にあたるものは無いのに、幹のまっすぐな木も、丸く茂る木も育つ。

画面の苗も、このモデルで1年ずつ育つ。太陽の取っ手をドラッグすると日の向きが変わり、枝か葉をタップするとそこから先が切れる。地面をタップすれば、新しい苗を植えられる。

仕組み

芽は、まわりのますにたまった影で日当たりを知る

Palubickiらのモデルは、木のまわりの空間を小さなますに分け、そこに影をためていく。芽は、自分のいるますと、その下のますに影を落とす(画面の木では、葉のある節が落とす)。1段下るごとに影は横へ1ますずつ広がり、薄くなっていく。影の形は、上下を逆さにした四角すいになる。

は影を落とす芽のますから下へ数えた段数で、その段のますにの影を足す。は影の濃さ、は薄まり方を決める数である。画面では、とし、6段下まで影を落とす。ますの一辺は0.35 mで、枝の1区切り(節)の15〜23 cmより長い。論文の計算では、ますを節とほぼ同じ長さにし、影を4〜8段下まで落としていた2。

芽のますにたまった影の合計をとすると、その芽の日当たりは次の式で決まる。

は、芽が自分の影を受けないようにするための項だ。上に葉が多い芽ほどが大きく、は0に近づく。木の内側の芽が暗くなり、外側の芽が明るいのはこのためである。

真上から光が来るとき (Palubickiらのモデル) 太陽の向きへ傾けた影 (画面の木) 丸印の芽は、q段下のますにa·b−qの影を足す。斜線が細かいますほど影が濃い
芽は、下のますへ四角すいの形に影を落とす。画面の木では、その四角すいを太陽の向きへ傾けている

元のモデルは真上からの光だけを考えているが、画面の木では四角すいを、太陽の向きと真上を半分ずつ混ぜた向きへ傾けた。建物と林の大きな木については、芽から太陽へ1本、空の7つの向きへ7本の線を引いて、さえぎられる光を数える。

仕組み

養分は明るい芽へ多く、主軸へ少し多く回る

日当たりが決まると、次は芽に配る養分である。Palubickiらが使ったのは、BorchertとHondaが1984年に示した配り方を、光で動くように変えたものだ3。まず枝先から根元へ、芽の日当たりを足し上げる。根元に集まった合計に比例する量の養分を、今度は根元から枝先へ配っていく。枝分かれのたびに、まっすぐ続く主軸と横枝へ、次の比で分ける。

は枝分かれに届いた養分、とは主軸の側と横枝の側にある芽の日当たりの合計だ。が0.5なら日当たりに比例して分け、0.5より大きいほど主軸がひいきされる。芽は受け取った養分の整数部分の数だけ節を作り、1に届かない芽は伸びない。暗い芽に養分が回らず眠ったままになるのは、この割り算の結果である。

が表しているのは、勢いよく伸びる上の枝が、脇の枝の伸びを抑える働きだ。身近な例が頂芽優勢で、茎の先の芽(頂芽)があるあいだは葉の付け根の芽(腋芽)が伸びず、頂芽を失うと伸び出す。抑える仕組みには、オーキシンやサイトカイニン、2008年に枝分かれを抑えるホルモンだと分かったストリゴラクトンが関わる4。一方で、頂芽を切ったあと最初に脇芽を動かすのは糖だという研究もある5。Palubickiらは、上の枝が脇の枝を抑えるのが養分の取り合いによるのか、ホルモンによるのか、その両方なのかは分かっていないと断り、この配り方ではひとつで表した1。

日の当たらない枝は、やがて枯れ落ちる。画面では、伸びてから3年たった横枝について、受ける光の合計をその枝の節の数で割り、0.09より小さければ落とす。国立環境研究所の竹中明夫が1994年に、木の形が育つ様子のシミュレーションで使った考えである6。

枝の向きも光で変わる。画面の芽は、まわりの影が薄くなる向きへ曲がりながら伸び、その強さを屈光性で決める。本物の芽生えでは、光が横から当たると、先端で作られて下へ運ばれるオーキシンが光の当たらない側に多くなり、その側がよく伸びるので、茎が光の方を向くと考えられている。光を感じるのが先端で、その知らせが下の曲がる所へ伝わることは、ダーウィン父子が1880年の本で示した7。

仕組み

太さは葉の数で、葉の向きは開度で決まる

レオナルド・ダ・ヴィンチは手帳に、木の枝をどの高さで合わせても幹と同じ太さになる、と書き残した8。枝分かれの前の直径を、後の2本の直径を、とすると、次の形に書ける。

なら、枝分かれの前後で断面積の合計が変わらない。実際に測ると、多くの樹種では1.8〜2.3のあいだに入る8。

日本の研究者は、別の筋道からこの規則に近い考えにたどり着いた。大阪市立大学の篠崎ら4人は1964年、ある高さより上にある葉の量が、その高さの幹と枝の断面積の合計に比例することを、さまざまな植物の群落で示した。幹と枝は、決まった量の葉を支える細い管(パイプ)の束で、枝が落ちて使われなくなった管も幹に残る、という考えだ9。パイプモデルと呼ばれ、Palubickiらは上の式をその一つの形として使っている1。画面の木の太さもこの考えで決めていて、節の直径の乗が、その先についた葉の数(これまででいちばん多かったとき)に比例する。

葉にも並び方の決まりがある。茎のまわりの葉の並び(葉序)にはいくつかの型があり、とくによく見られるのが、続く2枚の葉の開き(開度)が約137.5°のらせんだ10。この角は、円を黄金比で分けたときの小さいほうにあたり、黄金角と呼ばれる。

茎の先では、新しい葉のもとが、すでにある葉のもとからいちばん離れたすき間にできる、という古くからの考えがある。これを式にしたモデルでも、条件しだいで137.5°のらせんがひとりでに現れる10。

開度137.5°(黄金角) 開度180° 同じ大きさの葉12枚を、下の葉から順に重ねた。上の葉に隠れた所には日が当たらない
同じ大きさの葉12枚を、開度137.5°と180°で茎のまわりに並べ、真上から見た

「葉どうしが重ならず、光を受けやすいから137.5°になった」という説明もよく見かける。ところが、葉を光の来る向きから平面に写し、ほかの葉に隠れない面積の割合を計算したStraussらの結果は、少し意外だった11。137.5°の値は確かに大きいが、99.5°など、似た数列から生まれる別の角も同じくらい大きい。小さくなるのは、180°や120°のような簡単な分数で書ける角だった。Straussらは、光のために137.5°そのものが選ばれたとは考えにくいとみている。画面の「葉に日が当たる割合」も、同じやり方で数えている。

試す

苗を育て、太陽を動かし、枝を切る

操作パネルには、経った年数、木Aの高さと幹の直径、木Aのいちばん高い芽の日当たり(開けた所を100%とした値)が出る。最後の行は場面ごとに変わる。

  1. 野原の苗は、30年で高さ約14 m、幹の直径約27 cmの木に育つ。6年目ごろから、上の枝の影に入った下の枝が枯れ落ちはじめ、葉は木の上のほうに集まっていく。頂芽優勢を0.47に下げると苗から育て直され、幹が根元近くで何本にも分かれ、低い所にも葉の茂る木(30年で約13 m)ができる。0.56に上げれば、主軸がほかの枝を引き離して30年で24 mほどの細長い木だ。わずかな差でも、枝分かれのたびに分け方が積み重なるので、形はここまで変わる。
  2. 2.2 m離して植えた2本では、向かい合う側の枝が互いの影に入り、伸びずに枯れ落ちる。24年たつと、木Aの葉の真ん中は根元から約0.7 m、木Bと反対の側へずれる(冠の片寄り)。地面の矢印がその向きを指す。暗い側の芽に養分が回らず、明るい外側の芽ばかりが伸びるので、2本は互いを避けるように冠を広げる。
  3. 南に高さ5 mの建物があり、苗はその影の中から伸びはじめる。初めの12年ほど、てっぺんの芽の日当たりは40%に届かない。高さが4 mを超えて影から抜けると90〜100%になり、1年に伸びる高さも0.2〜0.3 mから約0.45 mに増える。冠は建物から離れる北の側へ寄り、24年で片寄りは約1.3 mになる。屈光性を0にして育て直すと、片寄りは0.35 mほどに縮む。冠を寄せていたのは、主に明るい向きへ曲がる性質だったことになる。
  4. 主軸の優勢が強い()同じ種の苗を2本並べ、6年目に木Bの高さ3.5 mから上を切る。切ってから2年のあいだに、切った高さから下1 mの範囲で伸び出した横枝は、木Aが5本、木Bが16本だ。木Bでは、主軸の先へ回っていた養分が、切り口のすぐ下の芽と枝へ回る。好きな枝をタップして切れば、別の場所でも同じことが起きる。
  5. 高さ7〜14 mに冠をもつ木が4 m間隔で並ぶ林の真ん中に、苗が1本ある。林の床の明るさは開けた所の約7%で、苗は12年たっても1.6 mほどにしかならない。苗のまわりの4本をタップして切ると、林の床の明るさは約14%に上がる。12年目に4本を切ると、30年目の苗は高さ約4 mになり、枝の節の数は切らなかったときの10倍を超える。
  6. 1年で伸びた若い茎を、ほぼ真上から見る。葉は22枚で、太陽は高さ80°にある。開度137.5°では葉に日が当たる割合が約55%だが、180°にすると葉が2列に重なって約33%、120°では3列になって約36%に下がる。99.5°に変えると約53%で、137.5°とほとんど変わらない。重なりを減らす決め手は、137.5°そのものより、簡単な分数の角から離れていることにある。
現実では

間伐で明るくなる林と、強く切られた街路樹

木どうしの光の取り合いは、林業でもはっきり表れる。林野庁は間伐を「過密となった林内密度を調整する作業」と説明する。間伐をしないと林の床が暗くなって下草が消え、土が流れやすくなる。間伐をすれば光が差し込み、幹や根が太く育つ12。

どれくらい明るくなるのかは、長崎県農林技術開発センターの調べで分かる。60か所の間伐地を2009〜2013年度に調べたところ、林の中の明るさ(開けた所と比べた割合、相対照度)は、間伐前の平均5.5%から、木の本数の30%を切った直後に12.6%へ上がった。明るさはその後また下がり、3年目の平均は4.4%だった。それでも、間伐のころ9.7%だった地面を覆う植物の割合は、5年目には78.6%まで増えている13。画面の林の約7%と約14%も、これに近い。

頂芽優勢は、街路樹の手入れでも見られる。埼玉県の街路樹剪定マニュアルは、太い枝を短く切り詰める強剪定を、木を弱らせ樹形を崩すもとになるとして、やむを得ない場合に限っている。強剪定のあとは、1年目に切り口から小枝が十数本出てくるので、将来伸ばす3〜4本を決めてほかを取り除く14。国土技術政策総合研究所も、強剪定のあとには芽から伸びる枝が大量に出て樹冠が乱れ、太い枝の切り口からは腐朽が入りやすいと指摘している15。

省いていること

画面の木が省いていること

  • 節1つは15〜23 cm、葉1枚は長さ20 cmで、本物より大きい。葉の板1枚が葉のかたまりを表す。1つの芽が1年に作る節は4つまで、節の数は全部で8,000か1万までとした。季節と落葉は描かず、葉は伸びてから2年のあいだと、頂芽のある枝先にだけつく
  • 光は、影のますの四角すいと、建物や林の木へ引いた8本の線だけで見積もる。四角すいは6段(2.1 m)までで、それより離れた枝どうしは影を落とし合わない。太陽は1日や1年の動きをならした位置に止まっているとみなし、光の半分は太陽から、残りは空全体から来るとした
  • 養分は日当たりから決まる1つの量として扱い、糖やホルモン、根からの水と肥料は計算しない。論文にない決まりも3つ足した。日当たりから養分への換算は強い光で頭打ちにし、苗の最初の3年は種の養分を足し、頂芽は1に届かない年の養分を翌年に回す。後の2つは、暗い林でも苗が少しずつ伸びるようにするためである
  • 枝は重さや風でたわまず、太さはパイプモデルだけで決まる。幹や枝の強さは計算していない
  • 林の大きな木は育たず、冠は葉が一様に散らばった楕円体として扱う。光は、冠の中を通った長さに応じて指数関数的に弱まるとした。林は計算の範囲の先も同じように続くとみなしている
参考文献

参考文献

  1. Palubicki, W., Horel, K., Longay, S., Runions, A., Lane, B., Měch, R., Prusinkiewicz, P.「Self-organizing tree models for image synthesis」『ACM Transactions on Graphics』28(3), 58:1–58:10, 2009. https://www.algorithmicbotany.org/papers/selforg.sig2009.html
  2. Shinozaki, K., Yoda, K., Hozumi, K., Kira, T.「A quantitative analysis of plant form: the pipe model theory. I. Basic analyses」『日本生態学会誌』14(3), 97–105, 日本生態学会, 1964. https://www.jstage.jst.go.jp/article/seitai/14/3/14_KJ00001775191/_article/-char/ja
  3. Yonekura, T., Iwamoto, A., Fujita, H., Sugiyama, M.「Mathematical model studies of the comprehensive generation of major and minor phyllotactic patterns in plants with a predominant focus on orixate phyllotaxis」『PLoS Computational Biology』15(6), e1007044, 2019. https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1007044
  4. Strauss, S., Lempe, J., Prusinkiewicz, P., Tsiantis, M., Smith, R. S.「Phyllotaxis: is the golden angle optimal for light capture?」『New Phytologist』225(1), 499–510, 2020. https://www.algorithmicbotany.org/papers/Strauss2019pit.pdf
  5. 勝見允行「屈性」『日本大百科全書(ニッポニカ)』小学館. https://kotobank.jp/word/%E5%B1%88%E6%80%A7-55559
  6. 長崎県農林技術開発センター 森林研究部門「間伐による林内の相対照度と植生被覆率の変化」2013年度 成果情報. https://www.pref.nagasaki.jp/e-nourin/nougi/theme/result/H25seika-jouhou/gyousei/G-25-09.pdf
  7. 埼玉県県土整備部道路環境課『街路樹剪定マニュアル』2020年12月. https://www.pref.saitama.lg.jp/documents/4144/gairozyusenntei1.pdf

注

  1. 参考文献1。サックスとノボプランスキーの指摘は、参考文献1がSachs, T., Novoplansky, A.「Tree form: Architectural models do not suffice」『Israel Journal of Plant Sciences』43(3), 203–212, 1995から引いたもの。上の枝が脇の枝を抑える仕組みが分かっていないという断りと、パイプモデルの式の形も参考文献1による。 ↩ ↩2 ↩3 ↩4 ↩5

  2. 参考文献1。 ↩

  3. Borchert, R., Honda, H.「Control of Development in the Bifurcating Branch System of Tabebuia rosea: A Computer Simulation」『Botanical Gazette』145(2), 184–195, 1984。光で動くように変えた形と、による分け方の式は参考文献1による。 ↩

  4. 日本植物生理学会の学術誌PCPの紹介記事「植物の腋芽成長を調節する重力応答」(2008年 Vol.49(6))と「ストリゴラクトン:植物ホルモンのニューフェース」(2009年 Vol.50(8))による。https://jspp.org/pcp/photo_gallery/back_number/year/detail.html?id=140 https://jspp.org/pcp/photo_gallery/back_number/year/detail.html?id=150 ↩

  5. Mason, M. G., Ross, J. J., Babst, B. A., Wienclaw, B. N., Beveridge, C. A.「Sugar demand, not auxin, is the initial regulator of apical dominance」『Proceedings of the National Academy of Sciences』111(16), 6092–6097, 2014。 ↩

  6. Takenaka, A.「A simulation model of tree architecture development based on growth response to local light environment」『Journal of Plant Research』107(3), 321–330, 1994。参考文献1が、枝を落とす決まりの出どころとして引いている。 ↩

  7. オーキシンの偏りは参考文献5による。先端で光を感じることは、Darwin, C.『The Power of Movement in Plants』(Francis Darwinが協力、1880年)の9章による。https://www.gutenberg.org/ebooks/5605 ↩

  8. 手帳の言葉は、Richter, J. P.訳『The Notebooks of Leonardo da Vinci』の394節による。https://www.gutenberg.org/ebooks/5000 指数の範囲は、Eloy, C.「Leonardo's Rule, Self-Similarity, and Wind-Induced Stresses in Trees」『Physical Review Letters』107(25), 258101, 2011による。https://arxiv.org/abs/1105.2591 ↩ ↩2

  9. 参考文献2。 ↩

  10. 参考文献3。137.5°のらせんがいちばん多いことは参考文献4による。 ↩ ↩2

  11. 参考文献4。参考文献4は、開度は向かい合わせに葉がつく場合を除けば、葉の形などほかの特徴ほど光の受け方を左右しないとした研究として、Valladares, F., Brites, D.「Leaf phyllotaxis: Does it really affect light capture?」『Plant Ecology』174(1), 11–17, 2004を挙げている。 ↩

  12. 林野庁「間伐とは?」2023年3月3日更新。https://www.rinya.maff.go.jp/j/kanbatu/suisin/kanbatu.html ↩

  13. 参考文献6。年ごとの値は、その年に調べた6〜20か所の平均。植生被覆率は、高さ0.5 m以下の草、0.5〜2 mの低木、それより高い木の3つの層の被覆率を合わせた値。 ↩

  14. 参考文献7。 ↩

  15. 国土技術政策総合研究所 環境研究部 緑化生態研究室「道路緑化における効果的・効率的な施工・管理手法に関する研究」(研究期間2010〜2012年度)。https://www.nilim.go.jp/lab/ddg/seika/nendopdf/1110.pdf ↩

プログラム
プログラム

このシミュレーションのプログラム

このシミュレーションは、1年を12の刻みに分け、「影をますにためる → 芽の日当たりを測る → 養分を配り、暗い枝を落とす → 芽が節を作る → 測る」をくり返している。立体の描画・影・視点はthree.jsが受け持ち、このファイルが計算するのは、枝の1区切り(節)ごとの位置と向きと太さである。描くときは、その結果を円柱と葉の板の行列にしてthree.jsに渡す。

長さはメートル、1年は12刻み

長さの単位はメートルで、時間は1年を12刻み(STEPS)に分けて進める。画面の速さを変えても刻みの長さは変わらないので、同じ場面からは必ず同じ木が育つ。新しい枝は9刻みかけて伸びきるように描く(GROW)。

定数と単位(src/sims/plant-growth.ts の 31〜60 行)
// 長さは m、時間は年。1 年 = 12 刻み(STEPS)。新しい枝は 9 刻みかけて伸びきるように描く(GROW)const STEPS = 12, GROW = 9;const SH_A = 0.1, SH_B = 1.5, QMAX = 6; // 影の四角すい: 自分のますの影 a、1 段下るごとに 1/b、6 段までconst DIRECT = 0.5; // 日の光のうち、太陽から直接来る割合(残りは空全体から)const ETA = 0.03; // 上へ向かう性質(負の重力屈性)の重みconst G0 = 0.15; // 影の勾配がこれより弱いときは、曲がり方も弱くするconst KA = 2; // 空がふさがれた偏りを、曲がる向きにどれだけ効かせるかconst K_SAT = 0.25; // 光合成が頭打ちになる日当たりの目安const KL = 0.3; // 大きな木の冠の中で光が弱まる割合(1 m あたり。葉面積密度 0.6 m²/m³ × 吸光係数 0.5)const STAND_R = 12, STAND_FILL = 0.55; // 林はこの半径の先も続くとみなす。その先は、冠が占める割合 0.55 の一様な茂みとするconst SHED_T = 0.09, SHED_AGE = 3; // 受ける光 ÷ 節の数 がこの値より小さい横枝を落とす。見るのは、伸びてから 3 年たった横枝だけconst CAP = [8000, 8000, 10000]; // 節の数の上限(画質 0・1・2)。場面の木は 30 年で 7,600 節ほどなので、どの画質でも同じ木が育つconst MAXTREES = 8;interface Look {  l0: number; cell: number; leafL: number; leafW: number; d0: number; rmin: number;  alpha: number; nmax: number; leafYears: number; budLife: number; half: number; top: number; shed: number; seed: number;}// 木の場面と、葉の並びを近くで見る場面で、大きさを変える// l0 節の基準の長さ・cell 影のます・leafL と leafW 葉の長さと幅・d0 葉 1 枚が支える管の直径・rmin 描くときの最も細い枝の半径・// alpha 根元の養分の係数 α・nmax 1 つの芽が 1 年に作る節の上限・leafYears 葉が残る年数・budLife 脇芽が眠ったまま生きる年数・// half と top 計算する範囲(水平の半分の幅と高さ)・shed 枝を落とす境目・seed 種の養分const TREE: Look = { l0: 0.15, cell: 0.35, leafL: 0.2, leafW: 0.11, d0: 0.004, rmin: 0.006, alpha: 2.5, nmax: 4, leafYears: 2, budLife: 4, half: 10, top: 30, shed: SHED_T, seed: 2 };const SHOOT: Look = { l0: 0.018, cell: 0.03, leafL: 0.085, leafW: 0.04, d0: 0.0012, rmin: 0.0022, alpha: 30, nmax: 21, leafYears: 99, budLife: 99, half: 0.6, top: 0.9, shed: 0, seed: 0 };const DEG = Math.PI / 180;// 空の 7 つの向き(高さ 35° の 6 方向と真上)const SKY: number[][] = [];for (let k = 0; k < 6; k++) { const az = (k / 6) * Math.PI * 2, el = 35 * DEG; SKY.push([Math.cos(el) * Math.cos(az), Math.sin(el), Math.cos(el) * Math.sin(az)]); }SKY.push([0, 1, 0]);const SKY_Y = (6 * Math.sin(35 * DEG) + 1) / 7; // 空の 7 つの向きを平均した向き(真上)の長さconst sunFrom = (elev: number, az: number): [number, number, number] => [Math.cos(elev * DEG) * Math.sin(az * DEG), Math.sin(elev * DEG), Math.cos(elev * DEG) * Math.cos(az * DEG)];

SH_A・SH_B・QMAXは本文の、と、影を落とす段数である。Lookは場面ごとの大きさで、木の場面では節の基準の長さl0を15 cm、影のますcellを35 cmにした。葉の並びの場面だけは、若い茎を近くで見るために、すべてを小さくした別の組(SHOOT)を使う。CAPは節の数の上限で、画質「高」では1万、ほかでは8,000にした。場面の木は30年で7,600節ほどなので、どの画質でも同じ木が育つ。

影をますにためる

castShadowsは、葉のある節から下へ6段、四角すいの形にますへ影を足す。段を1つ下るごとに、四角すいの中心をsx・szますだけ横へずらす。これが、影を光の来る向きへ傾ける仕組みで、本文の図の右側にあたる。

光と影の格子(src/sims/plant-growth.ts の 108〜203 行)
// 光の来る向き: 太陽の向きと真上を、直射と空の光の割合で混ぜるfunction lightDir(F: Forest, out: number[]) {  const s = F.sun, x = DIRECT * s[0], y = DIRECT * s[1] + (1 - DIRECT), z = DIRECT * s[2], n = Math.hypot(x, y, z);  out[0] = x / n; out[1] = y / n; out[2] = z / n;}const L3 = [0, 1, 0];// 葉のある節(from 〜 to 番)が、光の来る向きの反対側へ四角すいの影を落とすfunction castShadows(F: Forest, from: number, to: number) {  const { nx, ny, nz, grid } = F, c = F.look.cell;  lightDir(F, L3);  const sx = -L3[0] / L3[1], sz = -L3[2] / L3[1]; // 1 段下るごとに横へずれるます  for (let i = from; i < to; i++) {    if (!F.leaf[i]) continue;    const ex = F.px[i] + F.dx[i] * F.len[i], ey = F.py[i] + F.dy[i] * F.len[i], ez = F.pz[i] + F.dz[i] * F.len[i];    const I = Math.floor((ex - F.ox) / c), J = Math.floor(ey / c), K = Math.floor((ez - F.oz) / c);    let w = SH_A;    for (let q = 0; q <= QMAX && J - q >= 0; q++, w /= SH_B) {      const j = J - q; if (j >= ny) continue;      const ci = Math.round(I + q * sx), ck = Math.round(K + q * sz);      const i0 = Math.max(0, ci - q), i1 = Math.min(nx - 1, ci + q), k0 = Math.max(0, ck - q), k1 = Math.min(nz - 1, ck + q);      for (let k = k0; k <= k1; k++) { const row = (j * nz + k) * nx; for (let ii = i0; ii <= i1; ii++) grid[row + ii] += w; }    }  }}// 点 (x, y, z) のますの影。地面の下と建物の中は濃い影にして、枝が入らないようにするfunction shadeAt(F: Forest, x: number, y: number, z: number) {  const c = F.look.cell, i = Math.floor((x - F.ox) / c), j = Math.floor(y / c), k = Math.floor((z - F.oz) / c);  if (j < 0) return 9;  if (i < 0 || k < 0 || i >= F.nx || k >= F.nz || j >= F.ny) return 0;  const n = (j * F.nz + k) * F.nx + i;  return F.grid[n] + (F.wall[n] ? 2 : 0);}// 点から向き d へ進む光が、建物と大きな木の冠を通り抜けて届く割合function reach(F: Forest, x: number, y: number, z: number, d: number[]) {  for (const b of F.boxes) {    // x・y・z の向きごとに、線が箱の厚みの中にある区間を求めて重ねる。重なりが残れば線は箱を通る    let t0 = 0, t1 = 1e9;    const slab = (o: number, dd: number, lo: number, hi: number) => {      if (Math.abs(dd) < 1e-9) { if (o < lo || o > hi) t1 = -1; return; }      const ta = (lo - o) / dd, tb = (hi - o) / dd;      t0 = Math.max(t0, Math.min(ta, tb)); t1 = Math.min(t1, Math.max(ta, tb));    };    slab(x, d[0], b.x0, b.x1); slab(y, d[1], b.y0, b.y1); slab(z, d[2], b.z0, b.z1);    if (t1 > t0) return 0;  }  let T = 1, y0 = Infinity, y1 = -Infinity;  for (const cr of F.crowns) {    y0 = Math.min(y0, cr.y0); y1 = Math.max(y1, cr.y1);    if (!cr.alive) continue;    const cy = (cr.y0 + cr.y1) / 2, ry = (cr.y1 - cr.y0) / 2;    const ox = (x - cr.x) / cr.r, oy = (y - cy) / ry, oz = (z - cr.z) / cr.r;    const ddx = d[0] / cr.r, ddy = d[1] / ry, ddz = d[2] / cr.r;    const A = ddx * ddx + ddy * ddy + ddz * ddz, B = ox * ddx + oy * ddy + oz * ddz, C = ox * ox + oy * oy + oz * oz - 1;    const D = B * B - A * C; if (D <= 0) continue;    const sq = Math.sqrt(D), ta = Math.max(0, (-B - sq) / A), tb = (-B + sq) / A;    if (tb > ta) T *= Math.exp(-KL * (tb - ta));  }  // 林の外側に続く茂み: 光が冠の高さの層を、林の中心から STAND_R より外で通る長さ  if (F.crowns.length && d[1] > 1e-3) {    const ta = Math.max(0, (y0 - y) / d[1]), tb = (y1 - y) / d[1];    if (tb > ta) {      const h = Math.hypot(d[0], d[2]), rx = x, rz = z; // 水平に進む向きで、半径 STAND_R の円を出る点      const b = (rx * d[0] + rz * d[2]) / Math.max(h * h, 1e-9), c = (rx * rx + rz * rz - STAND_R * STAND_R) / Math.max(h * h, 1e-9);      const tOut = h > 1e-6 ? -b + Math.sqrt(Math.max(0, b * b - c)) : Infinity;      const L = Math.max(0, tb - Math.max(ta, tOut));      if (L > 0) T *= Math.exp(-KL * STAND_FILL * L);    }  }  return T;}// 建物と大きな木を通して届く光(0〜1)。out には、空のどちら側がふさがれているかの偏り// (届いた光で重みをつけた向きの和 − どの向きも同じだけ弱まったときの和)を入れる。開けた所では 0function skyLight(F: Forest, x: number, y: number, z: number, out: number[]) {  out[0] = 0; out[1] = 0; out[2] = 0;  if (!F.boxes.length && !F.crowns.length) return 1;  const ts = reach(F, x, y, z, F.sun) * DIRECT;  let T = ts, lx = ts * F.sun[0], ly = ts * F.sun[1], lz = ts * F.sun[2];  for (const d of SKY) { const t = (reach(F, x, y, z, d) * (1 - DIRECT)) / SKY.length; T += t; lx += t * d[0]; ly += t * d[1]; lz += t * d[2]; }  out[0] = lx - T * DIRECT * F.sun[0]; out[1] = ly - T * (DIRECT * F.sun[1] + (1 - DIRECT) * SKY_Y); out[2] = lz - T * DIRECT * F.sun[2];  return T;}// 芽の日当たり Q(0〜1)。影の格子の分と、建物や大きな木の分を掛けるfunction budLight(F: Forest, x: number, y: number, z: number, out: number[]) {  const q = Math.min(1, Math.max(0, 1 + SH_A - shadeAt(F, x, y, z)));  return q * skyLight(F, x, y, z, out);}const O3 = [0, 0, 0];// 頂芽と、眠っている脇芽の日当たりを測る(from 〜 to 番の節)function measure(F: Forest, from: number, to: number) {  for (let i = from; i < Math.min(to, F.m); i++) {    const ex = F.px[i] + F.dx[i] * F.len[i], ey = F.py[i] + F.dy[i] * F.len[i], ez = F.pz[i] + F.dz[i] * F.len[i];    F.tq[i] = 0; F.bq[i] = 0;    if (F.tip[i]) { F.tq[i] = budLight(F, ex, ey, ez, O3); F.lx[i] = O3[0]; F.ly[i] = O3[1]; F.lz[i] = O3[2]; }    if (F.bud[i]) { F.bq[i] = budLight(F, ex + 0.1 * F.rx[i], ey + 0.1 * F.ry[i], ez + 0.1 * F.rz[i], O3); if (!F.tip[i]) { F.lx[i] = O3[0]; F.ly[i] = O3[1]; F.lz[i] = O3[2]; } }  }}

芽の日当たりはbudLightで、本文の式をそのまま計算し、建物や林の木でさえぎられる分を掛ける。建物と林の大きな木は、ますではなく線で調べる。reachは、芽から向きdへ引いた線が箱(建物)に当たるかを、x・y・zの3つの向きで線が箱に入る区間と出る区間を重ねて確かめる。楕円体(林の木の冠)は、座標を冠の大きさで割って球に直し、線と球の交わりの式から、冠の中を通る長さを出す。skyLightはこれを太陽と空の7つの向きで行い、届いた光の合計と、空のどちら側がふさがれているかの偏りを返す。

根元へ集めて、枝先へ配る

節はすべて、親が子より前に来る順で配列に入っている。そのためallocateは、配列を後ろから前へ1回たどるだけで、枝先の日当たりを根元へ足し上げられる。続けて前から後ろへ1回たどると、根元の養分を枝先へ配れる。

養分の配り方(src/sims/plant-growth.ts の 205〜251 行)
// Borchert と Honda の方法。1. 先から根元へ、日当たりを足し上げる 2. 根元から先へ、養分を配るfunction allocate(F: Forest, g: Gp) {  const m = F.m, lam = g.lambda;  const P = (q: number) => (q > 0 ? (q * (1 + K_SAT)) / (q + K_SAT) : 0);  for (let i = 0; i < m; i++) { F.qm[i] = P(F.tq[i]); F.ql[i] = P(F.bq[i]); F.cnt[i] = 1; }  for (let i = m - 1; i > 0; i--) {    const p = F.par[i]; if (p < 0) continue;    const q = F.qm[i] + F.ql[i];    if (F.lat[i]) F.ql[p] += q; else F.qm[p] += q;    F.cnt[p] += F.cnt[i];  }  for (let i = 0; i < m; i++) {    const p = F.par[i];    // 根元: v = α ΣP(Q)。苗の最初の 3 年は種の養分を足す    const vin = p < 0 ? F.look.alpha * (F.qm[i] + F.ql[i]) + (F.step - F.born[i] < 3 * STEPS + GROW ? F.look.seed : 0) : F.lat[i] ? F.vl[p] : F.vm[p];    const den = lam * F.qm[i] + (1 - lam) * F.ql[i];    F.vm[i] = den > 0 ? (vin * lam * F.qm[i]) / den : 0; // 主軸(頂芽の側)へ    F.vl[i] = den > 0 ? vin - F.vm[i] : 0; // 横枝(脇芽の側)へ  }}// 受ける光 ÷ 節の数 が小さい横枝(伸びてから 3 年以上たったもの)を落とす。落とした数を返すfunction shed(F: Forest) {  if (F.look.shed <= 0) return 0;  let n = 0;  for (let i = 0; i < F.m; i++) {    F.kill[i] = 0;    const p = F.par[i];    if (p >= 0 && F.kill[p]) { F.kill[i] = 1; continue; }    if (p >= 0 && F.lat[i] && F.step - F.born[i] > STEPS * SHED_AGE && (F.qm[i] + F.ql[i]) / F.cnt[i] < F.look.shed) { F.kill[i] = 1; n++; }  }  if (n) compact(F);  return n;}// kill の付いた節を消して詰める(親が子より前にある並びは保つ)function compact(F: Forest) {  const map = new Int32Array(F.m); let j = 0;  const arrs = [F.tree, F.lat, F.px, F.py, F.pz, F.dx, F.dy, F.dz, F.len, F.rx, F.ry, F.rz, F.born, F.tip, F.bud, F.budAge, F.rt, F.pipes, F.leaf, F.vm, F.vl, F.tq, F.bq, F.lx, F.ly, F.lz];  for (let i = 0; i < F.m; i++) {    if (F.kill[i]) { map[i] = -1; continue; }    map[i] = j;    if (j !== i) for (const a of arrs) a[j] = a[i];    F.par[j] = F.par[i] >= 0 ? map[F.par[i]] : -1;    j++;  }  for (const t of F.trees) t.root = t.root >= 0 ? map[t.root] : -1;  F.m = j;}

P(q)は日当たりを養分の元にするときの換算で、日当たりが強くなると頭打ちになる。葉の光合成が強い光で飽和することを、簡単な分数の形で表した。主軸と横枝への分け方は、本文のとの式のとおりである。shedは、伸びてから3年たった横枝ごとに「受ける光 ÷ 節の数」を調べ、0.09より小さい枝とその先を消す。消したあとはcompactで配列を詰め、親が子より前という順を保つ。

芽が節を作り、向きを決める

addShootは、養分vを受け取った芽から、整数部分nの数だけ節を作る。1年に作る節は4つ(nmax)まで、1節の長さはで、1.5倍を上限にした。節の数が上限に届いているあいだは、growが芽を使わずに残し、枝が落ちて空きができた年に伸ばす。

芽が伸びる(src/sims/plant-growth.ts の 253〜328 行)
// 芽(from 番の節の先)が、養分 v で新しい枝を伸ばす。lateral なら脇芽から、そうでなければ頂芽からfunction addShoot(F: Forest, g: Gp, from: number, v: number, lateral: boolean, tr: Tree, limit: number) {  const look = F.look, c = look.cell;  const n = Math.min(look.nmax, Math.floor(v)); if (n < 1) return;  const l = Math.min(1.5, v / n) * look.l0; // 1 節の長さ ℓ0·v/n(1.5 倍まで)  let x = F.px[from] + F.dx[from] * F.len[from], y = F.py[from] + F.dy[from] * F.len[from], z = F.pz[from] + F.dz[from] * F.len[from];  let dx: number, dy: number, dz: number, rx = F.rx[from], ry = F.ry[from], rz = F.rz[from];  const cb = Math.cos(g.beta), sb = Math.sin(g.beta);  if (lateral) { // 脇芽の向き: 茎から枝の角度 β だけ開く    dx = cb * F.dx[from] + sb * rx; dy = cb * F.dy[from] + sb * ry; dz = cb * F.dz[from] + sb * rz;    rx = F.dx[from]; ry = F.dy[from]; rz = F.dz[from];  } else { dx = F.dx[from]; dy = F.dy[from]; dz = F.dz[from]; }  const Lx = F.lx[from], Ly = F.ly[from], Lz = F.lz[from];  let parent = from;  for (let k = 0; k < n && F.m < limit; k++) {    // 影の薄い向き(影の格子の勾配の逆)に、空がふさがれた偏りの逆(開けた側)を足す    const gx = shadeAt(F, x - c, y, z) - shadeAt(F, x + c, y, z), gy = shadeAt(F, x, y - c, z) - shadeAt(F, x, y + c, z), gz = shadeAt(F, x, y, z - c) - shadeAt(F, x, y, z + c);    const gk = 1 / Math.max(Math.hypot(gx, gy, gz), G0);    let vx = gx * gk + KA * Lx, vy = gy * gk + KA * Ly, vz = gz * gk + KA * Lz;    const vn = Math.hypot(vx, vy, vz); if (vn > 1) { vx /= vn; vy /= vn; vz /= vn; }    const jit = 0.06; // 芽ごとの小さなばらつき    dx += g.xi * vx + (tr.rng() - 0.5) * jit;    dy += g.xi * vy + ETA + (tr.rng() - 0.5) * jit;    dz += g.xi * vz + (tr.rng() - 0.5) * jit;    let dn = Math.hypot(dx, dy, dz) || 1; dx /= dn; dy /= dn; dz /= dn;    if (y + dy * l < 0.05) { dy = Math.max(dy, 0.2); dn = Math.hypot(dx, dy, dz); dx /= dn; dy /= dn; dz /= dn; }    const ex = x + dx * l, ey = y + dy * l, ez = z + dz * l;    let blocked = false; // 建物の中へは伸びない    for (const b of F.boxes) if (ex > b.x0 - 0.05 && ex < b.x1 + 0.05 && ey < b.y1 + 0.05 && ez > b.z0 - 0.05 && ez < b.z1 + 0.05) blocked = true;    if (blocked) break;    // 葉と脇芽の向き: 前の節の向きを、新しい茎に直角な面へ写してから、茎のまわりに開度 δ だけ回す    const dot = rx * dx + ry * dy + rz * dz; rx -= dot * dx; ry -= dot * dy; rz -= dot * dz;    let rn = Math.hypot(rx, ry, rz);    if (rn < 1e-6) { rx = -dz; ry = 0; rz = dx; rn = Math.hypot(rx, ry, rz) || 1; if (rn < 1e-6) { rx = 1; rz = 0; rn = 1; } }    rx /= rn; ry /= rn; rz /= rn;    const a = g.div + (tr.rng() - 0.5) * 0.03, ca = Math.cos(a), sa = Math.sin(a);    const ux = dy * rz - dz * ry, uy = dz * rx - dx * rz, uz = dx * ry - dy * rx;    const nrx = ca * rx + sa * ux, nry = ca * ry + sa * uy, nrz = ca * rz + sa * uz;    rx = nrx; ry = nry; rz = nrz;    const i = F.m++;    F.par[i] = parent; F.tree[i] = F.tree[from]; F.lat[i] = k === 0 && lateral ? 1 : 0;    F.px[i] = x; F.py[i] = y; F.pz[i] = z; F.dx[i] = dx; F.dy[i] = dy; F.dz[i] = dz; F.len[i] = l;    F.rx[i] = rx; F.ry[i] = ry; F.rz[i] = rz; F.born[i] = F.step;    F.tip[i] = 0; F.bud[i] = 1; F.budAge[i] = 0; F.pipes[i] = 0; F.leaf[i] = 1; F.tq[i] = 0; F.bq[i] = 0;    F.rt[i] = 0;    F.lx[i] = Lx; F.ly[i] = Ly; F.lz[i] = Lz;    parent = i; x = ex; y = ey; z = ez;  }  if (parent !== from) F.tip[parent] = 1; // 新しい枝の先に頂芽}// 養分が 1 以上届いた芽が伸びる。頂芽は、1 に届かない年の養分をためて次の年に回す(暗い所でも少しずつ伸びる)。// 脇芽はためない(1 に届かなければ眠ったまま)。そのあと、眠った脇芽が 1 年ぶん年をとる。// 節の数が上限に届いているときは、芽を使わずに残す(枝が落ちて空きができたら、次の年に伸びる)function grow(F: Forest, g: Gp, limit: number) {  const m0 = F.m;  for (let i = 0; i < m0; i++) {    const tr = F.trees[F.tree[i]];    if (F.tip[i]) { const v = F.vm[i] + F.rt[i]; if (v >= 1 && F.m < limit) { F.tip[i] = 0; F.rt[i] = 0; addShoot(F, g, i, v, false, tr, limit); } else F.rt[i] = Math.min(v, 1); }    if (F.bud[i] && F.vl[i] >= 1 && F.m < limit) { F.bud[i] = 0; addShoot(F, g, i, F.vl[i], true, tr, limit); }  }  for (let i = 0; i < m0; i++) if (F.bud[i] && ++F.budAge[i] > F.look.budLife) F.bud[i] = 0;}// 葉は若い節と頂芽のある節にだけ残す。パイプモデル: 節が支える葉の数を根元へ足し上げ、いちばん多かった数を覚えるfunction leavesAndPipes(F: Forest) {  const ly = F.look.leafYears * STEPS;  for (let i = 0; i < F.m; i++) { F.leaf[i] = F.tip[i] || F.step - F.born[i] < ly ? 1 : 0; F.cnt[i] = F.leaf[i]; }  for (let i = F.m - 1; i > 0; i--) { const p = F.par[i]; if (p >= 0) F.cnt[p] += F.cnt[i]; }  for (let i = 0; i < F.m; i++) if (F.cnt[i] > F.pipes[i]) F.pipes[i] = F.cnt[i];}// 枝 i とその先をすべて切るfunction cutAt(F: Forest, i0: number) {  for (let i = 0; i < F.m; i++) { const p = F.par[i]; F.kill[i] = i === i0 || (p >= 0 && F.kill[p]) ? 1 : 0; }  let n = 0; for (let i = 0; i < F.m; i++) n += F.kill[i];  compact(F);  return n;}

節の向きは、前の節の向きに、影の薄い向き・開けた空の向き・上向きを足して決める。影の薄い向きは、まわりのますの影を左右・上下・前後で比べた差(影の勾配)の逆向きである。勾配が弱いときは足す量も小さくなるよう、G0より短い勾配はG0で割る。開けた空の向きは、建物や林で空の一部がふさがれているときだけ0でなくなる。この2つの和に屈光性を掛け、上向きのETAと、芽ごとの小さなばらつきを加える。

葉と脇芽の向きは、茎に直角な向きrで持つ。新しい節では、前のrを新しい茎に直角な面へ写してから、茎のまわりに開度だけ回す。回すときは、rと「茎の向き × r」の2本を、とで混ぜる。脇芽は、茎から枝の角度だけrの側へ開いた向きに伸びる。

leavesAndPipesはパイプモデルの計算で、節ごとに、その先にある葉の数を根元へ足し上げ、これまでで最も多かった数をpipesに残す。描くときの直径はpipesで、は「太さの指数」のつまみである。

重い計算を1年の刻みに分ける

影をますにためる計算は、節の数×四角すいのます(最大455)だけ足し算をする。1万節近い木では、これを1コマで行うと重い。そこでdoStepは、刻み0〜3で影を4分の1ずつ、刻み4と5で日当たりを半分ずつ、刻み6で養分と枝落とし、刻み7で成長、刻み8で測る、と仕事を分けている。

1年の進め方(src/sims/plant-growth.ts の 365〜497 行)
// 1 刻みぶん進める。重い計算は 1 年のうちの別々の刻みに分けて、1 コマが重くなりすぎないようにする//   刻み 0〜3 影の格子(4 分の 1 ずつ) / 4・5 芽の日当たり(半分ずつ) / 6 養分を配り、日の当たらない枝を落とす / 7 芽が伸びる / 8 測るfunction doStep(api: Api, s: S) {  const F = s.F, k = F.step % STEPS, g = gpOf(api);  if (F.year < s.R.stop) {    if (k <= 3) {      if (k === 0) { F.grid.fill(0); s.castM = F.m; }      const m = Math.min(s.castM, F.m);      castShadows(F, Math.floor((m * k) / 4), Math.floor((m * (k + 1)) / 4));    } else if (k === 4) measure(F, 0, s.castM >> 1);    else if (k === 5) { measure(F, s.castM >> 1, F.m); tipLight(s); }    else if (k === 6) {      allocate(F, g);      if (shed(F)) { s.dirty = true; onceNote(api, s, "shed", "日の当たらない枝が、枯れて落ちはじめた"); }    } else if (k === 7) {      const m0 = F.m;      grow(F, g, s.limit);      // 切ってから 2 年のあいだに、切った高さから 1 m 下までの節の脇芽から伸び出した横枝を数える(木 A・木 B)      if (s.cutStep >= 0 && F.step - s.cutStep <= 2 * STEPS) for (let i = m0; i < F.m; i++) if (F.lat[i] && F.tree[i] < 2 && F.py[i] < s.cutH + 0.01 && F.py[i] > s.cutH - 1) s.sprout[F.tree[i]]++;      F.year++;      leavesAndPipes(F);      if (F.m >= s.limit) onceNote(api, s, "cap", "節の数が上限に届いた。枝が落ちて空きができるまで、新しい枝は伸びない");      const c = s.R.cut;      if (c && F.year === c.year) cutLeader(api, s, c.tree, c.frac);      if (F.year >= s.R.stop) onceNote(api, s, "stop", s.R.look === SHOOT ? "1年分の茎ができた。開度を変えると、すぐに育て直す" : `${s.R.stop}年たったので成長を止めた。場面を選び直すと苗からやり直せる`);      s.dirty = true;    }  }  if (k === 8 && s.statYear !== F.year) { s.statYear = F.year; measureStats(s); }  F.step++;}// 木 t の主軸を、高さの frac の所で切るfunction cutLeader(api: Api, s: S, t: number, frac: number) {  const F = s.F, tr = F.trees[t]; if (!tr || tr.root < 0) return;  const main = new Int32Array(F.m).fill(-1);  for (let i = 0; i < F.m; i++) if (F.par[i] >= 0 && !F.lat[i]) main[F.par[i]] = i;  const axis: number[] = []; for (let i = tr.root; i >= 0; i = main[i]) axis.push(i);  const top = axis[axis.length - 1], h = F.py[top] + F.dy[top] * F.len[top], at = h * frac;  const i0 = axis.find((i) => F.py[i] >= at);  if (i0 === undefined) return;  s.cutStep = F.step; s.cutH = F.py[i0];  cutAt(F, i0); s.dirty = true;  api.note(`木${String.fromCharCode(65 + t)}の先端を、高さ ${fmt(s.cutH, 1)} m から上で切った`, 3000);}// 葉に日が当たる割合: 葉を太陽の向きから見た平面に写し、重なりを除いた面積 ÷ 葉の面積の合計(画素で数える)const CAPN = 256, capBuf = new Uint8Array(CAPN * CAPN);const LF = new Float32Array(12);// 節 i の葉の付け根・葉の向き b・葉の面の向き nfunction leafFrame(F: Forest, i: number, out: Float32Array) {  let hx = F.rx[i], hz = F.rz[i], hn = Math.hypot(hx, hz);  if (hn < 0.3) { hx = F.dx[i]; hz = F.dz[i]; hn = Math.hypot(hx, hz); if (hn < 1e-6) { hx = 1; hz = 0; hn = 1; } }  hx /= hn; hz /= hn;  let bx = hx, by = 0.25, bz = hz; const bn = Math.hypot(bx, by, bz); bx /= bn; by /= bn; bz /= bn; // 少し上を向いた葉  let nx = -by * bx, ny = 1 - by * by, nz = -by * bz; const nn = Math.hypot(nx, ny, nz); nx /= nn; ny /= nn; nz /= nn;  const L = F.look.leafL, ex = F.px[i] + F.dx[i] * F.len[i], ey = F.py[i] + F.dy[i] * F.len[i], ez = F.pz[i] + F.dz[i] * F.len[i];  out[0] = ex + bx * 0.1 * L; out[1] = ey + by * 0.1 * L; out[2] = ez + bz * 0.1 * L;  out[3] = bx; out[4] = by; out[5] = bz; out[6] = nx; out[7] = ny; out[8] = nz;  out[9] = by * nz - bz * ny; out[10] = bz * nx - bx * nz; out[11] = bx * ny - by * nx; // 葉の幅の向き b × n}const CU = new Float32Array(4), CW = new Float32Array(4);let capPts = new Float32Array(0);function lightCapture(F: Forest) {  const sd = F.sun, L = F.look.leafL, W = F.look.leafW;  let ux = -sd[2], uz = sd[0]; const un = Math.hypot(ux, uz); if (un < 1e-6) { ux = 1; uz = 0; } else { ux /= un; uz /= un; }  const wx = sd[1] * uz, wy = sd[2] * ux - sd[0] * uz, wz = -sd[1] * ux; // w = 太陽の向き × u  if (capPts.length < F.m * 8) capPts = new Float32Array(F.cap * 8);  const P = capPts, A = [0, 0.45 * L, L, 0.45 * L], B = [0, 0.5 * W, 0, -0.5 * W];  let n = 0, u0 = Infinity, u1 = -Infinity, w0 = Infinity, w1 = -Infinity;  for (let i = 0; i < F.m; i++) {    if (!F.leaf[i]) continue;    leafFrame(F, i, LF);    for (let c = 0; c < 4; c++) {      const x = LF[0] + LF[3] * A[c] + LF[9] * B[c], y = LF[1] + LF[4] * A[c] + LF[10] * B[c], z = LF[2] + LF[5] * A[c] + LF[11] * B[c];      const pu = x * ux + z * uz, pw = x * wx + y * wy + z * wz;      P[n * 8 + c * 2] = pu; P[n * 8 + c * 2 + 1] = pw;      if (pu < u0) u0 = pu; if (pu > u1) u1 = pu; if (pw < w0) w0 = pw; if (pw > w1) w1 = pw;    }    n++;  }  if (!n) return NaN;  const sc = (CAPN - 2) / Math.max(1e-6, u1 - u0, w1 - w0);  capBuf.fill(0);  let total = 0, union = 0;  for (let k = 0; k < n; k++) {    let a0 = CAPN, a1 = 0, b0 = CAPN, b1 = 0;    for (let c = 0; c < 4; c++) {      const X = (P[k * 8 + c * 2] - u0) * sc, Y = (P[k * 8 + c * 2 + 1] - w0) * sc; CU[c] = X; CW[c] = Y;      if (X < a0) a0 = X; if (X > a1) a1 = X; if (Y < b0) b0 = Y; if (Y > b1) b1 = Y;    }    const i0 = Math.max(0, Math.floor(a0)), i1 = Math.min(CAPN - 1, Math.ceil(a1)), j0 = Math.max(0, Math.floor(b0)), j1 = Math.min(CAPN - 1, Math.ceil(b1));    for (let py = j0; py <= j1; py++) { // 1 行ずつ、葉の四角形が覆う区間を塗る      const cy = py + 0.5; let xa = Infinity, xb = -Infinity;      for (let e = 0; e < 4; e++) {        const f = (e + 1) & 3, ya = CW[e], yb = CW[f];        if (cy < ya === cy < yb) continue;        const x = CU[e] + ((cy - ya) / (yb - ya)) * (CU[f] - CU[e]);        if (x < xa) xa = x; if (x > xb) xb = x;      }      const p0 = Math.max(i0, Math.ceil(xa - 0.5)), p1 = Math.min(i1, Math.floor(xb - 0.5));      if (p1 < p0) continue;      total += p1 - p0 + 1;      for (let q = py * CAPN + p0, qe = py * CAPN + p1; q <= qe; q++) if (!capBuf[q]) { capBuf[q] = 1; union++; }    }  }  return total ? union / total : NaN;}// 木 A のてっぺんの芽(いちばん高い頂芽)の日当たり。伸びる前に測るfunction tipLight(s: S) {  const F = s.F; let q = NaN, h = -1;  for (let i = 0; i < F.m; i++) if (F.tree[i] === 0 && F.tip[i]) { const y = F.py[i] + F.dy[i] * F.len[i]; if (y > h) { h = y; q = F.tq[i]; } }  s.st.tipQ = q;}// 木 A の高さ・幹の直径と、場面ごとの値function measureStats(s: S) {  const F = s.F, st = s.st, n = s.pipeN;  const t0 = F.trees[0];  st.h = NaN; st.d = NaN; st.cx = NaN; st.cz = NaN;  if (t0 && t0.root >= 0) {    let h = 0, lx = 0, lz = 0, nl = 0;    for (let i = 0; i < F.m; i++) {      if (F.tree[i] !== 0) continue;      h = Math.max(h, F.py[i] + F.dy[i] * F.len[i]);      if (F.leaf[i]) { lx += F.px[i]; lz += F.pz[i]; nl++; }    }    st.h = h; st.d = F.look.d0 * Math.pow(F.pipes[t0.root], 1 / n);    if (nl) { st.cx = lx / nl; st.cz = lz / nl; }  }  const ex = s.R.extra;  if (ex === "capture") st.extra = lightCapture(F);  else if (ex === "shift") st.extra = t0 && t0.root >= 0 ? Math.hypot(st.cx - t0.x, st.cz - t0.z) : NaN;  else if (ex === "floor") st.extra = t0 ? skyLight(F, t0.x, 0.3, t0.z, O3) : NaN;  else if (ex === "sprout") { st.extra = s.sprout[0]; st.extra2 = s.sprout[1]; }}

「葉に日が当たる割合」はlightCaptureで数える。葉の四角形を太陽の向きから見た平面に写し、256×256の画素に1行ずつ塗る。どれかの葉が塗った画素の数(重なりを除いた面積)を、葉ごとに数えた画素の合計で割った値である。本文で紹介したStraussらの計算と同じ考え方だ。

three.jsに渡すのは行列だけ

枝の円柱と葉の板は、それぞれ1つのInstancedMesh(同じ形をたくさん並べる部品)で描く。節1つにつき行列を1つ書き込むだけなので、1万本の枝でも形を作り直さずに済む。

描く(src/sims/plant-growth.ts の 521〜637 行)
// three.js が受け持つのは、立体(枝の円柱・葉の板・地面・建物)と影と視点。ここでは、各節の円柱と葉の板の置き場所(行列)を書き込むconst put = (a: Float32Array, k: number, X: number[], Y: number[], Z: number[], P: number[]) => {  const o = k * 16;  a[o] = X[0]; a[o + 1] = X[1]; a[o + 2] = X[2]; a[o + 3] = 0;  a[o + 4] = Y[0]; a[o + 5] = Y[1]; a[o + 6] = Y[2]; a[o + 7] = 0;  a[o + 8] = Z[0]; a[o + 9] = Z[1]; a[o + 10] = Z[2]; a[o + 11] = 0;  a[o + 12] = P[0]; a[o + 13] = P[1]; a[o + 14] = P[2]; a[o + 15] = 1;};const VX = [0, 0, 0], VY = [0, 0, 0], VZ = [0, 0, 0], VP = [0, 0, 0];// 節 i を描く。f は伸びた割合(0〜1)。根元は親の描いた先端につなぐfunction drawNode(s: S, api: Api, i: number, now: number) {  const F = s.F, look = F.look, p = F.par[i];  const f = clamp((now - F.born[i]) / GROW, 0, 1);  const x = p >= 0 ? s.ex[p] : F.px[i], y = p >= 0 ? s.ey[p] : F.py[i], z = p >= 0 ? s.ez[p] : F.pz[i];  const L = Math.max(1e-4, F.len[i] * f);  s.ex[i] = x + F.dx[i] * L; s.ey[i] = y + F.dy[i] * L; s.ez[i] = z + F.dz[i] * L;  const r = Math.max(look.rmin, 0.5 * look.d0 * Math.pow(Math.max(1, F.pipes[i]), 1 / s.pipeN)) * (0.4 + 0.6 * f); // 描く最も細い枝は rmin  const dx = F.dx[i], dy = F.dy[i], dz = F.dz[i], rx = F.rx[i], ry = F.ry[i], rz = F.rz[i];  VX[0] = rx * r; VX[1] = ry * r; VX[2] = rz * r;  VY[0] = dx * L; VY[1] = dy * L; VY[2] = dz * L;  VZ[0] = (ry * dz - rz * dy) * r; VZ[1] = (rz * dx - rx * dz) * r; VZ[2] = (rx * dy - ry * dx) * r;  VP[0] = x; VP[1] = y; VP[2] = z;  put(s.wood.instanceMatrix.array as Float32Array, i, VX, VY, VZ, VP);  const k = s.leafIdx[i];  if (k >= 0) {    leafFrame(F, i, LF);    const g = f * f, sx = look.leafL * g, sz = look.leafW * g;    VX[0] = LF[3] * sx; VX[1] = LF[4] * sx; VX[2] = LF[5] * sx;    VY[0] = LF[6]; VY[1] = LF[7]; VY[2] = LF[8];    VZ[0] = LF[9] * sz; VZ[1] = LF[10] * sz; VZ[2] = LF[11] * sz;    VP[0] = LF[0] - F.px[i] - F.dx[i] * F.len[i] + s.ex[i]; VP[1] = LF[1] - F.py[i] - F.dy[i] * F.len[i] + s.ey[i]; VP[2] = LF[2] - F.pz[i] - F.dz[i] * F.len[i] + s.ez[i];    put(s.leaves.instanceMatrix.array as Float32Array, k, VX, VY, VZ, VP);  }}// 全部の節を描き直す(年に 1 回と、切ったり植えたりしたとき)function rebuild(s: S, api: Api) {  const F = s.F, now = F.step + s.acc;  let nl = 0;  s.animFrom = F.m;  for (let i = 0; i < F.m; i++) {    if (s.animFrom === F.m && now - F.born[i] < GROW) s.animFrom = i;    if (F.leaf[i]) { s.leafIdx[i] = nl; s.leafOf[nl] = i; nl++; } else s.leafIdx[i] = -1;  }  s.leafAnimFrom = nl;  for (let i = s.animFrom; i < F.m; i++) if (s.leafIdx[i] >= 0) { s.leafAnimFrom = s.leafIdx[i]; break; }  for (let i = 0; i < F.m; i++) drawNode(s, api, i, now);  s.nLeaves = nl;  for (const mesh of [s.wood, s.leaves]) { mesh.instanceMatrix.clearUpdateRanges(); mesh.instanceMatrix.needsUpdate = true; mesh.boundingSphere = null; mesh.boundingBox = null; }  s.wood.count = F.m; s.leaves.count = nl;}// 伸びている途中の節だけ計算し直す(毎コマ)。GPU へは配列全体を送り直す(場面を切り替えたあとも確実に描けるように)function animate(s: S, api: Api) {  const F = s.F, now = F.step + s.acc;  if (s.animFrom >= F.m) return;  for (let i = s.animFrom; i < F.m; i++) drawNode(s, api, i, now);  s.wood.instanceMatrix.needsUpdate = true;  if (s.leafAnimFrom < s.nLeaves) s.leaves.instanceMatrix.needsUpdate = true;  if (now - F.born[F.m - 1] >= GROW) s.animFrom = F.m;}// 建物と、林の大きな木(幹と、楕円体の冠)。冠の形は、光の計算で使う楕円体と同じfunction buildStatics(s: S) {  const F = s.F, b = F.boxes[0];  s.building.visible = !!b;  if (b) { s.building.scale.set(b.x1 - b.x0, b.y1 - b.y0, b.z1 - b.z0); s.building.position.set((b.x0 + b.x1) / 2, (b.y0 + b.y1) / 2, (b.z0 + b.z1) / 2); }  const wa = s.cWood.instanceMatrix.array as Float32Array, ca = s.cCrown.instanceMatrix.array as Float32Array;  let n = 0;  F.crowns.forEach((c, ci) => {    if (!c.alive) return;    const top = c.y0 + 0.5 * (c.y1 - c.y0), ry = (c.y1 - c.y0) / 2;    VX[0] = 0.13; VX[1] = 0; VX[2] = 0; VY[0] = 0; VY[1] = top; VY[2] = 0; VZ[0] = 0; VZ[1] = 0; VZ[2] = 0.13; VP[0] = c.x; VP[1] = 0; VP[2] = c.z;    put(wa, n, VX, VY, VZ, VP);    VX[0] = c.r; VX[1] = 0; VX[2] = 0; VY[0] = 0; VY[1] = ry; VY[2] = 0; VZ[0] = 0; VZ[1] = 0; VZ[2] = c.r; VP[0] = c.x; VP[1] = c.y0 + ry; VP[2] = c.z;    put(ca, n, VX, VY, VZ, VP);    s.cOf[n++] = ci;  });  s.cWood.count = n; s.cCrown.count = n;  for (const mesh of [s.cWood, s.cCrown]) { mesh.instanceMatrix.needsUpdate = true; mesh.boundingSphere = null; mesh.boundingBox = null; }}const R_SUN = (s: S, r: number) => (s.R.look === SHOOT ? r / 25 : r / 15);function placeSun(s: S) {  const t = s.v.orbit.target, d = s.F.sun;  const r = s.R.look === SHOOT ? s.sunR : Math.max(s.sunR, s.v.orbit.dist * 0.42);  s.sunMesh.position.set(t.x + d[0] * r, t.y + d[1] * r, t.z + d[2] * r);  s.sunMesh.scale.setScalar(R_SUN(s, r));  s.v.sunAt([d[0], d[1], d[2]], new THREE.Vector3(t.x, 0, t.z));}// 描画面の文字と印: 太陽の取っ手・木の名前・冠の片寄りの矢印・長さの目盛りfunction overlay(api: Api, s: S) {  const F = s.F, v = s.v, shoot = F.look === SHOOT;  const sp = v.project(s.sunMesh.position), m = api.u(26);  const hx = clamp(sp.x, m, api.w - m), hy = clamp(sp.y, m, api.h - m);  s.handle.x = hx; s.handle.y = hy;  api.circle(hx, hy, api.u(15), { w: api.u(1.4), dash: [api.u(3), api.u(3)] });  if (hx !== sp.x || hy !== sp.y) { // 太陽が画面の外にあるときは、取っ手を縁に置き、太陽のある向きを矢印で示す    const dx = sp.x - hx, dy = sp.y - hy, n = Math.hypot(dx, dy) || 1;    api.arrow(hx, hy, hx + (dx / n) * api.u(13), hy + (dy / n) * api.u(13), { w: api.u(1.2), head: api.u(6) });  }  const right = hx > api.w - api.u(70);  api.label("太陽", hx + (right ? -api.u(20) : api.u(20)), hy + api.u(5), { size: api.u(12), align: right ? "right" : "left" });  if (!shoot) F.trees.forEach((t, k) => {    if (t.root < 0) return;    const p = v.project([t.x, 0, t.z]);    if (p.on) api.label(String.fromCharCode(65 + k), p.x + api.u(6), p.y + api.u(16), { size: api.u(13), weight: 600 });  });  const t0 = F.trees[0];  if (s.R.extra === "shift" && t0 && t0.root >= 0 && Number.isFinite(s.st.cx)) {    const a = v.project([t0.x, 0.02, t0.z]), b = v.project([s.st.cx, 0.02, s.st.cz]);    if (a.on && b.on && Math.hypot(b.x - a.x, b.y - a.y) > api.u(6)) api.arrow(a.x, a.y, b.x, b.y, { w: api.u(2), head: api.u(8) });  }  // 長さの目盛り(木の場面は 1 m、葉の並びは 10 cm)  const unit = shoot ? 0.1 : 1, bx = t0 ? t0.x + (shoot ? 0.18 : -2.6) : 1.5, bz = t0 ? t0.z + (shoot ? 0.12 : 1.4) : 0;  const a = v.project([bx, 0.01, bz]), b = v.project([bx + unit, 0.01, bz]);  if (a.on && b.on && Math.hypot(b.x - a.x, b.y - a.y) > api.u(8)) {    api.line(a.x, a.y, b.x, b.y, { w: api.u(2) });    api.label(shoot ? "10 cm" : "1 m", (a.x + b.x) / 2, Math.max(a.y, b.y) + api.u(16), { size: api.u(11), align: "center", font: "mono" });  }}

行列の3つの列には、円柱の横向き(r×半径)、縦向き(茎の向き×長さ)、もう1つの横向き(2つの外積×半径)を入れ、4列目に根元の位置を入れる。伸びている途中の節だけは、親の先端に根元をつなぎ直しながら毎コマ計算し直す。木全体を描き直すのは、1年に1回と、枝を切ったり苗を植えたりしたときだけである。白と黒の網点と輪郭線への変換は、共通部品が受け持つ。

太陽をつかむ、枝を切る、苗を植える

描画面を押したとき、太陽の取っ手の近くなら太陽を動かす。横のドラッグで方角、縦のドラッグで高さが変わり、高さは12〜88°に収める。

操作(src/sims/plant-growth.ts の 655〜696 行)
// 太陽の取っ手をドラッグすると太陽が動く(横で方角、縦で高さ)。枝か葉をタップすると切り、地面をタップすると苗を植える。// 林の大きな木をタップすると切り倒す。それ以外のドラッグは視点を回すconst GROUND = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);function tap(api: Api, s: S) {  const F = s.F, v = s.v, x = api.pointer.x, y = api.pointer.y;  const objs: THREE.Object3D[] = [s.wood, s.leaves, s.cWood, s.cCrown, s.building];  const hit = v.pick(x, y, objs.filter((o) => o.visible && (!(o instanceof THREE.InstancedMesh) || o.count > 0)));  if (hit && hit.instanceId !== undefined && (hit.object === s.wood || hit.object === s.leaves)) {    const i = hit.object === s.wood ? hit.instanceId : s.leafOf[hit.instanceId];    if (i < 0 || i >= F.m) return;    const t = F.tree[i], whole = F.par[i] < 0, n = cutAt(F, i);    s.dirty = true; measureStats(s);    api.note(whole ? `木${String.fromCharCode(65 + t)}を根元から切った` : `枝を切った(${n}節)。すぐ下の脇芽に養分が回る`, 2600);    return;  }  if (hit && hit.instanceId !== undefined && (hit.object === s.cWood || hit.object === s.cCrown)) {    const ci = s.cOf[hit.instanceId];    const c = F.crowns[ci]; if (!c || !c.alive) return;    c.alive = false; buildStatics(s); measureStats(s);    const left = F.crowns.filter((q) => q.alive).length;    api.note(`大きな木を 1 本切った(残り ${left} 本)。林の床が明るくなる`, 2600);    return;  }  if (hit && hit.object === s.building) return;  const g = v.onPlane(x, y, GROUND);  if (!g) return;  const lim = F.look.half - 1;  if (Math.abs(g.x) > lim || Math.abs(g.z) > lim) { api.note("ここは計算の範囲の外なので植えられない", 2000); return; }  if (F.boxes.some((b) => g.x > b.x0 - 0.3 && g.x < b.x1 + 0.3 && g.z > b.z0 - 0.3 && g.z < b.z1 + 0.3)) return;  if (F.trees.some((t) => t.root >= 0 && Math.hypot(t.x - g.x, t.z - g.z) < 0.5)) return;  if (F.trees.length >= MAXTREES) { api.note(`苗は ${MAXTREES} 本まで`, 2000); return; }  if (plant(F, g.x, g.z, api.rng(500 + F.trees.length)) >= 0) { s.dirty = true; api.note(`苗を植えた(木${String.fromCharCode(64 + F.trees.length)})`, 2000); }}function dragSun(api: Api, s: S) {  const p = api.pointer, d = s.F.sun;  let az = Math.atan2(d[0], d[2]), el = Math.asin(clamp(d[1], -1, 1));  const k = 3.2 / Math.max(320, Math.min(api.w, api.h * 1.6));  az += (p.x - s.handle.x) * k * 0.6; el = clamp(el - (p.y - s.handle.y) * k * 0.6, 12 * DEG, 88 * DEG);  s.handle.x = p.x; s.handle.y = p.y;  s.F.sun = sunFrom(el / DEG, az / DEG);  placeSun(s);}

指を動かさずに離したときは、光線が当たったものから順に、枝か葉ならcutAtでその節から先を切り、林の木なら切り倒し、何にも当たらなければ地面に苗を植える。それ以外のドラッグは視点を回す操作として、共通部品のorbitに渡す。

画面の部品(描画・場面の切り替え・画質の自動調整・GPU の網点)は全記事で共通です。シミュレーションの共通の仕組みで解説しています。

プログラム全体を見る(src/sims/plant-growth.ts・816 行)

読み込んでいます

姉妹サイト

株式会社Game が運営するメディアです。