derivative 関数(dFdx, dFdy)

実行結果

今回のサンプルの実行結果

標準機能になった derivative 関数

前回は MRT(Multiple Render Targets)を扱いました。

一度のレンダリングの実行で複数のカラーバッファ(テクスチャ)に値を出力することができる MRT は様々なテクニックに利用できる汎用的な技術だと思います。

WebGL 2.0 ではこれが標準機能に格上げされたため、より手軽に、気兼ねなく使っていくことができます。ぜひ活用しましょう。

さて今回ですが、derivative 関数derivative functions)をやってみましょう。

当サイトを頻繁にご覧になっている方であれば、実は今回の derivative 関数も WebGL 1.0 の頃から利用できたことをご存知かもしれません。この GLSL で利用できる特殊な関数は WebGL 1.0 の頃は拡張機能でした。MRT と同じように、WebGL 2.0 ではこれが標準に格上げになったので普通に使えるようになりました。

今回は、前回のおさらいも兼ねて MRT を利用して flat 修飾子によるフラットシェーディングとデリバティブ関数を利用したフラットシェーディングを比較してみましょう。

どうして結果が変わるのか

さて、今回は異なる手法でフラットシェーディングを実装してみることになるのですが……

冒頭の実行結果の画像を見たり、実際にサンプルの実行を行って確認してみればわかるのですが、結果が異なるものになります。

画面左側の、比較的直方体っぽい面で構成されているほうの結果が「derivative functions」を用いたもの。一方画面右側は、GLSL の flat 修飾子を使って法線を扱った場合の結果です。

見た目の違いは非常にわかりやすいですね。flat 修飾子を利用したほうは面がまるでひし形のような感じになっていて、見比べてみると、ちょっと不自然な感じもすると思います。

このような違いは、実は両者のアルゴリズムというか計算のプロセスの違いによって発生します。

まず最初に flat 修飾子を利用した法線の場合。こちらは、頂点の情報がフラグメントベースに変換される際の、その補間方法がフラットになり……つまり均一になり、結果的にまるで面が等しく同じ法線で統一されているかのような結果を得られるというものです。

実はこの flat 修飾子の挙動はちょっと独特で、面を構成する頂点のうちの どれかひとつの値 が面全体に適用されるという挙動になります。本家 OpenGL の新しいバージョンなどの場合、このとき「どの頂点から情報を抜粋するのか」を指定したりできるのですが、WebGL 2.0 ではそのような指定を行うことはできません。ですから、同じ頂点を共有しているような隣り合う面では、偶然に「抽出される法線」が同じものになってしまう場合があり、それによって一見するとひし形の面であるかのような結果になってしまう場合があるのですね。

これに対しデリバティブ関数を利用した場合はどうなるのでしょうか。

デリバティブ(deribative)を直訳すると日本語では「微分」となります。つまり、平たく言うと傾きを求めることができるわけですね。

GLSL 内部で dFdx, dFdy といった derivative functions を利用すると、ピクセルレベルで微分が行われます。今回の場合は面の傾きに相当する値をシェーダ内で動的に計算することで、面の上で法線が均一になるような計算結果を得ています。ですから、ひとつの頂点から抜き出された情報が共有されたりするようなことはなく、しっかりとそれぞれの面の向きが正しく均一に保たれるというわけなんですね。

両者の処理をおさらい

さて、今回のテクニックは修飾子を使うものにせよデリバティブ関数を使うものにせよ、以前に当サイトで既に紹介している技術です。

詳細についてはそちらも併せてご覧いただくとして、今回はおさらいとして基本的な部分だけさらっと見ていきましょう。

まずは今回の主役であるデリバティブ関数ですが、これはかつて、WebGL 1.0 のころは拡張機能でしたので、事前にこれを有効化したりするなどの特殊な処理を行ってやる必要がありました。WebGL 2.0 ではこれが標準機能になっていますので、事前になにか準備したりする必要はなくいきなり GLSL の中で普通に使ってしまって問題ありません。

また flat 修飾子についても同様ですね。WebGL 2.0 準拠の GLSL ES 3.0 でシェーダを記述しているのであればそのまま普通に使えます。

では、今回のサンプルの頂点シェーダから見てみましょう。

頂点シェーダのソースコード

#version 300 es
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;

uniform mat4 mMatrix;
uniform mat4 mvpMatrix;
uniform mat4 normalMatrix;

out vec3 vPosition;
flat out vec3 vNormal;

void main(){
    vPosition = (mMatrix * vec4(position, 1.0)).xyz;
    vNormal = (normalMatrix * vec4(normal, 0.0)).xyz;
    gl_Position = mvpMatrix * vec4(position, 1.0);
}

さて比較的シンプルな内容ですが、途中で flat 修飾子が出てきているのがわかるでしょうか。

法線をフラグメントシェーダに送る際に補間をフラットモードで行うように、out に指定した法線の、その修飾子としてフラットを指定します。

頂点シェーダ側ではそれ以外に特別なことはありませんが、モデル座標変換後の頂点座標を同時にフラグメントシェーダに送っておく必要があるので、それだけ気をつけましょう。この頂点座標は、フラグメントシェーダでデリバティブ関数を使った法線の算出に使われます。

それでは肝心のフラグメントシェーダです。

フラグメントシェーダのソースコード

#version 300 es
precision highp float;

in vec3 vPosition;
flat in vec3 vNormal;

layout (location = 0) out vec4 outColor0;
layout (location = 1) out vec4 outColor1;

void main(){
    vec3 nx = dFdx(vPosition);
    vec3 ny = dFdy(vPosition);
    vec3 n = normalize(cross(normalize(nx), normalize(ny)));
    outColor0 = vec4(n, 1.0);
    outColor1 = vec4(vNormal, 1.0);
}

まず今回のサンプルは MRT を利用していることに留意しましょう。

よく見ると out 修飾子の付いている色の出力先が複数あるのがわかりますね。これにより、MRT でそれぞれ個別に、法線の算出結果を別のテクスチャに書き出しています。

ひとつ目の出力には、デリバティブ関数を利用した場合の法線を出力します。

デリバティブ関数は WebGL 2.0 + GLSL ES 3.0 の場合は dFdxdFdy のふたつが利用できます。

これらに頂点のモデル座標変換後の座標を与えることで微分を計算、さらに外積を利用して法線変換します。

一方で flat 修飾子を利用してフラグメントシェーダに渡した法線のほうは、特になにもせず、そのまま色としてもうひとつのテクスチャの方へ出力していますね。ひとつのシェーダで複数のテクスチャに書き出せる MRT は本当に便利ですな。

まとめ

今回のサンプルでは、上記のシェーダを実行した結果から得られるふたつの出力を、同時に並べて最終的な描画結果としています。

実際にサンプルを実行してみると、アニメーションしている様子を眺めながら両者の結果を比較することができますので、活用してみてください。

同じフラットシェーディングという概念でも、その実装方法によって結果がまったく異なるものになるのが興味深いですが、普通に考えるとデリバティブ関数を用いる場合のほうが若干負荷が高くなるはずですので、そのあたりは要件と見た目にどこまでこだわるかで決めるといいかなと思います。

今回も実際に動作するサンプルは以下のリンクから確認できます。

entry

PR

press Z key