ハーフトーンシェーディング
今回のサンプルの実行結果
久しぶりのシェーディングテクニック
前回は WebGL の拡張機能の一つであるインスタンシングを用いたレンダリングについて解説しました。
インスタンシングを用いたレンダリングでは、同一モデルを大量に、しかも一度のドローコールで効率よくレンダリングすることができます。将来的にはスタンダードなテクニックになっていくでしょうから、今のうちから是非モノにしておきたいところです。
さて、今回からは WebGL カテゴリとしては久しぶりに、拡張機能から少し離れてプログラマブルシェーダでいろいろとやってみたいと思います。浮動小数点数テクスチャに始まり、VAO や異方性フィルタリングなど、ここのところ拡張機能を利用した技術ばかり解説していました。少し息抜きも兼ねて、簡単なシェーディングテクニックを扱っていきたいと考えています。
今回まずやってみたいのはハーフトーンシェーダです。
ハーフトーンってなんやねんと思う方もいらっしゃるかもしれませんが、要はドットを用いて陰影を表現するような手段と言えばわかりやすいでしょうか。
新聞などで古くから利用されてきたハーフトーン。日本語では網点(あみてん)などと言いますね。
中間色が出力できず、純粋な白黒で陰影を表現しなければならなかった時代には重宝された手法です。薄いグレーの部分は小さな黒点で、逆に濃いグレーは大きな黒点で、うまく色の階調を表現しようとしたわけです。
ハーフトーンの例
あえて趣のある、レトロな雰囲気に仕上がるハーフトーンシェーダ。
それほど概念も難しくありませんので、楽な気持で取り組んでみましょう。
シェーダでドットを打つ
今回のハーフトーンシェーダでは、通常であればライティングによって滑らかに切り替わる陰影を、ドットの大きさに変換してやることで陰となる部分を表現します。通常のライティングでは、ライトベクトルと頂点法線を用いて拡散光(diffuse)の影響を計算しますね。この拡散光の影響力を、そのまま点の大きさに置き換えてやるだけで、ハーフトーンシェーダの実装はできてしまいます。意外と、簡単です。
ただ、実際にやってみるとドットの大きさだけで陰影を表現すると若干物足りない仕上がりになってしまったので、今回のサンプルでは同時にトゥーンレンダリングに近いこともやっています。
参考:トゥーンレンダリング
上記参考テキストの場合、トゥーンレンダリングを行うためにテクスチャを利用しました。今回は、テクスチャは使わずに、全てシェーダの中で計算してしまいます。そういう意味では、よりシェーダを活用した例ということになります。
テクスチャを使わずにどうやってトゥーンレンダリングを行うのか……そのあたりも含めて、シェーダのコードを見てみましょう。
まずは、頂点シェーダです。
頂点シェーダのソース
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
varying float vDiffuse;
varying vec4 vColor;
void main(void){
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
vDiffuse = clamp(dot(normal, invLight), 0.0, 1.0);
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
attribute 変数と、uniform 変数に関しては、割とお馴染のメンバーですね。要は、通常の平行光源によるライティングを行うのに必要な、法線やライトベクトルなどのデータが入ってくる感じですね。
この頂点シェーダでは varying 変数として vDiffuse
を定義しています。これが、頂点シェーダで計算された拡散光の影響力をフラグメントシェーダに渡す役割を果たします。拡散光に関しては内積を使っていつもどおり計算しているだけです。
さて、それでは続いてフラグメントシェーダです。
フラグメントシェーダのソース
precision mediump float;
uniform float dotScale;
varying float vDiffuse;
varying vec4 vColor;
void main(void){
vec2 v = gl_FragCoord.xy * dotScale;
float f = (sin(v.x) * 0.5 + 0.5) + (sin(v.y) * 0.5 + 0.5);
float s;
if(vDiffuse > 0.6){
s = 1.0;
}else if(vDiffuse > 0.2){
s = 0.6;
}else{
s = 0.4;
}
gl_FragColor = vec4(vColor.rgb * (vDiffuse + vec3(f)) * s, 1.0);
}
フラグメントシェーダでは、uniform 変数を一つだけ受け取るようにしています。
ここで登場する dotScale
という uniform 変数は、HTML 上に設置してある input 要素から値を受け取りシェーダに送られてくるデータです。単純に、ハーフトーンシェーダ全体のドットサイズを、リアルタイムに変更することができるようにするためにこのような作りにしてあります。
頂点シェーダからは、先述のとおり二種類の varying 変数が入ってきます。ひとつは拡散光の影響力を表す vDiffuse
、そして頂点色を表す vColor
ですね。
フラグメントシェーダの main
関数の冒頭では、ちょっと特殊な計算を行っています。
main 関数冒頭の計算部分
vec2 v = gl_FragCoord.xy * dotScale;
float f = (sin(v.x) * 0.5 + 0.5) + (sin(v.y) * 0.5 + 0.5);
ここではまず vec2
型の変数 v
に、処理対象座標を表す gl_FragCoord
に先ほど登場した dotScale
を掛け合わせた値を取得しています。uniform 変数 dotScale
には、HTML 上に設置されている input 要素から 1.0 ~ 2.0 の範囲の値が送られてきます。
その下の行を見ると、変数 v
の値を sin
に渡していますね。つまり dotScale
の値が大きければ大きいほど、 sin
が返すサイン波の間隔が狭く(周期が短く)なることになります。ここで生成したサイン波を元にしてスクリーン上にドットが打たれますので、 dotScale
の値が大きければ大きいほどより細かな点が密集してレンダリングされることになるのですね。
さて、続いて出てくるのがいくつかの分岐処理です。
ここでは、テクスチャを用いずにトゥーンレンダリングと同じようなことをやるための、ちょっとしたチェックを行っています。
トゥーンシェーディング
float s;
if(vDiffuse > 0.6){
s = 1.0;
}else if(vDiffuse > 0.2){
s = 0.6;
}else{
s = 0.4;
}
varying 変数として頂点シェーダから渡された vDiffuse
には、ライトベクトルと法線との内積の結果がそのまま入っています。その値をチェックしつつ、変数 s
に代入する数値を変えているのがわかると思います。
vDiffuse
には、0.0 ~ 1.0 の値が入っているはずです。そして、その数値の大小がそのまま拡散光の影響力の強さを表しています。一定の強さより大きい値(今回の場合 0.6)であれば s
には 1.0 を入れます。そうでない場合でも、さらにチェックを重ねて最終的には変数 s
には三種類の値のうちどれか(1.0 or 0.6 or 0.4)が入ることになります。
そして、ここまでで求めた値をすべて使って、最終的な出力カラーを決めます。
最終出力カラー
gl_FragColor = vec4(vColor.rgb * (vDiffuse + vec3(f)) * s, 1.0);
この辺は、見栄えをいろいろと変えやすい部分だと思いますので、自分なりに修正してみてもいいと思います。ちなみに今回のサンプルの場合は、トゥーンシェーディングの係数のほか、拡散光の影響力を表す vDiffuse
も加味した色付けになっています。
まとめ
今回のサンプルは、javascript 側では特別なことはしていないのでシェーダのソースだけ掲載しておきます。
サンプルページからソースを見てもよくわからないようでしたら、twitter などから質問していただいても大丈夫です。当サイトの過去のテキストで何度も登場している内容なので、順に読み進めてきていれば問題ないでしょう。
さて、ハーフトーンシェーダ、いかがでしたか。
シェーダを自前で書かなくてはならない WebGL は、どうしても最初のうちは取っ付きにくく、またわかりにくいものです。GLSL を同時に習得しなければならないという点だけを見ても、お世辞にも簡単とは言い難いと思います。しかし、今回のようにちょっとした工夫ひとつで、非常に面白い表現ができるのもプログラマブルシェーダのいいところ。この楽しさを体感できるようになってくると、いろいろなアイデアが湧いてきて、さらに GLSL が楽しくなっていくでしょう。
今回はシェーダのソースもそれほど行数が多くありませんし、工夫しやすいシンプルな構造だと思います。
自分なりに、いろいろ手を加えて遊んでみてください。
実際に動作するサンプルは以下のリンクから見ることができます。