2014年4月5日土曜日

[Android] 非同期処理はAsyncTaskで十分じゃないの? Loader、AsyncTaskLoader徹底解剖 番外編 Part2


おそらくそもそもLoader、AsyncTaskLoaderとは?から来た人が多いと思うので説明は不要でしょうが、
この記事はLoader、AsyncTaskLoaderについての記事の番外編です。


非同期処理はAsyncTaskで十分じゃないの?


さて、Androidで非同期処理をする方法の1つに
Androidが用意しているAsyncTaskを使用する
というものがあります。
すでに非同期処理の仕組みを用意してるのに
なぜわざわざLoader、AsyncTaskLoaderを後から用意したのでしょう?
いくつか理由があるようですが、私が思う1番の理由は

ActivityやFragmentのライフサイクルに対応していない

ということだと思います。

他にもAsyncTaskのonPostExecuteメソッドでUIやDialogをいじることが多く、
ActivityやFragmentの構成に左右されやすいというのも問題としてあげられることもありますが、
これに関しては上手に継承を使ったり自前でインターフェイスを定義してあげるなりすれば、
解決できると思うので私は問題というほどではないと思ってます。

では、ActivityやFragmentのライフサイクルに対応していないと何が問題なのでしょうか?


ActivityやFragmentのライフサイクルに対応してないことで発生する問題


ActivityやFragmentが破棄された後もonPostExecuteが呼ばれる


例えば以下のパターンを見てみいきましょう。

Activityのコード

protected final static String _TAG="AsyncTaskBadCase1";

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    new BadAsyncTask().execute();
    finish();
}

@Override
protected void onDestroy()
{
    super.onDestroy();
    Log.d(_TAG,"onDestroy");
}

onCreateメソッド内でAsyncTaskを実行し、そのまますぐActivityを終了させてます。
また、onDestroyメソッド内でログを出力させています。

AsyncTaskのコード

protected final static String _TAG="AsyncTaskBadCase1";

@Override
protected Void doInBackground(Void...params)
{
    Log.d(_TAG,"doInBackground");
    try
    {
        Thread.sleep(3000);
    }
    catch(InterruptedException e)
    {
        //サンプルなので例外処理は省略
    }
    return null;
}

@Override
protected void onPostExecute(Void result)
{
    super.onPostExecute(result);
    Log.d(_TAG,"onPostExecute");
}

doInBackgroundメソッド内、つまり非同期で処理をするところでは、
ログを出力し、今のスレッドを3秒間ストップさせてます。
そしてonPostExecuteメソッド内、つまり非同期処理が終了した時に実行するメソッドでも
ログを出力しています。

これを実際に実行してみると下記のようなログが出力されます。

doInBackground
onDestroy
onPostExecute

これを見るとonDestroy・・・つまりActivityが破棄された後も処理が続き、
onPostExecuteが呼ばれているということがわかります。

例えば下記コードのようにonPostExecuteでDialog.dismissメソッドを呼んでいるとします。

@Override
protected void onPostExecute(Void result)
{
    super.onPostExecute(result);
    dialog.dissmiss();
}

すると、Activityが破棄された後にonPostExecuteが呼ばれた場合、
下記エラーでアプリケーションがクラッシュします。

java.lang.IllegalArgumentException: View not attached to window manager

onDestroyの時にAsyncTaskのcancelメソッドを呼ぶとonPostExecuteは実行されなくなる仕様ですが、
Androidのバージョン(Android 2.3未満)によってはonDestroyの時に
AsyncTaskのcancelメソッドを呼んでもonPostExecuteが実行されてしまうというバグがあります。


Configuration Changeが起きた時にバグが発生しやすい


Configuration Changeが発生した(例えば端末を傾けて画面を回転させた)場合、
Activityは一旦破棄された後、再度生成されます。
つまり、onDestroyが呼ばれるわけですがそうなるとさっき言ったように
Dialogなどの操作をonPostExecuteで行っているとクラッシュする可能性があります。

