マウスで3Dモデルを回転させる。その3


http://jsdo.it/babu_baboo/inv0

<!DOCTYPE html>
<meta charset="utf-8">
<title>GAME</title>
<style>
body {
  color: #ccc; background: black;
}
</style>

<body>
<canvas width="800" height="700" id="c1"></canvas>

<script>
//___________________________________

{ //アニメーションの環境か?

  const
    win = this;

  const
    substitution =
      function (callBackFunc, that) {
        let tmpFunc = function () {
          let timestamp = +(new Date);
          callBackFunc (timestamp);
        };
        win.setTimeout (tmpFunc, Math.floor (1000/60));
      };

  if ('undefined' === typeof win.requestAnimationFrame)
    win.requestAnimationFrame =
      win.requestAnimationFrame ||
      win.webkitRequestAnimationFrame ||
      win.mozRequestAnimationFrame ||
      win.oRequestAnimationFrame ||
      win.msRequestAnimationFrame ||
      substitution;

  if ('undefined' === typeof win.cancelAnimationFrame)
    win.cancelAnimationFrame =
      win.cancelAnimationFrame ||
      win.mozCancelAnimationFrame ||
      win.webkitCancelAnimationFrame ||
      win.msCancelAnimationFrame;
}


{ //色を定義する
  const
      MAX_COLOR   = 255,
      MAX_OPACITY = 1,
      MIN         = Math.min,
      MAX         = Math.max,
      INT         = Math.floor;


  class Color {

    constructor (r = MAX_COLOR, g = MAX_COLOR, b = MAX_COLOR, a = MAX_OPACITY) {
      this.r = MAX (0, MIN (r, MAX_COLOR));
      this.g = MAX (0, MIN (g, MAX_COLOR));
      this.b = MAX (0, MIN (b, MAX_COLOR));
      this.a = MAX (0, MIN (a, MAX_OPACITY));
    }

    //色の加算
    addColor (colorObj) {
      let {r, g, b, a} = colorObj;
      this.r = MAX (0, r * a + this.r);
      this.g = MAX (0, g * a + this.g);
      this.b = MAX (0, b * a + this.b);
    }

    //色のコピーを作る
    crone () {
      return new Color (this.r, this.g, this.b, this.a);
    }


    //乗算したコピーを作る
    multiplication (x) {
      return new Color (
        MAX (0, this.r * x),
        MAX (0, this.g * x),
        MAX (0, this.b * x),
        this.a
      );
    }


    //文字列で返す(透明度含む)
    toStringRGBA () {
      return 'rgba(' + [
        MIN (INT (this.r), MAX_COLOR),
        MIN (INT (this.g), MAX_COLOR),
        MIN (INT (this.b), MAX_COLOR),
        MIN (this.a, MAX_OPACITY)
      ].join (',') + ')';
    }


    //文字列で返す
    toSrtingRGB () {
      return 'rgb(' + [
        MIN (INT (this.r), MAX_COLOR),
        MIN (INT (this.g), MAX_COLOR),
        MIN (INT (this.b), MAX_COLOR)
      ].join (',') + ')';
    }
  }

  //__________

  this.Color = Color;
}



{ //点を定義する

  //単純な2次元の点として定義
  class Point2D {
    constructor (x = 0, y = 0) {
      this.x = x;
      this.y = y;
    }
  }


  //単純な3次元の点
  class Point3D extends Point2D {
    constructor (x, y, z = 0) {
      super (x, y);
      this.z = z;
    }
    
    static average (...vertex) {
      let
        len = vertex.length,
        ax = 0, ay = 0, az = 0;

      vertex.forEach (p => {
        let {x, y, z} = p;
        ax += x, ay += y, az += z;
      });

      return new Point3D (ax / len, ay / len, az / len);
    }
  }


  //__________

  this.Point2D = Point2D;
  this.Point3D = Point3D;
}



