Skip to content

gl.enchant.jsでかる〜いノリでトゥーンシェーディングをやってみる

shi3zです。

WebGLを手軽に扱えることでお馴染みのgl.enchant.jsですが、3Dって工夫しないとどうも黒っちい画面になりがちなのよね。

その点、ハコニワの人の作品の数々は黒っちくなくて素晴らしいのですが、僕もなんとか可愛くやってみたぞ

それがこれだっ!

そう。なんかマンガ風。
こういう表現を「トゥーンシェーディング」と呼ぶんだ。
トゥーンはマンガのこと。
シェーディングは影の付け方って感じかな。

gl.enchantjsにはもともとトゥーンシェーディング用の命令はないので、とりあえずgl.enchant.jsを改造する方式でやってみたぞ。

まあちょっとトゥーンシェーディングを試してみたいだけなら上記のページからいつものようにソースをダウンロードして来て改造してポン、で終わりだ。

さて、実際のソースコードを見てみよう。
お決まりのクラス宣言はすっ飛ばして、重要なのはいつも通り初期化の部分。ここは非常にテキトーな書き方になってる。

    game.onload = function(){
    		vshader = toonvshader;
    		fshader = toonfshader;
    		scene = new Scene3D();
    		scene.backgroundColor="#44eeff"
    		
    		
    		//トゥーンシェーディング用いろんな処理
    	       toonMap = new Texture("toon.png");
    	       gl.activeTexture(gl.TEXTURE1); //テクスチャ1を有効化
    	       gl.bindTexture(gl.TEXTURE_2D, toonMap._glTexture);
    	      toonSampler = gl.getUniformLocation(scene.shaderProgram, 'uToonSampler');
    	       gl.uniform1i(toonSampler, 1);
	    	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP);
          	toonUse = gl.getUniformLocation(scene.shaderProgram, 'uUseToon');
           	gl.activeTexture(gl.TEXTURE0);

ここではScene3Dを初期化する前にvshaderとfshaderという二つの変数を書き換えてる。

これはWebGLで使用するふたつのシェーダーというプログラムだ。
まあこのへん、いまは泥臭いけど既にgl.enchant.jsの開発者、@rtsanは承知済みで次のバージョンあたりでいい感じになるのではないだろうか。

3Dプログラミングの世界では、画面になにかを描画することをレンダリングと呼ぶんだけれど、レンダリングのなかでも、特に陰影をつけて立体感を出したりする処理をシェーディング(影付け)と呼ぶ。ここがレンダリングの肝なわけだ。

シェーディングには無数のアルゴリズムがあり、典型的なものもあれば特殊なものもある。

トゥーンシェーディングはそういう意味ではNPR(ノン・フォトリアリスティク・レンダリング、つまり非写真的なリアルさを求めるレンダリング)技法のひとつだから特殊な部類に入るだろう。

レンダリングの中でも最も複雑なシェーディングはピクセル単位で行うため、JavaScriptでいちいち1ピクセルごとに処理していたらヘソで茶が湧く、というか日が暮れてしまう。

実際、昔のコンピュータでは画面一枚をレンダリング(シェーディング)するのに丸一日掛かった、なんてことも珍しくない。

そこで、GPUの出番になる。
GPUは、シェーディングを専門に行うための高度なスーパーコンピューターを内蔵している。GPUによって違うが、この計算コアは8〜128個くらい搭載されている。

それが一気に並列計算をするわけ。

昔、シェーダーはGPU上にハードウェア的に実装されていたんだけど、Xboxあたりから、シェーダー自体をプログラミングする、プログラマブルシェーダーという技術が産まれた。

これには専用の言語を使うことになる。これをシェーダー言語と呼ぶ。
これによって非常に多彩な表現をリアルタイムに行うことが可能になり、ゲームグラフィックスは格段に進歩した、というわけだ。

今回のトゥーンシェーディングの実現にも、シェーダーを使うことにした。

シェーダーには、頂点シェーダーとピクセルシェーダー(フラグメントシェーダーとも呼ぶ)の二種類がある。

このソースではvshaderが頂点シェーダー、fshaderがフラグメントシェーダーだ。

今回、シェーダーのソースはgl.enchant.jsからコピペしたものを書き換えて使用した。

実際に変更したのはfshaderの方だ。

