WebGL (3)


座標変換ライブラリ

OpenGLでは用意されている座標変換のための関数が、WebGLでは用意されて いないので自前で用意するか、誰かの作成したライブラリを利用する必要があります。 ここでは、教科書に付属している cuon-matrix.js を使うことにします。 このライブラリでは Matrix4 というjavascriptのオブジェクトが定義されています。

Matrix4では、乗算は右方向から行うことに注意して下さい。

Matrix4のプロパティ
elements
行列の要素を保持する型付き配列(Float32Array)
Matrix4のメソッド
setIdentity()
  単位行列に設定する。
  @return this
set(src)
  渡された行列の要素をコピーする。
  @param src 要素をコピーしてくる行列
  @return this
multiply(other)
  渡された行列を右からかける。
  @param other かける行列
  @return this
multiplyVector3(pos)
  渡されたベクトルをかける。
  @param pos かける行列
  @return この行列を掛けた結果(Float32Array)
multiplyVector4(pos)
  渡されたベクトルをかける。
  @param pos かける行列
  @return この行列を掛けた結果(Float32Array)
transpose()
  行列を転置する。
  @return this
invert()
  この行列の逆行列を計算して、内容を置き換える。
  @return this
ortho(left, right, bottom, top, near, far)
  正射影行列を右からかける。
  @param left 左クリップ平面のX座標
  @param right 右クリップ平面のX座標
  @param bottom 下クリップ平面のY座標
  @param top 上クリップ平面のY座標
  @param near 近クリップ平面までの距離。平面が視点の後方にある場合は負数
  @param far 遠クリップ平面までの距離。平面が視点の後方にある場合は負数
  @return this
frustum(left, right, bottom, top, near, far)
  透視射影行列を右からかける。
  @param left 近クリップ平面上における左クリップ平面のX座標
  @param right 近クリップ平面上における右クリップ平面のX座標
  @param bottom 近クリップ平面上における下クリップ平面のY座標
  @param top 近クリップ平面上における上クリップ平面のY座標
  @param near 近クリップ平面までの距離。正数でなくてはならない
  @param far 遠クリップ平面までの距離。正数でなくてはならない
  @return this
perspective(fovy, aspect, near, far)
  透視射影行列を右からかける。
  @param fovy 垂直視野角 [度]
  @param aspect 視野のアスペクト比(幅 / 高さ)
  @param near 近クリップ平面までの距離。正数でなくてはならない
  @param far 遠クリップ平面までの距離。正数でなくてはならない
  @return this
scale(x, y, z)
 * スケーリング行列を右からかける。
 * @param x X方向の倍率
 * @param y Y方向の倍率
 * @param z Z方向の倍率
 * @return this
translate(x, y, z)
  平行移動行列を右からかける。
  @param x X方向の移動量
  @param y Y方向の移動量
  @param z Z方向の移動量
  @return this
rotate(angle, x, y, z)
  回転行列を右からかける。
  回転軸の方向ベクトルは正規化されていなくても構わない。
  @param angle 回転角 [度]
  @param x 回転軸の方向ベクトルのX成分
  @param y 回転軸の方向ベクトルのY成分
  @param z 回転軸の方向ベクトルのZ成分
  @return this
lookAt(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ)
  視野変換行列を右からかける。
  @param eyeX, eyeY, eyeZ 視点の位置
  @param centerX, centerY, centerZ 注視点の位置
  @param upX, upY, upZ カメラの上方向を表す方向ベクトル
  @return this
dropShadow(plane, light)
  頂点を平面上に射影するような行列を右からかける。
  @param plane 平面方程式 Ax + By + Cz + D = 0 の係数[A, B, C, D]を格納した配列
  @param light 光源の同次座標を格納した配列。light[3]=0の場合、平行光源を表す
  @return this
dropShadowDirectionally(normX, normY, normZ, planeX, planeY, planeZ, lightX, lightY, lightZ)
  平行光源により頂点を平面上に射影するような行列を右からかける。
  @param normX, normY, normZ 平面の法線ベクトル(正規化されている必要はない)
  @param planeX, planeY, planeZ 平面上の点
  @param lightX, lightY, lightZ ライトの方向(正規化されている必要はない)
  @return this

