WebGL (5)


物体の変換と法線

物体のモデル行列を用いて、物体を平行移動、回転、拡大縮小などの 座標変換してみます。 下の例では、立方体をz軸まわりに45度回転し、 xyz軸方向にそれぞれ(0.7, 0.3, 0.6)倍してから、 最後に平行移動(0,0,-1)しています。

  変換1, 変換2, 変換3の場合は
    var m = new Matrix4();
    m.set変換3(パラメータ);
    m.変換2(パラメータ);
    m.変換1(パラメータ);
  の順でモデル行列mを設定します。

元々の OpenGL に用意されている行列計算の関数でもそうですが、 Matrix4 では右から行列をかけるので、 物体への操作とは逆順に行列の乗算をおこなっているように 見えるかもしれません。 ですが、正しく

    変換3の行列 * 変換2の行列 * 変換1の行列
の計算を行っていることになります。

ParallelLightCube5.html の変更点

*** ParallelLightCube4.html	Tue Jul  1 18:28:45 2014
--- ParallelLightCube5.html	Fri May 11 12:23:46 2018
***************
*** 74,79 ****
--- 74,82 ----
  
      viewMatrix.setLookAt(-3.0, 5.0, 5.0, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
+     modelMatrix.setTranslate(0, 0, -1);
+     modelMatrix.scale(0.7, 0.3, 0.6);
+     modelMatrix.rotate(45, 0, 0, 1);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
      gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
  

さて、実はこの画像には問題が発生しています。 物体を拡大縮小したり回転したり(座標変換)すると法線の向きが変わってしまうことが あるので、上の例では面の明るさが正しく計算できていないのです。

教科書 p.395, 図8.15

物体を座標変換する行列をモデル行列と呼ぶのですが、 「座標変換によって変化した法線の向き」は 「『モデル行列の逆転置行列』と『元の法線』の乗算」で求まります。 「逆転置行列」とは「逆行列を転置した行列」のことです。

cuon-matrix.js 中で定義されている Matrix4 オブジェクトには、逆転転置行列 を計算するのに役立つ関数が用意されています。

Matrix4.setInverseOf(other) 渡された行列other の逆行列を計算して、この行列に代入する。
Matrix4.transpose() 行列を転置して、上書きする。
ParallelLightCube6.html の変更点

*** ParallelLightCube5.html	Fri May 11 12:23:46 2018
--- ParallelLightCube6.html	Fri May 11 12:29:33 2018
***************
*** 15,27 ****
  attribute vec4 a_Color;\n\
  attribute vec4 a_Normal;\n\
  uniform mat4 u_MvpMatrix;\n\
  uniform vec3 u_LightColor;\n\
  uniform vec3 u_LightDirection;\n\
  uniform vec3 u_AmbientLight;\n\
  varying vec4 v_Color;\n\
  void main() {\n\
      gl_Position = u_MvpMatrix * a_Position;\n\
!     vec3 normal = normalize(a_Normal.xyz);\n\
      float nDotL = max(dot(u_LightDirection,normal),0.0);\n\
      vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n\
      vec3 ambient = u_AmbientLight * a_Color.rgb;\n\
--- 15,28 ----
  attribute vec4 a_Color;\n\
  attribute vec4 a_Normal;\n\
  uniform mat4 u_MvpMatrix;\n\
+ uniform mat4 u_NormalMatrix;\n\
  uniform vec3 u_LightColor;\n\
  uniform vec3 u_LightDirection;\n\
  uniform vec3 u_AmbientLight;\n\
  varying vec4 v_Color;\n\
  void main() {\n\
      gl_Position = u_MvpMatrix * a_Position;\n\
!     vec3 normal = normalize(vec3(u_NormalMatrix * a_Normal));\n\
      float nDotL = max(dot(u_LightDirection,normal),0.0);\n\
      vec3 diffuse = u_LightColor * a_Color.rgb * nDotL;\n\
      vec3 ambient = u_AmbientLight * a_Color.rgb;\n\
***************
*** 53,62 ****
      if (! tsuda.setElementArrayBuffer(indices)) return;
  
      var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
      var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
      var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
      var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
!     if (! u_MvpMatrix || ! u_LightColor || ! u_LightDirection || ! u_AmbientLight) {
          console.log('failed to get location of uniform variable');
          return;
      }
--- 54,65 ----
      if (! tsuda.setElementArrayBuffer(indices)) return;
  
      var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
+     var u_NormalMatrix = gl.getUniformLocation(gl.program,'u_NormalMatrix');
      var u_LightColor = gl.getUniformLocation(gl.program, 'u_LightColor');
      var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
      var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
!     if (! u_MvpMatrix || ! u_LightColor || ! u_LightDirection || ! u_AmbientLight
!         || ! u_NormalMatrix) {
          console.log('failed to get location of uniform variable');
          return;
      }
***************
*** 71,76 ****
--- 74,80 ----
      var viewMatrix = new Matrix4();
      var projMatrix = new Matrix4();
      var mvpMatrix = new Matrix4();
+     var normalMatrix = new Matrix4();
  
      viewMatrix.setLookAt(-3.0, 5.0, 5.0, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
***************
*** 79,84 ****
--- 83,91 ----
      modelMatrix.rotate(45, 0, 0, 1);
      mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
      gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
+     normalMatrix.setInverseOf(modelMatrix);
+     normalMatrix.transpose();
+     gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements);
  
      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LEQUAL);


変換行列を変えながら同じ物体を何度も描く

ParallelLightCubes.html

*** ParallelLightCube6.html	Fri May 11 12:29:33 2018
--- ParallelLightCubes.html	Tue Jul  1 18:30:38 2014
***************
*** 76,100 ****
      var mvpMatrix = new Matrix4();
      var normalMatrix = new Matrix4();
  
-     viewMatrix.setLookAt(-3.0, 5.0, 5.0, 0, 0, 0, 0, 1, 0);
-     projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
-     modelMatrix.setTranslate(0, 0, -1);
-     modelMatrix.scale(0.7, 0.3, 0.6);
-     modelMatrix.rotate(45, 0, 0, 1);
-     mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
-     gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
-     normalMatrix.setInverseOf(modelMatrix);
-     normalMatrix.transpose();
-     gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements);
- 
      gl.enable(gl.DEPTH_TEST);
      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>
  </head>
--- 76,110 ----
      var mvpMatrix = new Matrix4();
      var normalMatrix = new Matrix4();
  
      gl.enable(gl.DEPTH_TEST);
      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);
  
