ギンの備忘録

デジタルアートやプログラミングやテクノロジー関連のこと。

【p5.js】L-systemで木を描く

Processing Advent Calendar 2022の20日目の記事になります。

adventar.org

目次

はじめに

今回はL-sysytemを用いて木を描く方法について解説をしていきます。

L-systemでは定義したルールに基づいて描画処理の命令を生成し、その命令に従いフラクタル図形や植物の形状を表現することができます。

フラクタルの性質

まずフラクタルの性質を利用して植物の形を表現するという概念を理解していきましょう。

ここで一つ例を挙げて話を進めます。

図の左のように矢印を1個用意します。 これを図の右の木の枝のような形をした6個の矢印に置き換えます。

この置換がL-systemを使って、植物のように見えるフラクタル図形を描く上で根底にある考え方になります。

置換を反復したり、置き換えのルールを拡張することで、複雑な図形を描くことが可能となります。

上の例だけでは、まだ納得できていないと思うので、これを反復していくとどうなるか見ていきましょう。

次の図で矢印の置換を繰り返した場合を考えていきます。

最初の状態は矢印を1個用意します。(図左上)

一回目の置換は上で説明した通りとなります。(図左下)

続いて二回目の置換を行います。(図右上)
6個の矢印をそれぞれ6個の矢印で置き換えた36個の矢印で構成された図形になります。

三回目の置換を同様に行うと、36個の矢印を216個の矢印に置き換えた図形となります。(図右下) この図形は植物のようにも見えると言えるでしょう。

この様にして、同じ形・配置の矢印の置き換えを繰り返すことで、 同じ図形が繰り返し現れるというフラクタルの性質を持った複雑な図形を描くことができます。

L-system

L-systemは次のようなものになります。

L-systemは形式文法の一種で、植物の成長プロセスを初めとした様々な自然物の構造を記述・表現できるアルゴリズムである。自然物の他にも、反復関数系のようないわゆる自己相似図形やフラクタル図形を生成する場合にも用いられる。

L-system - Wikipediaより

ここからL-systemをプログラムでどう実行していくか説明していきます。

先程の木の枝の形を模した6個の矢印を例に考えていきます。(図左)

p5.jsでこの6本の線を描くコードを示します。(図中央)
理解のため矢印で表していましたが、実際には直線6本で描画します。
移動や回転をし、直線を描画するコードとなります。

L-sysytemでは、その描画処理を文字列で表します。(図右) 予め描画処理と文字を対応付けておき、文字列を読み替えて描画します。 この例では6本の線で描く木の枝の形は"F[+FF]F[-F]F"となります。

L-systemの描画処理

先程の図でそれぞれの描画処理を文字に対応付けたのが下の表になります。

文字 描画処理内容 p5.jsの関数
F 線を描き前進 translate(0, 0, 0, distance)
line(0, distance)
+ 左に回転 rotate(-angle)
- 右に回転 rotate(+angle)
[ 現在の位置・回転の情報を保存 push()
] 直前に保存した情報の呼び出し pop()

distanceは線の長さ、angleは回転する角度のパラメータで、任意の値を設定できるものと考えて下さい。

それぞれの文字の示す描画処理を定義したところで、一度描画をしていきましょう。

6本の線で描く図形を描画する文字列は"F[+FF]F[-F]F"なので、これを上の表に従って描画します。 コードは下のようになります。

function setup(){
  createCanvas(w=720, w)
  background(255)

  //画面中心の底から上方向へ描き始めるため移動・回転
  translate(w*0.5, w)
  rotate(PI)

  strokeWeight(20)

  let command = "F[+FF]F[-F]F"

  //線の長さ
  let distance = 200
  //回転する角度
  let angle = radians(45)

  //commandの文字列を一文字ずつcに代入してループ処理
  for(const c of command){
    switch(c){
      case "F":
        line(0, 0, 0, distance)
        translate(0, distance)
        break
      case "+":
        rotate(-angle)
        break
      case "-":
        rotate(+angle)
        break
      case "[":
        push()
        break
      case "]":
        pop()
        break
      default:
        break
    }
  }
}

実行結果です。

このように描画処理と文字列を対応づけてフラクタル図形を描画する手法がL-systemになります。

L-systemの描画命令生成

矢印を置換していくことで、植物のような形になることを上の例で示しました。
矢印の描画を文字列と対応させたことから、矢印の置換というのが文字の置換になることは想像がつくと思います。