{ //点・線・面を定義する

  const
    DEF_COLOR  = [255, 255, 255, .5],
    DEF_OPTION = {
      disabled: false,
      size    : 2
    };


  //空間に表現するための「点」
  class Point extends Point3D {

    constructor ([x, y, z], color = new Color(...DEF_COLOR), option = DEF_OPTION) {
      super (x, y, z);
      this.color  = color;
      this.option = Object.assign ({ }, DEF_OPTION, option);
    }
  }
  
  
  //面を構成する「線」
  class Line {
    constructor (list = [ ], color = new Color(...DEF_COLOR), option = { }) {
      this.list      = list;
      this.color     = color;
      this.option    = Object.assign ({ }, DEF_OPTION, option);
    }
    
    addLine (vertex_no) {
      let len = this.list.length;
      this.list.push (vertex_no);
      return len;
    }
  }


  //「面」
  class Surface {

    constructor (lineList = [ ], color = new Color(...DEF_COLOR), option = { }) {
      this.lineList  = lineList;
      this.color     = color;
      this.option    = Object.assign ({ }, DEF_OPTION, option);
    }


    //面の3点を利用して面のベクトルを返す。面自体には点の座標の情報が無いので注意
    getVector (pointList) {
      let
        [i0, i1, i2] = this.lineList,
        {x: x0, y: y0, z: z0} = pointList[i0],
        {x: x1, y: y1, z: z1} = pointList[i1],
        {x: x2, y: y2, z: z2} = pointList[i2],

        [px, py, pz] = [x1 - x0, y1 - y0, z1 - z0], //p
        [qx, qy, qz] = [x2 - x1, y2 - y1, z2 - z1]; //q

      return new Vector (
        py * qz - pz * qy,
        pz * qx - px * qz,
        px * qy - py * qx
      );
    }


    getDistance (pointList) {
      let
        [i0, i1, i2] = this.lineList,
        {x: x0, y: y0, z: z0} = pointList[i0],
        {x: x1, y: y1, z: z1} = pointList[i1],
        {x: x2, y: y2, z: z2} = pointList[i2],

        x = (x0 + x1 + x2) / 3,
        y = (y0 + y1 + y2) / 3,
        z = (z0 + z1 + z2) / 3;

      return new Point3D (x, y, z);

    }
  }

  //__________

  this.Point   = Point;
  this.Line    = Line;
  this.Surface = Surface;
}



{ //モデルを定義する

  const
    DEF_COLOR  = new Color (),
    DEF_OPTION = {
      disabled: false
    };


  //__________

  class Model {

    constructor (vertex = [ ], surface = [ ], option = { }) {
      this.vertex  = vertex;  //頂点
      this.surface = surface; //面
      this.option  = Object.assign ({ }, DEF_OPTION, option);
    }


    //面を追加する
    addSurface (surface) {
      return this.surface.push (surface) -1;
    }


    //頂点リストに追加する
    addVertex (vertex) {
      return this.vertex.push (vertex) -1;
    }


    //頂点の色をセットする
    setVertexColor (color, no = null) {
      let i, p;

      if (null === no)
        for (i = 0; p = this.vertex[i]; i += 1)
          Object.assign (p.color, color);
      else
        if (p = this.vertex[no])
          Object.assign (p.color, color);
    }


    //面の色をセットする
    setSurfaceColor (color, no = null) {
      let p;
      if (null === no)
        for (let i = 0; p = this.surface[i]; i += 1)
          Object.assign (p.color, color);
      else
        if (p = this.surface[no])
          Object.assign (p.color, color);
    }


    //物体の頂点の配列を返す
    getViewPoint () {
      return this.vertex.map (p => [p.x, p.y, p.z]);
    }
  }

  //__________

  this.Model = Model;
}





{ //ベクトルの定義

  class Vector extends Point3D {

    constructor (x, y, z) {
      super (x, y, z);
    }


    //加算
    add ({x, y, z}) {
      this.x += x;
      this.y += y;
      this.z += z;
    }


    //内積
    innerProducts ({x, y, z}) {
      return this.x * x + this.y * y + this.z * z;
    }
  }

  //__________

  this.Vector = Vector;
}


//___________________________________
// 焦点距離(Focal Length), 画角(Field Of View)
//http://www.cyber.t.u-tokyo.ac.jp/~tani/class/mech_enshu/enshu2011mi2.pdf

{ // カメラ

  const
    FOCAL_LENGTH = 50, //mm 標準レンズの焦点距離
    DEF_POSITION = [0, 0, 1000],
    DEF_OPTION   = {
      disabled: false,
      scale: 1
    };


  class Camera  {

    constructor (position = new Point3D (...DEF_POSITION), focalLength = FOCAL_LENGTH, option = { }) {
      this.position = position;
      this.FOV      = focalLength; //2 * Math.atan ((APERTURE_X / 2) / focalLength);
      this.option   = Object.assign ({ }, DEF_OPTION, option);
      this.Z        = position.z;
      this.f0       = this.option.scale * (focalLength / (1 + focalLength / position.z))
    }


    //3次から2次へ投影
    project ({x, y, z}) {
      let {Z, f0} = this, s = f0 / (Z - z);
      return [x * s, y * s];
    }
  }

  //__________

  this.Camera = Camera;
}