今回はルックアップテーブル方式のトゥーンシェーディングを行った。
この方式だと立方体とかは正しくトゥーンシェーディングされない。
けど、縁が丸い物体が相手ならまあなんとかなるので今回はこれでいいや、ということにした。

アルゴリズムは意外と単純だ。
図で示すように、カメラから点に向かって伸びる視線ベクトルEと、点に割り当てられた法線ベクトルNがあるとする。

法線ベクトルというのは、向きを意味するベクトルで・・・ま、面倒から今回説明は省略。

見て解るように、カメラから見て角度が90度に近くなるほど輪郭に近くなる。

そこで90度に近いところは輪郭線を、そうでないところはわざと段階的に塗るとアニメっぽくなる。

90度に近いかどうかは、NとEのベクトル内積で簡単に出すことが出来る。そう。|N||E|cosθだ。ただし、NとEは単位ベクトルというのがお約束なので、単にcosθとなる。

このとき、下図のようなトゥーンシェーディング用マップを用意する。

そしてN・E(・は内積)を縦軸、本来描きたかった色のRGB値をそれぞれバラバラにしてそれを横軸にとると、描くべき色が自動的にルックアップ(見つけ出される)されるというわけだ。

実際のシェーダーのコードは以下のようになる。
複雑だから読み飛ばして良い。

var toonfshader = '\n\
precision highp float;\n\
\n\
uniform sampler2D uSampler;\n\
uniform sampler2D uToonSampler;\n\
uniform vec3 uLightColor;\n\
uniform vec3 uLookVec;\n\
uniform vec4 uAmbient;\n\
uniform vec4 uDiffuse;\n\
uniform vec4 uSpecular;\n\
uniform vec4 uEmission;\n\
uniform vec4 uDetectColor;\n\
uniform float uDetectTouch;\n\
uniform float uUseTexture;\n\
uniform float uUseLighting;\n\
uniform float uShininess;\n\
uniform float uUseToon;\n\
uniform vec3 uLightDirection;\n\
\n\
varying vec2 vTextureCoord;\n\
varying vec4 vColor;\n\
varying vec3 vNormal;\n\
\n\
\n\
void main() {\n\
    vec4 texColor = texture2D(uSampler, vTextureCoord);\n\
    vec4 baseColor = vColor*uDiffuse ;\n\
    baseColor *= texColor * uUseTexture + vec4(1.0, 1.0, 1.0, 1.0) * (1.0 - uUseTexture);\n\
    float alpha = uDetectColor.a * uDetectTouch + baseColor.a * (1.0 - uDetectTouch);\n\
    if (alpha < 0.2) {\n\
        discard;\n\
    }\n\
    else {\n\
        vec4 tmpA = vec4(0.1,0.1,0.1,1.0);\n\
        vec4 phongColor = uAmbient;\n\
        vec3 N = normalize(vNormal);\n\
        vec3 L = normalize(uLightDirection);\n\
        vec3 E = normalize(uLookVec);\n\
        vec3 R = reflect(-L, N);\n\
        float lamber = max(dot(N, L) , 0.0);\n\
        phongColor += uDiffuse * lamber;\n\
        float s = max(dot(R,-E), 0.0);\n\
        vec4 specularColor= uSpecular * pow(s, uShininess) * sign(lamber);\n\
        vec4 tmp = (uEmission* baseColor + specularColor + vec4(baseColor.rgb * phongColor.rgb * uLightColor.rgb, baseColor.a)) \n\
            * (1.0 - uDetectTouch) + uDetectColor * uDetectTouch;\n\
        if(uUseToon>0.1){\n\
             float toon = pow( max(dot(N,-E), 0.0),2.0);\n\
             gl_FragColor=vec4( \n\
                     texture2D(uToonSampler,vec2(tmp.r,toon)).r * tmp.r, \n\
                     texture2D(uToonSampler,vec2(tmp.g,toon)).g* tmp.g, \n\
                     texture2D(uToonSampler,vec2(tmp.b,toon)).b* tmp.b, \n\
                     tmp.a);\n\
	}else{\n\
		gl_FragColor=tmp;\n\
         }\n\
     }\n\
}';

末尾に「\n\」と付いているのは、これがJavaScriptのコードではなくてプログラマブルシェーダーのコードであるため。