描画命令の文字列は下のルールを定めて、文字の置換を行うことで生成できます。

  • 使用文字: F, +, -, [, ]
  • 初期状態: F
  • 置換規則: (F→F[+FF]F[-F]F)

置換規則で示すのが"F"を"F[+FF]F[-F]F"に置換するという意味です。 これは1本の矢印を6本の矢印に置き換えることに相当します。

この置換を繰り返せば下のように文字列を生成できます。

置換回数 文字列
初期状態 F
置換一回 F[+FF]F[-F]F
置換二回 F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F
置換三回 F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F[+F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]FF[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F]F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F]F[+FF]F[-F]F[+F[+FF]F[-F]FF[+FF]F[-F]F]F[+FF]F[-F]F[-F[+FF]F[-F]F]F[+FF]F[-F]F

置換を重ねればより複雑な図形になりますが、その分描画命令の文字列が長くなっています。 そのため、描画命令の文字列もコードで生成していきましょう。 コードにすると下になります。

  //初期状態をcommandに代入
  let command = "F"
  let repeat = 3

  for(let i=0;i<repeat;i++){
    let com = []
    for(const k of command){
      //置換規則
      switch(k){
        case "F": com += "F[+FF]F[-F]F"
          break
        default: com += k
          break
      }
    }
    command = com
  }

L-systemの実行コード

描画処理の文字列との対応、文字列の置換による描画処理の生成を説明しました。

これらを組み合わせることでL-systemを用いた描画処理が実行できます。

それが下のコードとなります。

 function setup() {
  createCanvas(w=720, w)

  background(255)

  let repeat = 3
  let command = gen_command(repeat)

  let distance = 200
  let angle = radians(45)

  push()
  translate(w*0.5, w)
  rotate(PI)

  for(const c of command){
    switch(c){
      case "F":
        line(0, 0, 0, distance)
        translate(0, distance)
        break
      case "+":
        rotate(-angle)
        break
      case "-":
        rotate(+angle)
        break
      case "[":
        push()
        break
      case "]":
        pop()
        break
      default:
        break
    }
  }
  pop()
}

function gen_command(repeat){
  let command = "F"
  for(let i=0;i<repeat;i++){
    let com = []
    for(const k of command){
      switch(k){
        case "F": com += "F[+FF]F[-F]F"
          break
        default: com += k
          break
      }
    }
    command = com
  }

  return command
}

gen_command()の引数は繰り返し回数repeatにしていて、これを増やしたりすれば更に置換を実行した図形を描くことができます。

文字置換規則を増やす

使用文字と置換規則を増やして、より木に近い形を下のルールを用いて描いてみましょう。

  • 使用文字: A, F, +, -, [, ]
  • 初期状態: A
  • 置換規則: (F→FF), (A→F-[[A]+A]+F[+FA]-A)

"F"と"A"という文字の場合に置換をします。 また、"A"の描画処理は"F"と同じとします。

回転する角度angleは22.5°とします。

使用文字や置換規則を増やすことでより複雑な図形となります。

function setup(){
  createCanvas(w=720, w)
  background(255)

  translate(w*0.5, w)
  rotate(PI)

  let repeat = 6
  let command = gen_command(repeat)

  let distance = 4
  let angle = radians(22.5)

  for(const c of command){
    switch(c){
      case "A":
      case "F":
        line(0, 0, 0, distance)
        translate(0, distance)
        break
      case "+":
        rotate(-angle)
        break
      case "-":
        rotate(+angle)
        break
      case "[":
        push()
        break
      case "]":
        pop()
        break
      default:
        break
    }
  }
}

function gen_command(repeat){
  let command = "A"
  for(let i=0;i<repeat;i++){
    let com = []
    for(const k of command){
      switch(k){
        case "F": com += "FF"
          break
        case "A": com += "F-[[A]+A]+F[+FA]-A"
          break
        default: com += k
          break
      }
    }
    command = com
  }

  return command
}

