WebGL学習記録(8) ライティング


参考[webglorg](http://wgld.org/);
原則として上記ページを見ながら学習した記録です。

![Screen Shot 2015-10-30 at 20.44.11.png](https://qiita-image-store.s3.amazonaws.com/0/54225/fe1e78fb-a1c2-8214-0321-211324f2c8b8.png "Screen Shot 2015-10-30 at 20.44.11.png")


## 平行光源
平行光源(ディレクショナルライト)によるライティングは比較的簡単に実装可能。
無限に遠いところから、３次元空間全体に平行に降り注ぐ光。

## 法線ベクトルとライトベクトル
法線とは、向きを表すベクトルのこと。３次元における向きのこと。
正しいデータさえ用意すれば、WebGL側で計算してくれるので、あまり心配することはない。

## ディレクショナルライティングシェーダ
ではシェーダの実装から。

```GLSL
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);
}
```

attribute 変数に新たに nomal が追加されました。
これは頂点の法線情報を格納するための変数。
uniform 変数に二つの追加があるが、
invMatrix はモデル座標変換行列の逆行列を受け取るための変数。
lightDirection は平行光源から発せられる光の向きを表すベクトル。

続いてライティングを行うための係数を計算する。

```GLSL
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);
```


この部分では、まずinvLightを正規化している。
次に法線とライトベクトルの内積をとっている。
最後に算出したライト係数と、頂点色とを掛け合わせてフラグメントシェーダに varying 変数として渡している。


## VBOに放線情報を追加する
前回のトーラスモデルを修正。放線の情報も渡せるようにする。

```javascript
// トーラスを生成する関数
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];
}
```

この関数をもとにトーラス生成をし、VBOを作成する。

```javascript
// attributeLocationを配列に取得
var attLocation = new Array();
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'normal');
attLocation[2] = gl.getAttribLocation(prg, 'color');

// attributeの要素数を配列に格納
var attStride = new Array();
attStride[0] = 3;
attStride[1] = 3;
attStride[2] = 4;

// トーラスの頂点データを生成
var torusData = torus(32, 32, 1.0, 2.0);
var position = torusData[0];
var normal = torusData[1];
var color = torusData[2];
var index = torusData[3];

// VBOの生成
var pos_vbo = create_vbo(position);
var nor_vbo = create_vbo(normal);
var col_vbo = create_vbo(color);
```

uniform 装飾子付きの変数も増えたので、uniformLocation を取得する処理も追加する。

```javascript
// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'invMatrix');
uniLocation[2] = gl.getUniformLocation(prg, 'lightDirection');
```


## ライトに関する処理を追加
```javascript
// 各種行列の生成と初期化
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
var invMatrix = m.identity(m.create());

// ビュー×プロジェクション座標変換行列
m.lookAt([0.0, 0.0, 20.0], [0, 0, 0], [0, 1, 0], vMatrix);
m.perspective(45, c.width / c.height, 0.1, 100, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);

// 平行光源の向き
var lightDirection = [-0.5, 0.5, 0.5];
```

左やや後方から原点に向かって光をあてる。
invMatrixにデータをセットしているのは以下のコード。

```javascript
// カウンタをインクリメントする
count++;

// カウンタを元にラジアンを算出
var rad = (count % 360) * Math.PI / 180;

// モデル座標変換行列の生成
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0, 1, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);

// モデル座標変換行列から逆行列を生成
m.inverse(mMatrix, invMatrix);

// uniform変数の登録
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
gl.uniform3fv(uniLocation[2], lightDirection);
```

ライトベクトルは三つの要素を持つベクトルなので、行列を扱う場合とは違い uniform3fv を使うことに注意。


## HTML,スクリプト全文

```HTML
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">

        
        <title>WebGL TEST</title>
        <script src="minMatrix.js" type="text/javascript"></script>
        <script src="script.js" type="text/javascript"></script>

        <script id="vs" 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="fs" type="x-shader/x-fragment">
		precision mediump float;

		varying vec4 vColor;

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

    </head>
    <body>
        <canvas id="canvas" width="300" height="300"></canvas>
    </body>
</html>
```

```javascript
onload = function(){
    // canvasエレメントを取得
    var c = document.getElementById('canvas');
    c.width = 500;
    c.height = 300;
    
    // webglコンテキストを取得
    var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
    
    // 頂点シェーダとフラグメントシェーダの生成
    var v_shader = create_shader('vs');
    var f_shader = create_shader('fs');
    
    // プログラムオブジェクトの生成とリンク
    var prg = create_program(v_shader, f_shader);
    
    // attributeLocationを配列に取得
    var attLocation = new Array();
    attLocation[0] = gl.getAttribLocation(prg, 'position');
    attLocation[1] = gl.getAttribLocation(prg, 'normal');
    attLocation[2] = gl.getAttribLocation(prg, 'color');
    
    // attributeの要素数を配列に格納
    var attStride = new Array();
    attStride[0] = 3;
    attStride[1] = 3;
    attStride[2] = 4;
    
    // トーラスの頂点データを生成
    var torusData = torus(32, 32, 1.0, 2.0);
    var position = torusData[0];
    var normal = torusData[1];
    var color = torusData[2];
    var index = torusData[3];
    
    // VBOの生成
    var pos_vbo = create_vbo(position);
    var nor_vbo = create_vbo(normal);
    var col_vbo = create_vbo(color);
    
    // VBO を登録する
    set_attribute([pos_vbo, nor_vbo, col_vbo], attLocation, attStride);
    
    // IBOの生成
    var ibo = create_ibo(index);
    
    // IBOをバインドして登録する
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
    
    // uniformLocationを配列に取得
    var uniLocation = new Array();
    uniLocation[0] = gl.getUniformLocation(prg, 'mvpMatrix');
    uniLocation[1] = gl.getUniformLocation(prg, 'invMatrix');
    uniLocation[2] = gl.getUniformLocation(prg, 'lightDirection');
    
    // minMatrix.js を用いた行列関連処理
    // matIVオブジェクトを生成
    var m = new matIV();
    
    // 各種行列の生成と初期化
    var mMatrix = m.identity(m.create());
    var vMatrix = m.identity(m.create());
    var pMatrix = m.identity(m.create());
    var tmpMatrix = m.identity(m.create());
    var mvpMatrix = m.identity(m.create());
    var invMatrix = m.identity(m.create());
    
    // ビュー×プロジェクション座標変換行列
    m.lookAt([0.0, 0.0, 20.0], [0, 0, 0], [0, 1, 0], vMatrix);
    m.perspective(45, c.width / c.height, 0.1, 100, pMatrix);
    m.multiply(pMatrix, vMatrix, tmpMatrix);
    
    // 平行光源の向き
    var lightDirection = [-0.5, 0.5, 0.5];
    
    // カウンタの宣言
    var count = 0;
    
    // カリングと深度テストを有効にする
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);
    gl.enable(gl.CULL_FACE);
    
    // 恒常ループ
    (function(){
        // canvasを初期化
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clearDepth(1.0);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        
        // カウンタをインクリメントする
        count++;
        
        // カウンタを元にラジアンを算出
        var rad = (count % 360) * Math.PI / 180;
        
        // モデル座標変換行列の生成
        m.identity(mMatrix);
        m.rotate(mMatrix, rad, [0, 1, 1], mMatrix);
        m.multiply(tmpMatrix, mMatrix, mvpMatrix);
        
        // モデル座標変換行列から逆行列を生成
        m.inverse(mMatrix, invMatrix);
        
        // uniform変数の登録
        gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
        gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
        gl.uniform3fv(uniLocation[2], lightDirection);
        
        // モデルの描画
        gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
        
        // コンテキストの再描画
        gl.flush();
        
        // ループのために再帰呼び出し
        setTimeout(arguments.callee, 1000 / 30);
    })();
    
    // シェーダを生成する関数
    function create_shader(id){
        // シェーダを格納する変数
        var shader;
        
        // HTMLからscriptタグへの参照を取得
        var scriptElement = document.getElementById(id);
        
        // scriptタグが存在しない場合は抜ける
        if(!scriptElement){return;}
        
        // scriptタグのtype属性をチェック
        switch(scriptElement.type){
            
            // 頂点シェーダの場合
            case 'x-shader/x-vertex':
                shader = gl.createShader(gl.VERTEX_SHADER);
                break;
                
            // フラグメントシェーダの場合
            case 'x-shader/x-fragment':
                shader = gl.createShader(gl.FRAGMENT_SHADER);
                break;
            default :
                return;
        }
        
        // 生成されたシェーダにソースを割り当てる
        gl.shaderSource(shader, scriptElement.text);
        
        // シェーダをコンパイルする
        gl.compileShader(shader);
        
        // シェーダが正しくコンパイルされたかチェック
        if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
            
            // 成功していたらシェーダを返して終了
            return shader;
        }else{
            
            // 失敗していたらエラーログをアラートする
            alert(gl.getShaderInfoLog(shader));
        }
    }
    
    // プログラムオブジェクトを生成しシェーダをリンクする関数
    function create_program(vs, fs){
        // プログラムオブジェクトの生成
        var program = gl.createProgram();
        
        // プログラムオブジェクトにシェーダを割り当てる
        gl.attachShader(program, vs);
        gl.attachShader(program, fs);
        
        // シェーダをリンク
        gl.linkProgram(program);
        
        // シェーダのリンクが正しく行なわれたかチェック
        if(gl.getProgramParameter(program, gl.LINK_STATUS)){
        
            // 成功していたらプログラムオブジェクトを有効にする
            gl.useProgram(program);
            
            // プログラムオブジェクトを返して終了
            return program;
        }else{
            
            // 失敗していたらエラーログをアラートする
            alert(gl.getProgramInfoLog(program));
        }
    }
    
    // VBOを生成する関数
    function create_vbo(data){
        // バッファオブジェクトの生成
        var vbo = gl.createBuffer();
        
        // バッファをバインドする
        gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
        
        // バッファにデータをセット
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
        
        // バッファのバインドを無効化
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        
        // 生成した VBO を返して終了
        return vbo;
    }
    
    // VBOをバインドし登録する関数
    function set_attribute(vbo, attL, attS){
        // 引数として受け取った配列を処理する
        for(var i in vbo){
            // バッファをバインドする
            gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]);
            
            // attributeLocationを有効にする
            gl.enableVertexAttribArray(attL[i]);
            
            // attributeLocationを通知し登録する
            gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0);
        }
    }
    
    // IBOを生成する関数
    function create_ibo(data){
        // バッファオブジェクトの生成
        var ibo = gl.createBuffer();
        
        // バッファをバインドする
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
        
        // バッファにデータをセット
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
        
        // バッファのバインドを無効化
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        
        // 生成したIBOを返して終了
        return ibo;
    }
    
    // トーラスを生成する関数
    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;
    }
    
};
```