{
  const
    EYE_POINT    = new Point3D (3000, 0, 0),
    TRGET_POINT  = new Point3D (0, 0, 0),
    UP_VECTOR    = new Vector (0, 1, 0),
    FOCAL_LENGTH = 50, //mm 標準レンズの焦点距離
    DEF_POSITION = [0, 0, 1000],
    DEF_OPTION   = {
      disabled: false,
      scale: 1
    };


  class VCamera {
    constructor (eye = EYE_POINT, target = TRGET_POINT, upVector = UP_VECTOR, option = { }) {
      this.eye      = eye; //視点
      this.target   = target; //目標点
      this.upVector = upVector; //カメラの上方向のベクトル
      this.option   = Object.assign ({ }, DEF_OPTION, option);
    }

    //3次から2次へ投影
    project ({x, y, z}) {
      let
        { x: ex, y: ey, z: ez } = this.eye;
        

    }
  
  }
}


{ //照明(将来的に点光源など複数の照明を使えるようにするべき)

  const
    SQRT        = Math.sqrt,
    DEF_COLOR   = [255, 255, 255, 1],
    DEF_VECTOR  = [-1, 0, 0], //平行光なのでベクトルだけ
    DEF_OPTION  = {
      disabled     : false,
      distance     : 3000,
      targetPoint  : new Point3D (0, 0, 0),
      brightness   : 1, //明るさ
      ambientLight : 0, //環境光
      kc           : 0, //一定減衰定数
      kl           : .1, //1次減衰定数
      kq           : .2, //2次減衰定数
      kb           : 100000000 //光源の明るさ
    };

  const
    init = function () {
      let
        { distance, targetPoint } = this.option,
        { x, y, z} = this.vector,
        { x: tx, y: ty, z: tz } = targetPoint,
        dd = distance * distance;
        xyz = x * x + y * y + z * z;

      this.position = new Point3D (
        SQRT (dd * x / xyz) + tx,
        SQRT (dd * y / xyz) + ty,
        SQRT (dd * z / xyz) + tz
      );

      this.distance     = distance; //距離
    };


  class Light {

    constructor (vector = new Vector (...DEF_VECTOR), color = new Color (...DEF_COLOR), option = { }) {

      this.vector       = vector;
      this.color        = color;
      this.option       = Object.assign ({ }, DEF_OPTION, option);

      init.call (this);
    }
    
    
    getBrightness (p) {
      let 
        {x, y, z} = p,
        { x: tx, y: ty, z: tz } = this.position,
        { kc, kl, kq, kb } = this.option,
        dx = tx - x, dy = ty - y, dz = tz - z,
        d = SQRT (dx * dx + dy * dy + dz * dz);

      return kb / (kc + kl * d + kq * d * d);
    }
  }

  //__________

  this.Light = Light;
}




