2013年3月2日土曜日

[WebGL][JavaScript][HTML] 第2回 WebGLで遊んでみる(点の描画)

第1回は描画領域クリアまで説明した。
今回は最も簡単な図形・・・点の描画を説明していこうと思う。
また、今回からもっとも重要な要素の1つシェーダが登場する。

描画結果(画像)


真ん中に青い点が表示されている。
たったこれだけだが、シェーダが登場するため、
ソースコードは少し長い。

HTML部分。
//link・・・http://mio-koduki.blogspot.jp/2013/03/webgljavascripthtml-webgl2.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <script src="webgl.js"></script>
    </head>
    <body>
        <canvas id="canvas" width="500px" height="500px" style="width:500px;height:500px;"></canvas>
    </body>
</html>

まぁ、第1回と同じなので特に解説は要らないであろう。
canvasを用意しておくだけである。

JavaScript側。
//link・・・http://mio-koduki.blogspot.jp/2013/03/webgljavascripthtml-webgl2.html
//デバッグ用フラグ
var DEBUG_FLAG=true;

//WebGLのコンテキスト
var gl=null;

//頂点シェーダ
var vertexShaderSource='void main()\
{\
    gl_Position=vec4(0.0,0.0,0.0,1.0);\
    gl_PointSize=10.0;\
}';

//フラグメントシェーダ
var fragmentShaderSource='void main()\
{\
    gl_FragColor=vec4(0.0,0.0,1.0,1.0);\
}';

var main=function()
{
    //canvas要素を取得する
    var canvas=document.getElementById('canvas');
    //WebGLのコンテキスト取得
    gl=getContext(canvas);
    //nullだったらreturnしてmain関数を抜ける
    if(gl===null)
    {
        return;
    }
    //頂点シェーダを作成
    var vertexShader=createShader(gl.VERTEX_SHADER,vertexShaderSource);
    //nullだったらreturnしてmain関数を抜ける
    if(vertexShader===null)
    {
        return;
    }
    //フラグメントシェーダを作成
    var fragmentShader=createShader(gl.FRAGMENT_SHADER,fragmentShaderSource);
    //nullだったらreturnしてmain関数を抜ける
    if(fragmentShader===null)
    {
        return;
    }
    //プログラムを作成
    var program=createProgram(vertexShader,fragmentShader);
    //nullだったらreturnしてmain関数を抜ける
    if(program===null)
    {
        return;
    }
    //RGBAの順番にカラーバッファをクリアする色を指定する
    gl.clearColor(0.5,0.5,0.5,1.0);
    //カラーバッファをクリアする
    gl.clear(gl.COLOR_BUFFER_BIT);
    //clearメソッドのエラーチェック
    checkGLError('clear');
    //使用するプログラムをWebGLに設定する
    gl.useProgram(program);
    //useProgramメソッドのエラーチェック
    checkGLError('useProgram');
    //描画する
    gl.drawArrays(gl.POINTS,0,1);
    //drawArraysメソッドのエラーチェック
    checkGLError('drawArrays');
    //プログラム削除
    deleteProgram(program,vertexShader,fragmentShader);
    //頂点シェーダ削除
    deleteShader(vertexShader);
    //フラグメントシェーダ削除
    deleteShader(fragmentShader);
};
//ページロード時にmain関数実行
window.onload=main;

var checkGLError=function(errorString)
{
    //デバッグ時のみチェック
    if(DEBUG_FLAG)
    {
        var noError=gl.NO_ERROR;
        var error=noError;
        //エラーを取得しNO_ERROR以外だったらコンソールにメッセージを出す
        while((error=gl.getError())!=noError)
        {
            console.log(error+" : "+errorString);
        }
    }
};

var getContext=function(canvas)
{
    //WebGLのコンテキストを入れる変数
    var gl=null;
    //getContextの引数を配列で用意
    var tryContext=
    [
        'webgl'
        ,'experimental-webgl'
        ,'webkit-3d'
        ,'moz-webgl'
    ];
    //順番にgetContextを実行しWebGLのコンテキストを取得を試みる
    for(var i in tryContext)
    {
        gl=canvas.getContext(tryContext[i]);
        //WebGLのコンテキスト取得に成功したらループを抜ける
        if(gl)
        {
            break;
        }
    }
    //WebGLのコンテキストを取得できなかったらコンソールにメッセージを出しreturnで関数を抜ける
    if(!gl)
    {
        console.log('no gl');
        return null;
    }
    return gl;
};

