Web上ではWebGLを使うことでシェーダーによる画像処理が実現でき、HTML5コンテンツに多彩なグラフィカル表現をもたらすことができます。シェーダーはGPUの恩恵を受けれるため高速に実行でき、HTML5の他の代替手法(例えばcanvas要素Context2Dオブジェクトによる画像処理等)よりも負荷が軽いことがメリットです。今回はWebGLの定番ライブラリ「Three.js」とGLSLというシェーダー言語を使った、8種類の画像処理の実装方法を紹介します。

※Three.jsでフラグメントシェーダー(断片シェーダー)を適用するコードはデモ「postprocessing」を参考にしています。このソースコードはThree.jsのGitHubから確認できます。

デモ

次のデモでは複数のシェーダーを適用することができます。画面左上のチェックボックスで画像加工を選択でき、ラジオボタンから画像とビデオを切り替えることができます。

Photo : Flower by Yasunobu Ikeda
Video : Big Buck Bunny by Blender Foundation | www.blender.org

フラグメントシェーダー

Three.jsでシェーダーを使用する際のフラグメントシェーダーの必要最低限の計算は以下となります。vUvはフラグメントシェーダーから送られてきた値で、texture2D関数でtDiffuseとvUvからシェーダーが現在画像処理しようとしている部分のピクセルカラーを取得出来ます。gl_FragColorにピクセルカラーをセットすると実際に画面に表示されます。

varying vec2 vUv;
uniform sampler2D tDiffuse;
void main() {
    vec4 color = texture2D( tDiffuse, vUv );
    gl_FragColor = color;
}

※今回はaltJSとしてTypeScriptを採用し、クラスコード内へ上記のようなシェーダーをstring型で埋め込んでいます。

ネガポジ反転

ネガポジ反転 ネガポジ反転ということで、ピクセルカラーを反転させます。colorに入る値は「0.0〜1.0」になるので、1.0から色の値を引くだけで簡単に色が反転されます。ちなみに色の値は「x」が赤、「y」が青、「z」が緑になります。

varying vec2 vUv;
uniform sampler2D tDiffuse;
void main() {
    vec4 color = texture2D( tDiffuse, vUv );
    gl_FragColor =  vec4(1.0 - color.x, 1.0 - color.y, 1.0 - color.z, 1.0);
}

セピア調

セピア調 表示をセピア調に変更するシェーダーです。まずはじめにRGB各色から輝度を計算します。本来なら( r + g + b ) / 3.0 を輝度の値にしたいところですが、同じ合計値でも色によって感じる明るさが異なるため、輝度計算する計算式を使用しています。輝度を計算後、セピア色になるように掛け算を行います。

#define R_LUMINANCE 0.298912
#define G_LUMINANCE 0.586611
#define B_LUMINANCE 0.114478

varying vec2 vUv;
uniform sampler2D tDiffuse;
void main() {

    vec4 color = texture2D(tDiffuse, vUv);

    float v = color.x * R_LUMINANCE + color.y * G_LUMINANCE + color.z * B_LUMINANCE;

    color.x = v * 0.9;
    color.y = v * 0.7;
    color.z = v * 0.4;

    gl_FragColor = vec4(color);
}

2値化(threshold)

threshold 画像の色を黒と白のみで表現するシェーダーです。セピア調と同じように輝度の計算後、0.533333 以上なら白として1.0を、未満なら黒として0.0を設定します。そうすると明るめの場所は白に、暗めの場所は黒として表示されます。

#define R_LUMINANCE 0.298912
#define G_LUMINANCE 0.586611
#define B_LUMINANCE 0.114478

varying vec2 vUv;
uniform sampler2D tDiffuse;
void main() {

    vec4 color = texture2D(tDiffuse, vUv);
    float v = color.x * R_LUMINANCE + color.y * G_LUMINANCE + color.z * B_LUMINANCE;
    if (v <= 0.53333) {
        v = 1.0;
    } else {
        v = 0.0;
    }
    gl_FragColor = vec4(vec3(v, v, v), 1.0);
}

2値化(ランダムディザ)