コード(fractal tree 00 - OpenProcessing

実際の木のように見える形が描けました。

引き続き置換規則を変えて、木の形がどうなるのか見ていきましょう。

次の置換規則をルールを用いてみます。

  • 置換規則: (F→FF), (A→F[+A]F[-A]A)

コード(fractal tree 01 - OpenProcessing

次の置換規則をルールを用いてみます。

  • 置換規則: (F→FF), (A→F[+[FA[-A]F]][-A]FA)

コード(fractal tree 02 - OpenProcessing

文字置換を確率的に実行

更にルールを複雑にしていきます。

置換規則を複数用意して、それをランダムに選ぶ様にしてみます。

  • 使用文字: A, F, +, -, [, ]
  • 初期状態: A
  • 置換規則: (F→FF), (A→F-[[A]+A]+F[+FA]-A or F[+A]F[-A]A or F[+[FA[-A]F]][-A]FA)

Aの文字を置換するとき、3つからランダムに選択して1つの置き換えをします。

コードは命令文字列の生成部分を下のように変えます。

function gen_command(repeat){
  let command = "A"
  let Agenerater = ["F-[[A]+A]+F[+FA]-A", "F[+A]F[-A]A", "F[+[FA[-A]F]][-A]FA"]

  for(let i=0;i<repeat;i++){
    let com = []
    for(let k of command){
      switch(k){
        case "F": com += "FF"
          break
        case "A": com += random(Agenerater)
          break
        default: com += k
          break
      }
    }
    command = com
  }

  return command
}

描画したのが下の図です。

コード(fractal tree 03 - OpenProcessing

確率的に置換するので、違いがわかる様に16個の結果を並べてみました。 ランダム性が加わって、毎回異なる形の木を描ける様になりました。

線の長さと回転する角度に乱数を付加

線の長さdistanceと回転する角度angleを描画時にランダムにすることで、描画される形のバリエーションを更に増やすことができます。

回転する角度については回転の左右を入れ替えるため、1と-1をランダムで掛けます。

コードは命令文字列の生成部分を下のように変えます。

  for(let c of command){
    let rnd_dist = distance + random(-1,1)*distance*0.5
    let rnd_angle = angle + random(-1,1)*angle*0.5
    rnd_angle *= random([-1,1])
    switch(c){
      case "A":
      case "F":
        line(0, 0, 0, rnd_dist)
        translate(0, rnd_dist)
        break
      case "+":
        rotate(-rnd_angle)
        break
      case "-":
        rotate(+rnd_angle)
        break
      case "[":
        push()
        break
      case "]":
        pop()
        break
      default:
        break
    }
  }

描画したのが下の図です。

コード(fractal tree 04 - OpenProcessing

更にランダム性が加わったことで、木の形に複雑なバリエーションが生まれました。

描画処理で着色

"A"の文字の時の描画命令を少し変えてみましょう。 "A"の場合は線を描く際に、線の色を緑色にして、描画する線の長さを長くしてみます。

コードは命令文字列の生成部分を下のように変えます。

  for(let c of command){
    let rnd_dist = distance + random(-1,1)*distance*0.5
    let rnd_angle = angle + random(-1,1)*angle*0.5
    rnd_angle *= random([-1,1])
    switch(c){
      case "A":
        push()
        stroke("#00AA00")
        line(0, 0, 0, rnd_dist*2)
        pop()
        translate(0, rnd_dist)
        break
      case "F":
        line(0, 0, 0, rnd_dist)
        translate(0, rnd_dist)
        break
      case "+":
        rotate(-rnd_angle)
        break
      case "-":
        rotate(+rnd_angle)
        break
      case "[":
        push()
        break
      case "]":
        pop()
        break
      default:
        break
    }
  }

描画したのが下の図です。

コード(fractal tree 05 - OpenProcessing

葉の付いた木のような描画になりました。 これでより植物らしさが出たと思います。

応用例

ここまでの説明から更に発展して作成したものを紹介します。 詳しい解説はしませんが、コードをリンクくしておくので、作品作りのアイデアにしてみて下さい。

サンゴっぽい絵

線を描画する際に、横方向にランダムな成分を付加した例。

コード(fractal tree next 00 - OpenProcessing

更にリアルな木

葉の描画をする際に、葉の色をランダムにし、重ねて描いた例。

コード(fractal tree next 01 - OpenProcessing

森を生成

ランダムな位置に何個も木の描画した例。

コード(fractal tree next 02 - OpenProcessing

葉と実を別文字へ割当

木に葉と実を付けて、それぞれ描画処理を新たな文字へ割り当てて描画した例。

コード(fractal tree next 03 - OpenProcessing

ドラゴン曲線

L-systemで別のフラクタル図形を描く例。

コード(Dragon Curve - OpenProcessing

おわりに

p5.jsでL-systemを利用し植物のような形の描き、ランダム要素を加えリアルな木に見える描き方を解説しました。

この記事をきっかけに、更に木を描くことを深く掘り下げたり、 L-systemで別のフラクタル図形を描いたり、 フラクタル全般に興味を持ったりしてもらえたら嬉しいです。

参考文献

zenn.dev

note.com

taq.hatenadiary.jp