変換行列を頂点に作用させる

3次元座標系
   y             
   |
   |
   /---------x
  /
 /
z

頂点シェーダの中で、頂点の座標に対して変換行列をかけるようにしましょう。 モデル変換行列、ビュー変換行列、透視投影変換行列をこの順に右からかけた 結果が u_MvpMatrixです。

LookAtTriangles.html の変更点
*** js_sample18.html	Sun Jun 22 10:33:17 2014
--- LookAtTriangles.html	Sun Jun 22 10:40:53 2014
***************
*** 6,19 ****
    <script src="lib/webgl-utils.js"></script>
    <script src="lib/webgl-debug.js"></script>
    <script src="lib/cuon-utils.js"></script>
    <script language="JavaScript" type="text/javascript">
      //<![CDATA
  var VSHADER_SOURCE = "\
  attribute vec4 a_Position;\n\
  attribute vec4 a_Color;\n\
  varying vec4 v_Color;\n\
  void main() {\n\
!     gl_Position = a_Position;\n\
      v_Color = a_Color;\n\
  }\n\
  ";
--- 6,21 ----
    <script src="lib/webgl-utils.js"></script>
    <script src="lib/webgl-debug.js"></script>
    <script src="lib/cuon-utils.js"></script>
+   <script src="lib/cuon-matrix.js"></script>
    <script language="JavaScript" type="text/javascript">
      //<![CDATA
  var VSHADER_SOURCE = "\
  attribute vec4 a_Position;\n\
  attribute vec4 a_Color;\n\
+ uniform mat4 u_MvpMatrix;\n\
  varying vec4 v_Color;\n\
  void main() {\n\
!     gl_Position = u_MvpMatrix * a_Position;\n\
      v_Color = a_Color;\n\
  }\n\
  ";
***************
*** 81,86 ****
--- 83,105 ----
      gl.enableVertexAttribArray(a_Color);
      gl.bindBuffer(gl.ARRAY_BUFFER,null);
  
+     var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
+     if (! u_MvpMatrix) {
+         console.log('failed to get location of u_MvpMatrix');
+         return;
+     }
+ 
+     var modelMatrix = new Matrix4();
+     var viewMatrix = new Matrix4();
+     var projMatrix = new Matrix4();
+     var mvpMatrix = new Matrix4();
+ 
+     //modelMatrix.setRotate(-10, 0, 1, 0);
+     viewMatrix.setLookAt(1.0, 1.5, 1.5, 0, 0, 0, 0, 1, 0);
+     projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
+     mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
+     gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
+ 
      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LEQUAL);
  

モデル行列で、y軸回りに-10度回転させると次のようになります。

LookAtTriangles2.html の変更点
*** LookAtTriangles.html	Sun Jun 22 10:40:53 2014
--- LookAtTriangles2.html	Sun Jun 22 10:50:39 2014
***************
*** 94,100 ****
      var projMatrix = new Matrix4();
      var mvpMatrix = new Matrix4();
  
!     //modelMatrix.setRotate(-10, 0, 1, 0);
      viewMatrix.setLookAt(1.0, 1.5, 1.5, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
--- 94,100 ----
      var projMatrix = new Matrix4();
      var mvpMatrix = new Matrix4();
  
!     modelMatrix.setRotate(-10, 0, 1, 0);
      viewMatrix.setLookAt(1.0, 1.5, 1.5, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);


図形をELEMENTで表現する

図形を座標配列のインデックスの集合で表現することができます。

その場合は、頂点の座標を頂点シェーダの attribute 変数を 経由して gl_Position に設定した上で、 図形を表すインデックスの配列をバッファオブジェクトとして WebGLのELEMENT_ARRAY_BUFFERをターゲットとして送り込んでおき、 gl.drawElements()を使って描画します。

インデックスの配列のバッファオブジェクトをjavascriptからWebGLに転送した後、 一旦はELEMENT_ARRAY_BUFFERとバッファオブジェクトのバインドを解消しても 構いませんが、g.drawElements()で描画する時点では再びバインドしておく 必要があります。


下の例では、9個の頂点座標を使って、12個のインデックスを与え、 gl.TRIANGLESで描いているので合計4個の3角形が描画されています。

LookAtTrianglesElement.html の変更点
*** LookAtTriangles.html	Sun Jun 22 10:40:53 2014
--- LookAtTrianglesElement.html	Sun Jun 22 10:49:00 2014
***************
*** 53,62 ****
          -0.3, -0.5, -0.2, 0.0, 0.0, 1.0,
           0.7, -0.5, -0.2, 0.0, 0.0, 1.0
      ]);
      var xyz_size = 3;
      var c_size = 3;
      var size = xyz_size + c_size;
