ラベル JavaScript の投稿を表示しています。 すべての投稿を表示
ラベル JavaScript の投稿を表示しています。 すべての投稿を表示

2013年6月14日金曜日

[Android][Java][JavaScript] Android4.2以上でaddJavascriptInterfaceに制限がついた

AndroidではJavaとJavaScriptの連携が非常に簡単にできる。

public class AndroidAPI
{
    private final static String TAG="API";

    public void call()
    {
        Log.d(TAG,"JavaScript call");
    }
}

とJavaScriptに公開するクラスを用意しておき

WebView webView=new WebView(context);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new AndroidAPI(),"API");

とするだけでWebViewのJavaScriptからAndroidAPIというクラスのメソッドにアクセスできるようになる。
実際にJavaScript側でどう使うかと言えば

<script>
API.call();
</script>

とaddJavascriptInterfaceの第二引数で指定した名前のオブジェクトができているので、
後はJavaScriptのオブジェクトとしてアクセスできる。
そのためそのオブジェクトのメソッドを呼ぶことができる。

さて、ここで今回の本題だが
Nexus 7やGalaxy S4などのAndroid4.2以上を搭載している端末では、
オブジェクトはできているがメソッドが定義されていないというエラーがJavaScript側で出てしまう。

そこでWebView | Android DevelopersのaddJavascriptInterfaceを見てみると、
4.2以上からは@JavascriptInterfaceがついているpublicメソッドのみアクセスできるとなっている。
なので先ほど定義したJavaScriptに公開するクラスのメソッドに

public class AndroidAPI
{
    private final static String TAG="API";

    @JavascriptInterface
    public void call()
    {
        Log.d(TAG,"JavaScript call");
    }
}

という風にアノテーションをきちんとつけてあげると、
無事にJavaScriptからメソッドが呼び出せるようになる。
ちなみに@JavascriptInterfaceはandroid.webkit.JavascriptInterfaceをimportすれば使えるようになる。

ちなみに4.2未満の場合は(継承されているクラスも含め)publicなメソッドであれば
すべてアクセスできていた。
特にこれでも問題ないように思うかもしれないがJavaにはリフレクションがあるため、
その気になればprivateなメンバやメソッドにもアクセスできてしまう。
privateメソッドなんかの名前はapkファイルをデコンパイルすれば難読化されてない限り簡単に調べることができる。
故に公開したクラスの中にcontextを持っているとJavaScriptからcontextを取得できてしまい、アプリケーションの権限によっては電話帳などの情報にもアクセスできてしまう。
そんな状態のアプリケーションが公開されXSS(クロスサイトスクリプティング)攻撃の恐れがあるサイトにWebViewでアクセスされると個人情報が抜き取られる恐れがある。

故にこのような仕様変更が起こったのであろう。
もし可能なら4.2未満の端末ではaddJavascriptInterfaceを使わないのが望ましい。

2013年3月21日木曜日

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

第2回では点の描画を通してシェーダやプログラムの使い方をやった。
しかし点の座標もシェーダに書いているため、JavaScriptから座標を制御して、
点の位置を変えたり移動したりすることができない。
そこで今回はJavaScriptから点の座標をシェーダに渡し、3つの点を描画してみる。

描画結果(画像)


3つの点が描画されている。

HTML部分。
//link・・・http://mio-koduki.blogspot.jp/2013/03/webgljavascripthtml-3-webgl3.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>

canvasをHTMLで用意しておく。

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

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

//頂点シェーダ
var vertexShaderSource='attribute vec4 a_Position;\
\
void main()\
{\
    gl_Position=a_Position;\
    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;
    }
    //頂点の配列
    var vertex=
    [
        //x,y
        0.0,0.5
        //x,y
        ,-0.5,-0.5
        //x,y
        ,0.5,-0.5
    ];
    //バッファを作成
    var buffer=createBuffer(vertex);
    //nullだったらreturnしてmain関数を抜ける
    if(buffer===null)
    {
        return;
    }
    //attribute変数取得
    var position=getAttribute(program,'a_Position');
    //nullだったらreturnしてmain関数を抜ける
    if(position===null)
    {
        return;
    }
    //attribute変数にバッファをひもづけて有効にする
    bindAttribute(position,buffer,2);
    //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,3);
    //drawArraysメソッドのエラーチェック
    checkGLError('drawArrays');
    //attribute変数とバッファのひもづけを無効にする
    unbindAttribute(position);
    //バッファ削除
    deleteBuffer(buffer);
    //プログラム削除
    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');
};