{ //マウスのドラッグ操作で配列の回転を制御(QUATERNIONによる回転)

  const
    SQRT            = Math.sqrt,
    SIN             = Math.sin,
    COS             = Math.cos,
    PI              = Math.PI,
    DEG             = PI / 180,

    TOUCH_EVENT     = ['touchstart', 'touchend', 'touchmove'],
    MOUSE_EVENT     = ['mousedown', 'mouseup', 'mousemove', 'mouseout'],
    DEF_OPTION      = {
      inertia     : true, //慣性モード有効
      sensitivity : 500,  //マウスの感度
      gain        : 0.999 //慣性移動の減速率
    };


  //ローテーションクラス本体
  class RotationController {

    constructor (element = document.body, model, option = {}) {
      this.target      = element;
      this.model       = model;
      this.option      = Object.assign ({}, DEF_OPTION, option);
      this.mouseX      = null;//マウス座標の基点
      this.mouseY      = null;//マウス座標の基点
      this.touched     = false; //ドラッグ中か?
      this.animeId     = 0;
      this.timeStamp   = null; //慣性を行うかどうかのもう一つの基準
      this.dx          = 0;//マウスの慣性移動量
      this.dy          = 0;//マウスの慣性移動量
      this.sensitivity = 1 / this.option.sensitivity; //mouse移動の感度
      this.inertia_min = 0.001; //最小の移動量で完成移動を止める

      //touchイベントがあるなら優先
      (window.TouchEvent ? TOUCH_EVENT: MOUSE_EVENT)
        .forEach (addEventType, this)
    }


    //各イベント処理
    handleEvent (event) {
      let e = event.target;
      switch (event.type) {

      // 制御終了
      case 'mouseup' :
      case 'mouseout' :
      case 'touchend' :
        if (this.touched){
          this.touched = false;
          if (event.timeStamp - this.timeStamp < 50) //mouseup から 50mm秒以内なら慣性モードへ
            if (this.option.inertia)
              inertia.call (this);//制御を慣性に移す
        }
        break;

      // 制御開始
      case 'mousedown' :
      case 'touchstart' :
        if (this.animeId) {
          cancelAnimationFrame (this.animeId);
          this.animeId = 0;
        }
        this.dx = 0;
        this.dy = 0;
        this.mouseX = event.pageX;
        this.mouseY = event.pageY;
        this.touched = true;
        break;

      // 回転制御中
      case 'mousemove' :
      case 'touchmove' :
        if (this.touched) {
          event.preventDefault ();//ipadなどでスクロールさせないため
          this.timeStamp = event.timeStamp;
          let
            {pageX, pageY} = event,
            {mouseX, mouseY, sensitivity} = this,
            sx = mouseX - pageX,
            sy = mouseY - pageY;
          if (sx || sy) {
            this.dx = sx * sensitivity;
            this.dy = sy * sensitivity;

            rotation.call (this);
            this.mouseX = pageX;
            this.mouseY = pageY;
          }
        }
        break;
      }
    }

    //quaternion rotation
    //回転の関数の独立した呼び出し
    static rotation (aryPoint3D, qx, qy, qz, qa) {
      let ql = qx * qx + qy * qy + qz * qz;

      if (ql) {
        let qh = qa * DEG * .5, s = SIN (qh) / SQRT (ql);
        product (aryPoint3D, qx * s, qy * s, qz * s, COS (qh));
      }
    }
  }


  const
    //イベントの登録
    addEventType =
      function (eventType) {
        this.target.addEventListener (eventType, this, false);
      },


    //慣性モードに移行
    inertia =
      function () {
        let gain = this.option.gain;
        this.dx *= gain;
        this.dy *= gain;
        if (this.inertia_min < rotation.call (this))
          this.animeId = requestAnimationFrame (inertia.bind (this));
      },


    //回転を行う
    rotation =
      function () {
        //画面の2次元移動量から3次元の回転量を求める
        let
          {dx, dy} = this,
          t = dx * dx + dy * dy;

        if (t) {
          // クオータニオンによる回転
          t = SQRT (t);
          let as = -SIN (t) / t;
          product (this.model.vertex, dy * as, dx * as, 0, COS (t));
        }
        return t; //移動距離を返す(慣性?)
      },


    //積(座標)の計算
    product =
      function (point, q0, q1, q2, q3) {
        for (let i = 0, p; p = point[i]; i++) {
          let
            {x, y, z} = p,
            a0 =  q3 * x + q1 * z - q2 * y,
            a1 =  q3 * y + q2 * x - q0 * z,
            a2 =  q3 * z + q0 * y - q1 * x,
            a3 = -q0 * x - q1 * y - q2 * z;

          point[i].x = a0 * q3 - a3 * q0 - a1 * q2 + a2 * q1;
          point[i].y = a1 * q3 - a3 * q1 - a2 * q0 + a0 * q2;
          point[i].z = a2 * q3 - a3 * q2 - a0 * q1 + a1 * q0;
        }
      };

  //__________

  this.RotationController = RotationController;
}



