Chapter 69

L-System

miku
miku
2021.11.21に更新

前章の反復関数系ではアフィン変換のパラメータをルールとしていたが、本章で扱うL-Systemはルールが F+ のような文字になっているものだ。

L-System

L-Systemでは左回転を +、右回転を - のように機能を1つの文字として表す。

文字 機能
+ 現在の角度から左に angle 度回転する
- 現在の角度から右に angle 度回転する
[ 配列に現在の位置と角度を追加する
] 配列の末尾にある要素を取り出して、現在の位置と角度に設定する
それ以外の文字 現在の位置から angle 度方向に length 進んだ位置まで線を引く

文字は1文字であれば好きな別の文字に変更してもいいし、新たな機能を追加してもいい。解説では上記の文字と機能を割り当てる。ここで出てきた anglelength や、初期位置などを設定したいので、それをまとめるオブジェクトを用意する。

const data = {
  command: "+_+__+___+____+_____+______+_______", // 命令
  length: 10, // 線を描画するときの長さ
  x: width / 2, // 初期位置
  y: height / 2,
  startAngle: 0, // 初期角度
  angle: 60, // `+` や `-` のときに回転する角度
};

このオブジェクトと上記命令文字に応じた機能で描画を行う。

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  stroke(240);
  noFill();

  const data = {
    command: "+_+__+___+____+_____+______+_______",
    length: 10,
    x: width / 2,
    y: height / 2,
    startAngle: 0,
    angle: 60,
  };

  const stack = [];
  let angle = data.startAngle;
  let x = data.x;
  let y = data.y;
  for (const c of data.command) {
    switch (c) {
      case "+":
        angle -= data.angle;
        break;
      case "-":
        angle += data.angle;
        break;
      case "[":
        stack.push({ angle, x, y });
        break;
      case "]":
        const d = stack.pop();
        [angle, x, y] = [d.angle, d.x, d.y];
        break;
      default:
        const tx = x + cos(angle) * data.length;
        const ty = y + sin(angle) * data.length;
        line(x, y, tx, ty);

        x = tx;
        y = ty;
        break;
    }
  }
}

これでコマンドに応じた描画をすることができたが、フラクタルを描画するのにコマンド部分をすべて手動で入力することになるので、再帰的にコマンドを生成できるように拡張したい。

ルールによるコマンドの生成

const data = {
  start: "ABCA",
  rules: { A: "1", B: "23" },
};

start に書かれている文字列が初期のコマンドになり、それを rules に書かれているルールをもとにコマンドを拡張していく。start の文字を1文字ずつ取り出して、その文字が rules のキーになっているものがあれば、キーに対応する値に変換する。上記コード例だと、A1 に、B23 に変換されるので、ABCA123C1 になる。文字 C に対応するルールは定義されていないので C は変換されずにそのままになる。

const data = {
  start: "A",
  rules: { A: "AA" },
  depth: 3,
};

ルールのキーと値には同じ文字を指定できるので、AAA に変換のようにコマンドを増殖させることができる。これが再帰にあたり、再帰には回数が必要なので depth というルールを適用する回数を定義する。上記コード例では depth: 3 になっているので、start に対して rules を3回適用する。

回数 コマンド
1回目 AA
2回目 AAAA
3回目 AAAAAAAA
const data = {
  none: ["X", "Y"],
};

+- のような機能を表すルール以外は全て描画されるようになっていたが、コマンドを増殖させる際にフックとなる文字まで描画されると困る場合があるので、この文字がきたら何もしないという機能を追加したい。none は文字の配列になっており、中に入っている文字はコマンドの実行の際に何も行わないという意味になる。

コッホ曲線

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  stroke(240);
  noFill();

  const data = {
    start: "F",
    rules: { F: "F+F--F+F" },
    length: 5,
    depth: 5,
    x: 0,
    y: height / 2,
    startAngle: 0,
    angle: 60,
    none: [],
  };

  drawLSystem(data);
}

function addCommand(command, rules) {
  return command
    .split("")
    .map((key) => {
      const value = rules[key];
      return value ? value : key;
    })
    .join("");
}