var createBuffer=function(value)
{
    //バッファ作成
    var buffer=gl.createBuffer();
    //createBufferメソッドのエラーチェック
    checkGLError('createBuffer');
    //作成できたかチェック
    if(buffer===null)
    {
        console.log('no buffer');
        return;
    }
    //バッファをバインドする
    bindBuffer(buffer);
    //バッファに値を書き込む
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(value),gl.STATIC_DRAW);
    //bufferDataメソッドのエラーチェック
    checkGLError('bufferData');
    //バッファをアンバインドする
    unbindBuffer();
    //バッファを返す
    return buffer;
};

var bindBuffer=function(buffer)
{
    //バッファをバインドする
    gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
    //bindBufferメソッドのエラーチェック
    checkGLError('bindBuffer');
};

var unbindBuffer=function()
{
    //バッファをアンバインドする
    gl.bindBuffer(gl.ARRAY_BUFFER,null);
    //bindBufferメソッドのエラーチェック
    checkGLError('bindBuffer');
};

var deleteBuffer=function(buffer)
{
    //バッファを削除する
    gl.deleteBuffer(buffer);
    //deleteBufferメソッドのエラーチェック
    checkGLError('deleteBuffer');
};

var getAttribute=function(program,name)
{
    //プログラムからattribute変数を取得する
    var location=gl.getAttribLocation(program,name);
    //getAttribLocationメソッドのエラーチェック
    checkGLError('getAttribLocation');
    //取得できたかチェック
    if(location===-1)
    {
        console.log('no attribute');
        return;
    }
    //attribute変数を返す
    return location;
};

var bindAttribute=function(location,buffer,size)
{
    //バッファをバインドする
    bindBuffer(buffer);
    //attribute変数にバッファをひもづける
    gl.vertexAttribPointer(location,size,gl.FLOAT,false,0,0);
    //vertexAttribPointerメソッドのエラーチェック
    checkGLError('vertexAttribPointer');
    //バッファをアンバインドする
    unbindBuffer();
    //バッファのひもづけを有効にする
    gl.enableVertexAttribArray(location);
    //enableVertexAttribArrayメソッドのエラーチェック
    checkGLError('enableVertexAttribArray');
};

var unbindAttribute=function(location)
{
    //バッファのひもづけを無効にする
    gl.disableVertexAttribArray(location);
    //disableVertexAttribArrayメソッドのエラーチェック
    checkGLError('disableVertexAttribArray');
};

シェーダなどは第2回で説明しているので、もし不安があれば見なおしてみてほしい。
では、第2回からの変更点を見ていこう。


まず、8行目~15行目に書かれている頂点シェーダのソースコード。
//頂点シェーダ
var vertexShaderSource='attribute vec4 a_Position;\
\
void main()\
{\
    gl_Position=a_Position;\
    gl_PointSize=10.0;\
}';
ソースコードの一番最初にattribute vec4 a_Position;というのがある。
これは、a_Positionという名前でvec4型のattribute変数を定義している。
attribute変数というのは頂点ごとに値が変わる場合に使用出来る変数で、
今回はまさに頂点の座標の値を渡すためattribute変数を使う。
そしてmain関数の中をみてみると、gl_Positionにa_Positionを代入している。
なんとこれだけで、頂点ごとに異なる座標を指定する頂点シェーダが出来上がる。


フラグメントシェーダは変化なしなので、JavaScript側のmain関数内の変更点を見よう。55行目~64行目。
//頂点の配列
var vertex=
[
    //x,y
    0.0,0.5
    //x,y
    ,-0.5,-0.5
    //x,y
    ,0.5,-0.5
];
配列にたくさんの値を入れて初期化しているがこれは頂点座標の値で、
最初の行の0.0が1つ目の頂点のx座標、0.5がy座標、
次の行の-0.5が2つ目の頂点のx座標、-0.5がy座標、
3行目の0.5が3つ目の頂点のx座標、-0.5がy座標という風に並べて代入している。


65行目~66行目にcreateBufferという関数が使われている。
//バッファを作成
var buffer=createBuffer(vertex);
引数に先ほどの頂点座標を渡している。


中で何をしているかを見るために273行目~295行目を見てみる。
var createBuffer=function(value)
{
    /*
    省略
    */
};
具体的に何をしているかを少しずつ見ていこう。


まず275行目~278行目。
//バッファ作成
var buffer=gl.createBuffer();
//createBufferメソッドのエラーチェック
checkGLError('createBuffer');
createBufferメソッドを使ってバッファを作成している。
実はバッファなしでも頂点座標を渡すことが出来る。
ではなぜバッファが必要かというと、
キャラクタのポリゴンなどを描画しようと思うと何万もの頂点座標が必要になったりする。
描画するたびにJavaScriptからWebGLにコピーして渡すのは非常に非効率で低速なので、
WebGLのバッファに予め頂点座標の値を渡しておき、
バッファを描画の時に使うようにすることによって高速な描画が可能になる。


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