var createShader=function(type,source)
{
    //渡されたtype引数のシェーダを作成
    var shader=gl.createShader(type);
    //createShaderメソッドのエラーチェック
    checkGLError('createShader');
    //作成できたかチェック
    if(shader===null)
    {
        console.log('no shader');
        return null;
    }
    //シェーダとソースコードをひもづける
    gl.shaderSource(shader,source);
    //shaderSourceメソッドのエラーチェック
    checkGLError('shaderSource');
    //ソースコードをコンパイルする
    gl.compileShader(shader);
    //compileShaderメソッドのエラーチェック
    checkGLError('compileShader');
    //コンパイル状態を取得
    var result=gl.getShaderParameter(shader,gl.COMPILE_STATUS);
    //getShaderParameterメソッドのエラーチェック
    checkGLError('getShaderParameter');
    //コンパイルが成功しているかどうか
    if(result===false)
    {
        console.log('failed compile');
        //シェーダのログを取得
        var log=gl.getShaderInfoLog(shader);
        //getShaderInfoLogメソッドのエラーチェック
        checkGLError('getShaderInfoLog');
        if(log===null)
        {
            console.log('no log');
        }
        else
        {
            console.log(log);
        }
        return null;
    }
    //シェーダを返す
    return shader;
};

var createProgram=function(vertexShader,fragmentShader)
{
    //プログラム作成
    var program=gl.createProgram();
    //作成できたかチェック
    if(program===null)
    {
        console.log('no program');
        return null;
    }
    //頂点シェーダをプログラムにひもづける
    gl.attachShader(program,vertexShader);
    //attachShaderメソッドのエラーチェック
    checkGLError('attachShader');
    //フラグメントシェーダをプログラムにひもづける
    gl.attachShader(program,fragmentShader);
    //attachShaderメソッドのエラーチェック
    checkGLError('attachShader');
    //頂点シェーダとフラグメントシェーダにリンク処理を行う
    gl.linkProgram(program);
    //linkProgramメソッドのエラーチェック
    checkGLError('linkProgram');
    //リンクの状態を取得
    var result=gl.getProgramParameter(program,gl.LINK_STATUS);
    //getProgramParameterメソッドのエラーチェック
    checkGLError('getProgramParameter');
    //リンクが成功しているかどうか
    if(result===false)
    {
        console.log('failed link');
        //プログラムのログを取得
        var log=gl.getProgramInfoLog(program);
        //getProgramInfoLogメソッドのエラーチェック
        checkGLError('getProgramInfoLog');
        if(log===null)
        {
            console.log('no log');
        }
        else
        {
            console.log(log);
        }
        return null;
    }
    //プログラムを返す
    return program;
};

var deleteProgram=function(program,vertexShader,fragmentShader)
{
    //頂点シェーダとプログラムのひもづけを切る
    gl.detachShader(program,vertexShader);
    //detachShaderメソッドのエラーチェック
    checkGLError('detachShader');
    //フラグメントシェーダとプログラムのひもづけを切る
    gl.detachShader(program,fragmentShader);
    //detachShaderメソッドのエラーチェック
    checkGLError('detachShader');
    //プログラムを削除する
    gl.deleteProgram(program);
    //deleteProgramメソッドのエラーチェック
    checkGLError('deleteProgram');
};

var deleteShader=function(shader)
{
    //シェーダを削除する
    gl.deleteShader(shader);
    //deleteShaderメソッドのエラーチェック
    checkGLError('deleteShader');
};

では、第1回から変わったところを詳しく見ていこう。