function drawLSystem(data) {
  let command = data.start;
  for (let i = 0; i < data.depth; i++) {
    command = addCommand(command, data.rules);
  }

  const stack = [];
  let angle = data.startAngle;
  let x = data.x;
  let y = data.y;
  for (const c of command) {
    switch (c) {
      case "+":
        angle -= data.angle;
        break;
      case "-":
        angle += data.angle;
        break;
      case "[":
        stack.push({ angle, x, y });
        break;
      case "]":
        const d = stack.pop();
        [angle, x, y] = [d.angle, d.x, d.y];
        break;
      default:
        if (data.none.indexOf(c) === -1) {
          const tx = x + cos(angle) * data.length;
          const ty = y + sin(angle) * data.length;

          line(x, y, tx, ty);

          x = tx;
          y = ty;
        }
        break;
    }
  }
}

シェルピンスキーのギャスケット

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  stroke(240);
  noFill();

  const data = {
    start: "A",
    rules: { A: "B-A-B", B: "A+B+A" },
    length: 5,
    depth: 6,
    x: 0,
    y: height / 2,
    startAngle: 0,
    angle: 60,
    none: [],
  };

  drawLSystem(data);
}

function addCommand(command, rules) {
  return command
    .split("")
    .map((key) => {
      const value = rules[key];
      return value ? value : key;
    })
    .join("");
}

function drawLSystem(data) {
  let command = data.start;
  for (let i = 0; i < data.depth; i++) {
    command = addCommand(command, data.rules);
  }

  const stack = [];
  let angle = data.startAngle;
  let x = data.x;
  let y = data.y;
  for (const c of command) {
    switch (c) {
      case "+":
        angle -= data.angle;
        break;
      case "-":
        angle += data.angle;
        break;
      case "[":
        stack.push({ angle, x, y });
        break;
      case "]":
        const d = stack.pop();
        [angle, x, y] = [d.angle, d.x, d.y];
        break;
      default:
        if (data.none.indexOf(c) === -1) {
          const tx = x + cos(angle) * data.length;
          const ty = y + sin(angle) * data.length;

          line(x, y, tx, ty);

          x = tx;
          y = ty;
        }
        break;
    }
  }
}

ドラゴン曲線

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  stroke(240);
  noFill();

  const data = {
    start: "FX",
    rules: { X: "X+YF", Y: "FX-Y" },
    length: 5,
    depth: 12,
    x: width / 2,
    y: height / 2,
    startAngle: 0,
    angle: 90,
    none: ["X", "Y"],
  };

  drawLSystem(data);
}

function addCommand(command, rules) {
  return command
    .split("")
    .map((key) => {
      const value = rules[key];
      return value ? value : key;
    })
    .join("");
}

function drawLSystem(data) {
  let command = data.start;
  for (let i = 0; i < data.depth; i++) {
    command = addCommand(command, data.rules);
  }

  const stack = [];
  let angle = data.startAngle;
  let x = data.x;
  let y = data.y;
  for (const c of command) {
    switch (c) {
      case "+":
        angle -= data.angle;
        break;
      case "-":
        angle += data.angle;
        break;
      case "[":
        stack.push({ angle, x, y });
        break;
      case "]":
        const d = stack.pop();
        [angle, x, y] = [d.angle, d.x, d.y];
        break;
      default:
        if (data.none.indexOf(c) === -1) {
          const tx = x + cos(angle) * data.length;
          const ty = y + sin(angle) * data.length;

          line(x, y, tx, ty);

          x = tx;
          y = ty;
        }
        break;
    }
  }
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  stroke(240);
  noFill();

  const data = {
    start: "X",
    rules: { X: "F-[[X]+X]+F[+FX]-X", F: "FF" },
    length: 3,
    depth: 6,
    x: width / 2,
    y: height,
    startAngle: -90,
    angle: 25,
    none: ["X"],
  };

  drawLSystem(data);
}

function addCommand(command, rules) {
  return command
    .split("")
    .map((key) => {
      const value = rules[key];
      return value ? value : key;
    })
    .join("");
}

function drawLSystem(data) {
  let command = data.start;
  for (let i = 0; i < data.depth; i++) {
    command = addCommand(command, data.rules);
  }

  const stack = [];
  let angle = data.startAngle;
  let x = data.x;
  let y = data.y;
  for (const c of command) {
    switch (c) {
      case "+":
        angle -= data.angle;
        break;
      case "-":
        angle += data.angle;
        break;
      case "[":
        stack.push({ angle, x, y });
        break;
      case "]":
        const d = stack.pop();
        [angle, x, y] = [d.angle, d.x, d.y];
        break;
      default:
        if (data.none.indexOf(c) === -1) {
          const tx = x + cos(angle) * data.length;
          const ty = y + sin(angle) * data.length;

          line(x, y, tx, ty);

          x = tx;
          y = ty;
        }
        break;
    }
  }
}