見出し画像

Three.jsでトゥーンシェーディング

※この記事はtkmh.me上で掲載している記事 (2015.09.24 公開) を転載、加筆・修正したものです。

---------

3Dの表現のなかでわりと好きなトゥーンシェーディングをThree.jsで実装してみました。

DEMO

トゥーンシェーディングの主な特徴は

・エッジがはっきりと縁取られている
・陰影がはっきりしている

です。アニメっぽい表現のシェーディングです。アニメキャラが登場するゲームでよく使われる手法です。

最近だと以下のミュージックビデオコンテンツもWebGLのトゥーンシェーディングっぽい表現で作られていました。

今回のデモは、缶の3Dモデル(objファイル)をThree.jsで読み込み、テクスチャを適用して表示しています。

3Dデータは以下のサイトでフリーのものをダウンロードしました。

ダウンロードしたデータをblenderでインポートし、テクスチャをつくるためにUV展開図なるものを生成しました。

要は缶の展開図なのですが、この形に合わせてテクスチャを作れば、3Dモデルに綺麗に貼り付けてくれます。

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

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

トゥーンシェーディングに加え、金属光沢を再現するために、3D空間上の点光源が反射しているような表現と、凹凸感を表現をするためのバンプマップ(法線マップ)という手法をGLSLで実装しています。

バンプマップ用の法線マップ画像はなんとphotoshopのフィルタ機能で作ることができました。楽ちん。

この画像を使用して、GLSLでゴニョゴニョっとやってあげるだけで、凹凸感を実現できます。缶側面のgithubのロゴの部分がへこんだように見えるかと思いますが、これは3Dモデルが実際にへこんだ形になっているのではなく、擬似的にそう見えるように計算して表示させる技法です。

※上記の表現技法の実装は正直手前実装は難しいので、以下の記事を参考にしています。

ソースコードは以下のリンクから。

主な処理はすべて
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を使うと表現の幅が一気に広がりますね。

---------

上記の実装だと、頂点法線をそのまま反射の計算などに使ってしまっているので、オブジェクトを回転、拡大、移動などをすると (要はmodelMatrixで変形すると)、意図した見た目にならないですね。

なので頂点シェーダにmodelMatrixの逆行列を渡して、カメラやライトの位置をモデルの空間に合わせてあげる必要があります。

そのへんの説明は以下の記事に記載されています。doxas先生様々です。。



この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

サポートいただければ、レッドブルを飲んでより頑張れると思います。翼を授けてください。

ありがとうございます!あなたの「スキ」が力になります。
2

Takumi Hasegawa (unshift)

東京でフリーランスのdeveloperをやっています。 ポートフォリオがリニューアルしました → https://unshift.jp/ | Facebook Pageはこちら → https://fb.com/unshift.jp/
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。