シェーダーのプログラミングは慣れると凄く快感だ。
なにしろかなり複雑な処理を書いてもGPUがアッという間に処理してくれるからだ。

さて、実際にトゥーンシェーディングの処理をしているのは「if(uUseToon>0.1)」以降のブロックだ。これもかなりいい加減に書いてしまったが本当はもっとマシな書き方がある。しかし適当に書いても動いちゃうのが現代科学の凄いところだ。

わかりやすいようにそこだけ抜き出して\n\をとってコメントを付け足してみよう。

        //↓この行はいろいろややこしい計算をして本来のシェーディングをしてる
        vec4 tmp = (uEmission* baseColor + specularColor + vec4(baseColor.rgb * phongColor.rgb * uLightColor.rgb, baseColor.a)) 
            * (1.0 - uDetectTouch) + uDetectColor * uDetectTouch;

        if(uUseToon>0.1){
             float toon = pow( max(dot(N,-E), 0.0),2.0); //NとEの内積を求める
             gl_FragColor=vec4( //ルックアップテーブルを使って色を決める
                     texture2D(uToonSampler,vec2(tmp.r,toon)).r * tmp.r, 
                     texture2D(uToonSampler,vec2(tmp.g,toon)).g* tmp.g, 
                     texture2D(uToonSampler,vec2(tmp.b,toon)).b* tmp.b, 
                     tmp.a);
	}else{
		gl_FragColor=tmp; //トゥーンシェーディングしない
         }

ちょっとはわかりやすくなっただろうか。
まずtmpに、本来のシェーディングをする。これをそのままgl_FlagColorに渡すと、トゥーンシェーディングなしの美しいシェーディングが行われる。

実際にトゥーンシェーディングをしているのはtoonのあたり

             float toon = pow( max(dot(N,-E), 0.0),2.0); //NとEの内積を求める

NとEの内積を求めるとコメントしてあるが正確にはNとEの内積を求めて二乗している。

内積を求める命令はdotで、powは与えられたパラメータを与えられたぶんだけ累乗する。maxは最大値が0未満になるようにしてる。

ここで何乗するかによって、輪郭線の太さが変わる。
今回は2乗くらいがちょうど良かった。

こんな感じでバッチグーと思っていたんだけど、gl.enchant.jsがマルチテクスチャリングを完全に無視していたので、そこだけ改造が必要になった。

gl.enchant.jsの1620行あたりをこんな風に改造した。

            if (this.mesh.texture._image) {
       gl.activeTexture(gl.TEXTURE0); //テクスチャ1を有効化
                gl.bindTexture(gl.TEXTURE_2D, this.mesh.texture._glTexture);
       gl.bindTexture(gl.TEXTURE_2D, toonMap._glTexture);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP);
                gl.uniform1f(scene.shaderUniforms.useTexture, 1.0);
                
            } else {
                gl.uniform1f(scene.shaderUniforms.useTexture, 0.0);
            }
               if(this.noToon==true)
	                gl.uniform1f(toonUse, 0.0);
	             else
	                gl.uniform1f(toonUse, 1.0);

WebGLのプログラムあまり書いたことないからすげー無駄なことしてるのかもしれないけどともあれ動いた。

百万遍の理屈よりも動くコードは正しいのだ。

@rtsanに、マルチテクスチャリングや変わったシェーダーをサポートできるようにして欲しいと要望を出したら「実はもう取りかかってます」とのことだったので遠からずgl.enchant.jsはさらにバージョンアップするだろう。

なによりソースが公開されているからこんなふうに自分で改造しても遊べるしね。

というわけでまた。

このエントリーをはてなブックマークに追加
はてなブックマーク - gl.enchant.jsでかる〜いノリでトゥーンシェーディングをやってみる
Post to Google Buzz
Share on GREE

Related posts:

  1. iPhoneでシェーダーをプログラムするっ!:ShaderEdit
  2. WebGL+enchant.jsでモグラ叩き!ゲームをつくる!(221行)
  3. 3D野郎は寄ってたかれ!WebGLでグリグリ遊べるgl.enchant.jsがついにβ公開!
  4. gl.enchant.js開発者コミュニティが活発化!チュートリアルやサンプルが続々と
  5. gl.enchant.jsがクォータニオンに対応した0.3βにバージョンアップ!

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*