WebGL 教科書の写経、その3

平行光源によるライティング

<!DOCTYPE html>
<meta charset="utf-8">
<title>WebGL</title>

<style>
body { background: black; }
</style>
<body>
  <canvas width="512" height="512"</canvas>
  

<!-- ※頂点シェーダ -->
<script id="vshader" type="x-shader/x-vertex">

  attribute vec3 position;
  attribute vec3 normal;
  attribute vec4 color;
  uniform   mat4 mvpMatrix;
  uniform   mat4 invMatrix;
  uniform   vec3 lightDirection;
  varying   vec4 vColor;

  void main (void) {
    vec3  invLight = normalize (invMatrix * vec4 (lightDirection, 0.0)).xyz;
    float diffuse  = clamp (dot (normal, invLight), 0.1, 1.0);
    vColor         = color * vec4 (vec3 (diffuse), 1.0);
    gl_Position    = mvpMatrix * vec4 (position, 1.0);
  }

</script>

<!-- ※フラグメントシェーダ -->
<script id="fshader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 vColor;

  void main(void){
    gl_FragColor = vColor;
  }

</script>


<script>
{
  class Matrix4 {

    constructor (ary = [1,0,0,0,  0,1,0,0,  0,0,1,0,  0,0,0,1]) {
      let
        F = Math.fround,
        M = new Float32Array (16);
      
      for (let i = 0; i < 16; i++)
        M[i] = F (ary[i] || 0);

      this.M = M;
    }


    multiply (mat1) {
      let
        [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = this.M,
        [A, B, C, D, E, F, G, H, I, J, K, L, M, N ,O, P] = mat1.M;

      this.M = new Float32Array ([
        A * a + B * e + C * i + D * m,
        A * b + B * f + C * j + D * n,
        A * c + B * g + C * k + D * o,
        A * d + B * h + C * l + D * p,
        E * a + F * e + G * i + H * m,
        E * b + F * f + G * j + H * n,
        E * c + F * g + G * k + H * o,
        E * d + F * h + G * l + H * p,
        I * a + J * e + K * i + L * m,
        I * b + J * f + K * j + L * n,
        I * c + J * g + K * k + L * o,
        I * d + J * h + K * l + L * p,
        M * a + N * e + O * i + P * m,
        M * b + N * f + O * j + P * n,
        M * c + N * g + O * k + P * o,
        M * d + N * h + O * l + P * p
      ]);
      
      return this;
    }


    scale ([x,y,z]) {
      let [a,b,c,d, e,f,g,h, i,j,k,l, m,n,o,p] = this.M;
      this.M = new Float32Array ([
        a * x, b * x, c * x, d * x,
        e * y, f * y, g * y, h * y,
        i * z, j * z, k * z, l * z,
        m, n, o, p
      ]);
      
      return this;
    }


    translate ([x, y, z]) {
      let
        [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p] = this.M;
        
      this.M = new Float32Array ([
        a, b, c, d,
        e, f, g, h,
        i, j, k, l,
        a * x + e * y + i * z + m,
        b * x + f * y + j * z + n,
        c * x + g * y + k * z + o,
        d * x + h * y + l * z + p
      ]);

      return this;
    }


    rotate (angle, [a, b, c]) {
      let
        sq = Math.sqrt(a * a + b * b + c * c) || Number.MIN_VALUE;

//      if (! sq) throw new Error ('ベクトルが値が0です');
      if (sq != 1)//一応これが高速化?
        sq = 1 / sq; a *= sq; b *= sq; c *= sq; //単位ベクトルにしている

      let
        d = Math.sin (angle),
        e = Math.cos (angle),
        f = 1 - e,
        [g,h,i,j,k,l,m,n,o,p,q,r, B,C,D,E] = this.M,
        s = a * a * f + e,
        t = b * a * f + c * d,
        u = c * a * f - b * d,
        v = a * b * f - c * d,
        w = b * b * f + e,
        x = c * b * f + a * d,
        y = a * c * f + b * d,
        z = b * c * f - a * d,
        A = c * c * f + e;

      this.M = new Float32Array ([
        g * s + k * t + o * u,
        h * s + l * t + p * u,
        i * s + m * t + q * u,
        j * s + n * t + r * u,
        g * v + k * w + o * x,
        h * v + l * w + p * x,
        i * v + m * w + q * x,
        j * v + n * w + r * x,
        g * y + k * z + o * A,
        h * y + l * z + p * A,
        i * y + m * z + q * A,
        j * y + n * z + r * A,
        B, C, D,E
      ]);
      
      return this;
    }
    

    inverse () {
      let
        [a,b,c,d, e,f,g,h, i,j,k,l, m,n,o,p] = this.M,
        q = a * f - b * e,  r = a * g - c * e,
        s = a * h - d * e,  t = b * g - c * f,
        u = b * h - d * f,  v = c * h - d * g,
        w = i * n - j * m,  x = i * o - k * m,
        y = i * p - l * m,  z = j * o - k * n,
        A = j * p - l * n,  B = k * p - l * o,
        ivd = 1 / (q * B - r * A + s * z + t * y - u * x + v * w);

      this.M = new Float32Array ([
        ( f * B - g * A + h * z) * ivd,
        (-b * B + c * A - d * z) * ivd,
        ( n * v - o * u + p * t) * ivd,
        (-j * v + k * u - l * t) * ivd,
        (-e * B + g * y - h * x) * ivd,
        ( a * B - c * y + d * x) * ivd,
        (-m * v + o * s - p * r) * ivd,
        ( i * v - k * s + l * r) * ivd,
        ( e * A - f * y + h * w) * ivd,
        (-a * A + b * y - d * w) * ivd,
        ( m * u - n * s + p * q) * ivd,
        (-i * u + j * s - l * q) * ivd,
        (-e * z + f * x - g * w) * ivd,
        ( a * z - b * x + c * w) * ivd,
        (-m * t + n * r - o * q) * ivd,
        ( i * t - j * r + k * q) * ivd
      ]);
      
      return this;
    }
 
    
    
    copy () {
      return new Matrix4 (this.M);
    }
    

    static lookAt (eye, center, up) {
      let
        [eyeX, eyeY, eyeZ] = eye,
        [upX,  upY,  upZ]  = up,
        [centerX, centerY, centerZ] = center;

      if (eyeX == centerX && eyeY == centerY && eyeZ == centerZ)
        return (new this).identity ();

      let
        sqrt = Math.sqrt,
        z0 = eyeX - centerX,
        z1 = eyeY - centerY,
        z2 = eyeZ - centerZ,
        l = 1 / sqrt (z0 * z0 + z1 * z1 + z2 * z2);

      z0 *= l; z1 *= l; z2 *= l;

      let
        x0 = upY * z2 - upZ * z1,
        x1 = upZ * z0 - upX * z2,
        x2 = upX * z1 - upY * z0;

      l = sqrt(x0 * x0 + x1 * x1 + x2 * x2);
      if (! l) {
        x0 = 0; x1 = 0; x2 = 0;
      } else {
        l = 1 / l;
        x0 *= l; x1 *= l; x2 *= l;
      }

      let
        y0 = z1 * x2 - z2 * x1,
        y1 = z2 * x0 - z0 * x2,
        y2 = z0 * x1 - z1 * x0;
      l = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
      if (! l) {
        y0 = 0; y1 = 0; y2 = 0;
      } else {
        l = 1 / l;
        y0 *= l; y1 *= l; y2 *= l;
      }

      return new this ([
        x0, y0, z0, 0,
        x1, y1, z1, 0,
        x2, y2, z2, 0,
        -(x0 * eyeX + x1 * eyeY + x2 * eyeZ),
        -(y0 * eyeX + y1 * eyeY + y2 * eyeZ),
        -(z0 * eyeX + z1 * eyeY + z2 * eyeZ),
        1
      ]);
    }


    static perspective (fovy, aspect, near, far) {
      let
        t = near * Math.tan (fovy * Math.PI / 360),
        r = t * aspect,
        a = r * 2, b = t * 2, c = far - near;

      return new this ([
        near * 2 / a, 0, 0, 0,
        0, near * 2 / b, 0, 0,
        0, 0, -(far + near) / c, -1,
        0, 0, -(far * near * 2) / c, 0
      ]);
    }

  }
  

  this.Matrix4 = Matrix4;
}



{// WebGL Shader Object 
  const
    ATT_STRIDE = {position: 3, normal: 3, color: 4};


  class Shader {
    constructor (canvas, vertex, fragment) {
      this.canvas   = canvas;
      this.gl       = canvas.getContext('webgl');
      this.v_shader = create_shader.call (this, vertex);
      this.f_shader = create_shader.call (this, fragment);
      this.prg      = create_program.call (this);
      this.vbo      = { };
      this.uniLocation = { mvp: null, inv: null, light: null };

      let gl = this.gl;
      gl.enable (gl.DEPTH_TEST);
      gl.depthFunc (gl.LEQUAL);
      gl.enable (gl.CULL_FACE);      

      this.clear ();
    }
    
    
    // canvas 初期化
    clear (r = 0, g = 0, b = 0, a = 1) {
      let gl = this.gl;
      gl.clearColor (r, g, b, a);
      gl.clearDepth (1.0);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

      return this;
    }
    
    
    // Shader に VBO をセットする type = [ position, normal, color ]
    setVBO (data = [], type = 'position') {
      let
        { gl, prg } = this,
        attLocation = gl.getAttribLocation (prg, type),
        attStride = ATT_STRIDE[type],
        vbo = create_vbo.call (this, data);

      gl.bindBuffer (gl.ARRAY_BUFFER, vbo);
      gl.enableVertexAttribArray (attLocation);
      gl.vertexAttribPointer (attLocation, attStride, gl.FLOAT, false, 0, 0);
      this.vbo[type] = vbo;

      return this;
    }
    
    
    setIBO (data = []) {
      let
        gl = this.gl,
        ibo = create_ibo.call (this, data);

      gl.bindBuffer (gl.ELEMENT_ARRAY_BUFFER, ibo);
      this.ibo = ibo;
      
      return this;
    }


    setLocation () {
      let {gl, prg} = this;
      this.uniLocation = {
        mvp:   gl.getUniformLocation (prg, 'mvpMatrix'),
        inv:   gl.getUniformLocation (prg, 'invMatrix'),
        light: gl.getUniformLocation (prg, 'lightDirection')
      };
      
      return this;
    }
       
    
    draw (mvpMatrix, invMatrix, lightDirection, vertex_index, flush = true) {
      let
        {gl, prg, uniLocation} = this,
        {mvp, inv, light} = uniLocation;

      gl.uniformMatrix4fv (mvp, false, mvpMatrix);
      gl.uniformMatrix4fv (inv, false, invMatrix);
      gl.uniform3fv (light, lightDirection);

      gl.drawElements (gl.TRIANGLES, vertex_index.length, gl.UNSIGNED_SHORT, 0);
      
      if (flush)
        gl.flush ();
      
      return this;
    }
    
  }

//__________________  

  const
    SHADER_TYPE = {
      'x-shader/x-vertex': 'VERTEX_SHADER',
      'x-shader/x-fragment': 'FRAGMENT_SHADER'},


    create_shader =
      function (target = null) {
        if (target) {
          let
            gl = this.gl,
            {text, type} = target,
            shader = gl.createShader (gl[SHADER_TYPE[type]]);

          gl.shaderSource (shader, text);
          gl.compileShader (shader);
          if (! gl.getShaderParameter (shader, gl.COMPILE_STATUS))
            throw new Error (['シェーダーのコンパイルエラーがありました',
              gl.getShaderInfoLog (shader)].join ('\n'))

          return shader;
        }
        throw new Error ('要素がありません');
      },


    // プログラムオブジェクトを生成しシェーダをリンクする関数
    create_program =
      function () {
        let
          gl = this.gl,
          program = gl.createProgram ();

        gl.attachShader (program, this.v_shader);
        gl.attachShader (program, this.f_shader);
        gl.linkProgram (program);

        if (! gl.getProgramParameter(program, gl.LINK_STATUS))
          throw new Error (['プログラムのオブジェクトの生成に失敗しました',
            gl.getProgramInfoLog (program)].join ('\n'));

        gl.useProgram (program);
        return program;
      },


    create_vbo =
      function (data) {
        let
          gl = this.gl,
          vbo = gl.createBuffer (),
          buf = gl.ARRAY_BUFFER;

        gl.bindBuffer (buf, vbo);
        gl.bufferData (buf, new Float32Array (data), gl.STATIC_DRAW);
        gl.bindBuffer (buf, null);

        return vbo;
      },
    
    
    create_ibo =
      function (data) {
        let
          gl = this.gl,
          ibo = gl.createBuffer (),
          buf = gl.ELEMENT_ARRAY_BUFFER;

        gl.bindBuffer (buf, ibo);
        gl.bufferData (buf, new Int16Array (data), gl.STATIC_DRAW);
        gl.bindBuffer (buf, null);

        return ibo;
      };
      

  //_______
  
  this.Shader = Shader;
}

//_________
  function torus(row, column, irad, orad){
    var pos = new Array(), nor = new Array(),
      col = new Array(), idx = new Array();
    for(var i = 0; i <= row; i++){
      var r = Math.PI * 2 / row * i;
      var rr = Math.cos(r);
      var ry = Math.sin(r);
      for(var ii = 0; ii <= column; ii++){
        var tr = Math.PI * 2 / column * ii;
        var tx = (rr * irad + orad) * Math.cos(tr);
        var ty = ry * irad;
        var tz = (rr * irad + orad) * Math.sin(tr);
        var rx = rr * Math.cos(tr);
        var rz = rr * Math.sin(tr);
        pos.push(tx, ty, tz);
        nor.push(rx, ry, rz);
        var tc = hsva(360 / column * ii, 1, 1, 1);
        col.push(tc[0], tc[1], tc[2], tc[3]);
      }
    }
    for(i = 0; i < row; i++){
      for(ii = 0; ii < column; ii++){
        r = (column + 1) * i + ii;
        idx.push(r, r + column + 1, r + 1);
        idx.push(r + column + 1, r + column + 2, r + 1);
      }
    }
    return [pos, nor, col, idx];
  }  
  // HSVカラー取得用関数
  function hsva(h, s, v, a){
    if(s > 1 || v > 1 || a > 1){return;}
    var th = h % 360;
    var i = Math.floor(th / 60);
    var f = th / 60 - i;
    var m = v * (1 - s);
    var n = v * (1 - s * f);
    var k = v * (1 - s * (1 - f));
    var color = new Array();
    if(!s > 0 && !s < 0){
      color.push(v, v, v, a); 
    } else {
      var r = new Array(v, n, m, m, k, v);
      var g = new Array(k, v, v, n, m, m);
      var b = new Array(m, m, k, v, v, n);
      color.push(r[i], g[i], b[i], a);
    }
    return color;
  }



{//実行スクリプト
  let
    doc = document,
    canvas = doc.querySelector ('canvas'),
    shader = new Shader (
      canvas,
      doc.querySelector ('script[type="x-shader/x-vertex"]'),
      doc.querySelector ('script[type="x-shader/x-fragment"]'),
    ),

    // 各種行列の生成と初期化
    matrix = Matrix4.perspective (45, canvas.width / canvas.height, 0.1, 100)
             .multiply (Matrix4.lookAt ([0, 0, 10.0], [0, 0, 0], [0, 1, 0]))
             .multiply (new Matrix4),
    // 光のベクトル
    lightDirection = [-0.5, 0.5, 0.5],

    // モデル(頂点)データ
    [vertex_position, vector_normal, vertex_color, vertex_index]
      = torus (72, 72, 1.0, 2.0);


  shader.setLocation ();
  shader.setVBO (vertex_position, 'position');
  shader.setVBO (vector_normal, 'normal');
  shader.setVBO (vertex_color, 'color');
  shader.setIBO (vertex_index);

  //_____
  
  let cnt = 0, pi = Math.PI, deg = pi / 180;

  const loop = function (timeStamp) {
    
    shader.clear ();

    let
      rad = (cnt++ %360) * deg,
      m = (new Matrix4).rotate (rad, [0, 1, 1]),
      mMatrix = matrix.copy ().multiply (m),      
      invMatrix = m.copy ().inverse ();
    
    shader.draw (mMatrix.M, invMatrix.M, lightDirection, vertex_index);

    requestAnimationFrame (loop);
  }
  
  loop ();
}

</script>