Three.jsでトゥーンシェーディング
2015.09.24
Category
Tags
3Dの表現のなかで割りと好きなトゥーンシェーディングをThree.jsで実装してみました。
トゥーンシェーディングの主な特徴は
・エッジがはっきりと縁取られている
・陰影がはっきりしている
です。アニメっぽい表現のシェーディングです。アニメキャラが登場するゲームでよく使われる手法です。
最近だと以下のミュージックビデオコンテンツもWebGLのトゥーンシェーディングっぽい表現で作られていました。
今回のデモは、缶の3Dモデル(objファイル)をThree.jsで読み込み、テクスチャを適用して表示しています。
3Dデータは以下のサイトでフリーのものをダウンロードしました。
3D Models, Download 3D Models at TurboSquid
http://www.turbosquid.com/Search/3D-Models/free
ダウンロードしたデータをblenderでインポートし、テクスチャをつくるためにUV展開図なるものを生成しました。

要は缶の展開図なのですが、この形に合わせてテクスチャを作れば、3Dモデルに綺麗に貼り付けてくれます。
このUV展開図に合わせてつくったテクスチャが以下のものです。側面と上の方を青くしたいだけなので、その部分を青で塗りつぶし、それ以外の領域はグレーにしています。雑です。

実際は以下の画像はテクスチャとして使用していませんが、UV展開図とテクスチャを重ねると、どの部分を青くしてるのかわかりやすいかと思います。

トゥーンシェーディングに加え、金属光沢を再現するために、3D空間上の点光源が反射しているような表現と、凹凸感を表現をするためのバンプマップ(法線マップ)という手法をGLSLで実装しています。
バンプマップ用の法線マップ画像はなんとphotoshopのフィルタ機能で作ることができました。楽ちん。

この画像を使用して、GLSLでゴニョゴニョっとやってあげるだけで、凹凸感を実現できます。缶側面のgithubのロゴの部分がへこんだように見えるかと思いますが、これは3Dモデルが実際にへこんだ形になっているのではなく、擬似的にそう見えるように計算して表示させる技法です。
※上記の表現技法の実装は正直手前実装は難しいので、以下の記事を参考にしています。
wgld.org | WebGL: トゥーンレンダリング |
wgld.org | WebGL: 反射光によるライティング |
wgld.org | WebGL: バンプマッピング |
WebGLの能力を引き出すプログラマブルシェーダー (3/5)
GLSLシェーダによるカートゥーンレンダリング
Three.jsでトゥーンシェーダ(とShaderMaterial)
ソースコードはこちら。
主な処理はすべて
https://github.com/takumi0125/E-Can/blob/master/gulp/src/assets/js/index.coffee
に記述されています。
実装した頂点シェーダ・フラグメントシェーダを以下に記載します。
Three.jsでGLSLでマテリアルを記述できる仕組み (THREE.ShaderMaterialクラス)を使用しており、GLSLの組み込み変数に加えてThree.js独自の組み込み変数も存在します。詳しくはこちら。
//頂点シェーダ
uniform float edgeWidthRatio;
uniform bool edge;
uniform vec3 lightPosition;
varying vec2 vUv;
varying vec3 vEyeDirection;
varying vec3 vLightDirection;
void main() {
vec3 pos = (modelMatrix * vec4(position, 1.0)).xyz;
if(edge) {
pos += normal * edgeWidthRatio;
} else {
vec3 eye = cameraPosition - pos;
vec3 light = lightPosition - pos;
vec3 t = normalize(cross(normal, vec3(0.0, 1.0, 0.0)));
vec3 b = cross(normal, t);
vEyeDirection = normalize(vec3(dot(t, eye), dot(b, eye), dot(normal, eye)));
vLightDirection = normalize(vec3(dot(t, light), dot(b, light), dot(normal, light)));
vUv = uv;
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
//フラグメントシェーダ
uniform vec3 lightDirection;
uniform sampler2D stepTexture;
uniform sampler2D texture;
uniform sampler2D normalMap;
uniform samplerCube envMap;
uniform bool edge;
uniform vec4 edgeColor;
varying vec2 vUv;
varying vec3 vEyeDirection;
varying vec3 vLightDirection;
void main(void){
if(edge) {
gl_FragColor = edgeColor;
} else {
vec3 mNormal = (texture2D(normalMap, vUv) * 2.0 - 1.0).rgb;
vec3 halfLE = normalize(vLightDirection + vEyeDirection);
float step = clamp(dot(mNormal, vLightDirection), 0.1, 1.0);
float specular = pow(clamp(dot(mNormal, halfLE), 0.0, 1.0), 100.0);
gl_FragColor = texture2D(texture, vUv) * texture2D(stepTexture, vec2(step, 1.0)) + vec4(vec3(specular), 1.0);
}
}
縁取りを実現する方法はいくつかあるようですが、今回は頂点シェーダで頂点の位置を頂点法線ベクトルの方向に少しだけ位置を移動し、指定の単色で塗りつぶしたたオブジェクトを、ポリゴンの裏側だけ描画することによって実現しています。
ポリゴンの裏側だけをレンダリングするには、shaderMaterialのsideプロパティをTHREE.BackSideにセットします。
@toonShaderMaterial.side = THREE.BackSide;
同じオブジェクトを本体と縁取りの分、2回レンダリングしているのですが、2回のレンダリングは同じシェーダを使用しています。
縁取りをレンダリングする際は、edgeという変数にtrueを渡し、edgeWidthRatioに縁取りの太さ (厳密には太さの割合)、edgeColorに縁取りの色を渡します。
頂点シェーダでは、カメラの位置と光源の位置を用いて、頂点の色を決定するための各種ベクトルを計算しています。
視線とカメラのベクトルを接平面に変換して云々かんぬん・・・というけっこう難しい話なのですが、こちらの記事に詳しく記載されています。
こちらのブログを執筆している方は相当賢いのではないか。。
簡単な3D表現であればCSSでも実現できますが、WebGLを使うと表現の幅が一気に広がりますね。