-     var n_vertices = vertices.length / size;
  
      var vertexBuffer = gl.createBuffer();
      if (! vertexBuffer) {
--- 53,67 ----
          -0.3, -0.5, -0.2, 0.0, 0.0, 1.0,
           0.7, -0.5, -0.2, 0.0, 0.0, 1.0
      ]);
+     var indices = new Uint8Array([
+         0, 1, 2,
+         3, 4, 5,
+         6, 7, 8,
+         0, 6, 3 // added triangle
+     ]);
      var xyz_size = 3;
      var c_size = 3;
      var size = xyz_size + c_size;
  
      var vertexBuffer = gl.createBuffer();
      if (! vertexBuffer) {
***************
*** 83,88 ****
--- 88,102 ----
      gl.enableVertexAttribArray(a_Color);
      gl.bindBuffer(gl.ARRAY_BUFFER,null);
  
+     var indexBuffer = gl.createBuffer();
+     if (! indexBuffer) {
+         console.log('failed to create buffer');
+         return;
+     }
+     gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
+     gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
+     gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
+ 
      var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
      if (! u_MvpMatrix) {
          console.log('failed to get location of u_MvpMatrix');
***************
*** 106,112 ****
      gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
!     gl.drawArrays(gl.TRIANGLES, 0, n_vertices);
  }
  //]]>
    </script>
--- 120,126 ----
      gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
!     gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
  }
  //]]>
    </script>


図形をELEMENTで表現する(2)

底面が一辺の長さが2の正方形で高さが2の四角錐を表示するのが LookAtPyramidElement.htmlです。

頂点の座標        色
 P0:( 0, 2, 0)    (1,1,1)
 P1:( 1, 0, 1)    (1,0,0)
 P2:( 1, 0,-1)    (0,1,0)
 P3:(-1, 0,-1)    (0,0,1)
 P4:(-1, 0, 1)    (0.5,0.5,0)
視点:(6.0, -2.0, 4.0), 注視点:(0.0, 0.0, 0.0), 上方向:(0.0, 1.0, 0.0)
LookAtPyramidElement.html の変更点
*** LookAtTrianglesElement.html	Sun Jun 22 10:49:00 2014
--- LookAtPyramidElement.html	Thu Jun 22 15:47:34 2017
***************
*** 39,63 ****
          console.log("cannot initiate shader"); return;
      }
  
      var vertices = new Float32Array([
!          // x, y, z, r, g, b
!          0.0,  0.5, -0.1, 1.0, 0.0, 0.0,
!         -0.5, -0.5, -0.1, 1.0, 0.0, 0.0,
!          0.5, -0.5, -0.1, 1.0, 0.0, 0.0,
! 
!          0.1,  0.5, -0.3, 0.0, 1.0, 0.0,
!         -0.4, -0.5, -0.3, 0.0, 1.0, 0.0,
!          0.6, -0.5, -0.3, 0.0, 1.0, 0.0,
! 
!          0.2,  0.5, -0.2, 0.0, 0.0, 1.0,
!         -0.3, -0.5, -0.2, 0.0, 0.0, 1.0,
!          0.7, -0.5, -0.2, 0.0, 0.0, 1.0
      ]);
      var indices = new Uint8Array([
!         0, 1, 2,
!         3, 4, 5,
!         6, 7, 8,
!         0, 6, 3 // added triangle
      ]);
      var xyz_size = 3;
      var c_size = 3;