285行目~286行目。
//バッファをバインドする
bindBuffer(buffer);
bindBufferという関数を呼び出しバッファを渡している。


bindBufferは297行目~303行目に定義されている。
var bindBuffer=function(buffer)
{
    //バッファをバインドする
    gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
    //bindBufferメソッドのエラーチェック
    checkGLError('bindBuffer');
};
bindBufferメソッドを呼び出し、ARRAY_BUFFER定数と引数で受け取ったバッファを渡している。
これは、WebGLのARRAY_BUFFERに渡されたバッファをひもづけるという効果がある。


287行目~290行目に戻り更に見ていこう。
//バッファに値を書き込む
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(value),gl.STATIC_DRAW);
//bufferDataメソッドのエラーチェック
checkGLError('bufferData');
bufferDataメソッドを呼び出している。
引数をみてみると先ほど登場したARRAY_BUFFERが再び登場している。
また第2引数をよくみてみると、
createBuffer関数で受け取った値を引数としてFloat32Arrayというクラスのインスタンスを作っている。
これはJavaScriptの型付き配列と呼ばれるものだ。
JavaScriptは型が柔軟な言語だがWebGLは型が厳密なので、そのままの値を渡されると都合が悪い。
そのため、何の型の値なのかを明示的に伝えるべく型付き配列で渡すことになっている。
第3引数はこのデータの使われ方をヒントとしてWebGLに教えるためのものだ。
基本的にSTATIC_DRAW定数で問題ないと思う。
このメソッドを呼ぶことにより値がARRAY_BUFFERに書き込まれ、
事前にARRAY_BUFFERとバッファをひもづけていたため、
結果バッファに値が書き込まれることになる。


291行目~292行目。
//バッファをアンバインドする
unbindBuffer();
unbindBufferという関数を呼び出している。


unbindBufferの定義は305行目~311行目にある。
var unbindBuffer=function()
{
    //バッファをアンバインドする
    gl.bindBuffer(gl.ARRAY_BUFFER,null);
    //bindBufferメソッドのエラーチェック
    checkGLError('bindBuffer');
};
bindBufferメソッドを呼び出し、ARRAY_BUFFER定数とnullを渡している。
ぱっとみbindBuffer関数と同じに見えるが、第2引数にnullを渡しているところが異なる。
nullを渡すことによってARRAY_BUFFERとバッファのひもづけを切る効果がある


最後に293行目~294行目。
//バッファを返す
return buffer;
値が書き込まれたバッファを返している。


JavaScriptのmain関数に戻り67行目~71行目。
//nullだったらreturnしてmain関数を抜ける
if(buffer===null)
{
    return;
}
バッファが作成できなかった場合は、
returnしてmain関数を抜けている。


72行目~73行目でgetAttributeという関数が呼ばれている。
//attribute変数取得
var position=getAttribute(program,'a_Position');
引数としてプログラムとattribute変数の名前を渡している。


321行目~335行目にある定義を見てみよう。
var getAttribute=function(program,name)
{
    /*
    省略
    */
};
では中で何をしているかを見ていこう。


323行目~326行目。
//プログラムからattribute変数を取得する
var location=gl.getAttribLocation(program,name);
//getAttribLocationメソッドのエラーチェック
checkGLError('getAttribLocation');
getAttribLocationメソッドにプログラムとattribute変数の名前を渡すと、
渡された名前のattribute変数(厳密にはattribute変数が格納されている場所)が取得できる。
ただし取得できなかった時には-1が返ってくるようになっている。


327行目~332行目で取得できたかチェックする。
//取得できたかチェック
if(location===-1)
{
    console.log('no attribute');
    return;
}
attribute変数が取得できなかった場合は、
コンソールにメッセージを出してnullを返している。


最後に333行目~334行目。
//attribute変数を返す
return location;
attribute変数を返している。


74行目~78行目に戻ろう。
//nullだったらreturnしてmain関数を抜ける
if(position===null)
{
    return;
}
attribute変数が取得できなかった場合は、
returnしてmain関数を抜けている。


次に79行目~80行目。
//attribute変数にバッファをひもづけて有効にする
bindAttribute(position,buffer,2);
第1引数にattribute変数、第2引数にバッファ、第3引数に頂点あたりの値の数(1~4)を渡している。
今回はx座標とy座標を頂点に渡すので頂点あたりの値の数は2である。


bindAttributeの定義は337行目~351行目にある。
var bindAttribute=function(location,buffer,size)
{
    /*
    省略
    */
};
では具体的に見ていこう。


339行目~340行目。
//バッファをバインドする
bindBuffer(buffer);
再びbindBuffer関数を呼び出している。


