トゥーンレンダリング

実行結果

今回のサンプルの実行結果

アニメ調レンダリング

前回はフレームバッファを用いることでリアルタイムにキューブマップをレンダリングし、それをキューブマップテクスチャとして適用する動的キューブマッピングについて解説しました。

フレームバッファを使って動的にキューブマップテクスチャを生成することで、リアルな映り込みをダイレクトに実現することができました。少し長いソースコードのサンプルでしたが、要点を抑えれば理解できると思います。

さて、今回は少々趣向を変えて、トゥーンレンダリングを解説しようと思います。トゥーンレンダリングは、別名トゥーンシェーディング、あるいはセルシェーディングなどと呼ばれます。

トゥーンレンダリングを施すと、レンダリング結果はアニメ調、あるいは漫画調になります。これは陰影付けが今まで行なってきたような滑らかな濃淡の変化ではなく、段階的にくっきりと行なわれることによって実現できます。また、今回はその段階的な陰影付けのほか、エッジの描画についても行なってみます。エッジをレンダリングすると、モデルの輪郭線(のように見えるモデル)が描画されます。これによってさらにアニメ調の雰囲気が高まります。

トゥーンレンダリングの基本は、基礎的なライティング計算とテクスチャマッピングとの合わせ技です。これはどういうことかと言うと、今までのライティングの計算では、フラグメントシェーダ内でライティング計算の結果に応じて直接[ 色 ]を出力していましたね。この色の部分を[ テクスチャ座標 ]に置き換えて、シェーダ内でテクスチャを参照してマッピングするようにします。

参照するテクスチャには、以下のような画像を使います。

トゥーンレンダリング用テクスチャに適用する画像

トゥーン用 PNG

本来、ライティングの計算(法線とライトベクトルとの内積から求める値)によって得られる数値は、負の数値になることもあります。今まで行なってきたライティングではシェーダ内でこの値を 0 ~ 1 の範囲にクランプするなどしてから、それを色として適用し出力していました。

トゥーンレンダリングを行なう際には、このクランプされた 0 ~ 1 の範囲の数値を、そのままテクスチャ座標として使います。周知の通り、テクスチャ座標もまた 0 ~ 1 の範囲で表されるものですから、ライティング係数をそのままテクスチャ座標として簡単に利用することができるわけですね。

先ほど載せたトゥーンレンダリング用の画像をテクスチャとして用意しておき、そのテクスチャ座標の s 要素にライティング係数を適用すれば、光が強く当たっているほど右側のテクセルを参照することになり、結果的にモデルの色がそのまま出力されます。逆に光が当たっていない部分ほど左側のテクセルを参照することになるので、モデルの色が若干暗くなるわけですね。

このように、モデルに適用される陰影付けにテクスチャを用いることで、トゥーンレンダリングの第一の特徴である段階的陰影付けが行なえるわけですね。

エッジのレンダリング

さて、トゥーンレンダリングが持つさらなる特徴の一つがエッジのレンダリングです。エッジとはモデルの輪郭線、つまりアウトラインのことですね。

以前、ステンシルバッファを用いたアウトラインの描画を解説したことがありましたが、トゥーンレンダリングにはこの手法は適していません。複数のモデルが重なり合うような場面で、正しくエッジを描画することができなくなってしまうためです。

エッジのレンダリングにはいくつかの手法がありますが、今回は一番簡単で、最も一般的な方法を使ってやってみます。

そのとある方法では、まずカリングを有効にする必要があります。そして、同じモデルを二回レンダリングする必要があります。同じものを二回描画するのは少々非効率に感じるかもしれませんが、その代わり実装は非常に簡単なのでここは目をつぶりましょう。

考え方としてはこうです。

まず普通にモデルをレンダリングします。このとき、カリングが有効になっていれば実際にレンダリングされるのはカメラから見て表向きになっているポリゴンのみですね。その状態を上から眺めた図式が以下の図です。

カリングを有効にしてモデルをレンダリング

一つ目のモデルをレンダリング

カメラから見れば、そこには紛れもなくモデルがレンダリングされていますが、これを上から見た場合には手前半分だけがレンダリングされた状態であることがわかりますね。

さて、続いてカリングの設定を変更してレンダリングされる面を逆転します。その上で、今度は法線方向に少しだけ膨らませたモデルを黒く塗り潰し、もう一度レンダリングします。すると以下のような状態になります。

アウトライン用のモデルをレンダリング

二つ目のモデルをレンダリング

この状態をカメラの方向から見れば、見事にアウトラインが引かれたように見えるわけです。これが今回利用するエッジ描画の手法になります。

シェーダの記述

今回は段階的に色をつけるトゥーン調のレンダリングのほか、エッジをレンダリングする処理もシェーダに組み込む必要があります。最初はマルチシェーダでやろうかとも思ったのですが、あえて一つのシェーダで処理できるようにコードを書きました。

頂点シェーダ

attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mvpMatrix;
uniform   bool edge;
varying   vec3 vNormal;
varying   vec4 vColor;

void main(void){
	vec3 pos    = position;
	if(edge){
		pos    += normal * 0.05;
	}
	vNormal     = normal;
	vColor      = color;
	gl_Position = mvpMatrix * vec4(pos, 1.0);
}

