WebGLを使って正多面体ビューアーを作る

WebGL の作法を学ぶため作ってみました。

「[正多面体のデータを作る[改] (HTML+JavaScript版)](https://qiita.com/ikiuo/items/d7486f40f2378af15288)」の大幅改造版です。

select, input 等で設定を変更したり、正多面体の向き等をマウスイベントを使って操作可能にしています。

<p class="codepen" data-height="1100" data-slug-hash="NWBaZWO" data-user="ikiuo" style="height: 1100px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ikiuo/pen/NWBaZWO">
  Regular Polyhedron Viewer</a> by Ikiuo (<a href="https://codepen.io/ikiuo">@ikiuo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

<details><summary>スクリーンショット</summary><div>

![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226677/7159d571-d86a-4be4-d015-2c37ffdada8f.png)

</div></details>

単体の HTML ファイルで動作します。他のファイルに依存していません。

動作確認は Google Chrome バージョン: 108.0.5359.124 (macOS版) で行っています。

<details><summary><strong>RegularPolyhedronViewer.html</strong>（長いので折りたたみ）</summary>

```html:RegularPolyhedronViewer.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset='utf-8'>
    <title>正多面体表示</title>
    <style type="text/css">
      body {
          margin: auto;
          width: 640px;
      }
      canvas {
          border: 0px;
          background-color: black;
      }
      table {
          border: solid 0px;
          border-left: solid 1px lightgray;
          border-top: solid 1px lightgray;
          border-collaspe: collaspe;
          border-spacing: 0;
      }
      th, td {
          border: solid 0px;
          border-right: solid 1px lightgray;
          border-bottom: solid 1px lightgray;
          padding: 4px;
      }
      .slider {
          width: 6em;
      }
    </style>
  </head>
  <body>
    <h2 style="text-align: center;">正多面体表示</h2>
    <canvas id="canvas" width="640" height="480">
      このブラウザは &lt;canvas&gt; をサポートしてますか？
    </canvas>
    <table>
      <tr>
        <td>
          正多面体
        </td>
        <td>
          <table>
            <tr>
              <td>
                <select id="nfacet">
                  <option value="4">正四面体</option>
                  <option value="6">正六面体</option>
                  <option value="8">正八面体</option>
                  <option value="12">正十二面体</option>
                  <option value="20" selected>正二十面体</option>
                </select>
                <input id="dual"  type="checkbox"><label id="rpdual" for="dual">対</label>
              </td>
              <td><input id="dpnt"  type="checkbox"><label for="dpnt">点</label></td>
              <td><input id="dline" type="checkbox" checked><label for="dline">線</label></td>
              <td><input id="dfct"  type="checkbox" checked><label for="dfct">面</label></td>
              <td><input id="dnrml" type="checkbox"><label for="dnrml">法線</label></td>
              <td><input id="cface" type="checkbox"><label for="cface">裏面</label></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          向き
        </td>
        <td>
          <table>
            <tr>
              <td>
                <input type="button" value="初期化"
                       style="font-size: small;" onclick="resetAnimation();">
              </td>
              <td>
                <input id="arotate" type="checkbox"><label for="arotate">自動回転</label>
                | 速度 <input id="aspeed" class="slider" type="range" min="-5.0" max="5.0" step="0.1" value="0.4">
                <label for="aspeed"><small id="aspeed_value"></small></label>
              </td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          カメラ
        </td>
        <td>
          <table>
            <tr>
              <td>
                ズーム <input id="zoom" class="slider" type="range" min="0.50" max="50.0" step="0.1" value="10.0">
                <small id="zoom_value"></small>
              </td>
              <td>
                Z位置 <input id="zpos" class="slider" type="range" min="2.0" max="50.0" step="0.1" value="12.0">
                <small id="zpos_value"></small>
              </td>
              <td>
                仰角 <input id="pitch" class="slider" type="range" min="-80.0" max="+80.0" step="0.1" value="-20.0">
                <small id="pitch_value"></small>
              </td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          背景色
        </td>
        <td>
          <table>
            <tr>
              <td>赤 <input id="bgc_r" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.1"></td>
              <td>緑 <input id="bgc_g" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.2"></td>
              <td>青 <input id="bgc_b" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.4"></td>
              <td><small>(R,G,B) = <span id="background_color_value">(0.1, 0.2, 0.4)</span></small></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          点の色
        </td>
        <td>
          <table>
            <tr>
              <td>赤 <input id="pnt_r" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="1.0"></td>
              <td>緑 <input id="pnt_g" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="1.0"></td>
              <td>青 <input id="pnt_b" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="1.0"></td>
              <td><small>(R,G,B) = <span id="point_color_value">(1.0, 1.0, 1.0)</span></small></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          線の色
        </td>
        <td>
          <table>
            <tr>
              <td>赤 <input id="line_r" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td>緑 <input id="line_g" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td>青 <input id="line_b" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td><small>(R,G,B) = <span id="line_color_value">(0.8, 0.8, 0.8)</span></small></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          面の色
        </td>
        <td>
          <table>
            <tr>
              <td>赤 <input id="fct_r" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.0"></td>
              <td>緑 <input id="fct_g" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.6"></td>
              <td>青 <input id="fct_b" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.6"></td>
              <td><small>(R,G,B) = <span id="facet_color_value">(0.0, 0.6, 0.6)</span></small></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td>
          法線の色
        </td>
        <td>
          <table>
            <tr>
              <td>赤 <input id="nrml_r" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td>緑 <input id="nrml_g" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td>青 <input id="nrml_b" class="slider" type="range" min="0.0" max="1.0" step="0.01" value="0.8"></td>
              <td><small>(R,G,B) = <span id="normal_color_value">(0.8, 0.8, 0.8)</span></small></td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
    <small>
      <ul>
        <li>正多面体のデータは半径１の球に内接しています（頂点は単位ベクトル）</li>
        <li>
          対は頂点と法線を入れ替えてできる正多面体です<br/>
          面数の対応は 4→4, 6→8, 8→6, 12→20, 20→12 になります
        </li>
      </ul>
    </small>
    <p>
      <details><summary id="rpdata_name"></summary>
        <p><pre id="rpdata_vertex"></pre></p>
        <p><pre id="rpdata_normal"></pre></p>
        <p><pre id="rpdata_facet"></pre></p>
        <p><pre id="rpdata_edge"></pre></p>
      </details>
    </p>

    <!-- Shader: Vertex  -->

    <script id="vertex-shader" type="x-shader/x-vertex">
      uniform float uPointSize;
      attribute vec4 aVertexPosition;
      attribute vec4 aVertexNormal;
      uniform vec4 uVertexColor;

      uniform float uLightLevelMin;
      uniform vec4 uLightVector;
      uniform mat4 uProjectionMatrix;
      uniform mat4 uModelViewMatrix;

      varying lowp vec4 vColor;

      void main()
      {
          gl_PointSize = uPointSize;
          gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition.xyz, 1.0);

          highp vec4 vNormal = uModelViewMatrix * vec4(aVertexNormal.xyz, 0.0);
          highp float light = max(dot(uLightVector.xyz, vNormal.xyz), uLightLevelMin);
          light = max(min(1.0, light * 0.4 + 0.6), 0.0);
          // light = light * light;
          vColor = uVertexColor * light;
      }
    </script>

    <!-- Shader: Flagment  -->

    <script id="fragment-shader" type="x-shader/x-fragment">
      varying lowp vec4 vColor;

      void main()
      {
          gl_FragColor = vColor;
      }
    </script>

    <!-- JavaScript  -->

    <script>
      /* ********************************** */

      // 度(degree)をラジアンに変換.
      function DegToRad(deg) {
          return (deg % 360.0) * Math.PI / 180.0
      }

      // 画角（縦）の拡大率を角度(degree)から求める.
      function FovToZoom(deg) {
          return 1.0 / Math.tan(DegToRad(deg / 2));
      }

      /*
       * ベクトル
       */

      class Vector4 {

          // コンストラクタ.
          constructor() {
              this.S = Vector4;
              this.M = Matrix4;

              const args = ([...Array(arguments.length)]
                            .map((_, i) => arguments[i]));

              let vector;
              if (args.length == 0)
                  vector = Array(4).fill(0);
              else if (args.length == 1) {
                  vector = args[0].copyWithin();
                  if (vector.length == 3)
                      vector.push(1.0);
                  while (vector.length < 4)
                      vector.push(0.0);
                  vector = vector.slice(0, 4);
              } else if (args.length == 3) {
                  vector = args;
                  vector.push(1.0);
              } else {
                  vector = args;
                  while (vector.length < 4)
                      vector.push(0.0);
                  vector = vector.slice(0, 4);
              }

              this.vector = vector;
              this.isMatrix = false;
              this.isVector = true;
          }

          // ベクトルの大きさ (w=false で 3 要素)
          abs(w=false) {
              return Math.sqrt(this.dot(this, w));
          }

          // 逆 (w=false で[3]はそのまま)
          neg(rhs, w=false) {
              const vl = this.vector;
              return new this.S([
                  -vl[0],
                  -vl[1],
                  -vl[2],
                  (w ? -vl[3] : vl[3]),
              ]);
          }

          // 和 (w=false で[3]はそのまま)
          add(rhs, w=false) {
              const vl = this.vector;
              const vr = rhs.vector;
              return new this.S([
                  vl[0] + vr[0],
                  vl[1] + vr[1],
                  vl[2] + vr[2],
                  (w ? (vl[3] + vr[3]) : vl[3]),
              ]);
          }

          // 差 (w=false で[3]はそのまま)
          sub(rhs, w=false) {
              const vl = this.vector;
              const vr = rhs.vector;
              return new this.S([
                  vl[0] - vr[0],
                  vl[1] - vr[1],
                  vl[2] - vr[2],
                  (w ? (vl[3] - vr[3]) : vl[3]),
              ]);
          }

          // 乗算 (w=false で 3 要素)
          mul(rhs, w=false) {
              return ((typeof(rhs) == "number")
                      ? this.mulN(rhs, w)
                      : this.mulM(rhs));
          }

          // 実数倍 (w=false で[3]はそのまま)
          mulN(n, w=false) {
              const vl = this.vector;
              return new this.S([
                  vl[0] * n,
                  vl[1] * n,
                  vl[2] * n,
                  (w ? (vl[3] * n): vl[3]),
              ]);
          }

          // 行列との積 (w=false で[3]はそのまま)
          mulM(rhs, w=false) {
              const vl = this.vector;
              const mr = rhs.matrix;
              return this.M([
                  vl[0] * mr[0] + vl[1] * mr[4] + vl[2] * mr[ 8] + vl[3] * mr[12],
                  vl[0] * mr[1] + vl[1] * mr[5] + vl[2] * mr[ 9] + vl[3] * mr[13],
                  vl[0] * mr[2] + vl[1] * mr[6] + vl[2] * mr[10] + vl[3] * mr[14],
                  (w ? vl[0] * mr[3] + vl[1] * mr[7] + vl[2] * mr[11] + vl[3] * mr[15] : vl[3]),
              ]);
          }

          // 内積 (w=false で 3 要素).
          dot(rhs, w=false) {
              const vl = this.vector;
              const vr = rhs.vector;
              return (vl[0] * vr[0] +
                      vl[1] * vr[1] +
                      vl[2] * vr[2] +
                      (w ? vl[3] * vr[3] : 0.0));
          }

          // 外積 (3要素のみ)
          cross(rhs, w=0) {
              const vl = this.vector;
              const vr = rhs.vector;
              return new this.S([
                  vl[1] * vr[2] - vl[2] * vr[1],
                  vl[2] * vr[0] - vl[0] * vr[2],
                  vl[0] * vr[1] - vl[1] * vr[0],
                  w,
              ]);
          }

          // 実数除算 (w=false で[3]はそのまま).
          div(n, w=false) {
              const vl = this.vector;
              if (n != 0)
                  return new this.S([
                      vl[0] / n,
                      vl[1] / n,
                      vl[2] / n,
                      (w ? (vl[3] / n): vl[3]),
                  ]);
              throw 'division by zero';
          }

          // 単位ベクトルを得る (w=false で 3 要素)
          normalize(w=false) {
              return this.div(this.abs(w), w);
          }

          // 整数に近い数値を整数にする.
          static round(x) {
              const thr = 1 / (1 << 24);
              const xi = Math.trunc(x);
              return ((Math.abs(x - xi) < thr) ? xi : x);
          }
          fixInt() {
              const S = Vector4;
              const vector = this.vector;
              return new this.S([
                  S.round(vector[0]),
                  S.round(vector[1]),
                  S.round(vector[2]),
                  S.round(vector[3]),
              ]);
          }
      }

      /*
       * 行列
       */

      class Matrix4 {

          // -----------------

          // 単位行列を配列[16]で返す.
          static Identity() {
              return [
                  1.0, 0.0, 0.0, 0.0,
                  0.0, 1.0, 0.0, 0.0,
                  0.0, 0.0, 1.0, 0.0,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // 各軸の係数行列を配列[16]で返す.
          static Scale(x, y, z, w=1) {
              return [
                  + x, 0.0, 0.0, 0.0,
                  0.0,   y, 0.0, 0.0,
                  0.0, 0.0,   z, 0.0,
                  0.0, 0.0, 0.0,   w,
              ];
          }

          // 平行移動行列を配列[16]で返す.
          static Translate(x, y, z) {
              return [
                  1.0, 0.0, 0.0,   x,
                  0.0, 1.0, 0.0,   y,
                  0.0, 0.0, 1.0,   z,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // X 軸回転行列を配列[16]で返す.
          static XRotate(rad) {
              const rc = Math.cos(rad);
              const rs = Math.sin(rad);
              return [
                  1.0, 0.0, 0.0, 0.0,
                  0.0, +rc, -rs, 0.0,
                  0.0, +rs, +rc, 0.0,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // Y 軸回転行列を配列[16]で返す.
          static YRotate(rad) {
              const rc = Math.cos(rad);
              const rs = Math.sin(rad);
              return [
                  +rc, 0.0, +rs, 0.0,
                  0.0, 1.0, 0.0, 0.0,
                  -rs, 0.0, +rc, 0.0,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // Z 軸回転行列を配列[16]で返す.
          static ZRotate(rad) {
              const rc = Math.cos(rad);
              const rs = Math.sin(rad);
              return [
                  +rc, -rs, 0.0, 0.0,
                  +rs, +rc, 0.0, 0.0,
                  0.0, 0.0, 1.0, 0.0,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // 任意軸回転行列を配列[16]で返す.
          static VRotate(rad, vec) {
              const [x, y, z, w] = vec.normalize().vector;
              const rc = Math.cos(rad);
              const rs = Math.sin(rad);
              const xs = x * sin;
              const ys = y * sin;
              const zs = z * sin;
              const nc1 = 1.0 - rc;
              const x1c = x * nc1;
              const y1c = y * nc1;
              const z1c = z * nc1;
              return [
                  x * x1c + rc, x * y1c - zs, x * z1c + ys, 0.0,
                  y * x1c + zs, y * y1c + rc, y * z1c - xs, 0.0,
                  z * x1c - ys, z * y1c + xs, z * z1c + rc, 0.0,
                  0.0,          0.0,          0.0,          1.0,
              ];
          }

          // -----------------

          // 行列式を返す.
          static Det(ml) {
              const rval = 0.0;
              const [
                  m00, m01, m02, m03,
                  m10, m11, m12, m13,
                  m20, m21, m22, m23,
                  m30, m31, m32, m33,
              ] = ml;
              const [
                  m00_11, m00_12, m00_13, // m00_10
                  m01_10, m01_12, m01_13, // m01_11
                  m02_10, m02_11, m02_13, // m02_12
                  m03_10, m03_11, m03_12, // m03_13
                  m20_31, m20_32, m20_33, // m20_30
                  m21_30, m21_32, m21_33, // m21_31
                  m22_30, m22_31, m22_33, // m22_32
                  m23_30, m23_31, m23_32, // m23_33
              ] = [
                  m00 * m11, m00 * m12, m00 * m13,
                  m01 * m10, m01 * m12, m01 * m13,
                  m02 * m10, m02 * m11, m02 * m13,
                  m03 * m10, m03 * m11, m03 * m12,
                  m20 * m31, m20 * m32, m20 * m33,
                  m21 * m30, m21 * m32, m21 * m33,
                  m22 * m30, m22 * m31, m22 * m33,
                  m23 * m30, m23 * m31, m23 * m32,
              ];

              rval += m00_11 * m22_33; rval -= m00_11 * m23_32;
              rval += m00_12 * m23_31; rval -= m00_12 * m21_33;
              rval += m00_13 * m21_32; rval -= m00_13 * m22_31;

              rval += m01_12 * m20_33; rval -= m01_12 * m23_30;
              rval += m01_13 * m22_30; rval -= m01_13 * m20_32;
              rval += m01_10 * m23_32; rval -= m01_10 * m22_33;

              rval += m02_13 * m20_31; rval -= m02_13 * m21_30;
              rval += m02_10 * m21_33; rval -= m02_10 * m23_31;
              rval += m02_11 * m23_30; rval -= m02_11 * m20_33;

              rval += m03_10 * m22_31; rval -= m03_10 * m21_32;
              rval += m03_11 * m20_32; rval -= m03_11 * m22_30;
              rval += m03_12 * m21_30; rval -= m03_12 * m20_31;

              return rval;
          }

          // 逆行列を配列[16]で返す.
          static Inverse(ml) {
              const det = Matrix4.Det(ml);
              if (det == 0.0)
                  throw 'division by zero';
              const idet = 1.0 / det;
              const [
                  m00, m01, m02, m03,
                  m10, m11, m12, m13,
                  m20, m21, m22, m23,
                  m30, m31, m32, m33,
              ] = this.matrix;
              const [
                  i00, i01, i02, i03,
                  i10, i11, i12, i13,
                  i20, i21, i22, i23,
                  i30, i31, i32, i33
              ] = [
                  idet * m00, idet * m01, idet * m02, idet * m03,
                  idet * m10, idet * m11, idet * m12, idet * m13,
                  idet * m20, idet * m21, idet * m22, idet * m23,
                  idet * m30, idet * m31, idet * m32, idet * m33,
              ];
              const [
                  m10_21, m10_22, m10_23, // m10_20
                  m10_31, m10_32, m10_33, // m10_30
                  m11_20, m11_22, m11_23, // m11_21
                  m11_30, m11_32, m11_33, // m11_31
                  m12_20, m12_21, m12_23, // m12_22
                  m12_30, m12_31, m12_33, // m12_32
                  m13_20, m13_21, m13_22, // m13_23
                  m13_30, m13_31, m13_32, // m13_33
                  m20_31, m20_32, m20_33, // m20_30
                  m21_30, m21_32, m21_33, // m21_31
                  m22_30, m22_31, m22_33, // m22_32
                  m23_30, m23_31, m23_32, // m23_33
              ] = [
                  m10 * m21, m10 * m22, m10 * m23,
                  m10 * m31, m10 * m32, m10 * m33,
                  m11 * m20, m11 * m22, m11 * m23,
                  m11 * m30, m11 * m32, m11 * m33,
                  m12 * m20, m12 * m21, m12 * m23,
                  m12 * m30, m12 * m31, m12 * m33,
                  m13 * m20, m13 * m21, m13 * m22,
                  m13 * m30, m13 * m31, m13 * m32,
                  m20 * m31, m20 * m32, m20 * m33,
                  m21 * m30, m21 * m32, m21 * m33,
                  m22 * m30, m22 * m31, m22 * m33,
                  m23 * m30, m23 * m31, m23 * m32,
              ];
              return new this.S([
                  i11 * m22_33 - i11 * m23_32 + i12 * m23_31 - i12 * m21_33 + i13 * m21_32 - i13 * m22_31,
                  i01 * m23_32 - i01 * m22_33 + i02 * m21_33 - i02 * m23_31 + i03 * m22_31 - i03 * m21_32,
                  i01 * m12_33 - i01 * m13_32 + i02 * m13_31 - i02 * m11_33 + i03 * m11_32 - i03 * m12_31,
                  i01 * m13_22 - i01 * m12_23 + i02 * m11_23 - i02 * m13_21 + i03 * m12_21 - i03 * m11_22,

                  i10 * m23_32 - i10 * m22_33 + i12 * m20_33 - i12 * m23_30 + i13 * m22_30 - i13 * m20_32,
                  i00 * m22_33 - i00 * m23_32 + i02 * m23_30 - i02 * m20_33 + i03 * m20_32 - i03 * m22_30,
                  i00 * m13_32 - i00 * m12_33 + i02 * m10_33 - i02 * m13_30 + i03 * m12_30 - i03 * m10_32,
                  i00 * m12_23 - i00 * m13_22 + i02 * m13_20 - i02 * m10_23 + i03 * m10_22 - i03 * m12_20,

                  i10 * m21_33 - i10 * m23_31 + i11 * m23_30 - i11 * m20_33 + i13 * m20_31 - i13 * m21_30,
                  i00 * m23_31 - i00 * m21_33 + i01 * m20_33 - i01 * m23_30 + i03 * m21_30 - i03 * m20_31,
                  i00 * m11_33 - i00 * m13_31 + i01 * m13_30 - i01 * m10_33 + i03 * m10_31 - i03 * m11_30,
                  i00 * m13_21 - i00 * m11_23 + i01 * m10_23 - i01 * m13_20 + i03 * m11_20 - i03 * m10_21,

                  i10 * m22_31 - i10 * m21_32 + i11 * m20_32 - i11 * m22_30 + i12 * m21_30 - i12 * m20_31,
                  i00 * m21_32 - i00 * m22_31 + i01 * m22_30 - i01 * m20_32 + i02 * m20_31 - i02 * m21_30,
                  i00 * m12_31 - i00 * m11_32 + i01 * m10_32 - i01 * m12_30 + i02 * m11_30 - i02 * m10_31,
                  i00 * m11_22 - i00 * m12_21 + i01 * m12_20 - i01 * m10_22 + i02 * m10_21 - i02 * m11_20,
              ]);
          }

          // 転置行列を配列[16]で返す.
          static Transpose(ml) {
              return [
                  ml[0], ml[4], ml[ 8], ml[12],
                  ml[1], ml[5], ml[ 9], ml[13],
                  ml[2], ml[6], ml[10], ml[14],
                  ml[3], ml[7], ml[11], ml[15],
              ];
          }

          // -----------------

          // 配列[16]との和差を配列[16]で返す.
          static Add(ml, mr) { return ml.map((l, i) => l + mr[i]); }
          static Sub(ml, mr) { return ml.map((l, i) => l - mr[i]); }

          // 実数倍との積を配列[16]を返す.
          static MultiplyN(ml, r) {
              return [
                  ml[ 0] * r, ml[ 1] * r, ml[ 2] * r, ml[ 3] * r,
                  ml[ 4] * r, ml[ 5] * r, ml[ 6] * r, ml[ 7] * r,
                  ml[ 8] * r, ml[ 9] * r, ml[10] * r, ml[11] * r,
                  ml[12] * r, ml[13] * r, ml[14] * r, ml[15] * r,
              ];
          }

          // 配列[4]との積を配列[4]で返す.
          static MultiplyV(ml, vr) {
              return [
                  ml[ 0] * vr[0] + ml[ 1] * vr[1] + ml[ 2] * vr[ 2] + ml[ 3] * vr[3],
                  ml[ 4] * vr[0] + ml[ 5] * vr[1] + ml[ 6] * vr[ 2] + ml[ 7] * vr[3],
                  ml[ 8] * vr[0] + ml[ 9] * vr[1] + ml[10] * vr[ 2] + ml[11] * vr[3],
                  ml[12] * vr[0] + ml[13] * vr[1] + ml[14] * vr[ 2] + ml[15] * vr[3],
              ];
          }

          // 配列[16]との積を配列[16]で返す.
          static MultiplyM(ml, mr) {
              return [
                  ml[ 0] * mr[0] + ml[ 1] * mr[4] + ml[ 2] * mr[ 8] + ml[ 3] * mr[12],
                  ml[ 0] * mr[1] + ml[ 1] * mr[5] + ml[ 2] * mr[ 9] + ml[ 3] * mr[13],
                  ml[ 0] * mr[2] + ml[ 1] * mr[6] + ml[ 2] * mr[10] + ml[ 3] * mr[14],
                  ml[ 0] * mr[3] + ml[ 1] * mr[7] + ml[ 2] * mr[11] + ml[ 3] * mr[15],
                  ml[ 4] * mr[0] + ml[ 5] * mr[4] + ml[ 6] * mr[ 8] + ml[ 7] * mr[12],
                  ml[ 4] * mr[1] + ml[ 5] * mr[5] + ml[ 6] * mr[ 9] + ml[ 7] * mr[13],
                  ml[ 4] * mr[2] + ml[ 5] * mr[6] + ml[ 6] * mr[10] + ml[ 7] * mr[14],
                  ml[ 4] * mr[3] + ml[ 5] * mr[7] + ml[ 6] * mr[11] + ml[ 7] * mr[15],
                  ml[ 8] * mr[0] + ml[ 9] * mr[4] + ml[10] * mr[ 8] + ml[11] * mr[12],
                  ml[ 8] * mr[1] + ml[ 9] * mr[5] + ml[10] * mr[ 9] + ml[11] * mr[13],
                  ml[ 8] * mr[2] + ml[ 9] * mr[6] + ml[10] * mr[10] + ml[11] * mr[14],
                  ml[ 8] * mr[3] + ml[ 9] * mr[7] + ml[10] * mr[11] + ml[11] * mr[15],
                  ml[12] * mr[0] + ml[13] * mr[4] + ml[14] * mr[ 8] + ml[15] * mr[12],
                  ml[12] * mr[1] + ml[13] * mr[5] + ml[14] * mr[ 9] + ml[15] * mr[13],
                  ml[12] * mr[2] + ml[13] * mr[6] + ml[14] * mr[10] + ml[15] * mr[14],
                  ml[12] * mr[3] + ml[13] * mr[7] + ml[14] * mr[11] + ml[15] * mr[15],
              ];
          }

          // -------------------------------------

          // 行列用配列の作成.
          static argsToMatrix(args) {
              if (args.length == 0)
                  return Matrix4.Identity();
              if (args.length == 1) {
                  const arg = args[0];
                  if (arg.isMatrix)
                      return arg.matrix.copyWithin();
                  return Matrix4.argsToMatrix(arg);
              }
              if (args.length == 4)
                  return [
                      args[0], args[1], 0.0, 0.0,
                      args[2], args[3], 0.0, 0.0,
                      0.0,     0.0,     1.0, 0.0,
                      0.0,     0.0,     0.0, 1.0,
                  ];
              if (args.length == 9)
                  return [
                      args[0], args[1], args[2], 0.0,
                      args[3], args[4], args[5], 0.0,
                      args[6], args[7], args[8], 0.0,
                      0,0,     0.0,     0.0,     1.0,
                  ];

              let matrix = ([...Array(args.length)].map((_, i) => args[i]));
              while (matrix.length < 16)
                  matrix.push(0.0);
              matrix = matrix.slice(0, 16);
              return matrix;
          }

          // -------------------------------------

          // コンストラクタ.
          constructor() {
              const S = Matrix4;
              this.S = S;
              this.V = Vector4;
              this.isMatrix = true;
              this.isVector = false;
              this.matrix = S.argsToMatrix(arguments);
          }

          // Float32Array の配列を返す.
          toFloat32() { return new Float32Array(this.matrix); }

          // -----------------

          // 行列式.
          det() { return this.S.Det(this.matrix); }

          // 逆行列.
          aInverse() { return this.S.Inverse(this.matrix); }
          inverse() { return new this.S(this.aInverse()); }

          // 転置.
          aTranspose() { return this.S.Transpose(this.matrix); }
          transpose() { return new this.S(this.aTranspose()); }

          // -----------------

          // 和.
          aAdd(rhs) { return this.S.Add(this.matrix, rhs.matrix); }
          add(rhs) { return new this.S(this.aAdd(matrix)); }

          // 差.
          aSub(rhs) { return this.S.Sub(this.matrix, rhs.matrix); }
          sub(rhs) { return new this.S(this.aSub(matrix)); }

          // 積.
          aMulM(rhs) { return this.S.MultiplyM(this.matrix, rhs); }
          aMulV(rhs) { return this.S.MultiplyV(this.matrix, rhs); }
          aMulN(rhs) { return this.S.MultiplyN(this.matrix, rhs); }
          mulM(rhs) { return new this.S(this.aMulM(rhs.matrix)); }
          mulV(rhs) { return new this.V(this.aMulV(rhs.vector)); }
          mulN(rhs) { return new this.S(this.aMulN(rhs)); }

          // 回転.
          aRotateX(rad) { return this.aMulM(this.S.XRotate(rad)); }
          aRotateY(rad) { return this.aMulM(this.S.YRotate(rad)); }
          aRotateZ(rad) { return this.aMulM(this.S.ZRotate(rad)); }
          aRotateV(rad, vec) { return this.aMulM(this.S.VRotate(rad, vec)); }

          aRotateXDeg(deg) { return this.aRotateX(DegToRad(deg)); }
          aRotateYDeg(deg) { return this.aRotateY(DegToRad(deg)); }
          aRotateZDeg(deg) { return this.aRotateZ(DegToRad(deg)); }
          aRotateVDeg(rad, vec) { return this.aRotateVDeg(DegToRad(deg), vec); }

          rotateX(rad) { return new this.S(this.aRotateX(rad)); }
          rotateY(rad) { return new this.S(this.aRotateY(rad)); }
          rotateZ(rad) { return new this.S(this.aRotateZ(rad)); }
          rotateV(rad, vec) { return new this.S(this.aRotateV(rad, vec)); }

          rotateXDeg(deg) { return this.rotateX(DegToRad(deg)); }
          rotateYDeg(deg) { return this.rotateY(DegToRad(deg)); }
          rotateZDeg(deg) { return this.rotateZ(DegToRad(deg)); }
          rotateVDeg(deg, vec) { return this.rotateV(DegToRad(deg), vec); }

          // 各軸の係数.
          aScale(x, y, z, w=1.0) { return this.aMulM(this.S.Scale(x, y, x, w)); }
          scale(x, y, z, w=1.0) { return new this.S(this.aScale(x, y, z, w)); }

          // 平行移動.
          aTranslate(x, y, z) { return this.aMulM(this.S.Translate(x, y, z)); }
          translate(x, y, z) { return new this.S(this.aTranslate(x, y, z)); }

          // -----------------

          // 単位行列を設定する.
          loadIdentity() { this.matrix = this.S.Identity(); return this; }

          // 和差で更新.
          updateAdd(mr) { this.matrix = this.aAdd(mr); return this; }
          updateSub(mr) { this.matrix = this.aSub(mr); return this; }

          // -----------------

          // 各要素との積で更新.
          updateMulM(mr) { this.matrix = this.aMulM(mr.matrix); return this; }
          updateMulV(vr) { this.matrix = this.aMulV(vr.vector); return this; }
          updateMulN(nr) { this.matrix = this.aMulN(nr); return this; }

          // 各軸係数との積で更新.
          updateScale(x, y, z, w=1) {
              this.matrix = this.aScale(x, y, z, w);
              return this;
          }

          // 平行移動との積で更新.
          updateTranslate(x, y, z) {
              this.matrix = this.aTranslate(x, y, z);
              return this;
          }

          // 各軸回転との積で更新.
          updateRotX(rad) { this.matrix = this.aRotateX(rad); return this; }
          updateRotY(rad) { this.matrix = this.aRotateY(rad); return this; }
          updateRotZ(rad) { this.matrix = this.aRotateZ(rad); return this; }

          // 任意軸回転との積で更新.
          updateRotV(rad, vec) {
              this.matrix = this.aRotateV(rad, vec);
              return this;
          }

          // 各軸回転(degree)との積で更新.
          updateRotXDeg(deg) { return this.updateRotX(DegToRad(deg)); }
          updateRotYDeg(deg) { return this.updateRotY(DegToRad(deg)); }
          updateRotZDeg(deg) { return this.updateRotZ(DegToRad(deg)); }

          // 任意軸回転(degree)との積で更新.
          updateRotVDeg(deg, vec) {
              return this.updateRotV(DegToRad(deg), vec);
          }
      }

      /* ********************************** */

      /*
       * 3D オブジェクト用行列
       */

      class Object3D extends Matrix4 {

          // 回転行列の補正を配列[16]で返す.
          static Correct(ml) {
              const [
                  m00, m01, m02, m03,
                  m10, m11, m12, m13,
                  m20, m21, m22, m23,
                  m30, m31, m32, m33,
              ] = ml;

              const l0 = new Vector4(m00, m01, m02, 0.0);
              const l1 = new Vector4(m10, m11, m12, 0.0);
              const l2 = new Vector4(m20, m21, m22, 0.0);
              const v0 = l0.add(l1.cross(l2)).normalize();
              const v1 = l1.add(l2.cross(l0)).normalize();
              const v2 = l2.add(v0.cross(v1)).normalize();

              return [
                  v0.vector[0], v0.vector[1], v0.vector[2], m03,
                  v1.vector[0], v1.vector[1], v1.vector[2], m13,
                  v2.vector[0], v2.vector[1], v2.vector[2], m23,
                  m30, m31, m32, m33,
              ];
          }

          // コンストラクタ.
          constructor(...args) {
              super(...args);
              this.S = Object3D;
              this.matrix_stack = [];
          }

          // 回転行列の補正.
          correct() {
              const S = this.S;
              return new S(S.Correct(this.matrix));
          }

          // 回転行列の補正で更新.
          updateCorrect() {
              this.matrix = this.S.Correct(this.matrix);
              return this;
          }

          // 行列を複写してスタックに積み上げる.
          pushMatrix(m) {
              this.matrix_stack.push(this.matrix.map((d) => d));
              return this.updateMulM(m);
          }

          // スタックから行列を取り出す.
          popMatrix() {
              this.matrix = this.matrix_stack.pop();
              return this;
          }
      }

      /*
       * カメラ視点用行列
       */

      class ModelView3D extends Object3D {

          // コンストラクタ.
          constructor(...args) {
              super(...args);
              this.S = ModelView3D;
          }

          // Object3D をカメラ視点として設定.
          setCamera(rhs) {
              const [
                  m00, m01, m02, m03,
                  m10, m11, m12, m13,
                  m20, m21, m22, m23,
                  m30, m31, m32, m33,
              ] = rhs.matrix;
              this.matrix = [
                  m00, m10, m20, - (m00 * m03 + m10 * m13 + m20 * m23),
                  m01, m11, m21, - (m01 * m03 + m11 * m13 + m21 * m23),
                  m02, m12, m22, - (m02 * m03 + m12 * m13 + m22 * m23),
                  m30, m31, m32, m33,
              ];
              return this;
          }
      }

      /*
       * 透視変換用行列
       */

      class Projection3D extends Object3D {

          // Z 座標を正規座化する行列を配列[16]で返す.
          static Ortho(znear, zfar, zoom, aspect, zflip) {
              const sz = zflip ? -1.0 : +1.0;
              const zden = zfar - znear;
              const m00 = zoom / aspect;
              const m11 = zoom;
              const m22 = sz * 2.0 / zden;
              const m23 = - (zfar + znear) / zden;
              return [
                  m00, 0.0, 0.0, 0.0,
                  0.0, m11, 0.0, 0.0,
                  0.0, 0.0, m22, m23,
                  0.0, 0.0, 0.0, 1.0,
              ];
          }

          // 透視変換行列を配列[16]で返す.
          static Perspective(znear, zfar, zoom, aspect, zflip) {
              const sz = zflip ? -1.0 : +1.0;
              const zden = zfar - znear;
              const m00 = zoom / aspect;
              const m11 = zoom;
              const m22 = sz * (zfar + znear) / zden;
              const m23 = - (zfar * znear * 2.0) / zden;
              const m32 = sz;
              return [
                  m00, 0.0, 0.0, 0.0,
                  0.0, m11, 0.0, 0.0,
                  0.0, 0.0, m22, m23,
                  0.0, 0.0, m32, 0.0,
              ];
          }

          // -------------------------------------

          // コンストラクタ.
          constructor(...args) {
              super(...args);
              this.S = Projection3D;
          }

          // Ortho 行列を設定する.
          setOrtho(znear, zfar, zoom, aspect, zflip) {
              this.matrix = this.S.Ortho(znear, zfar, zoom, aspect, zflip);
              return this;
          }

          // 透視変換行列を設定する.
          setPerspective(znear, zfar, zoom, aspect, zflip) {
              this.matrix = this.S.Perspective(znear, zfar, zoom, aspect, zflip);
              return this;
          }

          // 画面拡大率との積で更新.
          updateZoom(zoom) {
              return this.updateScale(zoom, zoom, 1.0);
          }
      }

      /* ********************************** */

      /*
       * WebGL
       */

      class Render3D {

          // コンストラクタ.
          constructor(canvas) {
              const S = Render3D;
              this.S = S;
              this.Object3D = Object3D;
              this.ModelView3D = ModelView3D;
              this.Projection3D = Projection3D;
              this.valid = true;

              const gl = canvas.getContext('webgl');
              // const gl = canvas.getContext('webgl2');
              if (!gl) {
                  this.valid = false;
                  alert("WebGL を初期化出来ませんでした。このブラウザでは表示できない項目があります。");
                  return;
              }
              this.context = gl;

              const gl_attr = gl.getContextAttributes();
              this.attribute = gl_attr;

              if (!this.initShader()) {
                  destroy();
                  return;
              }

              this.width = gl.drawingBufferWidth;
              this.height = gl.drawingBufferHeight;
              this.aspect = this.width / Math.max(1, this.height);

              this.modelview = new this.ModelView3D();
              this.projection = new this.Projection3D();
          }

          // バッファの破棄.
          deleteBuffer(buffer) {
              if (buffer)
                  this.context.deleteBuffer(buffer);
              return this;
          }

          // オブジェクトの破棄.
          destroy() {
              if (!this.valid)
                  return;

              const gl = this.context;
              const shader = this.shader_program;

              if (shader)
                  gl.deleteProgram(shader);

              this.shader_info = null;
              this.shader_program = null;
              this.shader_attrib = null;
              this.shader_uniform = null;

              this.projection = null;
              this.modelview = null;

              this.vertex_count = 0;

              this.valid = false;
          }

          // シェーダーの作成.
          loadShader(type, id) {
              const gl = this.context;
              const shader = gl.createShader(type);
              const source = document.getElementById(id).firstChild.nodeValue;
              gl.shaderSource(shader, source);
              gl.compileShader(shader);
              if (gl.getShaderParameter(shader, gl.COMPILE_STATUS))
                  return shader;
              console.log(gl.getShaderInfoLog(shader));
              gl.deleteShader(shader);
              return null;
          }

          // シェーダーの初期化.
          initShader() {
              const S = this.S;
              const gl = this.context;

              const shader = gl.createProgram();
              const vertex = this.loadShader(gl.VERTEX_SHADER, 'vertex-shader');
              const fragment = this.loadShader(gl.FRAGMENT_SHADER, 'fragment-shader');

              this.shader_program = shader;

              if (!shader || !vertex || !fragment)
                  return false;

              gl.attachShader(shader, vertex);
              gl.attachShader(shader, fragment);
              gl.linkProgram(shader);

              if (!gl.getProgramParameter(shader, gl.LINK_STATUS))
                  return false;

              this.shader_attrib = {
                  vertexPosition: gl.getAttribLocation(shader, "aVertexPosition"),
                  vertexNormal: gl.getAttribLocation(shader, "aVertexNormal"),
              }
              this.shader_uniform = {
                  pointSize: gl.getUniformLocation(shader, "uPointSize"),
                  vertexColor: gl.getUniformLocation(shader, "uVertexColor"),

                  lightLevelMin: gl.getUniformLocation(shader, "uLightLevelMin"),
                  lightVector: gl.getUniformLocation(shader, "uLightVector"),
                  projectionMatrix: gl.getUniformLocation(shader, "uProjectionMatrix"),
                  modelViewMatrix: gl.getUniformLocation(shader, "uModelViewMatrix"),
              }

              return true;
          }

          // 透視変換行列の設定.
          setPerspective(znear, zfar, zoom, aspect, zflip)
          {
              const projection = this.projection;
              projection.setPerspective(znear, zfar, zoom, aspect * this.aspect, zflip);
              return this;
          }

          // Object3D をカメラ視点行列として設定.
          setModelViewCamera(object) {
              this.modelview.setCamera(object);
              return this;
          }

          // シェーダー：プログラムの設定.
          setShaderProgram() {
              const gl = this.context;
              gl.useProgram(this.shader_program);
              return this;
          }

          // シェーダー：最低光量の設定.
          setShaderLightLevelMin(value) {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniform1f(loc.lightLevelMin, value);
              return this;
          }

          // シェーダー：平行光源ベクトルの設定.
          setShaderLightVector(vector) {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniform4fv(loc.lightVector, vector);
              return this;
          }

          // シェーダー：点描画時のサイズ.
          setShaderPointSize(size) {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniform1f(loc.pointSize, size);
              return this;
          }

          // シェーダー：頂点色.
          setShaderVertexColor(color) {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniform4fv(loc.vertexColor, color);
              return this;
          }

          // シェーダー：透視変換行列の設定.
          setShaderProjection() {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniformMatrix4fv(
                  loc.projectionMatrix, false,
                  this.projection.transpose().toFloat32()
              );
              return this;
          }

          // シェーダー：座標変換行列の設定.
          setShaderModelView(object) {
              const gl = this.context;
              const loc = this.shader_uniform;
              gl.uniformMatrix4fv(
                  loc.modelViewMatrix, false,
                  this.modelview.transpose().toFloat32()
              );
              return this;
          }

          // 配列バッファの作成と設定.
          setArrayBuffer(array) {
              const gl = this.context;
              const buffer = gl.createBuffer();
              gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
              gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
              return buffer;
          }

          // 要素バッファの作成と設定.
          setElementArray(array) {
              const gl = this.context;
              const buffer = gl.createBuffer();
              gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
              gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, array, gl.STATIC_DRAW);
              return buffer;
          }

          // 頂点データの設定.
          setVertexAttrib(array, index, ncomp, type, normalize) {
              const buffer = this.setArrayBuffer(array);
              const gl = this.context;
              gl.vertexAttribPointer(index, ncomp, type, normalize, 0, 0);
              gl.enableVertexAttribArray(index);
              return buffer;
          }

          // 頂点座標配列の設定.
          setVertexPosition(array, type) {
              const index = this.shader_attrib.vertexPosition;
              const ncomp = 4; // [x,y,z,w]
              this.vertex_count = Math.trunc(array.length / ncomp);
              return this.setVertexAttrib(array, index, ncomp, type, false);
          }

          // 法線座標配列の設定.
          setVertexNormal(array, type) {
              const index = this.shader_attrib.vertexNormal;
              const ncomp = 4; // [x,y,z,w]
              return this.setVertexAttrib(array, index, ncomp, type, false);
          }

          // 線の幅を設定.
          setLineWidth(width) {
              const gl = this.context;
              gl.lineWidth(width);
              return this;
          }

          // --------------------

          // 深度バッファのクリア.
          clearDepth() {
              const gl = this.context;
              gl.clearDepth(1.0);
              gl.enable(gl.DEPTH_TEST);
              gl.depthFunc(gl.LEQUAL);
              gl.clear(gl.DEPTH_BUFFER_BIT);
              return this;
          }

          // 色バッファのクリア.
          clearColorBuffer(r, g, b, a=1.0) {
              const gl = this.context;
              gl.clearColor(r, g, b, a);
              gl.clear(gl.COLOR_BUFFER_BIT);
              return this;
          }

          // 表面表示の設定.
          setBackFace(mode) {
              const gl = this.context;
              gl.enable(gl.CULL_FACE);
              // gl.frontFace(gl.CCW);
              gl.cullFace(mode ? gl.FRONT : gl.BACK);
              return this;
          }

          // 点を打つ.
          drawPoints(first, count) {
              const gl = this.context;
              gl.drawArrays(gl.POINTS, first, count);
              return this;
          }

          // 線分を引く.
          drawLines(first, count) {
              const gl = this.context;
              gl.drawArrays(gl.LINES, first, count);
              return this;
          }

          // 環状の線を引く.
          drawLineLoop(first, count) {
              const gl = this.context;
              gl.drawArrays(gl.LINE_LOOP, first, count);
              return this;
          }

          // 三角形を扇状で描く.
          drawTriangleFan(first, count) {
              const gl = this.context;
              gl.drawArrays(gl.TRIANGLE_FAN, first, count);
              return this;
          }
      }

      /* ********************************** */

      /*
       *  正多面体のデータを作る
       */

      class GenRegularPolyhedron {

          /* ベクトルのソート用(逆順比較) */
          static vecCmpR(a, b) {
              const va = a.vector;
              const vb = b.vector;
              const zc = va[2] - vb[2]; if (zc < 0.0) return +1; if (zc > 0.0) return -1;
              const yc = va[1] - vb[1]; if (yc < 0.0) return +1; if (yc > 0.0) return -1;
              const xc = va[0] - vb[0]; if (xc < 0.0) return +1; if (xc > 0.0) return -1;
              return 0;
          }

          /* ベクトルの一致を調査： 演算誤差を考慮して、1.0 との一致比較はしない */
          static vecEq = ((a, b) => (Math.abs(1.0 - a.dot(b)) < (1.0 / (1 << 20))));

          /* ベクトルの内積と比較値の一致を調査： 演算誤差を考慮して、1.0 との一致比較はしない */
          static vecDotEq = ((a, b, c) => (Math.abs(c - a.dot(b)) < (1.0 / (1 << 20))));

          /* 重複を排除しながら追加する */
          static vecAppend(l, v) {
              for (let q of l)
                  if (GenRegularPolyhedron.vecEq(v, q))
                      return false;
              l.push(v);
              return true;
          }

          /* ３ベクトルから残りのベクトルを求める */
          static vecFrom(p1, p2, p3) {
              const S = GenRegularPolyhedron;
              const vecDotEq = S.vecDotEq;
              const vecs = [p1, p2, p3];

              // 最初の２ベクトルから新しいベクトルを求めるための準備.
              const v1 = p1.normalize();
              const v2 = p1.cross(p2).normalize();
              const v3 = v1.cross(v2).normalize();

              const x_cos = v1.dot(p3);
              const y_cos = v2.dot(p3);
              const z_cos = v3.dot(p3);

              const p_cos = p1.dot(p2);

              // ２ベクトルに隣接するベクトルの追加関数.
              const vecAdd = function(q1, q2) {
                  const x = q1.normalize();
                  const y = q1.cross(q2).normalize();
                  const z = x.cross(y).normalize();
                  const v = x.mul(x_cos).add(y.mul(y_cos)).add(z.mul(z_cos));
                  S.vecAppend(vecs, v.normalize());
              }

              // 総当たりで求める.
              let last_veclen = 0;
              let new_veclen = vecs.length;
              do {
                  for (let i1 = last_veclen; i1 < new_veclen; i1++) {
                      const q1 = vecs[i1];
                      for (let i2 = 0; i2 < new_veclen; i2++) {
                          if (i1 == i2)
                              continue;
                          const q2 = vecs[i2];
                          if (!vecDotEq(q1, q2, p_cos))
                              continue;
                          vecAdd(q1, q2);
                          vecAdd(q2, q1);
                      }
                  }
                  last_veclen = new_veclen;
                  new_veclen = vecs.length;
              } while (last_veclen != new_veclen);

              vecs.sort(S.vecCmpR);
              return vecs;
          }

          // 面情報の一覧を作成.
          static polyFrom(vecs, norms, pp_cos, np_cos) {
              const vecEq = GenRegularPolyhedron.vecEq;
              const vecDotEq = GenRegularPolyhedron.vecDotEq;

              const nl = new Array();
              for (const n of norms) {
                  // 面に属する頂点を抽出(とインデックス)
                  const plist = vecs.filter(v => vecDotEq(v, n, np_cos));
                  const pindex = [...Array(plist.length)].map((_, i) => vecs.indexOf(plist[i]));

                  // 頂点の接続表を作成する.
                  const plink = new Array();
                  for (let i1 = 0; i1 < pindex.length; i1++) {
                      const vc = plist[i1];
                      let [vp, vn] = plist.filter(v => vecDotEq(v, vc, pp_cos));
                      if (!vecEq(n, (vc.sub(vp)).cross(vn.sub(vc)).normalize()))
                          [vp, vn] = [vn, vp];
                      const pi = plist.indexOf(vp);
                      const ni = plist.indexOf(vn);
                      plink.push([pi, ni]);
                  }

                  // 接続表から順に整列したインデックス表に変換.
                  const ilist = new Array();
                  let p = 0;
                  do {
                      ilist.push(pindex[p]);
                      p = plink[p][1];
                  } while (p != 0);

                  nl.push(ilist);
              }
              return nl;
          }

          // 辺情報の一覧を作成.
          static edgeFrom(poly) {
              const edge = new Array();
              for (let l of poly) {
                  let p = l[l.length - 1];
                  for (let q of l) {
                      const r = (Math.min(p, q) << 8) + Math.max(p, q);
                      if (edge.indexOf(r) < 0) edge.push(r);
                      p = q;
                  }
              }
              return [...Array(edge.length)].map(
                  (_, i) => [(edge[i] >> 8), (edge[i] & 255)]);
          }

          // WebGL データを生成する.
          static createWebGLData(vertex, normal, facet, facet_d) {
              const xyz2zxy = function(q) {
                  const [x, y, z, w] = q.vector;
                  return [y, z, x, w];
              }

              const count = facet[0].length;
              const nVertex = count * facet.length;

              const vertexPosition = new Array();
              const vertexNormal = new Array();

              for (let pi = 0; pi < facet.length; pi++) {
                  const pv = facet[pi];
                  const vpi = pi * count;
                  const vn = normal[pi];
                  for (let vi = 0; vi < count; vi++) {
                      const v = vpi + vi;
                      vertexPosition[v] = xyz2zxy(vertex[pv[vi]]);
                      vertexNormal[v] = xyz2zxy(vn);
                  }
              }

              const normalPosition = new Array();
              const normalDummy = new Array();

              for (let ni = 0; ni < normal.length; ni++) {
                  const e = normal[ni];
                  const s = e.mulN(facet_d);
                  const sv = xyz2zxy(s);
                  const ev = xyz2zxy(e);
                  normalPosition[ni * 2 + 0] = sv;
                  normalPosition[ni * 2 + 1] = ev;
                  normalDummy[ni * 2 + 0] = ev;
                  normalDummy[ni * 2 + 1] = ev;
              }

              return {
                  facet: {
                      ncomp: count,
                      position: new Float32Array(vertexPosition.flat()),
                      normal: new Float32Array(vertexNormal.flat()),
                  },
                  normal: {
                      position: new Float32Array(normalPosition.flat()),
                      dummy: new Float32Array(normalDummy.flat()),
                  },
              }
          }

          // コンストラクタ.
          constructor(M) {
              const S = GenRegularPolyhedron;
              const vecEq = S.vecEq;
              const vecAppend = S.vecAppend;
              const vecFrom = S.vecFrom;

              this.S = S;
              this.vecEq = vecEq;
              this.vecAppend = vecAppend;
              this.vecFrom = vecFrom;

              const mIndex = { 4:1, 6:2, 8:3, 12:4, 20:5 }[M];
              const V = [4, 4, 6, 8, 12, 20][mIndex^1];
              const F = [3, 3, 3, 4,  3,  5][mIndex];
              const N = [3, 3, 4, 3,  5,  3][mIndex];

              const PiDivF = Math.PI / F;  // π÷F
              const PiDivN = Math.PI / N;  // π÷N

              // 中心から面までの距離.
              this.facet_d = 1 / Math.tan(PiDivF) / Math.tan(PiDivN);

              // 最初の３頂点を求める.
              const rotXY = 2.0 * PiDivF;
              const cosXY = Math.cos(rotXY);
              const sinXY = Math.sin(rotXY);
              const cosZX = 2.0 * Math.pow(Math.cos(PiDivN) / Math.sin(PiDivF), 2) - 1.0;
              const sinZX = Math.sqrt(1.0 - Math.pow(cosZX, 2));

              const p1 = new Vector4([0.0, 0.0, 1.0]);
              const p2 = new Vector4([sinZX, 0.0, cosZX]);
              const p3 = new Vector4([sinZX*cosXY, sinZX*sinXY, cosZX]);

              // 頂点の一覧を作成する.
              this.vertex = vecFrom(p1, p2, p3);

              // 最初の３法線を求める.
              const rm = new Matrix4().rotateZ(rotXY);
              const n1 = p1.sub(p3).cross(p2.sub(p1), 1.0).normalize();
              const n2 = rm.mulV(n1).normalize();
              const n3 = rm.mulV(n2).normalize();

              // 法線の一覧を作成する.
              this.normal = vecFrom(n1, n2, n3);

              const pp_cos = p1.dot(p2);  // 頂点間の角度情報.
              const np_cos = p1.dot(n1);  // 頂点・法線間の角度情報.
              const nn_cos = n1.dot(n2);  // 法線間の角度情報.

              // 正多面体と対の両方の多角形データを作成する.
              this.facet1 = S.polyFrom(this.vertex, this.normal, pp_cos, np_cos);
              this.facet2 = S.polyFrom(this.normal, this.vertex, nn_cos, np_cos);

              // 正多面体と対の両方の辺データを作成する.
              this.edge1 = S.edgeFrom(this.facet1);
              this.edge2 = S.edgeFrom(this.facet2);

              // WebGL データを作成する.
              const color = [1.0, 1.0, 1.0, 1.0];
              this.wglData1 = S.createWebGLData(this.vertex, this.normal, this.facet1, this.facet_d);
              this.wglData2 = S.createWebGLData(this.normal, this.vertex, this.facet2, this.facet_d);
          }
      }

      /* ********************************** */

      /*
       * 正多面体描画
       */

      class RegularPolyhedronViewer {

          // コンストラクタ.
          constructor() {
              this.regular_polyhedron_data = {
                  4: new GenRegularPolyhedron(4),
                  6: new GenRegularPolyhedron(6),
                  8: new GenRegularPolyhedron(8),
                  12: new GenRegularPolyhedron(12),
                  20: new GenRegularPolyhedron(20),
              }
              this.rp_facets_dual = { 4: 4, 6: 8, 8: 6, 12: 20, 20: 12 }
              this.background_color = [0.0, 0.0, 0.0];
              this.light_vector = new Vector4([
                  Math.sin(DegToRad(-20)),
                  Math.sin(DegToRad(45)),
                  Math.cos(DegToRad(45)) * Math.cos(DegToRad(-20)),
                  0.0
              ]);

              this.point_size = 4.0;
              this.point_color = [1.0, 1.0, 1.0, 1.0];

              this.line_width = 2.0;
              this.line_color = [1.0, 1.0, 1.0, 1.0];
              this.facet_color = [1.0, 1.0, 1.0, 1.0];
              this.normal_color = [1.0, 1.0, 1.0, 1.0];

              this.regular_polyhedron = null;
              this.rp_dual = null;
              this.rp_facets = 0;
              this.rp_data = null;
              this.rp_data_facets = 0;

              this.render = null;

              this.draw_points = true;
              this.draw_lines = true;
              this.draw_facets = true;
              this.draw_normal = true;

              this.backface = false;

              this.camera_zoom = 8.0;
              this.camera_zpos = 12.0;
              this.camera_pitch = -20.0;
              this.camera_rotate = new Object3D();
              this.matrix = {
                  camera: new Object3D(),
                  model: new Object3D(),
                  animation: new Object3D(),
              }

              this.animation_speed = 1.0;
          }

          // アニメーション行列のリセット.
          resetAnimation() {
              this.matrix.animation = new Object3D();
              return this;
          }

          // 多面体のデータを設定.
          setPolyhedron(N, dual) {
              const data = this.regular_polyhedron_data[N];
              if (!data)
                  return this;
              this.regular_polyhedron = data;

              const dN = !dual ? N : this.rp_facets_dual[N];
              const bD = (this.rp_dual == dual);
              const bN = (this.rp_facets == N);

              if (bN && bD)
                  return this;

              this.rp_facets = N;
              this.rp_dual = dual;
              this.rp_data_facets = dN;
              this.rp_data = !dual ? data.wglData1 : data.wglData2;

              if (!bN)
                  this.resetAnimation();

              return this;
          }

          // canvas タグの設定.
          setCanvas(canvas) {
              const render = this.render;
              this.render = null;
              if (render) {
                  render.deleteBuffer(this.buffer.facet.vertex);
                  render.deleteBuffer(this.buffer.facet.normal);
                  render.destroy();
              }
              if (canvas)
                  this.render = new Render3D(canvas);
              return this;
          }

          // 背景色の設定.
          setBackgroundColor(color) {
              this.background_color = color;
              return this;
          }

          // 描画する種類を設定.
          setDrawMode(points, lines, facets, normal) {
              this.draw_points = points;
              this.draw_lines = lines;
              this.draw_facets = facets;
              this.draw_normal = normal;
              return this;
          }

          // 点の大きさを設定.
          setPointSize(size) {
              this.point_size = size;
              return this;
          }

          // 点の色を設定.
          setPointColor(color) {
              this.point_color = color;
              return this;
          }

          // 線の幅を設定.
          setLineWidth(width) {
              this.line_width = width;
              return this;
          }

          // 線の色を設定.
          setLineColor(color) {
              this.line_color = color;
              return this;
          }

          // 面の色を設定.
          setFacetColor(color) {
              this.facet_color = color;
              return this;
          }

          // 法線の色を設定.
          setNormalColor(color) {
              this.normal_color = color;
              return this;
          }

          // 裏面表示の設定.
          setBackFace(mode) {
              this.backface = mode;
              return this;
          }

          // 透視変換行列の設定.
          setProjection() {
              const znear = 1.0 / 1024.0;
              const zfar = 1024.0;
              const zoom = this.camera_zoom;
              const aspect = 1.0;
              const zflip = true;

              this.render.setPerspective(znear, zfar, zoom, aspect, zflip);
              return this;
          }

          // カメラの操作.
          resetCameraPositon() { this.matrix.camera.loadIdentity(); return this; }
          updateCameraPosition(x, y, z) { this.matrix.camera.updateTranslate(x, y, z); return this; }
          updateCameraRotX(rad) { this.matrix.camera.updateRotX(rad); return this; }
          updateCameraRotY(rad) { this.matrix.camera.updateRotY(rad); return this; }
          updateCameraRotZ(rad) { this.matrix.camera.updateRotZ(rad); return this; }
          updateCameraRotXDeg(deg) { this.matrix.camera.updateRotXDeg(deg); return this; }
          updateCameraRotYDeg(deg) { this.matrix.camera.updateRotYDeg(deg); return this; }
          updateCameraRotZDeg(deg) { this.matrix.camera.updateRotZDeg(deg); return this; }

          setCameraZoom(zoom) { this.camera_zoom = zoom; return this; }
          setCameraZPos(zpos) { this.camera_zpos = zpos; return this; }
          setCameraPicth(pitch) { this.camera_pitch = pitch; return this; }
          setCameraParma(zoom, zpos, pitch) {
              this.setCameraZoom(zoom);
              this.setCameraZPos(zpos);
              this.setCameraPicth(pitch);
              this.resetCameraPositon();
              this.updateCameraRotXDeg(pitch);
              this.camera_rotate = new Object3D(this.matrix.camera);
              this.updateCameraPosition(0.0, 0.0, zpos);
              return this;
          }

          // アニメーション設定.
          setAnimationSpeed(speed) { this.animation_speed = speed; return this; }

          // マウス操作.
          mouseEvent(dx, dy, event) {
              const clamp = function(val, min, max) {
                  return Math.max(Math.min(val, max), min);
              }
              const update = function(id, mag) {
                  const tag = document.getElementById(id);
                  let value = Number(tag.value);
                  value = clamp(value + (mag * dy / h), tag.min, tag.max);
                  tag.value = Math.trunc(value * 10) / 10;
                  document.getElementById(id + '_value').innerText = value;
                  onUpdate();
              }

              const bAlt = event.altKey;
              const bCtrl = event.ctrlKey;
              const bMeta = event.metaKey;
              const mod = (bCtrl ? 1 : 0) | (bAlt ? 2 : 0) | (bMeta ? 4 : 0);
              const w = this.render.width;
              const h = this.render.height;

              switch (mod) {
              case 0:
                  {
                      const am = this.matrix.animation;
                      const cm = this.camera_rotate;
                      let mm = new Object3D(cm);
                      mm.updateRotXDeg(180 * dy / w);
                      mm.updateRotYDeg(180 * dx / w);
                      mm.updateMulM(cm.transpose());
                      mm.updateMulM(am);
                      mm.updateCorrect();
                      this.matrix.animation = mm;
                  }
                  break;
              case 2: update('zoom', +50); break;
              case 4: update('zpos', -75); break;
              }
          }

          // canvas への描画.
          draw(animation) {
              const render = this.render;
              if (!render || !render.valid)
                  return;

              const ftype = render.context.FLOAT;
              // const stype = render.context.UNSIGNED_SHORT;

              const bgcolor = this.background_color;

              render.setModelViewCamera(this.matrix.camera);

              render.clearDepth();
              render.clearColorBuffer(bgcolor[0], bgcolor[1], bgcolor[2]);

              render.setBackFace(this.backface);

              render.setShaderProgram();
              render.setShaderLightVector(
                  render.modelview.mulV(this.light_vector).vector);
              render.setShaderProjection();

              const regular_polyhedron = this.regular_polyhedron;
              if (regular_polyhedron) {
                  render.modelview.pushMatrix(this.matrix.model);
                  render.modelview.updateMulM(this.matrix.animation);

                  render.setShaderModelView();
                  render.setShaderPointSize(this.point_size);
                  render.setLineWidth(this.line_width);

                  const facet = this.rp_data.facet;
                  const facet_vertex = render.setVertexPosition(facet.position, ftype);
                  const facet_normal = render.setVertexNormal(facet.normal, ftype);

                  if (this.draw_facets) {
                      render.setShaderLightLevelMin(-1.0);
                      render.setShaderVertexColor(this.facet_color);
                      const ncomp = facet.ncomp;
                      for (let i = 0; i < this.rp_data_facets; i++)
                          render.drawTriangleFan(i * ncomp, ncomp);
                  }
                  if (this.draw_lines) {
                      render.setShaderLightLevelMin(1.0);
                      render.setShaderVertexColor(this.line_color);
                      const ncomp = facet.ncomp;
                      for (let i = 0; i < this.rp_data_facets; i++)
                          render.drawLineLoop(i * ncomp, ncomp);
                  }
                  if (this.draw_points) {
                      render.setShaderLightLevelMin(1.0);
                      render.setShaderVertexColor(this.point_color);
                      render.drawPoints(0, facet.position.length >> 2);
                  }

                  render.deleteBuffer(facet_vertex);
                  render.deleteBuffer(facet_normal);

                  if (this.draw_normal) {
                      const normal = this.rp_data.normal;
                      const normal_position = render.setVertexPosition(normal.position, ftype);
                      const normal_dummy = render.setVertexNormal(normal.dummy, ftype);

                      render.setShaderLightLevelMin(1.0);
                      render.setShaderVertexColor(this.normal_color);
                      render.drawLines(0, normal.position.length >> 2);

                      render.deleteBuffer(normal_position);
                      render.deleteBuffer(normal_dummy);
                  }

                  render.modelview.popMatrix();
              }

              if (animation) {
                  const am = this.matrix.animation;
                  const mm = new Object3D();
                  mm.updateRotYDeg(this.animation_speed);
                  mm.updateMulM(am);
                  mm.updateCorrect();
                  this.matrix.animation = mm;
              }
              return this;
          }
      }

      /* ********************************** */

      /*
       * ユーザー インターフェース
       */

      const regular_polyhedron_name = {
          4: ["正四面体", "正四面体"],
          6: ["正六面体", "正八面体"],
          8: ["正八面体", "正六面体"],
          12: ["正十二面体", "正二十面体"],
          20: ["正二十面体", "正十二面体"],
      };

      let viewer = new RegularPolyhedronViewer();
      let viewer_animation = { mode: false, rotate: false, dual: false }
      let viewer_mouse_pos = null;

      // WebGL の描画時に呼び出される関数.
      function render(now) {
          viewer.draw(viewer_animation.rotate);
          if (viewer_animation.mode)
              window.requestAnimationFrame(render);

          if (viewer_animation.dual) {
              const nfacet = document.getElementById('nfacet');
              const dtag = document.getElementById('dual');
              dtag.checked = !dtag.checked;
              viewer.setPolyhedron(nfacet.value, dtag.checked);
          }
      }

      // 再描画.
      function redraw() {
          if (!viewer_animation.mode)
              window.requestAnimationFrame(render);
      }

      // 正多面体の向きを初期化.
      function resetAnimation() {
          viewer.resetAnimation();
          redraw();
      }

      // 色データの取得.
      function getColor(prefix, id) {
          const r = document.getElementById(prefix + '_r');
          const g = document.getElementById(prefix + '_g');
          const b = document.getElementById(prefix + '_b');
          const v = document.getElementById(id);
          if (v)
              v.innerText = "(" + r.value + ", " + g.value + ", " + b.value + ")";
          return [r, g, b];
      }

      // 表示設定の変更時に呼び出される関数.
      function onUpdate() {
          const checked = function(id) { return document.getElementById(id).checked; }
          const elemval = function(id) { return Number(document.getElementById(id).value); }
          const settext = function(id, s) { document.getElementById(id).innerText = s; }

          const nfacet = document.getElementById('nfacet');
          const dual = checked('dual');
          viewer.setPolyhedron(nfacet.value, dual);

          const rpnames = regular_polyhedron_name[viewer.rp_facets];
          // settext('rpdual', "対 (" + rpnames[1] + ")");
          settext('rpdata_name', rpnames[!dual ? 0 : 1] + (dual ? "(対)" : "") + "のデータ");
          {
              const rpdata = viewer.regular_polyhedron;
              const vertex = !dual ? rpdata.vertex : rpdata.normal;
              const normal = !dual ? rpdata.normal : rpdata.vertex;
              const facet = !dual ? rpdata.facet1 : rpdata.facet2;
              const edge = !dual ? rpdata.edge1 : rpdata.edge2;

              const vectors = function(vlist) {
                  let r = '[\n';
                  for (const v of vlist) {
                      const p = v.fixInt();
                      r += ('  ['
                            + p.vector[0] + ', '
                            + p.vector[1] + ', '
                            + p.vector[2] + '],\n'
                           );
                  }
                  return r + ']\n';
              }
              const elements = function(ilist) {
                  let r = '[\n'
                  for (const l of ilist) {
                      r += '  [';
                      s = '';
                      for (const n of l) {
                          r += s + n;
                          s = ', ';
                      }
                      r += '],\n';
                  }
                  return r + ']\n';
              }

              settext('rpdata_vertex', '頂点表 = ' + vectors(vertex));
              settext('rpdata_normal', '法線表 = ' + vectors(normal));
              settext('rpdata_facet', '面の頂点表 = ' + elements(facet));
              settext('rpdata_edge', '線の頂点表 = ' + elements(edge));
          }

          // カメラの状態.
          const camera_zoom = elemval('zoom');
          const camera_zpos = elemval('zpos');
          const camera_pitch = elemval('pitch');
          settext('zoom_value', camera_zoom);
          settext('zpos_value', camera_zpos);
          settext('pitch_value', camera_pitch);
          viewer.setCameraParma(camera_zoom, camera_zpos, camera_pitch);

          viewer.setDrawMode(checked('dpnt'), checked('dline'),
                             checked('dfct'), checked('dnrml'));
          viewer.setBackFace(checked('cface'));

          const [bgR, bgG, bgB] = getColor('bgc', 'background_color_value');
          const [pR, pG, pB] = getColor('pnt', 'point_color_value');
          const [lR, lG, lB] = getColor('line', 'line_color_value');
          const [fR, fG, fB] = getColor('fct', 'facet_color_value');
          const [nR, nG, nB] = getColor('nrml', 'normal_color_value');
          viewer.setBackgroundColor([bgR.value, bgG.value, bgB.value]);
          viewer.setPointColor([pR.value, pG.value, pB.value, 1.0]);
          viewer.setLineColor([lR.value, lG.value, lB.value, 1.0]);
          viewer.setFacetColor([fR.value, fG.value, fB.value, 1.0]);
          viewer.setNormalColor([nR.value, nG.value, nB.value, 1.0]);

          viewer.setProjection();

          const anim_speed = elemval('aspeed');
          settext('aspeed_value', anim_speed);
          viewer.setAnimationSpeed(anim_speed);

          const last_animation = viewer_animation.mode;
          viewer_animation.rotate = checked('arotate');
          viewer_animation.mode = (viewer_animation.rotate |
                                   viewer_animation.dual);
          if (!last_animation)
              window.requestAnimationFrame(render);
      }

      // ページ読み込み終了時に呼び出される関数.
      window.onload = function() {
          const mouse_position = function(e) { return [e.offsetX, e.offsetY]; }

          {// 引数処理.
              const bools = new Set(['1', 'on', 'set', 'true', 'y', 'yes']);
              const argbool = function(arg) {
                  return bools.has(arg.toLowerCase());
              }
              const setchk = function(name, id) {
                  const arg = search.get(name);
                  if (arg != null)
                      document.getElementById(id).checked = argbool(arg);
              }

              const url = new URL(window.location);
              const search = url.searchParams;
              const N = search.get('N');
              if (N != null) {
                  const select = document.getElementById('nfacet');
                  const options = select.options;
                  for (let oi = 0; oi < options.length; oi++) {
                      if (options[oi].value == N) {
                          select.selectedIndex = oi;
                          break;
                      }
                  }
              }
              setchk('D', 'dual');
              setchk('A', 'arotate');
          }

          const canvas = document.getElementById('canvas');
          viewer.setCanvas(canvas);

          canvas.addEventListener('mouseenter', e => { viewer_mouse7_pos = null; });
          canvas.addEventListener('mousedown', e => { viewer_mouse_pos = mouse_position(e); });
          canvas.addEventListener('mousemove', e => {
              const lpos = viewer_mouse_pos;
              if (!lpos) return;
              const cpos = mouse_position(e);
              const dpos = [cpos[0] - lpos[0], cpos[1] - lpos[1]];
              viewer_mouse_pos = cpos;
              viewer.mouseEvent(dpos[0], dpos[1], e);
              redraw();
          });
          canvas.addEventListener('mouseup', e => { viewer_mouse_pos = null; });
          canvas.addEventListener('mouseleave', e => { viewer_mouse_pos = null; });

          const onInput = function(query) { query.addEventListener('input', onUpdate); }
          document.querySelectorAll('select').forEach(onInput);
          document.querySelectorAll('input').forEach(onInput);

          onUpdate();
      }

      /* ********************************** */
    </script>
  </body>
</html>
```

</details>