ランダムディザ ランダムディザは、thresholdを改良したものです。同じく白と黒で表現するのですが、thresholdのように一律でこの値より上だったら白・黒というようなものでなく、ある程度はランダムで決まります。ちなみにGLSLにはランダム関数は無いので自分で実装する必要があります。今回は「ランダムな値を返す関数 on GLSL」を参考にしてrand()関数を作成しました。

#define R_LUMINANCE 0.298912
#define G_LUMINANCE 0.586611
#define B_LUMINANCE 0.114478

varying vec2 vUv;
uniform sampler2D tDiffuse;

float rand(vec2 co) {
    float a = fract(dot(co, vec2(2.067390879775102, 12.451168662908249))) - 0.5;
    float s = a * (6.182785114200511 + a * a * (-38.026512460676566 + a * a * 53.392573080032137));
    float t = fract(s * 43758.5453);
    return t;
}

void main() {

    vec4 color = texture2D(tDiffuse, vUv);

    float v = color.x * R_LUMINANCE + color.y * G_LUMINANCE + color.z * B_LUMINANCE;

    if (v < rand(vUv)) {
        color.x = 1.0;
        color.y = 1.0;
        color.z = 1.0;

    } else {
        color.x = 0.0;
        color.y = 0.0;
        color.z = 0.0;
    }
    // 描画
    gl_FragColor = color;
}

2値化(ベイヤーディザ)

ベイヤーディザ ベイヤーディザは上記2つの2値化より濃淡を綺麗に表示できるシェーダーです。4ピクセル×4ピクセルの閾値の表を用いて各ピクセルの色を決めていきます。現在のピクセルの輝度を閾値の表と比較し、輝度が大きければ白を小さければ黒となります。また、UV位置でなく4ピクセルごとにといったピクセル位置を取得する必要があるため、vScreenSizeとしてシェーダー外からスクリーンのサイズを設定しています。 ちなみにこのコードははじめ、配列に直接アクセスしようとスッキリしたコードを書いていたのですが、[]へのアクセスは定数でないといけないというエラー文に気が付き、全てif文に書きなおしたコードです。でもよく考えたらJavaScript上に配置している文字列なので自動生成できるよなぁと今更ながらに思っています。

#define R_LUMINANCE 0.298912
#define G_LUMINANCE 0.586611
#define B_LUMINANCE 0.114478

varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform vec2 vScreenSize;

void main() {

    vec4 color = texture2D(tDiffuse, vUv);

    float x = floor( vUv.x * vScreenSize.x  );
    float y = floor( vUv.y * vScreenSize.y );

    // 4ピクセルごとに使用する閾値の表
    mat4 m = mat4(
            vec4( 0.0,  8.0,    2.0,    10.0),
            vec4( 12.0, 4.0,    14.0,   6.0),
            vec4( 3.0,  11.0,   1.0,    9.0),
            vec4( 15.0, 7.0,    13.0,   5.0)
        );

    float xi = mod( x,4.0) ;
    float yi = mod( y,4.0) ;

    float threshold = 0.0;

    if( xi == 0.0 )
    {
        if( yi == 0.0 ) { threshold = m[0][0]; }
        if( yi == 1.0 ) { threshold = m[0][1]; }
        if( yi == 2.0 ) { threshold = m[0][2]; }
        if( yi == 3.0 ) { threshold = m[0][3]; }
    }
    if( xi == 1.0) {
        if( yi == 0.0 ) { threshold = m[1][0]; }
        if( yi == 1.0 ) { threshold = m[1][1]; }
        if( yi == 2.0 ) { threshold = m[1][2]; }
        if( yi == 3.0 ) { threshold = m[1][3]; }
    }
    if( xi == 2.0) {
        if( yi == 0.0 ) { threshold = m[2][0]; }
        if( yi == 1.0 ) { threshold = m[2][1]; }
        if( yi == 2.0 ) { threshold = m[2][2]; }
        if( yi == 3.0 ) { threshold = m[2][3]; }
    }
    if( xi == 3.0) {
        if( yi == 0.0 ) { threshold = m[3][0]; }
        if( yi == 1.0 ) { threshold = m[3][1]; }
        if( yi == 2.0 ) { threshold = m[3][2]; }
        if( yi == 3.0 ) { threshold = m[3][3]; }
    }

    color = color * 16.0;

    float v = color.x * R_LUMINANCE + color.y * G_LUMINANCE + color.z * B_LUMINANCE;

    if (v < threshold ) {
        color.x = 0.0;
        color.y = 0.0;
        color.z = 0.0;
    } else {
        color.x = 1.0;
        color.y = 1.0;
        color.z = 1.0;
    }
     // 描画
     gl_FragColor = color;

}