{// Render (Canvas)

  const
    DEF_OPTION = {
      surface     : true,
      hiddenSurface: true,
      lighting    : true,
      wireFrame   : false,
      vertex      : false,
      clsColor    : 'rgb(0,0,0)'
    },
    PI2 = Math.PI * 2;


  class Render {

    constructor (canvas, camera, light, option = { }) {
      this.canvas = canvas;
      this.ctx    = canvas.getContext ('2d');
      this.camera = camera;
      this.light  = light;
      this.option = Object.assign ({}, DEF_OPTION, option);
      this.width  = canvas.width;
      this.height = canvas.height;
      this.centerX= canvas.width  * 0.5 + 0.5;
      this.centerY= canvas.height * 0.5 + 0.5;
    }


    //CANVASを塗りつぶす
    clsScreen (rgba = this.option.clsColor) {
      let {ctx, width, height} = this;
      ctx.fillStyle = rgba;
      ctx.fillRect (0, 0, width, height);
    }


    //model を描く
    draw (model) {
      let
        cos  = Math.cos,
        acos = Math.acos,
        sqrt = Math.sqrt,
        max  = Math.max,
        {ctx, centerX, centerY, light, camera, option} = this,
        {vertex, surface} = model,
        zbuf = [ ],
        {distance, option: {brightness: lb, ambientLight: la}, vector: {x: lx, y: ly, z: lz}} = light;


      //カメラからの投影結果をxy[]に保存
      let xy = vertex.map (s => {
        let [px, py] = camera.project (s);
        return [centerX + px, centerY - py];
      });

      //面の構成が左回りを利用して2次元上で裏の向きを省く
      if (option.hiddenSurface) {
        let
          {x: cx, y: cy, z: cz} = camera.position;
        zbuf = surface.reduce ((a, b) => {
          let
            [p0, p2, p1] = b.lineList,//3点からベクトルの外積
            [[x0, y0], [x1, y1], [x2, y2]] = [xy[p0], xy[p1], xy[p2]];
          if ((x2 - x1) * (y0 - y1) - (y2 - y1) * (x0 - x1) > 0)
            a.push (b);
          return a;
        }, []);
      } 
      else
        zbuf = surface.slice (0);


      //距離でソート
      zbuf = zbuf.map (s => {
        let
          {x: cx, y: cy, z: cz} = camera.position,
          {x, y, z} = s.getDistance (vertex),
          dx = x - cx, dy = y - cy, dz = cz -z;
        return [dx*dx + dy*dy + dz*dz, s];
      })
        .sort ((a, b) => a[0] < b[0])
        .map ((a)=> a[1]);


      //面の描画
      let brightness = 1;
      zbuf.forEach (s => {
        let P = s.lineList;
        if (option.lighting) {
          let
            {x, y, z} = s.getVector (vertex),
            len = (x*lx + y*ly + z*lz) / (distance * sqrt (x*x + y*y + z*z)),
            av = Point.average (...P.map (n => vertex[n]));
          brightness = la + max (0, len * light.getBrightness (av));
        }
        ctx.fillStyle = s.color.multiplication (brightness).toStringRGBA ();
        ctx.beginPath ();
        ctx.moveTo (...xy[P[0]]);
        P.forEach ((p, i) => { ctx.lineTo (...xy[P[i]]) });
        ctx.fill ();
      });



    }
  }

  //__________

  this.Render = Render;
}