また、onCreateに書いてある非同期処理を何度もされるのは都合が悪い時があります。
例えば、サーバにデータを登録するための通信などが端末を傾けるたびに、
何度も送信されると2重3重にデータが登録されたりします。

じゃ、画面回転をさせなければいい。ということで回転を抑制する場合もありますが、
Configuration Changeは画面回転以外にも標準フォントのサイズを変えたり、
外部キーボードをつないだり、ユーザーインターフェースモードを変えたり、
新しいSIMを装着したりしても発生します。
標準フォントサイズは一応抑制できますが、外部キーボードや新しいSIMに関しては
ソフトウェアから抑制はできません。


まとめ


AsyncTaskはシンプルなコードで非同期処理を行うことができます。
しかし、ActivityやFragmentのライフサイクルにきちんと対応したコードを書くのは結構大変です。
非同期処理の最中にユーザがBackボタンを押したり、Homeボタンを押したり、端末を回転させたりと
様々なイベントが起きる可能性があります。
それら全てを自前でキャッチして対処するのはかなりAndroidに精通してないとなかなか大変でしょう。

[Android] なぜJava標準Threadクラスを基本的にAndroidで使用しないほうがいいのか? Loader、AsyncTaskLoader徹底解剖 番外編 Part1


おそらくそもそもLoader、AsyncTaskLoaderとは?から来た人が多いと思うので説明は不要でしょうが、
この記事はLoader、AsyncTaskLoaderについての記事の番外編です。


なぜJava標準Threadクラスを基本的にAndroidで使用しないほうがいいのか?


さて、Androidで非同期処理をする方法の1つに
Java標準のThreadクラスを使用する
というものがありますが、なぜ使用しないほうがいいのでしょう?
この方法には以下の欠点があります。
  1. AndroidにはViewを操作したい場合はUIスレッドからしか行えないという制限がある
  2. Threadクラスにはデフォルトで安全にキャンセルする方法がない
  3. Threadクラスにはデフォルトでプールがない
詳しく説明していきましょう。


AndroidにはViewを操作したい場合はUIスレッドからしか行えないという制限がある


まず、1.ですが、これはAndroid自体の制限で、Viewを操作するにはUIスレッドからしか行えない。
というルールがあります。
故に自分でスレッドを生成し、処理結果を元に自分で生成したスレッド内でViewを操作しようとすると
下記のようなエラーになります。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

しかし、この問題に関して言えばHanderというものを使えばその問題を解決できます。

new Handler(Looper.getMainLooper()).post(new Runnable()
{
    @Override
    public void run()
    {
        //View操作のコード
    }
});

というコードを自分で生成したスレッド内に書けば
そのスレッドからUIスレッドに「こういう処理をしてね」とお願いすることができます。
ただ、2.や3.に関しては自前で頑張るしかありません。


Threadクラスにはデフォルトで安全にキャンセルする方法がない


さて、2.のキャンセルに関してですが、
Threadクラスにはぱっと見stopやsuspend、resumeというメソッドがあるので、一見できそうです。
・・・がこれらのメソッドは非推奨になっています。
これに関しては正式にわざわざアナウンスされております。

Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?

スレッドを止める方法がないとActivityなどが破棄されスレッドの結果を受け取る先がなくなった際に、
すでに結果を返却する場所がないのに処理を続けてしまうという事になったりします。


Threadクラスにはデフォルトでプールがない


また、3.に関しては何度も何度も短いスパンで非同期処理を行うときの話ですが、
非同期処理のたびに「スレッドを生成し、処理が終了すると破棄する」という動作をさせると、
スレッドの生成や破棄のコスト(時間的にもCPUやメモリのリソース)もなかなか高くなります。
それが短い時間で何度も行われるとかなりの負荷になり得ます。
そこでうまくスレッドを再利用して無駄を減らすという仕組みをプールといいますが、
これを自前で作って管理するのもなかなか大変です。


