フラクタルを利用して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>