{ //物体の定義
  const
    PI     = Math.PI,
    ACOS   = Math.acos,
    ATAN2  = Math.atan2,
    SQRT   = Math.sqrt,
    SIN    = Math.sin,
    COS    = Math.cos,


    DEF_OPTION_MODEL = {
      size         : 1,
      pointColor   : new Color (255, 255, 255, 1),
      surfaceColor : new Color (255, 255, 255, .5)
    },



    //ドーナツの定義
    DEF_OPTION_DOUGHNUT = {
      radius       : 1,
      N            : 24,
      radius2      : .2,
      N2           : 12,
      pointColor   : new Color (255, 255, 255, 1),
      surfaceColor : new Color (255, 255, 255, .5)
    },
    
    Doughnut = function (option = { }) {
      option    = Object.assign ({ }, DEF_OPTION_DOUGHNUT, option);
      let
        {radius: r0, N: n0, radius2: r1, N2: n1} = option,
        pi2 = PI + PI,
        s0 = pi2 / n0, s1 = pi2 / n1,
        all = n0 * n1,
        point = [ ], surface = [ ];

      for (let i = 0; i < n0; i++) {
        let
          angle = s0 * i,
          idx = i * n1,
          idx2 = (idx + n1) % all;

        for (let j = 0; j < n1; j++) {
          let
            angle2 = s1 * j,
            r = r0 + SIN (angle2) * r1,
            jn = (j + 1) % n1;

          point.push (new Point (
            [SIN (angle) * r, COS (angle2) * r1, COS (angle) * r],
            option.pointColor.crone (),
            option
          ));

          surface.push (new Surface (
            [idx + j, idx + jn, idx2 + jn, idx2 + j],
            option.surfaceColor.crone (),
            option
          ));
        }
      }

      return [point, surface, option];
    },
    

    //立方体の定義
    Squares = function (option = { }) {
      option    = Object.assign ({ }, DEF_OPTION_MODEL, option);
      let
        {size, pointColor: pcol, surfaceColor: scol} = option,
        a = size * .5,
        point = [
          [-a, a, a], [a, a, a], [a, a, -a], [-a, a, -a],
          [-a,-a, a], [a,-a, a], [a,-a, -a], [-a,-a, -a]
        ].map (p => new Point (p, pcol.crone (), option)),
        
        surface = [
          [0,1,2,3], [0,4,5,1], [1,5,6,2], [2,6,7,3], [3,7,4,0], [4,7,6,5]
        ].map (p => new Surface (p, scol.crone (), option));

      return [point, surface, option];
    },


    //正二十面体を定義する
    DEF_OPTION_REGULAR_ICOSAHEDRON = {
      radius       : 100,
      pointColor   : new Color (255, 255, 255, 1),
      surfaceColor : new Color (255, 255, 255, .5)
    },

    RegularIcosahedron =
      function (option = { }) {
        option    = Object.assign ({ }, DEF_OPTION_REGULAR_ICOSAHEDRON, option);
        let
          r  = .5,
          gr = (1 + SQRT (5)) / 2,
          a = r / SQRT (1 + gr * gr),
          b = a * gr,

          point = [ //物体の頂点
            [ 0,-a,-b], [ 0, a,-b], [ 0,-a, b], [0, a, b],
            [-b, 0,-a], [-b, 0, a], [ b, 0,-a], [b, 0, a],
            [-a,-b, 0], [ a,-b, 0], [-a, b, 0], [a, b, 0]
          ].map (p => new Point (p, option.pointColor, option)),

          surface = [//辺の順序が面を現す
              [ 0, 1, 6], [ 1, 0, 4], [ 2, 3, 5], [ 3, 2, 7],
              [ 4, 5,10], [ 5, 4, 8], [ 6, 7, 9], [ 7, 6,11],
              [ 8, 9, 2], [ 9, 8, 0], [10,11, 1], [11,10, 3],
              [ 0, 6, 9], [ 0, 8, 4], [ 1, 4,10], [ 1,11, 6],
              [ 2, 5, 8], [ 2, 9, 7], [ 3, 7,11], [ 3,10, 5]
          ].map (p => new Surface (p, option.surfaceColor, option));

        return [point, surface, option];
      },


    //正四面体を定義する
    DEF_OPTION_REGULAR_TETRAHEDRON = {
      radius       : 100,
      pointColor   : new Color (255, 255, 255, 1),
      surfaceColor : new Color (255, 255, 255, .5)
    },

    RegularTetrahedron =
      function (option = { }) {
        option = Object.assign ({ }, DEF_OPTION_REGULAR_TETRAHEDRON, option);
        let
          r  = option.radius,
          a = 1/SQRT(3/8) * r,
          h = SQRT(2/3)*a,
          b = -h + r,

          point = [ //物体の頂点
            [0, r, 0], [0, b, r], [a / 2, b, b], [-a/2, b, b]
          ].map (p => new Point (p, option.pointColor, option)),

          surface = [//辺の順序が面を現す(先に登録された頂点の番号)
            [ 0, 1, 2], [ 0, 2, 3], [ 0, 3, 1], [ 3, 2, 1],
          ].map (p => new Surface (p, option.surfaceColor, option));

        return [point, surface, option];
      },
      
      
    //角柱を定義する
    DEF_OPTION_PRISM = {
      radius       : 100,
      pointColor   : new Color (255, 255, 255, 1),
      surfaceColor : new Color (255, 255, 255, .5)
    },

    Prism =
      function (option = { }) {
        option = Object.assign ({ }, DEF_OPTION_PRISM, option);
        let
          r  = option.radius,
          N  = option.N,
          h  = r / 2,
          S  = (PI + PI) / N,
          vertex = [ ], surface = [ ], bottom = [ ], top = [ ];
        
        for (let i = 0; i < N; i++) {
          let s = S * i, i2 = i * 2, sir = SIN (s) * r, cir = COS (s) * r;
          vertex.push (new Point ([sir, h, cir], option.pointColor, option));
          vertex.push (new Point ([sir, -h, cir], option.pointColor, option));
          surface.push (new Surface ([i2, i2 + 1, ((i2 + 2) % (N*2))+1, ((i2 + 2) % (N*2))], option.surfaceColor, option));
          top.push (i2);
          bottom.push ((i2 + 1));
        }
        surface.push (new Surface (top, option.surfaceColor, option));
        surface.push (new Surface (bottom.reverse (), option.surfaceColor, option));


        return [vertex, surface, option];
      };
      


  const
    createModel =
      function (type, option) {
        switch (type) {
        case 'regularIcosahedron' : return RegularIcosahedron (option); //正二十面体
        case 'regularTetrahedron' : return RegularTetrahedron (option); //正四面体
        case 'doughnut'           : return Doughnut (option); //ドーナツ
        case 'pyramid'            : return Pyramid (option); //多角推
        case 'squares'            : return Squares (option); //立方体
        case 'prism'              : return Prism (option); //角柱
        default :
          throw new Error ('無効な形状です')
        }
      };
  
  
  //________________________


const     //2つのベクトルの中間を返す
    createPoint = function (a, b, r) {
      let
        { x: ax, y: ay, z: az } = a,
        { x: bx, y: by, z: bz } = b;
        cx = (ax + bx) * .5, cy = (ay + by) * .5, cz = (az + bz) * .5,
        d = cx * cx + cy * cy + cz * cz, d2 = d * 2,
        e = SQRT (r * 2 * d2) / d2;
      return new Point ([cx * e, cy * e, cz * e]);
    };


  
  class Modeler extends Model {

    constructor (type, option) {
      super (...createModel (type, option));
    }
    
    
    //面の細分化
    subdivideSurfaces (n = 1) {
      if (n < 1 || 10 < n)
        throw new Error ('引数が範囲外です');

      let
        V = this.vertex,
        R = this.option.radius,
        RR = R * R;

      for (let i = 0; i < n; i++) {
        this.surface.forEach (s => {
          let
            {lineList, color, option} = s,
            [p0, p1, p2] = lineList,
            [ a,  b,  c] = [V[p0], V[p1], V[p2]],
            [ d,  e,  f] = [createPoint (a, b, RR), createPoint (b, c, RR), createPoint (c, a, RR)],
            [dn, en, fn] = [this.addVertex (d), this.addVertex (e), this.addVertex (f)];

          //従来の三角形を4分割した中央の三角形にする
          s.lineList = [dn, en, fn];
          [V[dn], V[en], V[fn]] = [d, e, f];
          //他の3つの三角形の面を追加する
          this.addSurface (new Surface ([p0, dn, fn], color, option));
          this.addSurface (new Surface ([p1, en, dn], color, option));
          this.addSurface (new Surface ([p2, fn, en], color, option));
        });
      }

      return this;
    }
    
    
    subdivide (surfaceNo = [ ], n = 1) {
      let
        V = this.vertex,
        A = [ ], //分割しない面
        B = [ ]; //分割対象面
      
      if (0 === surfaceNo.length)
        B = this.surface;
      else {
        let a = [...Array.from (this.surface).key ()];
        surfaceNo.forEach (n => a.splice (n, 1));
        A = a.map (n => this.surface[n]);
        B = surfaceNo.map (n => this.surface[n]);
      }
      
      for (let i = 0; i < n; i++) {
        let C = [ ];
        B.forEach (s => {
          let
            type = s.lineList.length,
            {lineList, color, option} = s;

          if (3 === type) {
            //面の2分化(三角形ABCの辺ABを二等分した点Dと、点Cを結ぶ線で二分する)
            //2つの三角形はそれぞれ、三角形CDBと三角形
            let
              [a, b, c] = lineList,
              d = this.addVertex (Point.average (...[V[a], V[b]]));

            C.push (new Surface ([c, a, d], color, option));
            C.push (new Surface ([b, c, d], color, option));

          } else if (3 < type) {
            //面の頂点の数が3を越える場合は、面の頂点の中心(平均)点を中心とした三角形を形成
            let
              n = this.addVertex (Point.average (...lineList.map (n => V[n])));

            for (let i = 0, I = lineList.length; i < I; i++) {
              C.push (new Surface ([lineList[i], lineList[(i+1)%I], n], color, option));
            }
          }

        });

        B = C;
      }
      this.surface = A.concat (B);
      
      return this;
    }
    




    //面を選択する(余計な面、点を排除する)
    selectTo (surfaceNo = []) {
      if (0 === surfaceNo.length)
        throw new Error ('引数がありません');

      let
        { vertex, surface } = this,
        V = [ ], //整理された頂点
        M = [ ], //頂点の変換表
        S = surfaceNo.map (n => surface[n]);//面を選択

      for (let i = 0, I = S.length; i < I; i++) {
        let l = S[i].lineList;
        for (let j = 0, J = l.length; j < J; j++) {
          let no = l[j], idx = M[no];
          if ('undefined' === typeof idx) //変換表にあるか?
            M[no] = idx = V.push (vertex[no]) -1;
          S[i].lineList[j] = idx;
        }
      }      

      this.surface = S;
      this.vertex = V;
    }
    
    
    
    //拡大・縮小
    scaleTo (nx = 1, ny = nx, nz = nx) {
      if (0 >= nx || 0 >= ny || 0 >= nz)
        throw new Error ('範囲外の数値が指定されました');
        
      this.vertex.forEach (p => { p.x *= nx, p.y *= ny, p.z *= nz});

      return this;
    }


    //移動
    moveTo (nx = 0, ny = 0, nz = 0) {
      this.vertex.forEach (p => { p.x += nx, p.y += ny, p.z += nz});
      return this;
    }
    
    //コピー
    copyTo () {
      let {vertex, surface, option} = this;
      return new Model (vertex, surface, option);
//      return Object.assign ({ }, this);
    }
    

    //回転
    rotationTo (angleX, angleY, angleZ, angle) {
      let
        deg = PI / 180,
        q0 = SIN (angleX * deg /2),
        q1 = SIN (angleY * deg /2),
        q2 = SIN (angleZ * deg /2),
        q3 = COS (angle * deg / 2);
      
      this.vertex.forEach (p => {
        let
          {x, y, z} = p,
          a0 =  q3 * x + q1 * z - q2 * y,
          a1 =  q3 * y + q2 * x - q0 * z,
          a2 =  q3 * z + q0 * y - q1 * x,
          a3 = -q0 * x - q1 * y - q2 * z;

        p.x = a0 * q3 - a3 * q0 - a1 * q2 + a2 * q1;
        p.y = a1 * q3 - a3 * q1 - a2 * q0 + a0 * q2;
        p.z = a2 * q3 - a3 * q2 - a0 * q1 + a1 * q0;      
      });

      return this;
    } 



  }

  //__________

  this.Modeler = Modeler;
}