341行目~342行目で実際にattribute変数にバッファをひもづけている。
//attribute変数にバッファをひもづける
gl.vertexAttribPointer(location,size,gl.FLOAT,false,0,0);
//vertexAttribPointerメソッドのエラーチェック
checkGLError('vertexAttribPointer');
vertexAttribPointerメソッドで第1引数にattribute変数を、
第2引数に頂点あたりの値の数(1~4)を、
第3引数に値の型を、
第4引数に値が整数の時に正規化をするかどうかのフラグを、
第5引数に値のストライドを、
第6引数にバッファのどこから値が始まるかを渡す。
第5引数については第8回で説明する。


345行目~346行目。
//バッファをアンバインドする
unbindBuffer();
unbindBuffer関数もきちんと呼んでおく。


347行目~350行目で実際にひもづけが有効になる。
//バッファのひもづけを有効にする
gl.enableVertexAttribArray(location);
//enableVertexAttribArrayメソッドのエラーチェック
checkGLError('enableVertexAttribArray');
enableVertexAttribArrayメソッドにattribute変数を渡すことによって、
attribute変数にひもづけられたバッファが有効になる。


91行目~94行目でdrawArraysメソッドを呼び実際に描画する。
//描画する
gl.drawArrays(gl.POINTS,0,3);
//drawArraysメソッドのエラーチェック
checkGLError('drawArrays');
第2回で説明したとおりdrawArraysメソッドの第3引数は描画する頂点の数なので、
今回は3つの点を描画するので3を指定する。
以上で3つの点が描画される。


95行目~96行目で後片付けをしている。
//attribute変数とバッファのひもづけを無効にする
unbindAttribute(position);
attribute変数とバッファのひもづけを無効化しておく。


353行目~359行目にそのunbindAttributeの定義がある。
var unbindAttribute=function(location)
{
    //バッファのひもづけを無効にする
    gl.disableVertexAttribArray(location);
    //disableVertexAttribArrayメソッドのエラーチェック
    checkGLError('disableVertexAttribArray');
};
disableVertexAttribArrayメソッドにattribute変数を渡すことによって、
ひもづいているバッファを無効化することが出来る。


次に97行目~98行目。
//バッファ削除
deleteBuffer(buffer);
バッファも使い終わったら削除してしまう。


deleteBufferが定義されてある313行目~319行目を見てみる。
var deleteBuffer=function(buffer)
{
    //バッファを削除する
    gl.deleteBuffer(buffer);
    //deleteBufferメソッドのエラーチェック
    checkGLError('deleteBuffer');
};
deleteBufferメソッドにバッファを渡すことによってそのバッファを削除することが出来る。


以上で説明は終わり。
今回はバッファとattribute変数の使い方を説明した。
これを使いこなすことによって自在に好きな座標に描画することが出来るようになる。
バッファの作成をまとめてみると。
  1. バッファを作成する。(createBuffer)
  2. バッファをバインドする。(bindBuffer)
  3. バッファに値を書き込む。(bufferData)
  4. バッファをアンバインドする。(bindBuffer)
要点はこれだけ。
同様にattribute変数もまとめてみると。
  1. attribute変数を取得する。(getAttribLocation)
  2. バッファをバインドする。(bindBuffer)
  3. attribute変数とバッファをひもづける。(vertexAttribPointer)
  4. バッファをアンバインドする。(bindBuffer)
  5. attribute変数とバッファのひもづけを有効にする。(enableVertexAttribArray)
相変わらずエラーチェックなどチェックが多く入るため、
複雑に感じるが実際はこれだけである。


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

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点の描画)へ

2013年2月17日日曜日

[WebGL][JavaScript][HTML] 第1回 WebGLで遊んでみる(描画領域のクリア)

最近OpenGLにハマってる。
やっぱり自分でアニメーションやエフェクト、3Dポリゴンなどをゴリゴリできるのは楽しい。
ブラウザでも実はWebGLというものがある。
まだ対応しているブラウザが多いわけではないがどんどん増えてきている。
また、CSSでもCSS shadersというものが規格されており、
これもWebGL・・・もっといえばOpenGLがベースとなっているのだ。(2013/02/17現在)

もっとも簡単なWebGL、それは描画領域のクリアである。

描画結果(画像)


描画領域をクリアすると上の画像のように一色で塗りつぶされる。
では、早速ソースコードを見ていこう。

まずはHTML部分から。
//link・・・http://mio-koduki.blogspot.jp/2013/02/webgljavascripthtml-webgl.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>

まず、ポイントはcanvas要素を作ること。
WebGLはこのcanvas要素に描画されることになる。
なので、このcanvasにidとサイズを設定してあげる。

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

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

