ラインシェード シェーダ
今回のサンプルの実行結果
点の次は線でしょ
前回はハーフトーンシェーディングと銘打ち、ドットの大きさによって陰影を表現する特殊なシェーダを解説しました。
今回は、そのハーフトーンシェーディングの応用的なシェーダを書いてみようと思います。やっていることはほとんど同じですが、ハーフトーンシェーディングが点描によって陰影を表現していたのに対し、今回のラインシェード シェーダではその名のとおり線を用いて陰影を表現します。
シェードなシェーダということで若干言葉がおかしなことになってますが、比較的プログラムや概念は簡単ですので、さっくりと実装してみましょう。今回も、前回と同様にシェーダの中でトゥーンシェーディングと同じ機構を使います。もし、このあたりの概念がわかりにくいようでしたら、前回の内容も参考にしてみてください。
汎用性の高い SIN を用いた陰影付け
前回のハーフトーンシェーディングでは、サイン(SIN)をうまく利用することで、ドットのサイズを拡大縮小して陰影を表現しました。ドットのサイズが大きければ、それだけ濃い色が塗られているように見えるのでしたね。
今回のラインシェードの場合も、基本的にはこれと同じ考え方です。線が太くなればなるほど、その部分には濃い色がつくことになります。これで陰になる部分を表現するわけですね。
ただ、今回の場合は線の太さをシェーダの中で太くしたり細くしたりするというよりは、トゥーンシェーディングとの合わせ技で結果的に線が太く見えるようになるだけです。ですから非常にシェーダもシンプルな実装になっています。
頂点シェーダに関しては前回と同じものですので割愛します。ここでは、フラグメントシェーダのソースを見てみましょう。
フラグメントシェーダ
precision mediump float;
uniform float lineScale;
varying float vDiffuse;
varying vec4 vColor;
void main(void){
vec2 v = gl_FragCoord.xy * lineScale;
float f = sin(v.x + v.y);
float s;
if(vDiffuse > 0.7){
s = 0.9;
}else if(vDiffuse > 0.3){
s = 0.6;
}else{
s = 0.3;
}
gl_FragColor = vec4(vColor.rgb * (vDiffuse + f + s), 1.0);
}
uniform 変数として、フラグメントシェーダには線の太さを表す係数が入ってくるようになっています。それが lineScale
です。ここには、HTML の input 要素から取得した値がプッシュされてきます。サンプルの場合だと、0.6 ~ 1.2 の範囲の値が任意に送られてくるようになっています。※この範囲に深い意味はないです
main
関数の冒頭では、処理対象ピクセルの座標を表す gl_FragCoord
に先ほどの lineScale
を掛け合わせています。さらに、この結果をもとにして sin
関数を利用しているのがわかりますね。
sin
に渡している値は、変数 v
の X と Y を足し合わせた数値です。このように、縦横の座標を足し合わせた数値を利用することで、45 度に傾斜したサイン波が得られます。普段からサインやコサインを扱っている人なら、すんなり理解できるでしょう。
ちょっと意味がわからないよ! という人もいるかもしれないので簡単にざっくりとですが説明すると、サイン波は下の画像のように規則正しく一定の周期で値が増減します。
サイン波のイメージ図
スクリーンの横方向、つまり X 座標の値を元にサイン波を生成すると、横に向かって規則正しく値が上下することになるわけですから、結果としては縦縞のラインが現れます。同様に、スクリーンの縦方向、Y 座標の値を元にして同様のことをすれば、結果的には横縞のラインが出るのですね。
今回の場合は、X と Y を足し合わせた値を sin
関数に渡しているので、縦縞でも横縞でもなく、斜めに傾いたラインが現れるというわけです。
シェーダ内では、変数 f
にこのサイン波を利用した値が入っており、これとトゥーンシェーダで算出した陰影情報とを利用して最終的な出力カラーを決めています。
ちょっとした応用
さて、先ほど少しだけ小難しいサイン波の話をしましたが、このあたりのことがある程度感覚的に理解できてくると、次の画像のように、線が交差したように現れるシェーダも簡単に書けるはずです。
クロスラインシェード
一番濃く陰が掛かる部分には、ちょうど十字を描くような感じで線が交差してレンダリングされていますね。
先ほど説明したように、スクリーン上の座標を元に、X と Y を足し合わせた場合には単一方向の斜線が描かれます。
これに垂直にクロスするような、もう一方向分の線を描くには、どのような計算をすればいいのでしょうか。
はい、正解は下記コードを見ればすぐにわかると思います。
クロスラインシェードのフラグメントシェーダ
precision mediump float;
uniform float lineScale;
varying float vDiffuse;
varying vec4 vColor;
void main(void){
vec2 v = gl_FragCoord.xy * lineScale;
float f = max(sin(v.x + v.y), 0.0);
float g = max(sin(v.x - v.y), 0.0);
float s;
if(vDiffuse > 0.6){
s = 0.8;
}else if(vDiffuse > 0.1){
s = 0.6 - pow(f, 5.0);
}else{
s = 0.4 - (pow(f, 5.0) + pow(g, 5.0));
}
gl_FragColor = vec4(vColor.rgb * s, 1.0);
}
先ほどとは違い、X と Y で減算する処理が入っているのがわかると思います。
足し算で処理した場合に描かれる斜線と、引き算で処理した場合に描かれる斜線は垂直に交わります。これは、先ほどのサイン波の話が理解できていればすぐにイメージできるでしょう。これらのふたつの情報を用いて出力される色を決定することで、クロスラインシェードにも簡単に対応することができるわけです。
ちなみに、シェーダの中で pow
関数を使っているところがありますが、これは単に線を細くより鮮明に表示するためです。スペキュラライティングするときに光沢をより鮮明にするのと同じ原理ですね。
まとめ
さて、今回はラインシェードということで簡単なシェーダの実装を見てきました。
前回のハーフトーンシェーディングもそうですが、3D プログラミングでは陰影ひとつ表現するためにも様々な手法が考えられます。比較的単純な陰影付けでも、見た目がガラリと変化するのは非常に面白いですね。創意工夫のしがいがある部分だと思いますので、ぜひいろいろと修正してオリジナルシェーダにチャレンジしてみてください。
また本文中でも触れたように、サイン波は割と様々なものに利用できる便利な概念です。これひとつ理解しているだけでもかなり応用の幅が広がると思いますので、いろいろ調べてみるといいのではないでしょうか。
実際に動作するサンプルは以下のリンクから見ることができます。