まず、8行目~13行目になにやら怪しいソースコードのようなものが書かれている。
//頂点シェーダ
var vertexShaderSource='void main()\
{\
 gl_Position=vec4(0.0,0.0,0.0,1.0);\
 gl_PointSize=10.0;\
}';
これは頂点シェーダという、頂点座標ごとの処理を定義しているソースコードで、
今回は描画する点の座標についての処理を定義している。
また、シェーダはGLSLという言語を用いて書く必要がある。
わざわざ新しい言語覚えるのかぁ・・・と思う必要もなく、そこまで特殊な文法は存在しない。
基本文法はC言語と似ていて、JavaScriptがわかるならある程度読み解けるだろう。
わからないことがあれば必要に応じて調べて覚えていく程度でもなんとかなるだろう。
さて、実際ここではvertexShaderSource変数に頂点シェーダのソースコードを文字列として代入している。
ソースコードは最初にmain関数を定義している。
これはGLSLもCと同じくmain関数から始まるという決まりがあるからだ。
main関数内ではgl_Positionにvec4(0.0,0.0,0.0,1.0);を代入している。
簡単に説明すると、x座標0.0、y座標0.0、z座標0.0、wに1.0を指定したvec4型を作り、
それを表示座標に代入している。
x座標、y座標、z座標はなんとなくわかるが、
wはあんまりピンと来ない。
すぐに使うわけじゃないのでざっくりとした説明だけにするが、
行列計算の時にあると便利だからついてる。
今はそのぐらいの認識でもいいと思う。
vec4型とはfloat型が4つ入る型。
もうちょっと言えば4つのfloat型によって表されるベクトルの値が入る型である。
次に、gl_PointSizeに10.0を代入しているが、これは点の大きさを定義しているだけだ。
なお、文字列中の各行末にある「\(バックスラッシュ、環境によっては円マーク)」はJavaScriptで複数行文字列リテラルを扱うために必要な物だ。


15行目~19行目も同様にフラグメントシェーダのソースコードがかかれている。
//フラグメントシェーダ
var fragmentShaderSource='void main()\
{\
 gl_FragColor=vec4(0.0,0.0,1.0,1.0);\
}';
頂点シェーダと同様にフラグメントシェーダもGLSLで書かれている。
頂点・・・はまぁ、なんとなくわかるが、フラグメントってなに?と思うだろうが、
ざっくりといえば頂点で結ばれた線の内側の一つ一つのピクセル(厳密にはピクセルではないが)ぐらいの認識でいいと思う。
つまり3つの頂点を定義し三角形を作ったとしたらその三角形の内部全てに
このフラグメントシェーダが適応される。
頂点シェーダと同様にフラグメントシェーダでもmain関数を定義している。
そして、その中でgl_FragColorにvec4(0.0,0.0,1.0,1.0)を代入している。
これは、このフラグメントの色をRGBAの順に指定したvec4型を作り代入している。
つまりは今回は青色でフラグメントを描画する。という指定だ。


次にJavaScript側のmain関数で行なっている処理を見ていこう。まず25行目~26行目。
//WebGLのコンテキスト取得
gl=getContext(canvas);
第1回にはなかったgetContextという関数にcanvas要素を渡し、
返り値をgl変数で受け取っている。


しかしよくよくgetContextが定義してある92行目~121行目を見てみると。
var getContext=function(canvas)
{
    //WebGLのコンテキストを入れる変数
    var gl=null;
    //getContextの引数を配列で用意
    var tryContext=
    [
        'webgl'
        ,'experimental-webgl'
        ,'webkit-3d'
        ,'moz-webgl'
    ];
    //順番にgetContextを実行しWebGLのコンテキストを取得を試みる
    for(var i in tryContext)
    {
        gl=canvas.getContext(tryContext[i]);
        //WebGLのコンテキスト取得に成功したらループを抜ける
        if(gl)
        {
            break;
        }
    }
    //WebGLのコンテキストを取得できなかったらコンソールにメッセージを出しreturnで関数を抜ける
    if(!gl)
    {
        console.log('no gl');
        return null;
    }
    return gl;
};
やってることは第1回と一緒でWebGLのコンテキストを取得しているだけである。
そしてそれを関数の返り値として返している。


そして27行目~31行目。
//nullだったらreturnしてmain関数を抜ける
if(gl===null)
{
    return;
}
WebGLのコンテキストが取得できなかった場合はreturnしてmain関数を終了している。


32行目~33行目にcreateShaderという関数が使われている。
//頂点シェーダを作成
var vertexShader=createShader(gl.VERTEX_SHADER,vertexShaderSource);
引数にWebGLの定数と上部で定義したvertexShaderSourceを渡している。