var main=function()
{
    //canvas要素を取得する
    var canvas=document.getElementById('canvas');
    //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でmain関数を抜ける
    if(!gl)
    {
        console.log('no gl');
        return;
    }
    //RGBAの順番にカラーバッファをクリアする色を指定する
    gl.clearColor(0.5,0.5,0.5,1.0);
    //カラーバッファをクリアする
    gl.clear(gl.COLOR_BUFFER_BIT);
    //clearメソッドのエラーチェック
    checkGLError('clear');
};
//ページロード時に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);
        }
    }
};

では、詳しく見ていこう。


まず、2行目~3行目にデバッグ用のフラグを用意している。
//デバッグ用フラグ
var DEBUG_FLAG=true;
WebGLに限らずプログラムを書くときにおいてこれは用意しておいたほうがいい。
特にWebGLと言うよりOpenGLは処理を高速化させるために、
本番ではエラーチェック処理を入れないということもしばしばある。
他にもデバッグの時だけしたい処理や逆にプロダクションだけでしたい処理なども出てきたりする。
用意して損はないだろう。


次に5行目~6行目にglという変数を用意している。
//WebGLのコンテキスト
var gl=null;
これはWebGLのコンテキストを代入するのに使う。
WebGLのコンテキストはWebGLの処理をするのに必ず必要な物なので、
グローバル変数として用意している。
なお、実際はあまりグローバル変数は好まれないので、
実際に自分で書くときはクラスのメンバ(JavaScript的にはプロパティ)などとして持たせるといいだろう。
今回はソース簡略化のためにグローバル変数にしている。


そして、8行目、次にちょっと飛んで44行目。
var main=function()
{
    /*
    省略
    */
};
//ページロード時にmain関数実行
window.onload=main;
main関数を定義し、ページがロードされた時に呼び出されるようにwindow.onloadに代入する。
こうすることによってDOMや画像が用意され終わった段階で、このmain関数を実行してくれるようになる。


次にmain関数の中を見ていこう。まず、10~11行目。
//canvas要素を取得する
var canvas=document.getElementById('canvas');
HTMLで用意しておいたcanvas要素を取ってくる。
id属性でcanvasと指定したものをとるというソースコードだが、
今回はcanvas要素自体に"canvas"というidを指定してるので、
結果的にcanvas要素が取れる。


さて、ようやくWebGLの第一歩、12行目~29行目。
//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;
    }
}
先ほど取得したcanvasのgetContextメソッドを呼びWebGLのコンテキストを取得するのだが、
これらはブラウザや実装状況によって引数の指定が異なる。
故に予め配列で引数の文字列を用意して、ループで回しWebGLのコンテキストが取得できたら、
ループを抜けるようにしている。


そして30行目~35行目。
//WebGLのコンテキストを取得できなかったらコンソールにメッセージを出しreturnでmain関数を抜ける
if(!gl)
{
    console.log('no gl');
    return;
}
WebGL未対応ブラウザだったりなにかしらのエラーやミスで、
WebGLのコンテキストが取得できなかった場合は素直にコンソールにメッセージを出し、
returnしてmain関数を終了している。


36行目~37行目にようやくWebGLのメソッドが登場!
//RGBAの順番にカラーバッファをクリアする色を指定する
gl.clearColor(0.5,0.5,0.5,1.0);
clearColorメソッドを呼び出し、
RGBAの順番にカラーバッファをクリア・・・つまり塗りつぶす色を指定する。
カラーバッファは描画領域のRGBA用バッファとして使われる。
つまりはカラーバッファをクリアすることによって描画領域もクリアできるのだ。
なお、色を指定するときはカラーコードでお馴染みの0~255ではなく、0.0~1.0である。


38行目~39行目。
//カラーバッファをクリアする
gl.clear(gl.COLOR_BUFFER_BIT);
clearメソッドを呼び出し、clearColorメソッドで指定した色を使って、
実際にカラーバッファをクリアする。


さて、40行目~41行目に何か関数がある。
//clearメソッドのエラーチェック
checkGLError('clear');
実はWebGLは・・・というかOpenGLは返り値や例外でエラーをキャッチするのではなく、
都度関数を呼んでエラーチェックを行う。
そのためそれらの処理を自前で関数として用意しまとめている。


それが46行目~59行目だ。
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);
        }
    }
};
引数は直前に呼び出したメソッド名。
関数の中で早速行なっているのはデバッグ用のフラグがtrueかどうかのチェックだ。
その理由は少し下のコードにある。
getErrorメソッドを呼び出し、NO_ERRORかどうかを見ている。
もしNO_ERRORでなかったらエラーの回数だけループするわけだが、
その処理をエラーチェックが必要な関数の都度することになる。
多少なら無視出来るレベルだが、ゲームやアニメーションを行う際には、
この多少が積み重なり処理速度を低下させるうる。
故にデバッグの時だけエラーチェックをしておき本番では行わないようにする。
今回はNO_ERRORではないとき、つまりエラーのときはコンソールに、
この関数の引数として渡した直前のメソッド名を、メッセージに含めて出している。
これによって何のメソッドでエラーが起きたかわかる。


