フラクタルを利用して3次元の木を描く

フラクタルを利用して3次元の木を描く

フラクタルな図形を2次元から3次元にするにあたり、空間ベクトルの向きを変えるのに苦労してしまった。 基本的な考えとしては、まず枝となる空間ベクトルのYの値を無視してXZ面で90度回転し、枝に対して水平に直行するベクトルを得る。 そのベクトルの長さを1とした単位ベクトルにし、その軸を基準にして上下方向にクオータニオンを使って回転する。 次に枝のベクトルを軸として更に回転する。 各枝の配列値はベクトルなのだが、点とみなして親から座標をたどって描画している。

もちろんマウスでドラッグするとグリグリ回ります。

<!DOCTYPE html>
<html lang="ja">
<meta charset="UTF-8">
<title></title>

<canvas width="1024" height="800" id="T1"></canvas>

<script>
//__________________________

class CG {
  constructor (canvas, distance = 1000, scale = 1000, offset = {x:0,y:0}) {
    this.canvas   = canvas;
    this.distance = distance;
    this.scale    = scale;
    this.offset   = offset;
    this.ctx      = canvas.getContext ('2d');
    canvas.width  = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
  }

  cls () {
    this.canvas.width = this.canvas.width;//高速な場合がある
    /*
    let {width, height}=this.canvas;
    this.ctx.clearRect(0,0,width,height)
    */
    return this;
  }

  line3D (obj, color = "black") {
    let
      ctx = this.ctx,
      cx = this.canvas.width*.5 + this.offset.x,
      cy = this.canvas.height*.5 + this.offset.y,
      d = this.distance,
      sc = this.scale;
    
    ctx.beginPath ();
    ctx.strokeStyle = color;

    for (let [s,e] of obj.line) {
      let
        {x,y,z} = obj.point[s],
        {x:X,y:Y,z:Z} = obj.point[e],
        z0 = sc / (d - z), z1 = sc / (d - Z);

      ctx.moveTo (cx + x * z0, cy - y * z0);
      ctx.lineTo (cx + X * z1, cy - Y * z1);
    }
    ctx.stroke ();
    return this;
  }
}
//__________________________

class WireFrame {
  constructor () {
    this.point = [ ];
    this.line = [ ];
    this.attr = [ ];
    this.index = null;
  }
  addPoint (...p) {
    return this.index = this.point.push (...p) - 1;
  }
  addLine (...ps) {
    if (1 == ps.length) ps.unshift (this.index);
    for (let i = 0, I = ps.length - 1; i < I; i++) {
      this.line.push ([ps[i], ps[i+1]])
    }
    return this;
  }
  addAttr (...at) {
    this.attr.push (...at)
  }
}



//__________________________

class P2{
  constructor(x=0,y=0){this.x=x;this.y=y}
  get clone(){return new this.constructor(this.x, this.y)}//複写
  get toArray(){return[this.x,this.y]}//単純配列化
  add({x=0,y=0},{x:X,y:Y}=this){this.x=X+x;this.y=Y+y;return this}//加算
  sub({x=0,y=0},{x:X,y:Y}=this){this.x=X-X;this.y=Y-y;return this}//減算
  mul({x=0,y=0},{x:X,y:Y}=this){this.x=X*x;this.y=Y*y;return this}//乗算
  div({x=0,y=0},{x:X,y:Y}=this){this.x=X/x;this.y=Y/y;return this}//除算
  sMul(n=0,{x:X,y:Y}=this){this.x=X*n;this.y=Y*n;return this}//スカラー倍
}