中で何をしているかは123行目~167行目に書いてある。
var createShader=function(type,source)
{
    /*
    省略
    */
};
具体的に何をしているかを少しずつ見ていこう。


まず125行目~128行目。
//渡されたtype引数のシェーダを作成
var shader=gl.createShader(type);
//createShaderメソッドのエラーチェック
checkGLError('createShader');
WebGLのコンテキストにあるcreateShaderメソッドに第1引数で渡ってきたtypeという変数を渡している。
上記でも少し触れたがシェーダは頂点シェーダとフラグメントシェーダがあるため、
どっちのシェーダを作るのかを指定するためにtypeを渡してもらう必要がある。
33行目ではgl.VERTEX_SHADERを渡しているので、頂点シェーダを作ることになる。
またその下の行では第1回説明したとおり返り値や例外でエラーをキャッチできないので、
エラーチェック用の関数を定義し呼んでいる。


次に129行目~134行目。
//作成できたかチェック
if(shader===null)
{
    console.log('no shader');
    return null;
}
シェーダが作成できなかった場合は、
コンソールにメッセージを出してnullを返している。


135行目~138行目。
//シェーダとソースコードをひもづける
gl.shaderSource(shader,source);
//shaderSourceメソッドのエラーチェック
checkGLError('shaderSource');
shaderSourceメソッドを使いではシェーダにソースコードをひもづけている。


そして139行目~164行目。
//ソースコードをコンパイルする
gl.compileShader(shader);
//compileShaderメソッドのエラーチェック
checkGLError('compileShader');
//コンパイル状態を取得
var result=gl.getShaderParameter(shader,gl.COMPILE_STATUS);
//getShaderParameterメソッドのエラーチェック
checkGLError('getShaderParameter');
//コンパイルが成功しているかどうか
if(result===false)
{
    console.log('failed compile');
    //シェーダのログを取得
    var log=gl.getShaderInfoLog(shader);
    //getShaderInfoLogメソッドのエラーチェック
    checkGLError('getShaderInfoLog');
    if(log===null)
    {
        console.log('no log');
    }
    else
    {
        console.log(log);
    }
    return null;
}
compileShaderメソッドを使いひもづけられているソースコードをコンパイルする。
コンパイルはC言語やJavaをやったことがある人にとっては馴染み深い言葉だが、
JavaScriptやPHPなどしかしたことない人にとっては初めて見る言葉かもしれない。
ざっくりといえばソースコードをコンピュータが理解しやすい形式(一般的にはバイナリコード)に変換することだ。
もしソースコードの文法に誤りがあったりした際にはコンパイルが失敗する。
そのため、getShaderParameterメソッドにシェーダとgl.COMPILE_STATUSを渡し、
コンパイルが成功したかどうかをチェックしている。
返り値がfalseのときはコンパイルに失敗しているため、
getShaderInfoLogメソッドを呼び、ログの取得を試みる。
もし、ログがあればコンソールにログを、なければ素直に簡単なメッセージを出し、return nullをしている。


最後に165行目~166行目。
//シェーダを返す
return shader;
すべての手順が成功した際にシェーダを返している。


再びJavaScriptのmain関数に戻り35行目~38行目。
//nullだったらreturnしてmain関数を抜ける
if(vertexShader===null)
{
    return;
}
シェーダが作成できなかった場合は、
returnしてmain関数を抜けている。


39行目~45行目で今度はフラグメントシェーダを作成する。
//フラグメントシェーダを作成
var fragmentShader=createShader(gl.FRAGMENT_SHADER,fragmentShaderSource);
//nullだったらreturnしてmain関数を抜ける
if(fragmentShader===null)
{
    return;
}
頂点シェーダの時と同じくcreateShader関数を呼んでいるが、
引数にgl.FRAGMENT_SHADERとフラグメントシェーダのソースコードを渡しているところが異なる。
そして、作成に失敗したときは同様にreturnでmain関数を抜けている。


そして46行目~47行目。
//プログラムを作成
var program=createProgram(vertexShader,fragmentShader);
今まで作った頂点シェーダとフラグメントシェーダを渡し、createProgramという関数を呼んでいる。