WebGLに対応しているブラウザで上記のソースコードを実行させると、
描画結果の画像のようにcavas内全体が灰色に塗りつぶされて表示される。
なぜならカラーバッファを灰色でクリアすることにより結果、描画領域も灰色でクリアされたからだ。
OpenGLひいてはWebGLではエラーの時真っ黒に表示される。
その場合はこのカラーバッファクリアも効かずに真っ黒になるので、
黒以外でクリアしておくとエラーが起きてるのかパラメータミスなのかの問題の切り分けがしやすい。
なので、デバッグのときは黒以外の色でクリアしておくといいかもしれない。


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

2012年3月16日金曜日

[PHP][JavaScript] Ajaxからの通信(アクセス)かどうか判定する

ファイルへのアクセスがAjaxから要求されたものなのか、
それともブラウザからアドレスを直接叩くなどしてAjaxを通さずにアクセスしてきたのか調べたい時がある。

jQueryやprototypeなどのライブラリを使ってAjax通信をするときは実はヘッダに
「X-Requested-With:XMLHttpRequest」
というデータをつけて送ってくる。
なので、ヘッダ情報に「X-Requested-With」があるかどうかを調べて
それが「XMLHttpRequest」という値になってるかをチェックすればいい。

//link・・・http://mio-koduki.blogspot.com/2012/03/phpjavascript-ajax.html
//返り値・・・Ajaxからの通信であればtrue、そうでなければfalse
function is_ajax()
{
    return isset($_SERVER['HTTP_X_REQUESTED_WITH'])&&strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])=='xmlhttprequest';
}

という感じになる。

では、jQueryなどのライブラリを使わずに自力でXMLHttpRequest(IEであればActiveXObject」)を使ってAjaxを行なっている場合はどうすればいいか?
その時は自分でヘッダに「X-Requested-With:XMLHttpRequest」をつけてしまえばいい。

//link・・・http://mio-koduki.blogspot.com/2012/03/phpjavascript-ajax.html

//Ajax通信用のオブジェクト作成関数
function create_ajax_object()
{
    //IE以外
    if(window.XMLHttpRequest)
    {
        try
        {
            return new XMLHttpRequest();
        }
        catch(e)
        {
            return null;
        }
    }
    //IE
    else if(window.ActiveXObject)
    {
        try
        {
            //IE6以降
            return new ActiveXObject('Msxml2.XMLHTTP');
        }
        catch(e)
        {
            try
            {
                //IE5
                return new ActiveXObject('Microsoft.XMLHTTP');
            }
            catch(e)
            {
                return null;
            }
        }
    }
    else
    {
        return null;
    }
}

//Ajax通信関数
function ajax_access(method,path,data,callback,async_flag)
{
    //パスが設定されていなければnullを返す
    if(path==null)
    {
        return null;
    }

    //Ajax通信用のオブジェクト作成
    var ajax_object=create_ajax_object();

    //Ajax通信用のオブジェクト作成に失敗したらnullを返す
    if(ajax_object==null)
    {
        return null;
    }

    //メソッドに何も設定されていなければGETを設定する
    method=method||'GET';
    //同期フラグに何も設定されていなければtrueを設定する
    async_flag=async_flag||true;

    //通信回線を開く
    ajax_object.open(method,path,async_flag);

    //content-typeを指定する
    //これがないとPOSTのデータを飛ばすときなどに失敗することがある
    ajax_object.setRequestHeader('Content-Type','application/x-www-form-urlencoded; charset=UTF-8');

    //「X-Requested-With:XMLHttpRequest」をヘッダにつける
    ajax_object.setRequestHeader('X-Requested-With','XMLHttpRequest');

    //通信時のイベント
    ajax_object.onreadystatechange=function()
    {
        //readyStateが4の時通信終了
        //statusが200の時通信成功
        if(ajax_object.readyState==4&&ajax_object.status==200)
        {
            //コールバック関数が設定されていれば呼び出す
            if(callback!=null)
            {
                //コールバック関数に結果を返す
                callback(ajax_object.responseText);
            }
        }
    }

    //データ送信
    ajax_object.send(data);
}

//Ajax通信を行う
ajax_access('post','test.php',encodeURIComponent('test')+'='+encodeURIComponent('テスト'),function(data){alert(data);},true);

とこんな感じになる。
久しぶりにJavaScriptでAjax書いた気がする。
ポイントはopenメソッドを呼び出した後に
setRequestHeaderを呼び出して「X-Requested-With:XMLHttpRequest」をヘッダにつけるところ

2012年3月12日月曜日