class V2 extends P2{
  constructor(...p){super(...p)}
  get length(){return(this.x**2+this.y**2)**.5}//長さ
  get negate(){this.x=-this.x;this.y=-this.y;return this}//逆向き
  get normalize(){let L=1/(this.length||1);this.x*=L;this.y*=L;return this}//単位化
  get angle(){return Math.atan2(-this.y,-this.x)+Math.PI}//原点を基準にした角度
  dot({x=0,y=0},{x:X,y:Y}=this){return X*x+Y*y}//ドット積
  cross({x=0,y=0},{x:X,y:Y}=this){return X*y-Y*x}//クロス積
  rotation(a){let m=Math,s=m.sin(a),c=m.cos(a),{x,y}=this;this.x=x*c+y*s;this.y=x*-s+y*c;return this}//回転
}

//__________________________

class P3{
  constructor(x=0,y=0,z=0){this.x=x,this.y=y,this.z=z}
  get clone(){return new this.constructor(this.x,this.y,this.z)}//複写
  get toArray(){return[this.x,this.y,this.z]}//単純配列化
  add({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X+x;this.y=Y+y;this.z=Z+z;return this}//加算
  sub({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X-x;this.y=Y-y;this.z=Z-z;return this}//減算
  mul({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X*x;this.y=Y*y;this.z=Z*z;return this}//乗算
  div({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X/x;this.y=Y/y;this.z=Z/z;return this}//除算
  sMul(n=0,{x:X,y:Y,z:Z}=this){this.x=X*n;this.y=Y*n;this.z=Z*n;return this}//スカラー倍
  applyQuaternion({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z}=this){//クオータニオンの適用
    let a=w*X+y*Z-z*Y,b=w*Y+z*X-x*Z,c=w*Z+x*Y-y*X,d=-x*X-y*Y-z*Z;
    return this.x=a*w+d*-x+b*-z-c*-y,this.y=b*w+d*-y+c*-x-a*-z,this.z=c*w+d*-z+a*-y-b*-x,this;
  }
  rotation(a=0,b=0,c=0){//ロール、ピッチ、ヨウ
    let {sin:S,cos:C,PI}=Math,{x,y,z}=this,d=PI/180,e=a*d,f=b*d,g=c*d,h=S(e),i=S(f),j=S(g),k=C(e),l=C(f),m=C(g);
    this.x=x*k*l+y*(k*i*j-h*m)+z*(k*i*m+h*j);this.y=x*h*l+y*(h*i*j+k*m)+z*(h*i*m-k*j);this.z=x*-i+y*l*j+z*l*m;return this;
  }
}

class Vector extends P3{
  constructor(...p){super(...p)}
  get length(){return(this.x**2+this.y**2+this.z**2)**.5}//長さ
  get clone(){return new this.constructor(this.x,this.y,this.z)}//複写
  get negate(){return this.sMul(-1)}//逆向き
  get normalize(){return this.sMul(1/(this.length||1))}//単位化
  dot({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){return X*x+Y*y+Z*z}
    cross ({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){return this.x=y*Z-z*Y,this.y=z*X-x*Z,this.z=x*Y-y*X,this}
  rotation (a=0,b=0) {//ピッチ、ロール
    let {sin:S,cos:C,PI}=Math,{x,y,z}=this,d=PI/360,e=a*d,f=b*d,g=S(e),h=S(f),
    A=(new Vector(-z,0,(!z&&!x?1:x)).normalize),B=(new Vector(x,y,z).normalize);
    return this.applyQuaternion(new Quaternion(A.x*g,A.y*g,A.z*g,C(e))).applyQuaternion(new Quaternion(B.x*h,B.y*h,B.z*h,C(f)));
  }
}

//__________________________

class Quaternion{
  constructor (x=0,y=0,z=0,w=1){this.x=x;this.y=y;this.z=z;this.w=w}
  get clone(){return new this.constructor(this.x,this.y,this.z,this.w)}//複写
  get length(){return(this.x**2+this.y**2+this.z**2+this.w**2)**.5}
    get normalize(){let L=this.length;return (L?(L=1/L,this.x*=L,this.y*=L,this.z*=L,this.w*=L):this.x=this.y=this.z=0,this.w=1),this}
  dot({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z,w:W}=this){return X*x+Y*y+Z*z+W*w}
  mul({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z,w:W}=this){return this.x=X*w+W*x+Y*z-Z*y,this.y=Y*w+W*y+Z*x-X*z,this.z=Z*w+W*z+X*y-Y*x,this.w=W*w-X*x-Y*y-Z*z,this}
  add({x,y,z,w}){return this.x+=x,this.y+=y,this.z+=z,this.w+=w,this}
  setFromAxisAngle(a=0,_){this.w*=Math.cos(_=a/2);this.x*=(_=Math.sin(_));this.y*=_;this.z*=_;return this}
}

/*__________________________________________________
マウスのドラッグ操作で配列の回転を制御(QUATERNIONによる回転)
currentQuaternionメゾットに3次元空間での回転量がクオータニオンとして
保存される。P3でインスンタス化したオブジェクトには、applyQuaternionメゾット
があるのでその引数にあてて、その都度座標変換する

const rc = new RotationController;
[P3,...].forEach (p=> p.applyQuaternion (rc.currentQuaternion));
*/
class RotationController {
  constructor (element = document.body, option = { }) {
    this.target      = element;//マウス操作の対象要素
    this.option      = Object.assign ({ },this.constructor.getDefaultOption (), option);
    this.touched     = false; //ドラッグ中か?
    this.quaternion  = new Quaternion;//積算され続ける
    this.distance    = [0, 0];//マウスの移動距離により回転のクオータニオンを生成する
    this.mousePoint  = [0, 0];//初期のマウスポインタ

    (window.TouchEvent //イベント登録
      ? ['touchstart', 'touchend', 'touchmove'] //touchイベントがあるなら優先
      : ['mousedown', 'mouseup', 'mousemove', 'mouseout']
    ).forEach (e=> element.addEventListener(e, this, false));
  }
  
  handleEvent (event) {//各イベント処理
    let { type, pageX, pageY } = event;
    switch (type) {
      case 'mousedown' : case 'touchstart': this.touched = true; break;// 制御開始
      case 'mouseup'   : case 'mouseout'  : case 'touchend' : this.touched = false; break;// 制御終了
      case 'mousemove' : case 'touchmove' :// 回転制御中
        event.preventDefault ();//ipadなどでスクロールさせないため
        this.touched && (this.moveMousePoint = [pageX, pageY]);
        break;
    }
    this.mousePoint = [pageX, pageY];
  }

  set moveMousePoint ([px, py]) {//マウスが動くことで差を求め距離をセットする
    let [mx, my] = this.mousePoint, s = this.option.sensitivity;
    this.distance = [(mx - px) * s, (my - py) * s];
  } 

  set distance ([x, y]) {//距離から回転クオータニオンを求める
    let d = (x**2 + y**2)**.5, s = -Math.sin (d) / (d || 1);//距離が0でも計算
    this._d = [x, y];
    this._q = new Quaternion (y * s, x * s, 0, Math.cos (d));
    this.quaternion.mul (this._q);
  }

  get currentQuaternion () {//現在の回転量
    let gain = this.option.gain;
    this.distance = this._d.map (n=> n * gain);
    this.quaternion.mul (this._q);
    return this._q;
  }

  static getDefaultOption (key = null) {
    const opt = {
      sensitivity : 1/300,  //マウスの感度
      gain        : .95, //慣性移動の減速率
    };
    return null === key ? opt: opt[key];
  }
}
//__________________________________________________



class Node {
  constructor (parent, attr, ...childs) {
    this.parent = parent;
    this.attr = attr;
    this.childs = childs;
  }
  append (...childs) { this.childs.push (...childs) }
  hasChild () { return !!this.childs.length;}
}


class Tree extends Node {
  constructor (parent, attr, iden, branch = 0) {
    super (parent, attr);
    this.iden = iden;

    if (branch) this.growup (branch);//枝分かれ
  }

  growup (cnt = 1) {
    const loop = (parent, cnt)=> {//再起呼び出し
      if (cnt--) {
        if (! parent.hasChild ())
          parent.append (...parent.iden (parent));//遺伝子を使って枝を生成
        parent.childs.forEach (t=> loop (t, cnt));
      }
    };

    loop (this, cnt);
    return this;
  }


  getTreeData (offset = new P3, wireFrame) {
    const loop = (e, position, cnt = 0, beginNo = null)=> {
      let
        vector = e.attr,
        p0 = position.clone,
        p1 = p0.clone.add (vector),
        endNo = null;

      if (null === beginNo)
        beginNo = wireFrame.addPoint (p0);

      endNo = wireFrame.addPoint (p1);
      wireFrame.addLine (beginNo, endNo);
      wireFrame.addAttr (cnt);

      e.childs.forEach (a=> loop (a, p1.clone, cnt + 1, endNo));
    }

    loop (this, offset);
    return wireFrame;
  }


  makeTree (...attrs) {
    let { attr, iden } = this, rst = [ ];
    for (let [len, a, b] of attrs)
      rst.push (new Tree (this, attr.clone.sMul (len).rotation (a,b), iden));
    return rst;
  }
}



//__________________________________________________
const
  //canvas
  C = new CG (document.querySelector ('#T1'), 220,500,new P2(0,200)),
  M = new RotationController (#T1),
  PLACE0 = new WireFrame,
  PLACE1 = new WireFrame,
  idenA = parent=> parent.makeTree ([.7,25,0],[.7,25,120],[.7,25,240]),
  TREE_A = new Tree (null, new Vector (0,50,0), idenA, 8),
  TREE_A_DATA = TREE_A.getTreeData (new P3(0,0,0), new WireFrame),

  idenB = parent=> parent.makeTree ([.7,20,0],[.7,20,90],[.7,20,180],[.7,20,270],[.9,0,0]),
  TREE_B = new Tree (null, new Vector (0,20,0), idenB, 4),
  TREE_B_DATA = TREE_B.getTreeData (new P3(50,0,50), new WireFrame),
  TREE_C_DATA = TREE_B.getTreeData (new P3(-50,0,50), new WireFrame);

//X,y,Z軸を定義
PLACE0.addPoint (
  new P3(-200,0,0), new P3(200,0,0),
  new P3(0,0,-200), new P3(0,0,200),
  new P3(0,-200,0), new P3(0,0,0),
);
PLACE0.addLine (0, 1); PLACE0.addLine (2, 3);PLACE0.addLine (4, 5);

//XZ軸の平面に格子模様を定義
let no = 0;
for (let i = -100; i <= 100; i+=10) {
  if (i) {
    PLACE1.addPoint (new P3(-100,0,i), new P3(100,0,i)); PLACE1.addLine (no, no+1);
    PLACE1.addPoint (new P3(i,0,-100), new P3(i,0,100)); PLACE1.addLine (no+2, no+3);
    no+=4;
  }
}


const
  demo = function loop () {
    C.cls ();
    let cq = M.currentQuaternion;
    PLACE0.point.forEach (p=> p.applyQuaternion (cq)); C.line3D (PLACE0, "rgba(255,0,0,.5)");
    PLACE1.point.forEach (p=> p.applyQuaternion (cq)); C.line3D (PLACE1, "rgba(0,0,0,.5)");
    TREE_A_DATA.point.forEach (p=> p.applyQuaternion (cq)); C.line3D (TREE_A_DATA, "rgba(0,128,0,.6)");
    TREE_B_DATA.point.forEach (p=> p.applyQuaternion (cq)); C.line3D (TREE_B_DATA, "rgba(255,0,0,.6)");
    TREE_C_DATA.point.forEach (p=> p.applyQuaternion (cq)); C.line3D (TREE_C_DATA, "rgba(255,100,0,.6)");
    requestAnimationFrame (loop);
  };

demo ();
</script>