createProgram関数の定義は169行目~215行目にある。
var createProgram=function(vertexShader,fragmentShader)
{
    /*
    省略
    */
}
createProgram関数は頂点シェーダとフラグメントシェーダを受け取り、プログラムを返すようになってる。
プログラムという言葉が初めて出てきたが、
簡単にいえばシェーダを管理する入れ物のようなもので、
プログラムにシェーダをひもづけることによって初めてシェーダが使えるようになる。
ではcreateProgram関数がどうやってプログラムを作っているか見てみよう。


最初に171行目~172行目。
//プログラム作成
var program=gl.createProgram();
WebGLのコンテキストにあるcreateProgramメソッドを呼ぶことによりプログラムが作られる。


次に173行目~178行目。
//作成できたかチェック
if(program===null)
{
    console.log('no program');
    return null;
}
プログラムが作成できなかった場合は、
コンソールにメッセージを出してnullを返している。


179行目~186行目。
//頂点シェーダをプログラムにひもづける
gl.attachShader(program,vertexShader);
//attachShaderメソッドのエラーチェック
checkGLError('attachShader');
//フラグメントシェーダをプログラムにひもづける
gl.attachShader(program,fragmentShader);
//attachShaderメソッドのエラーチェック
checkGLError('attachShader');
attachShaderメソッドにプログラムとシェーダを渡すことによってその2つをひもづけることができる。
プログラムには必ず頂点シェーダとフラグメントシェーダをひもづけなければならないので、
それぞれに対してattachShaderメソッドを呼んであげる。


そして187行目~212行目。
//頂点シェーダとフラグメントシェーダにリンク処理を行う
gl.linkProgram(program);
//linkProgramメソッドのエラーチェック
checkGLError('linkProgram');
//リンクの状態を取得
var result=gl.getProgramParameter(program,gl.LINK_STATUS);
//getProgramParameterメソッドのエラーチェック
checkGLError('getProgramParameter');
//リンクが成功しているかどうか
if(result===false)
{
    console.log('failed link');
    //プログラムのログを取得
    var log=gl.getProgramInfoLog(program);
    //getProgramInfoLogメソッドのエラーチェック
    checkGLError('getProgramInfoLog');
    if(log===null)
    {
        console.log('no log');
    }
    else
    {
        console.log(log);
    }
    return null;
}
linkProgramメソッドを使うと頂点シェーダとフラグメントシェーダでリンクさせるのに必要なチェックが行われる。
今回は使用してないが、attribute変数やuniform変数、varying変数などを使用する際に、
頂点シェーダとフラグメントシェーダでの整合性や、使用上限を超えてないかなどがチェックされる。
attribute変数は第3回、uniform変数は第4回、varying変数は第5回に説明する。
リンクに成功したかどうかはgetProgramParameterメソッドにプログラムとgl.LINK_STATUSを渡す事によって、
調べることができる。
返り値がfalseのときはリンクに失敗しているため、
getProgramInfoLogメソッドを呼び、ログの取得を試みる。
もし、ログがあればコンソールにログを、なければ素直に簡単なメッセージを出し、return nullをしている。


最後に213行目~214行目。
//プログラムを返す
return program;
すべての手順が成功した際にプログラムを返している。


再びJavaScriptのmain関数に戻り48行目~52行目。
//nullだったらreturnしてmain関数を抜ける
if(program===null)
{
    return;
}
プログラムが作成できなかった場合は、
returnしてmain関数を抜けている。


53行目~58行目では第1回でもやったようにカラーバッファをクリアしている。
//RGBAの順番にカラーバッファをクリアする色を指定する
gl.clearColor(0.5,0.5,0.5,1.0);
//カラーバッファをクリアする
gl.clear(gl.COLOR_BUFFER_BIT);
//clearメソッドのエラーチェック
checkGLError('clear');
今回も灰色でクリアする。


59行目~62行目で作成したプログラムを使ってる。
//使用するプログラムをWebGLに設定する
gl.useProgram(program);
//useProgramメソッドのエラーチェック
checkGLError('useProgram');
useProgramメソッドを呼ぶことによってWebGLにこれから使用するプログラムを教えてあげる。