モザイク

モザイク 現在のスクリーンを任意のピクセルごとに縦横に分割し、分割した中での中央のピクセルを見てピクセルカラーを設定することでなんちゃってモザイクが出来上がります。(本来は分割したピクセル内の平均値を設定するそうです)。fMosaicScaleはシェーダー外から設定をしているモザイクのピクセル数です。

varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform vec2 vScreenSize;
uniform float fMosaicScale;

void main() {

    vec2 vUv2 = vUv;

    vUv2.x = floor(vUv.x  * vScreenSize.x / fMosaicScale) / (vScreenSize.x / fMosaicScale) + (fMosaicScale/2.0) / vScreenSize.x;
    vUv2.y = floor(vUv.y  * vScreenSize.y / fMosaicScale) / (vScreenSize.y / fMosaicScale) + (fMosaicScale/2.0) / vScreenSize.y;

    vec4 color = texture2D(tDiffuse, vUv2);
    gl_FragColor = color;
}

すりガラス

すりガラス 周辺のピクセルからランダムでピクセルを取得します。このシェーダーを使用するとすりガラス越しに見たような、もしくはクレヨンでスケッチしたような効果を出すことが出来ます。

float rand(vec2 co) {
    float a = fract(dot(co, vec2(2.067390879775102, 12.451168662908249))) - 0.5;
    float s = a * (6.182785114200511 + a * a * (-38.026512460676566 + a * a * 53.392573080032137));
    float t = fract(s * 43758.5453);
    return t;
}

varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform vec2 vScreenSize;
void main() {

    float radius = 5.0;

    float x = (vUv.x * vScreenSize.x) + rand(vUv) * radius * 2.0 - radius;
    float y = (vUv.y * vScreenSize.y) + rand(vec2(vUv.y,vUv.x)) * radius * 2.0 - radius;

    vec4 textureColor = texture2D(tDiffuse, vec2( x, y ) / vScreenSize );
    gl_FragColor = textureColor;
}

うずまき

うずまき ある特定の円内を渦巻状にするシェーダーです。渦の中心位置を基準に少しずつ回転をさせた位置のピクセルカラーを取得し設定します。中心位置から離れるほど回転角度が強くなるようにすると渦が完成します。

uniform sampler2D tDiffuse; varying vec2 vUv;
    uniform vec2 vScreenSize;
    uniform vec2 vCenter;
    uniform float fRadius;
    uniform float fUzuStrength; 
    void main() { 
      vec2 pos = ( vUv * vScreenSize ) - vCenter; 
      float len = length(pos); if( len >= fRadius ) { 
        gl_FragColor = texture2D(tDiffuse,vUv); return; 
      } 
      float uzu = min( max( 1.0 - ( len / fRadius),0.0),1.0) * fUzuStrength; 
      float x = pos.x * cos(uzu) - pos.y * sin(uzu); // 横位置を回転
      float y = pos.x * sin(uzu) + pos.y * cos(uzu); // 縦位置を回転
      vec2 retPos = (vec2( x, y ) + vCenter ) / vScreenSize ; 
      vec4 color = texture2D(tDiffuse, retPos ); 
      gl_FragColor = color; 
    }

終わりに

私はシェーダーに一度挫折してから数年ほどシェーダーを書くことに抵抗がありました。しかし、最近Three.jsに触れたことでちょっとしたシェーダーを作成することは実はそんなに難しくはないことに気が付きびっくりしました。みなさんもより良いコンテンツづくりのためにシェーダーを使ってみませんか? デモのソースコードはGitHubで公開してありますので興味のある方はご覧ください。

参考にしたサイト