前回の続きです。
今回は、toon shading や edge 処理とかをやります。
いよいよシェーダーをいじります。
WebGLというかOpenGLでは、
GLSLというシェーダー言語でプログラミングします。
だいぶ昔、DirectX9の頃に
HLSLをちょっと使ったことはあるのですが、
GLSLは初めてだったりします。
ときに、
シェーダーには大きく分けて、
vertex shader と fragment shader の2つがあります。
後者は pixel shader とか呼ばれたりすることもあります。
それぞれ頂点、ピクセルを対象として演算を行います。
ところで、
前回までのテストでは、
three.js で用意されている MeshPhongMaterial、
いわゆる Phong シェーディングを使っていました。
そこで、
これによって生成されるGLSLコードを改造して、
ShaderMaterialを継承するという形で
toon shading や edge 処理を実装することにしました。
クラス名は MMDMaterial としました。
さて、
toon shading は cel shading とか「アニメ塗り」とか言われる技法です。
光源処理による連続的な陰影を、
テクスチャマップを応用して段階的なものにしてしまうことです。
技術的にはこちらとか参考になります。
MMDでは toon マップ用のテクスチャがプリセットで10個用意されています。
toon は拡散光(Diffuse)に応じた処理をします。
MeshPhongMaterialなGLSLコードでは拡散光の処理は以下の様な感じに、
fragment shader で処理されます。
法線と光線方向との内積から、陰影が計算されます。
※注意:実際のコードとは異なります
// this is image coding. not actual code.
float dotProduct = dot(normal, lightDirecton);
float diffuseWeight = max(dotProduct, 0.0);
vec3 diffuseColor = lightColor * diffuseWeight;
これに toon shading を導入すると以下のようになります。
toonMap なテクスチャはあらかじめセットされているものとします。
法線と光線方向との内積から、
テクスチャのV方向を参照し乗算することで段階的な陰影にするわけです。
U方向は参照しませんが、このような手法を使うと、
toon と 輪郭強調(輪郭抽出風味)を同時に出来たりしてお得かも(^_^)
// this is image coding. not actual code.
float dotProduct = dot(normal, lightDirecton);
vec3 diffuseWeight = texture2D(toonMap, vec2(0.0, max(dotProduct, 0.0))).xyz;
vec3 diffuseColor = lightColor * diffuseWeight;
上のような処理を、設定されているlightの数だけ行なって、
(ただし ambient light は除きます)
合計した結果がtoonな拡散光成分となります。
次に edge ですが、これは輪郭線を表現する処理です。
輪郭線を描くにはいくつか手法があるようです。
例えば、いったんオフスクリーンでレンダリングし、
こういった感じのフィルターで輪郭線を抽出して
後処理で合成するといった感じです。
後処理な手法は別途バッファが必要になったりするので、
もっと簡便な手法を採用します。
以下の様に描画を2回に分けて行います。
- 普通に描画。
- vertex shader では頂点を法線方向に少し引き伸ばし、
fragment shader では裏面を特定色で描画する。
これだけでそれっぽい輪郭線が表現できます。うまく考えたものですね。
今まで袖の内側やスカートの内側が描画されていなかったのは、
両面ポリゴン化し忘れているわけではなく、実は
edge 処理による裏面描画によって自動的にうまく行くようになってたりします。
MMDでは引き伸ばす量を「エッジ太さ」として
0~2くらいに設定しているようです。
おそらく単位はピクセルで、
輪郭線の太さを固定する手法を用いているものと思われます。
しかし今回は単純にローカル座標系で処理するので、
視点からの距離に応じて太さがスケールされます。
先の数値のままだと伸ばし過ぎることになりそうですので、
重みを掛けて処理します。
やってみた感じでは 0.02 ~ 0.03 程度にするのが良さそうです。
ちなみに通常描画で頂点を引き伸ばすと、
デブっちょな感じのミクさんになったりして面白いです(^_^;)
なお、
視点からの距離に関わらず輪郭線の太さを固定する方法は
まだよく分からないので、後々どうにかする予定です(^_^;)
ということで、
実装するには描画を2回に分けて行う必要があります。
マルチパスな描画ということになります。
mesh全体をシェーダーを切り替えて2回描画すればいいのですが、
これは処理コスト的に問題となります。
まずシェーダーを切り替えるということは、
関連付けられているmaterialも切り替えることになってしまいますので、
meshを再設定するようなことになってしまいます。
またmesh単位で描画を2回行うのもコストが気になります。
というのも SkinnedMesh の場合は、
内部的には複数のmaterialから構成されることになるからです。
前回でも書きましたが、
頂点や面情報で構成されたmeshがレンダリングされる際、
各要素は属するmaterial単位に分割されて処理されます。
materialに関連する情報を切り替えるのはコストがかかるので、
同じmaterialを使うのであれば、切り替えずに描画したいわけです。
ただ残念ながら、
現状の three.js ではこのようなマルチパス描画をサポートしていません。
オフスクリーンなバッファを使う後処理なマルチパスなら、
Composerという機能が用意されているのですが・・・。
仕方ないので three.js 本体を改造することにしました。
具体的には WebGLRenderer 内の renderBuffer() を以下のようにします。
※注意:実際のコードとは異なります。
// this is image coding. not actual code.
this.renderBuffer = function(...) {
if (updateBuffers) {
setupAttributeBuffers(); // vertices, normals, etc.
}
var passes = material.passes || 1;
for (var pass = 0; pass < passes; pass++) {
if (material.preRenderPass) {
material.preRenderPass(this, pass);
}
renderMesh(material);
if (material.postRenderPass) {
material.postRenderPass(this, pass);
}
}
};
改造のポイントは、renderMesh を複数回行えるようにして、かつ
前後でmaterialに登録された関数をコールバックできるようにしたことです。
フラグなパラメータ(uniform)を設定するようにすれば、
シェーダー内で if によって処理を切り分けることができるようになります。
つまり、シェーダーそのものを切り替えずに済むようになります。
ちなみに、HLSLでは言語レベルでpassを設定できるので、
もっとスマートに記述できると思います。
ついでに、
シェーダーのプログラムをセットする setProgram() にも手を入れました。
setProgram() ではシェーダーのパラメータ(uniform)を WebGL へ渡す際に、
material がどんなクラスのインスタンスなのかをチェックして、
それに応じたパラメータをセットアップするような作りになっています。
そのためこのタイミングにおいては、
three.js が認識してないパラメータをセットアップすることができません。
そこで以下のように、
material に登録された関数をコールバックして、
カスタマイズしたパラメータをセットアップできるように改造しました。
function setProgram(...) {
.
.
var refreshMaterial = false;
var program = material.program,
p_uniforms = program.uniforms,
m_uniforms = material.uniforms;
if ( program !== _currentProgram ) {
_gl.useProgram( program );
_currentProgram = program;
refreshMaterial = true;
}
.
.
if ( refreshMaterial ) {
.
.
// refresh custom uniforms
if (material.refreshUniforms) {
material.refreshUniforms(_this);
}
// load common uniforms
loadUniformsGeneric( program, material.uniformsList );
.
.
}
loadUniformsMatrices( p_uniforms, object );
.
.
return program;
}
ということで toon shading と edge 処理が実現できました。
ただ edge はコントラストが強くなるため、ジャギーが気になります。そこで
three.js で用意されているFXAAなアンチエイリアスを後処理で施しました。
あと今回、
material 単位で影の濃さを調整できるようにもしたので、
セルフシャドウを有効にしました。
さて、
なんか文章が長くなりましたが、お待たせしました(^_^;)
bone skinning + IK + morph + toon + edge + self shadow なミクさんの結果はこれです。
あ、そうそう、あと一言。
今回、GLSLなコードを組み込んだわけですが、
JavaScript には heredoc がないんで、
ソース行を文字列化した配列をjoinするしか方法がないのが
なんか激しくメンドイです。
python みたいな heredoc があればラクなんだけど・・・。
さて、
残りは物理演算。なんか手強そうです(^_^;)
いまだ完全にはうまく行ってないIKもどうにかしたいですね。