{ //demo

  const
    CANVAS = document.querySelector ('canvas'),
    CAMERA = new Camera (new Point3D (0, 0, 500), 50/*mm*/, {scale: 10}),

    OPACITY = 1,
    MCtrl_MODEL_OPT  = { radius: .5, N: 24, radius2: .1, N2: 24},
    MCtrl_ROTE_OPT   = {gain: 1},
    MCtrl_LIGHT_OPT  = {ambientLight: 0.1},
    MCtrl_RENDER_OPT = {hiddenSurface: true},

    MCtrl_MODEL      = new Modeler ('squares', MCtrl_MODEL_OPT),
    MCtrl_ROTE       = new RotationController (CANVAS, MCtrl_MODEL, MCtrl_ROTE_OPT),
    MCtrl_LIGHT      = new Light (new Vector (1, 5, 10), new Color (), MCtrl_LIGHT_OPT),
    MCtrl_RENDER     = new Render (CANVAS, CAMERA, MCtrl_LIGHT, MCtrl_RENDER_OPT),


    rnd =
      function () { return Math.floor (Math.random() * 128+128); },


    setRandomColor =
      function (model) {
        let i, surface;

        for (i = 0; surface = model.surface[i]; i++)
          surface.color = new Color (rnd (), rnd (), rnd (), OPACITY);
      },

    setSurfaceColor =
      function (model, col) {
        model.surface.forEach (s => s.color = col);
      },


    loop = function loop () {
      MCtrl_RENDER.clsScreen ();
      MCtrl_RENDER.draw (MCtrl_MODEL);

      requestAnimationFrame (loop);
    };
  
  //_____________

  MCtrl_MODEL.scaleTo (300, 300, 300);//subdivideSurfaces (2);
  MCtrl_MODEL.subdivide ([], 7);
  setSurfaceColor (MCtrl_MODEL, new Color (255,100,100,OPACITY));
  this.demo = loop;
}

//___________________________________

demo ();

</script>