--- 39,63 ----
          console.log("cannot initiate shader"); return;
      }
  
+     //       v0\
+     //    //  |  \
+     //  | /v4--\--\v3
+     //  |/      |/
+     //  v1------v2
      var vertices = new Float32Array([
!        // x, y, z, r, g, b
!        0.0,  2.0,  0.0,     1.0,  1.0,  1.0,  // v0
!       -1.0,  0.0,  1.0,     1.0,  0.0,  0.0,  // v1
!        1.0,  0.0,  1.0,     0.0,  1.0,  0.0,  // v2
!        1.0,  0.0, -1.0,     0.0,  0.0,  1.0,  // v3
!       -1.0,  0.0, -1.0,     0.5,  0.5,  0.0   // v4
      ]);
      var indices = new Uint8Array([
!       0, 1, 2,   // forward
!       0, 2, 3,    // right
!       0, 3, 4,    // back
!       0, 4, 1,    // left
!       1, 3, 2, 1, 4, 3    // bottom
      ]);
      var xyz_size = 3;
      var c_size = 3;
***************
*** 109,115 ****
      var mvpMatrix = new Matrix4();
  
      //modelMatrix.setRotate(-10, 0, 1, 0);
!     viewMatrix.setLookAt(1.0, 1.5, 1.5, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
      gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
--- 109,115 ----
      var mvpMatrix = new Matrix4();
  
      //modelMatrix.setRotate(-10, 0, 1, 0);
!     viewMatrix.setLookAt(6.0, -2.0, 4.0, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
      gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);


図形をELEMENTで表現する(3)

前の例だと頂点が複数の面(三角形)で共有されているため、 面ごとに色を変えようとしても混ざってしまっています。 そのような場合は、頂点が別の面に属する場合は、 同じ位置にある別の頂点と考えて定義します。

次回以降で面の法線の計算を行うため、必ず次の条件を満たすように 頂点のインデックスを記述して下さい。

三角形を表す3個のindexがi, (i+1), (i+2)であり、 それぞれのインデックスが表す頂点の座標を Pi , Pi+1 , Pi+2 とするとき、

LookAtPyramidElement2.html の変更点
*** LookAtPyramidElement.html	Thu Jun 22 15:47:34 2017
--- LookAtPyramidElement2.html	Thu Jun 22 15:53:25 2017
***************
*** 46,63 ****
      //  v1------v2
      var vertices = new Float32Array([
         // x, y, z, r, g, b
!        0.0,  2.0,  0.0,     1.0,  1.0,  1.0,  // v0
!       -1.0,  0.0,  1.0,     1.0,  0.0,  0.0,  // v1
!        1.0,  0.0,  1.0,     0.0,  1.0,  0.0,  // v2
!        1.0,  0.0, -1.0,     0.0,  0.0,  1.0,  // v3
!       -1.0,  0.0, -1.0,     0.5,  0.5,  0.0   // v4
      ]);
      var indices = new Uint8Array([
        0, 1, 2,   // forward
!       0, 2, 3,    // right
!       0, 3, 4,    // back
!       0, 4, 1,    // left
!       1, 3, 2, 1, 4, 3    // bottom
      ]);
      var xyz_size = 3;
      var c_size = 3;
--- 46,78 ----
      //  v1------v2
      var vertices = new Float32Array([
         // x, y, z, r, g, b
!        0.0,  2.0,  0.0,     1.0,  0.0,  0.0,  // 0 v0 :front
!       -1.0,  0.0,  1.0,     1.0,  0.0,  0.0,  // 1 v1
!        1.0,  0.0,  1.0,     1.0,  0.0,  0.0,  // 2 v2
! 
!        0.0,  2.0,  0.0,     0.0,  1.0,  0.0,  // 3 v0 :right
!        1.0,  0.0,  1.0,     0.0,  1.0,  0.0,  // 4 v2
!        1.0,  0.0, -1.0,     0.0,  1.0,  0.0,  // 5 v3
! 
!        0.0,  2.0,  0.0,     0.0,  0.0,  1.0,  // 6 v0 :back
!        1.0,  0.0, -1.0,     0.0,  0.0,  1.0,  // 7 v3
!       -1.0,  0.0, -1.0,     0.0,  0.0,  1.0,  // 8 v4
! 
!        0.0,  2.0,  0.0,     1.0,  1.0,  0.0,  // 9 v0 :left
!       -1.0,  0.0, -1.0,     1.0,  1.0,  0.0,  //10 v4
!       -1.0,  0.0,  1.0,     1.0,  1.0,  0.0,  //11 v1
! 
!       -1.0,  0.0,  1.0,     0.0,  1.0,  1.0,  //12 v1 :bottom
!       -1.0,  0.0, -1.0,     0.0,  1.0,  1.0,  //13 v4
!        1.0,  0.0, -1.0,     0.0,  1.0,  1.0,  //14 v3
!        1.0,  0.0,  1.0,     0.0,  1.0,  1.0   //15 v2
      ]);
      var indices = new Uint8Array([
        0, 1, 2,   // forward
!       3, 4, 5,    // right
!       6, 7, 8,    // back
!       9,10,11,    // left
!      12,13,14,  12,14,15    // bottom
      ]);
      var xyz_size = 3;
      var c_size = 3;


以下の部分を追記した。(2018/05/10)

複数の図形を描いてみる

モデル行列として「y軸回りに45度回転してから、x軸方向に-3平行移動する」という行列を設定してから MvpMatrixを再計算し、uniform変数 u_MvpMatrix 経由で WebGL に送りこんで描画させてみます。 前の描画を消さずに次の図形を描画したので、合計で2個の図形が描画されています。 あとから描いた図形が、先に描いた図形の後ろに正しく描画されていることに注意して下さい。

LookAtPyramidElement3.html の変更点
*** LookAtPyramidElement2.html	Thu Jun 22 15:53:25 2017
--- LookAtPyramidElement3.html	Thu May 10 12:08:07 2018
***************
*** 136,141 ****
--- 136,147 ----
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
      gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
+ 
+     modelMatrix.setTranslate(-3,0,0).rotate(45, 0, 1, 0);
+     mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
+     gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
+ 
+     gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
  }
  //]]>
    </script>