[JavaScript][PHP] オブジェクトからクエリストリングを作る

以前クエリストリング(URLパラメータ、GETパラメータって呼ばれたりもする)から
オブジェクトを作るということをやったので今度はその逆で
オブジェクトからクエリストリングを作るのをやってみたいと思う。
ちなみに、クエリストリングのキーの部分が数字のみになる場合のプレフィックスはつけてない。

//link・・・http://mio-koduki.blogspot.com/2012/03/javascriptphp_12.html
//第一引数・・・クエリストリングにしたいオブジェクト
//返り値・・・オブジェクトからビルドされたクエリストリング
function build_query(data)
{
    if(is_array(data)||is_object(data))
    {
        var params=[];
        for(var i in data)
        {
            if(is_array(data[i])||is_object(data[i]))
            {
                params.push(build(i,data[i]));
            }
            else if(is_can_format_value(data[i]))
            {
                params.push(url_encode(i)+"="+url_encode(format_value(data[i])));
            }
        }
        return params.join('&');
    }
    else
    {
        throw new Error('Parameter 1 expected to be Array or Object.');
    }

    function build(key,data)
    {
        var params=[];
        for(var i in data)
        {
            if(is_array(data[i])||is_object(data[i]))
            {
                params.push(build(key+'['+i+']',data[i]));
            }
            else if(is_can_format_value(data[i]))
            {
                params.push(url_encode(key+'['+i+']')+'='+url_encode(format_value(data[i])));
            }
        }
        return params.join('&');
    }

    function is_can_format_value(value)
    {
        return typeof value=='boolean'||typeof value=='string'||typeof value=='number';
    }

    function format_value(value)
    {
        if(value===true)
        {
            return 1;
        }
        else if(value===false)
        {
            return 0;
        }
        else
        {
            return value;
        }
    }

    function url_encode(string)
    {
        return encodeURIComponent((string+'')).replace(/!/g,'%21').replace(/'/g,'%27').replace(/\(/g,'%28').replace(/\)/g,'%29').replace(/\*/g,'%2A').replace(/%20/g,'+');
    }

    function is_array(data)
    {
        return data instanceof Array;
    }

    function is_object(data)
    {
        return data!=null&&typeof data=='object'&&Object.prototype.toString.call(data)!='[object Array]';
    }
}

と、こんな感じになると思う。
ちなみにPHPではhttp_build_queryが同じような挙動をする関数。

2012年3月9日金曜日

[JavaScript][PHP] クエリストリングからオブジェクトを作る

クエリストリングやURLパラメータ、GETパラメータと様々な呼び方があるが、
要するにURLの?以降にあるtest_key=test_valueの形式の文字列のことである。

PHPはわざわざ自前でパースしなくても、$_GETの中に配列として展開された形式で入ってるし、
parse_str関数を使えば好きなタイミングでパースすることが出るが、
JavaScriptにはそのような仕組みはなく、自力でパースしないといけない。

クエリストリングには配列として変換する仕組みがある。
test_key[]=test_value1&test_key[]=test_value2&test_key[test]=test_value3
というクエリストリングだと

array(1) {
  ["test_key"]=>
  array(3) {
    [0]=>
    string(11) "test_value1"
    [1]=>
    string(11) "test_value2"
    ["test"]=>
    string(11) "test_value3"
  }
}

という配列が生成される。

何サイトかJavaScriptでクエリストリングをオブジェクトに変える処理を見てみたが、
ほとんどのサイトでこの配列形式をサポートしてなかった。

なのでちょっと作ってみた。

//link・・・http://mio-koduki.blogspot.com/2012/03/javascriptphp.html
//第一引数・・・パースしたいクエリストリング。省略した場合は現在のURLのクエリストリング
//返り値・・・クエリストリングからパースされたオブジェクト
function parse_query(query)
{
    if(query==null)
    {
        query=(location.search+'').replace(/^[?&]/,'');
    }
    var separator1='=';
    var separator2='&';
    var data={};
    query=(query+'').replace(/^( |%20)*/g,'').split(separator2);
    for(var i=0;i<query.length;i++)
    {
        var tmp=query[i].split(separator1);
        if(tmp.length<2)
        {
            tmp=[tmp,''];
        }
        var key=url_decode(tmp.shift());
        if(key.length<=0)
        {
            continue;
        }
        data=merge(data,make_array(key,url_decode(tmp.join(separator1))));
    }
    return data;

    function url_decode(string)
    {
        return decodeURIComponent((string+'').replace(/\+/g,'%20'));
    }

    function make_array(key,value)
    {
        var tmp_key=[];
        var bracket_key='';
        var bracket=false;
        key=key.replace(/^ */g,'');
        if(key.length>0&&key.charAt(0)=='[')
        {
            key='';
        }
        for(var i=0;i<key.length;i++)
        {
            var char=key.charAt(i);
            if(!bracket&&char=='[')
            {
                if(bracket_key!='')
                {
                    tmp_key.push(bracket_key);
                    bracket_key='';
                }
                bracket=true;
                bracket_key+=char;
            }
            else if(char==']')
            {
                if(bracket)
                {
                    if(tmp_key==1)
                    {
                        tmp_key.push(bracket_key);
                    }
                    else
                    {
                        tmp_key.push(bracket_key.substr(1));
                    }
                    bracket_key='';
                    bracket=false;
                    if(key.charAt(i+1)!='[')
                    {
                        break;
                    }
                }
                else
                {
                    bracket_key+=char;
                }
            }
            else if(char=='\0')
            {
                break;
            }
            else
            {
                bracket_key+=char;
            }
        }

        if(bracket)
        {
            if(tmp_key.length==1)
            {
                tmp_key[tmp_key.length-1]+=bracket_key;
            }
        }
        else if(bracket_key!='')
        {
            tmp_key.push(bracket_key);
        }
        var count=0;
        if(tmp_key.length>0)
        {
            for(var i=0;i<tmp_key[0].length;i++)
            {
                var char=tmp_key[0].charAt(i);
                if(char==' '||char=='.'||char=='[')
                {
                    tmp_key[0]=tmp_key[0].substr(0,i)+'_'+tmp_key[0].substr(i+1);
                }
                //下の1文を変えれば$_GET準拠、現在はparse_str準拠
                //if(char=='['&&++count>0)
                if(char=='['&&count++>0)
                {
                    break;
                }
            }
        }
        for(var i=tmp_key.length-1;i>=0;i--)
        {
            var tmp_data={};
            tmp_data[tmp_key[i]]=value;
            value=tmp_data;
        }
        return value;
    }

    function merge(base,extend)
    {
        if(!is_object(base)||!is_object(extend))
        {
            return {};
        }
        extend=numbering(base,extend);
        for(var i in extend)
        {
            if(is_object(base[i])&&is_object(extend[i]))
            {
                base[i]=merge(base[i],extend[i]);
            }
            else
            {
                base[i]=extend[i];
            }
        }
        return base;
    }

    function numbering(base,extend)
    {
        var tmp={};
        for(var i in extend)
        {
            var index=(i.match(/^\s?$/)?max_index(base):i);
            if(is_object(extend[i]))
            {
                tmp[index]=numbering(is_object(base)?base[i]:null,extend[i]);
            }
            else
            {
                tmp[index]=extend[i];
            }
        }
        return tmp;
    }

    function max_index(data)
    {
        var max=0;
        if(!is_object(data))
        {
            return max;
        }
        for(var i in data)
        {
            if((i+'').match(/^(0|[1-9][0-9]*)$/)&&i>=max)
            {
                max=i-0+1;
            }
        }
        return max;
    }

    function is_object(data)
    {
        return data!=null&&typeof data=='object'&&Object.prototype.toString.call(data)!='[object Array]';
    }
}

とこんな感じ。
配列形式をサポートすると非常に面倒くさくなってくる。
第一引数にクエリストリングを渡す形式だが、省略すると現在のURLのクエリストリングをパースする。

余談だが、PHPのマニュアルには$_GETや$_POSTと同じ仕組でparse_strを動かしてあると書いているが、「a[b[c=d」というクエリの時に結果が違ってる。
地味に気になるw
この関数もどっちに準拠すべきか迷ったので、一応どっちにも切り替えられるようにしている。

この関数の逆の挙動をする関数も作ってみた。

2012年2月24日金曜日

[JavaScript][jQuery] inputのtype="file"であるときにvalueの値を空にする

jQueryで$(':file').val('')などをしても、実はtype="file"の時は消すことができない。

type="file"のvalueを操作させない。値が入力されても無視するという挙動は
HTMLの仕様ではなくて厳密にはブラウザのセキュリティポリシーに基づくものらしいが、
値をどうしても空にしたい時には困った挙動である。

inputのtype="reset"だとtype="file"の値も消すことはできるが、
フォームにあるinputやselect、textareaなどの値まで消してしまうのであまり使えない。

そういう時にはjQueryで

$(':file').replaceWith($(':file').clone())

と書くとうまいこと消すことができる。
:fileの部分はもちろん好きなセレクタで構わない。

やってることは単純で、type="file"のコピーを作り出し、
元のものと置き換えているだけである。

これは上記に述べた値を無視するという挙動を利用して、
新規に作成されたら(コピーされたら)値が消されるように仕向けている。

なお、イベントなどもコピーに引き継ぎたい場合は、
clone(true)と引数にtrueを入れるとイベントもコピーされる。