!     viewMatrix.setLookAt(-30.0, 50.0, 50.0, 0, 0, 0, 0, 1, 0);
!     projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
! 
!     var i;
!     for (i=0; i<30; i++) {
!         modelMatrix.setTranslate(tsuda.tr(20),tsuda.tr(20),tsuda.tr(20));
!         modelMatrix.rotate(Math.random()*90, 1, 0, 0);
!         modelMatrix.rotate(Math.random()*90, 0, 1, 0);
!         modelMatrix.rotate(Math.random()*90, 0, 0, 1);
!         mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
!         gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
!         normalMatrix.setInverseOf(modelMatrix);
!         normalMatrix.transpose();
!         gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements);
! 
!         gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
!     }
  }
+ 
+ tsuda.tr = function (d) {
+     return Math.random() * 2 * d - d;
+ };
+ 
  //]]>
    </script>
  </head>

頂点の色を何パターンか生成しておいて、繰り返しのときに色をランダムに 選択するようにしたのが次の例です。

ParallelLightCubes2.htmlの変更点

*** ParallelLightCubes.html	Tue Jul  1 18:30:38 2014
--- ParallelLightCubes2.html	Tue Jul  1 18:30:52 2014
***************
*** 44,54 ****
  
      var vertices = tsuda.cube.vertices;
      var indices = tsuda.cube.indices;
