JavaScript
WebGL
GLSL
フロントエンド
早稲田大学

GLSLという名の筆を使い、油絵を描く

はじめに

早稲田大学アドベントカレンダー2017、3日目の投稿です。
今年も母校のカレンダーが立っていたので、投稿します。

昨年、早稲田大学を卒業しまして、Web系の制作会社でフロントエンドエンジニアとして働いています。
制作会社に在籍しているからには「一通りの表現は極めたいな」と思うところがありまして、アニメーション系のスキルを身につけている最中です。
(とはいっても、最近は表現分野よりも設計分野に興味のベクトルが向いているのですが…)

昨年の振り返り

もう既に懐かしい記憶ですが、昨年も湯を沸かすほどの熱いGLSL - Qiitaという記事を書きました。

しかし、社会人エンジニアとなった今、業務ではこのような表現の使いどころがありません。
したがって、案件に適用しやすく、グラフィック系出身のデザイナーでも取り入れやすい表現を模索していく必要がありました。

そこで、「テクスチャをメインにした2D表現を実装を行い、それを同僚に共有する」といった、休日の趣味活動を時折りしています。

今回は、そんな趣味活動の一部をご紹介します。

油絵表現

12月-03-2017 12-00-36.gif

DEMO

※ もし、テクスチャが表示されない場合は、一度リロードしてみてください。
※ ホバーインタラクションが付いています。
※ 写真は、skyseeker様からお借りしました。

コード

メインのFragment Shaderのみを部分的に抜粋しつつ、解説します。

1. UV座標の取得とテクスチャロード

varying vec2 vUv;
uniform sampler2D colorMap;
uniform sampler2D noiseMap;

void main(){
    vec2 uv = vUv;
    ...
}

まずは通例通り、UV座標をVertexShaderから受け渡します。
さらに、JavaScript側からは、以下の2枚の画像を受け渡します。
「なぜ2枚も使うのか、それもモノクロノイズなのか」という点については、後ほど言及します。

オリジナルのテクスチャ

ノイズマップ

2. ノイズマップから、色情報を取得する

void main(){
    ...
    float gradientStep = GRADIENT_PARAMERTER;

    vec4 cxp = texture2D(noiseMap, vec2(uv.x + gradientStep, uv.y));
    vec4 cxn = texture2D(noiseMap, vec2(uv.x - gradientStep, uv.y));
    vec4 cyp = texture2D(noiseMap, vec2(uv.x, uv.y + gradientStep));
    vec4 cyn = texture2D(noiseMap, vec2(uv.x, uv.y - gradientStep));
    ...
}

texture2Dメソッドで、ノイズマップから色情報を取得します。
取得先の座標は4種類です。
1つ目と2つ目は、(UV座標のX軸値にgradientStepを加算/減算した値, UV座標のY軸値)
3つ目は4つ目は、(UV座標のX軸値, UV座標のY軸値にgradientStepを加算/減算した値)
それぞれ、ポジネガとして変数に格納しています。

3. 取得した色情報を1次元の値に変換する

void main(){
    ...
    vec3 origin = vec3(0.0);
    float dxp = distance(origin, cxp.xyz);
    float dxn = distance(origin, cxn.xyz);
    float dyp = distance(origin, cyp.xyz);
    float dyn = distance(origin, cyn.xyz);
    ...
}

「取得した3次元のRGBベクトル」と「原点」の距離計算を行い、1次元の値に変換します。(言ってしまえば、ベクトルの大きさになります)
ちなみに、ノイズマップがグレースケールのため、「取得した3次元のRGBベクトル」の次元ごとの値は、(0, 0, 0)や(125, 125, 125)のように等しくなっています。この規則性は、勾配計算のシンプルさに影響します。

4. 勾配を求めて、目的のUV座標を作成する

void main(){
    ...
    float advectStep = ADVECT_PARAMETER;

    vec2 grad = vec2(dxp - dxn, dyp - dyn);
    vec2 newuv = uv + (advectStep * normalize(grad));
    ...
}

先ほど1次元に変換した値をポジネガで減算して、2次元の勾配ベクトルを求めます。
その後、勾配ベクトルの単位ベクトルを求め、その値を元のUV座標に加算し、「ずれ」を引き起こさせます。
しかし、勾配ベクトルの単位ベクトルの値は、UV座標のレンジに対して大きすぎます。(実際に、適当な値で計算してみれば明白です)
したがって、advectStepで移流値を縮小させます。

5. オリジナルのテクスチャから色情報を取得する

void main(){
    ...
    vec3 baseColor = texture2D(colorMap, newuv).rgb;
    gl_FragColor = vec4(baseColor, 1.0);
    ...
}

最後に、先ほど作成した新しいUV座標を用いて、オリジナルのテクスチャから色情報を取得します。
その色情報をgl_FragColorに代入してしまえば、完成です。

さらに今回のDEMOでは、JavaScript側から様々なUniform属性を送信して、マウスインタラクションの実装を可能にしています。(が、割愛します)

まとめ

以上のような「20行程度のFragmentShader」と「テクスチャ」を組み合わせることで、CSSでは実現が厳しいアニメーションを簡単に実装することができます。
しかし、実装には多少の数学知識が必要になるので(といっても高校数学程度ですが)、ブラッシュアップの際にはデザイナーとのコミュニケーションが課題になる気がしています。そこを乗り越えれば、さらに良い表現が生み出せるのでしょう。