そして63行目~66行目によって実際に描画される。
//描画する
gl.drawArrays(gl.POINTS,0,1);
//drawArraysメソッドのエラーチェック
checkGLError('drawArrays');
drawArraysメソッドの第1引数にどういう方法で描画するかを指定する。
例えば線として描画や三角形として描画させるなどができるが、
今回は点を描画するのでgl.POINTSを指定している。
drawArraysと複数形になってるだけありJavaScriptから座標を渡してあげると、
このメソッドは複数の頂点を同時に描画できる。
そのため第2引数で何番目の頂点から描画するかを、
第3引数で何個描画するかを指定する。
今回はJavaScriptから頂点を渡さずに頂点シェーダに直接頂点を指定しているので、
第2引数には何を指定したとしても変わらない。
そして点を1個描画するので、第3引数は1になっている。
このメソッドを呼ぶことによって実際に画面に描画される。


さて、67行目~68行目。
//プログラム削除
deleteProgram(program,vertexShader,fragmentShader);
つかい終わったらいらないものは削除しておいたほうがいい。
deleteProgram関数にプログラムと頂点シェーダ、フラグメントシェーダを渡している。


deleteProgramが定義されてある217行目~231行目を見てみる。
var deleteProgram=function(program,vertexShader,fragmentShader)
{
    //頂点シェーダとプログラムのひもづけを切る
    gl.detachShader(program,vertexShader);
    //detachShaderメソッドのエラーチェック
    checkGLError('detachShader');
    //フラグメントシェーダとプログラムのひもづけを切る
    gl.detachShader(program,fragmentShader);
    //detachShaderメソッドのエラーチェック
    checkGLError('detachShader');
    //プログラムを削除する
    gl.deleteProgram(program);
    //deleteProgramメソッドのエラーチェック
    checkGLError('deleteProgram');
};
detacheShaderメソッドにプログラムとシェーダを渡すと、ひもづけを切ることができる。
もちろん頂点シェーダとフラグメントシェーダ両方を切らなければいけないので、
それぞれを引数に渡しdetachShaderメソッドを呼んでいる。
そして、deleteProgramメソッドにプログラムを渡すことでそのプログラムを削除している。


69行目~72行目では今度はシェーダを削除している。
//頂点シェーダ削除
deleteShader(vertexShader);
//フラグメントシェーダ削除
deleteShader(fragmentShader);
もちろん頂点シェーダもフラグメントシェーダも削除するので、それぞれを引数に渡して呼び出す。


定義は233行目~239行目にある。
var deleteShader=function(shader)
{
    //シェーダを削除する
    gl.deleteShader(shader);
    //deleteShaderメソッドのエラーチェック
    checkGLError('deleteShader');
};
やってることは単純でdeleteShaderメソッドにシェーダを渡してあげるだけである。


以上で説明は終わり。
とっても長かった・・・。
だが、WebGLを使う上でシェーダは避けては通れない。
シェーダを使いこなすことによって、よりリアルな3DCGキャラクターを表示させたり、
逆にアニメチックな(トゥーン処理)を行うことができる。
セピア調にしてみたりモノクロや、変形、回転、拡大縮小、魚眼レンズや彩度変更などなどなど。
上げればきりがないほど様々なことが可能になる。
ざっくりと今回のことをまとめよう。
  1. シェーダ(頂点シェーダとフラグメントシェーダ)を作る。
  2. プログラムをシェーダから作る。
  3. カラーバッファクリア。
  4. 描画。
  5. プログラム削除。
  6. シェーダ削除。
これだけといえばこれだけ。
また、シェーダの作成もまとめてみると。
  1. シェーダを作成する。(createShader)
  2. シェーダとソースをひもづける。(shaderSource)
  3. ソースをコンパイルする。(compileShader)
要点はこれだけ。
同様にプログラムの作成もまとめてみると。
  1. プログラムを作成する。(createProgram)
  2. プログラムとシェーダ(頂点シェーダとフラグメントシェーダ)をひもづける。(attachShader)
  3. リンクさせる。(linkProgram)
とほぼシェーダと同じ。
エラーチェックやコンパイルチェック、リンクチェックなどチェックが多く入るため、
長く複雑に感じるかもしれないが要点だけみてみると意外とシンプルである。


第1回 WebGLで遊んでみる(点の描画)へ
第3回 WebGLで遊んでみる(3点の描画)へ

0 件のコメント:

コメントを投稿