頂点シェーダは結構簡素ですね。今回のシェーダで入ってくる uniform 変数は二つ。一つ目は座標変換行列ですね。そして二つ目は真偽値で、これを使ってトゥーン調モデルをレンダリングするのか、それともエッジ用モデルをレンダリングするのかを判断します。

エッジをレンダリングする際には、法線を使ってモデルを膨らませているのがわかると思います。法線と頂点色は varying 変数としてそのままフラグメントシェーダに送るだけです。

続いてはフラグメントシェーダ。

フラグメントシェーダ

precision mediump float;

uniform mat4      invMatrix;
uniform vec3      lightDirection;
uniform sampler2D texture;
uniform vec4      edgeColor;
varying vec3      vNormal;
varying vec4      vColor;

void main(void){
	if(edgeColor.a > 0.0){
		gl_FragColor   = edgeColor;
	}else{
		vec3  invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
		float diffuse  = clamp(dot(vNormal, invLight), 0.0, 1.0);
		vec4  smpColor = texture2D(texture, vec2(diffuse, 0.0));
		gl_FragColor   = vColor * smpColor;
	}
}

フラグメントシェーダでは、平行光源による環境光の計算を行なっています。先述の通り、通常であれば色として出力する変数 diffuse の値を、今回はテクスチャの参照に使っているのがわかると思います。

ちょっとわかりにくいかなと思うのが main 関数の冒頭で行なっている if 文による分岐処理でしょう。

今回のシェーダでは、エッジとしてレンダリングするモデルの色をメインプログラムから uniform 変数で受け取るようにしています。そして、この uniform 変数として入ってくる色のアルファ値を見て、それがエッジ用なのかどうかを判断するようにしています。

アルファ値が 0.0 だった場合には、ライティングの計算が行なわれます。逆に、それ以上の数値だった場合にはエッジ用のレンダリングだと解釈して、uniform 変数として入ってきた色をそのまま出力するようになっています。メインプログラムのほうでは、エッジ用かどうかで uniform 変数に適切な色情報を指定して送ってやる必要があります。

javascript プログラムの記述

さて、続いてはメインプログラムを見ていきます。

実は、メインプログラムのほうではそれほど難しいことはありません。通常どおり、適切に手順を踏めば特別問題はないでしょう。

ただし、カリングの設定を変更する部分だけは、今まで一度も解説していない部分ですので詳しく説明します。まずは以下のコードを見てください。これは、恒常ループ内で行なっているモデルをレンダリングする部分のコードを抜粋したものです。

恒常ループ内の処理を抜粋

// トーラスの描画
set_attribute(tVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0, 1, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
gl.uniform3fv(uniLocation[2], lightDirection);
gl.uniform1i(uniLocation[3], 0);

// モデルをレンダリング
gl.cullFace(gl.BACK);
gl.uniform1i(uniLocation[4], false);
edgeColor = [0.0, 0.0, 0.0, 0.0];
gl.uniform4fv(uniLocation[5], edgeColor);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);

// エッジ用モデルをレンダリング
gl.cullFace(gl.FRONT);
gl.uniform1i(uniLocation[4], true);
edgeColor = [0.0, 0.0, 0.0, 1.0];
gl.uniform4fv(uniLocation[5], edgeColor);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);

モデルをレンダリング……とコメントが書かれている部分を見てください。ここで、カリングの設定を変更しています。

カリングとは、表か、裏か、とにかくどちらかの面を描画しないように設定することができる機能です。そして、そのどちらの面をレンダリングしないように設定するのかを設定できるのが cullFace メソッドです。この cullFace メソッドには組み込み定数で gl.BACK gl.FRONT のいずれかを指定できます。

カリングによって消去される面をどちらの面に設定するのか、これが cullFace メソッドの考え方です。

上記の抜粋コードに話を戻すと、トゥーン調でモデルをレンダリングする際には cullFace メソッドに gl.BACK が指定されていますね。この場合、陰面消去する面は BACK 、つまり裏面ですよと WebGL に通知することになりますね。逆に、エッジ用モデルをレンダリングする際には陰面消去する面を FRONT 、つまり表面にすることによって、結果的に裏面だけをレンダリングするようにしているわけです。

あとは、先ほどシェーダのところで解説したとおり、エッジ用の色を適切に設定して送ってやれば、シェーダ側でトゥーン調とエッジ用を切り替えながらレンダリングしてくれます。今回はエッジには黒色を使っています。

まとめ

さて、トゥーンレンダリング、いかがでしたか。なんとなくアニメ調の塗りにするだけで、すごく特殊な処理を行なったような気分になるから不思議です。

今回紹介したエッジ部分のレンダリングは、非常に実装が簡単なので気軽に使えるのが利点です。ただし、今回紹介した手法にはデメリットもあります。今回の方法の場合、単純にモデルの裏側を見ているだけなので、エッジとしてレンダリングされるモデルを膨らませる量を増やしてしまうと、非常に不自然な結果になります。

今回のサンプルでは、ほんの少しだけエッジ用モデルを膨らませているだけなのでそれほど目立ちませんが、シェーダのソースをいじって、法線方向への膨らまし処理をもっと大きな数値に変更すると、きっと不自然になってしまうということの意味がわかると思います。

サンプルへのリンクはいつものように以下に用意してあります。

実際に動作するサンプルを見ると、段階的陰影付けとエッジの描画によって、随分と雰囲気が変わるのだということがわかると思います。

entry

PR

press Z key