アニメーション

モデル行列の内容を変更しながら、 画面を消しては再描画することを繰り返すとアニメーションになります。

setIntevalの第1引数で指定する関数は、Globalスコープ (Globalオブジェクト)からアクセスできる 状態でなければいけないことに注意して下さい。

LookAtPyramidElement4.html の変更点
*** LookAtPyramidElement2.html	Thu Jun 22 15:53:25 2017
--- LookAtPyramidElement4.html	Thu May 10 12:30:52 2018
***************
*** 133,141 ****
      gl.depthFunc(gl.LEQUAL);
  
      gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
-     gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
!     gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
  }
  //]]>
    </script>
--- 133,152 ----
      gl.depthFunc(gl.LEQUAL);
  
      gl.clearColor(0.0, 0.0, 0.0, 1.0); // black
  
!     var theta = 0;
!     doStep = function() {
!       theta = theta + 5;
! 
!       modelMatrix.setRotate(theta, 0, 1, 0);
!       mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
!       gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
! 
!       gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
!       gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
!     }
! 
!     setInterval("doStep()", 100); // the function "doStep()" must be defined in the GlovalScope
  }
  //]]>
    </script>


アニメーション (2)

以下の部分を追記した。(2018/05/14)

元は同じ形ですが、1.5倍したものを (-4, 0, 0) だけ平行移動して後ろに表示してみます。 手前の物体はy軸回りに、奥の物体はx軸回りに回転しています。

LookAtPyramidElement5.html の変更点
*** LookAtPyramidElement4.html	Thu May 10 12:30:52 2018
--- LookAtPyramidElement5.html	Mon May 14 10:51:36 2018
***************
*** 144,149 ****
--- 144,156 ----
  
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
+ 
+       modelMatrix.setTranslate(-4,0,0).scale(1.5,1.5,1.5).rotate(theta, 1, 0, 0);
+       mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
+       gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
+ 
+       gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
+ 
      }
  
      setInterval("doStep()", 100); // the function "doStep()" must be defined in the GlovalScope