まとめ


上記などの理由によってAndroidではThreadで非同期処理はしないほうがいいと思います。
場合によってはThreadクラスを使った方がいいケースも存在しますが、
ある程度Androidやスレッドに深い理解がないと
誤った使い方や非効率な使い方をする可能性が高いため、
基本的にはThreadクラスの使用は避けたほうが無難でしょう。

[Android] そもそもLoader、AsyncTaskLoaderとは? Loader、AsyncTaskLoader徹底解剖 Part1


皆さんLoader、AsyncTaskLoaderをご存知ですか?
知らない人のために簡単にだけ説明すると、
Androidで非同期処理を行うための仕組みです。


Androidで非同期処理をする方法


Androidで非同期処理をする方法はいくつかあります。
  1. Java標準のThreadクラスを使用する
  2. Androidが用意しているAsyncTaskを使用する
  3. Androidが用意しているLoader、AsyncTaskLoaderを使用する
他にも特殊な方法としてServiceやBroadcastを使用する方法などもありますが、
通常ActivityやFragmentなどで非同期処理をするときは主に上記の3つだと思います。
しかし何故その中でわざわざLoader、AsyncTaskLoaderを使用しなければいけないのでしょう・・・?
Androidが用意した最新の仕組みだからそっちのほうがなんとなくいい・・・
ただそれだけの理由で使ってる人も案外いらっしゃるんじゃないでしょうか?
実際1.と2.にはいくつか問題があります。


Java標準のThreadクラスやAsyncTaskの問題点


それぞれを詳しく解説すると結構長くなってしまうので別途下記リンクにまとめました。
ThreadクラスやAsyncTaskの問題を解決し、更に便利な機能をつけた非同期処理の仕組みとして
Loader、AsyncTaskLoaderが提供されました。


Loader、AsyncTaskLoaderのメリット


それではLoader、AsyncTaskLoaderのメリットは何でしょうか?
  1. 基本的にJava標準のThreadクラスやAsyncTaskの問題点を解決している
  2. 処理結果をキャッシュとして持っているため、次回からは再処理することなく結果を取得できる
  3. Configuration Change発生後、Loaderを再設定すれば再処理することなく最終結果を取得できる
  4. Loaderはデータソースを監視してるため、内容が変更された際に新しい結果を取得できる
まず1.に関してはJava標準のThreadクラスやAsyncTaskの問題点のところにあった
リンクを参照するとどういう問題があるのかわかるでしょう。
基本的にLoader、AsyncTaskLoaderはこれらの問題を解決しています。


Loader、AsyncTaskLoaderってほんとうに便利なの?


これらだけ見ると何だかメリットばっかりで是が非でも使うべきみたいに見えますよね?
でも、実際に使ってみると・・・非常に使いづらい・・・。
  • メソッド名が紛らわしい、わかりづらい
  • ドキュメントの説明が紛らわしい、わかりづらい
  • 意外と詳しく解説しているサイトがない(特に日本語)
という情報面で色々苦労します。

さらに
  • 処理中にHomeボタンをおした時どういう動作するのか?
  • 処理中にBackボタン押した時はどういう動作をするのか?
  • 処理中にConfiguration Changeが発生(画面回転など)した時はどういう動作をするのか?
  • 処理後にConfiguration Changeが発生(画面回転など)した時はどういう動作をするのか?
など、どんな時にどんな挙動するのかを把握し正しく使用しないと
特定の場合のみ起こるバグに繋がったりします。


まとめ


使いづらいし、どういう動作をするかきちんと把握しないとバグにつながる。
しかし、きちんと使いこなせれば他の手法にはないメリットもあります。
つまり正しい使用方法を理解すればこの上ない武器になります。
ゆえにLoader、AsyncTaskLoaderを徹底解剖したいと思います!

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で遊んでみる(点の描画)へ