!     var colors = tsuda.get3Float32Array(0.0, 1.0, 0.0, vertices.length);
      var normals = tsuda.indexNormals(vertices,indices);
  
      if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
!     if (! tsuda.initArrayBuffer('a_Color',colors,3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
  
      if (! tsuda.setElementArrayBuffer(indices)) return;
--- 44,61 ----
  
      var vertices = tsuda.cube.vertices;
      var indices = tsuda.cube.indices;
!     var colorList = [];
!     colorList[0] = tsuda.get3Float32Array(1.0, 0.0, 0.0, vertices.length);
!     colorList[1] = tsuda.get3Float32Array(0.0, 1.0, 0.0, vertices.length);
!     colorList[2] = tsuda.get3Float32Array(0.0, 0.0, 1.0, vertices.length);
!     colorList[3] = tsuda.get3Float32Array(1.0, 1.0, 0.0, vertices.length);
!     colorList[4] = tsuda.get3Float32Array(1.0, 0.0, 1.0, vertices.length);
!     colorList[5] = tsuda.get3Float32Array(0.0, 1.0, 1.0, vertices.length);
!     colorList[6] = tsuda.get3Float32Array(1.0, 1.0, 1.0, vertices.length);
      var normals = tsuda.indexNormals(vertices,indices);
  
      if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
!     if (! tsuda.initArrayBuffer('a_Color',colorList[0],3,gl.FLOAT)) return;
      if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
  
      if (! tsuda.setElementArrayBuffer(indices)) return;
***************
*** 87,92 ****
--- 94,101 ----
  
      var i;
      for (i=0; i<30; i++) {
+         var colorIndex = Math.floor(Math.random()*colorList.length);
+         if (! tsuda.initArrayBuffer('a_Color',colorList[colorIndex],3,gl.FLOAT)) return;
          modelMatrix.setTranslate(tsuda.tr(20),tsuda.tr(20),tsuda.tr(20));
          modelMatrix.rotate(Math.random()*90, 1, 0, 0);
          modelMatrix.rotate(Math.random()*90, 0, 1, 0);

複数の種類の立体を利用してみましょう。

ParallelLightObjects.html

*** ParallelLightCubes2.html	Tue Jul  1 18:30:52 2014
--- ParallelLightObjects.html	Tue Jul  1 18:31:08 2014
***************
*** 42,64 ****
      var canvas = app.canvas;
      var gl = app.gl;
  
!     var vertices = tsuda.cube.vertices;
!     var indices = tsuda.cube.indices;
!     var colorList = [];
!     colorList[0] = tsuda.get3Float32Array(1.0, 0.0, 0.0, vertices.length);
!     colorList[1] = tsuda.get3Float32Array(0.0, 1.0, 0.0, vertices.length);
!     colorList[2] = tsuda.get3Float32Array(0.0, 0.0, 1.0, vertices.length);
!     colorList[3] = tsuda.get3Float32Array(1.0, 1.0, 0.0, vertices.length);
!     colorList[4] = tsuda.get3Float32Array(1.0, 0.0, 1.0, vertices.length);
!     colorList[5] = tsuda.get3Float32Array(0.0, 1.0, 1.0, vertices.length);
!     colorList[6] = tsuda.get3Float32Array(1.0, 1.0, 1.0, vertices.length);
!     var normals = tsuda.indexNormals(vertices,indices);
! 
!     if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
!     if (! tsuda.initArrayBuffer('a_Color',colorList[0],3,gl.FLOAT)) return;
!     if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
! 
!     if (! tsuda.setElementArrayBuffer(indices)) return;
  
      var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
      var u_NormalMatrix = gl.getUniformLocation(gl.program,'u_NormalMatrix');
--- 42,60 ----
      var canvas = app.canvas;
      var gl = app.gl;
  
!     var obj = [ tsuda.cube, tsuda.pyramid ];
!     var i,j;
!     for (i=0; i<obj.length; i++) {
!         obj[i].colorList = [];
!         obj[i].colorList[0] = tsuda.get3Float32Array(1.0, 0.0, 0.0, obj[i].vertices.length);
!         obj[i].colorList[1] = tsuda.get3Float32Array(0.0, 1.0, 0.0, obj[i].vertices.length);
!         obj[i].colorList[2] = tsuda.get3Float32Array(0.0, 0.0, 1.0, obj[i].vertices.length);
!         obj[i].colorList[3] = tsuda.get3Float32Array(1.0, 1.0, 0.0, obj[i].vertices.length);
!         obj[i].colorList[4] = tsuda.get3Float32Array(1.0, 0.0, 1.0, obj[i].vertices.length);
!         obj[i].colorList[5] = tsuda.get3Float32Array(0.0, 1.0, 1.0, obj[i].vertices.length);
!         obj[i].colorList[6] = tsuda.get3Float32Array(1.0, 1.0, 1.0, obj[i].vertices.length);
!         obj[i].normals = tsuda.indexNormals(obj[i].vertices,obj[i].indices);
!     }
  
      var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
      var u_NormalMatrix = gl.getUniformLocation(gl.program,'u_NormalMatrix');
***************
*** 92,112 ****
      viewMatrix.setLookAt(-30.0, 50.0, 50.0, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
  
!     var i;
!     for (i=0; i<30; i++) {
!         var colorIndex = Math.floor(Math.random()*colorList.length);
!         if (! tsuda.initArrayBuffer('a_Color',colorList[colorIndex],3,gl.FLOAT)) return;
!         modelMatrix.setTranslate(tsuda.tr(20),tsuda.tr(20),tsuda.tr(20));
!         modelMatrix.rotate(Math.random()*90, 1, 0, 0);
!         modelMatrix.rotate(Math.random()*90, 0, 1, 0);
!         modelMatrix.rotate(Math.random()*90, 0, 0, 1);
!         mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
!         gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
!         normalMatrix.setInverseOf(modelMatrix);
!         normalMatrix.transpose();
!         gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements);
! 
!         gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
      }
  }
  
--- 88,121 ----
      viewMatrix.setLookAt(-30.0, 50.0, 50.0, 0, 0, 0, 0, 1, 0);
      projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
  
!     var oidx;
!     for (oidx = 0; oidx < obj.length; oidx ++) {
!         var vertices = obj[oidx].vertices;
!         var indices = obj[oidx].indices;
!         var normals = obj[oidx].normals;
!         var colorList = obj[oidx].colorList;
!     
!         if (! tsuda.initArrayBuffer('a_Position',vertices,3,gl.FLOAT)) return;
!         if (! tsuda.initArrayBuffer('a_Color',colorList[0],3,gl.FLOAT)) return;
!         if (! tsuda.initArrayBuffer('a_Normal',normals,3,gl.FLOAT)) return;
!     
!         if (! tsuda.setElementArrayBuffer(indices)) return;
! 
!         for (i=0; i<30; i++) {
!             var colorIndex = Math.floor(Math.random()*colorList.length);
!             if (! tsuda.initArrayBuffer('a_Color',colorList[colorIndex],3,gl.FLOAT)) return;
!             modelMatrix.setTranslate(tsuda.tr(20),tsuda.tr(20),tsuda.tr(20));
!             modelMatrix.rotate(Math.random()*90, 1, 0, 0);
!             modelMatrix.rotate(Math.random()*90, 0, 1, 0);
!             modelMatrix.rotate(Math.random()*90, 0, 0, 1);
!             mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
!             gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
!             normalMatrix.setInverseOf(modelMatrix);
!             normalMatrix.transpose();
!             gl.uniformMatrix4fv(u_NormalMatrix,false,normalMatrix.elements);
!     
!             gl.drawElements(gl.TRIANGLES, indices.length,gl.UNSIGNED_BYTE,0);
!         }
      }
  }