DOT NOTES

Twitter:@dot_not_ GitHub:@ogrew

hsv2rgbを”完全に理解した”話。

最近にしてはTwitterの伸びが良かった投稿。 glslfan,glslsandbox等を見たものにglslスクールで学んだテクを織り込んだだけのものです。

その中でフラグメントシェーダーを扱うとき色の扱いが難しいと感じました。 というのも、一番最初に学んだ色の表現方法が vec3(r,g,b) の形式でこれだと求めていた色合いを表現するのに非常にあくせくします。

※以下のshaderはTouchDesigner上のGLSL TOPを利用しています。

uniform vec2 resolution;
uniform vec3 color;
uniform float time;

out vec4 fragColor;

void main() {
    vec2 st = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);

    float len = length(st);    
    float freq = 20.;
    float speed =2.;    
    float pattern = abs( sin(len * freq - time * speed) );

    float light = .5;
    pattern = light / pattern;
    
    vec3 color = vec3(color.x, color.y, color.z);
        
    vec4 colorOut = vec4(color * pattern, 1.0);
    fragColor = TDOutputSwizzle(colorOut);
}

f:id:taiga006:20191029235047g:plain

※resolution, color, timeはそれぞれGLSL TOPの設定でよしななものを入れています。

上ではred, blueをそれぞれ動かしていますが、色の変化がスムーズには感じられません。 そこでProcessing Community Day Tokyo 2019での一幕を思い出すのです。 そうです、HSV(HSB)を使います。

uniform vec2 resolution;
uniform vec2 params;
uniform float time;

out vec4 fragColor;

vec3 hsv(float h, float s, float v){
    vec4 t = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(vec3(h) + t.xyz) * 6.0 - vec3(t.w));
    return v * mix(vec3(t.x), clamp(p - vec3(t.x), 0.0, 1.0), s);
}

void main() {
    vec2 st = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);

    float len = length(st);    
    float freq = 20.;
    float speed =2.;    
    float pattern = abs( sin(len * freq - time * speed) );

    float light = .5;
    pattern = light / pattern;
    
    float h = abs(sin(time)); 
    float s = params.x; // saturation
    float v = params.y; // value
    vec3 color = hsv(h, s, v);        
        
    vec4 colorOut = vec4(color * pattern, 1.0);
    fragColor = TDOutputSwizzle(colorOut);
}

f:id:taiga006:20191029235155g:plain

※resolution, params, timeはそれぞれGLSL TOPの設定でよしななものを入れています。
※params.xy はそれぞれsaturation, value(brightness)に対応しています。

ここで注目すべきはhsv関数の部分です。

vec3 hsv(float h, float s, float v){
    vec4 t = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(vec3(h) + t.xyz) * 6.0 - vec3(t.w));
    return v * mix(vec3(t.x), clamp(p - vec3(t.x), 0.0, 1.0), s);
}

もちろん、この関数は自分で思いついたものではありません。(自力でこれは無理やー!)

以下の記事などに乗っている実装を参考に(というかコピペ)しています。

www.laurivan.com nogson2.hatenablog.com qiita.com

ではこの実質3行の関数の中では何が行われているのでしょうか?

マジックナンバーが多すぎて何がなんだかです。

検索するとwikipediaなどに詳しく書いてありました。

とりあえず読んでみます。

ja.wikipedia.org

hsvはよく円錐、円柱のモデルでわかりやすく説明されます。(それは知ってた。)

f:id:taiga006:20191029221239j:plainWikiより)

見て分かる通り、3D空間上の単位円の円周上をH(Hue)、円の半径をS(Saturation)、そして円柱の高さ(深さ?)をV(Value)としてそれぞれ見立てることができます。 なるほど?

ここでS=0であればr=g=b=Vとなることがわかります。(つまりグレースケールです。)

その後、急に場合分けの式が出てきてわけがわからないので他のサイトを見ていきます。

これらのサイトを読みました。

www13.plala.or.jp ofo.jp www.peko-step.com

だんだんわかってきました。 つまるところ、こんな感じです。

f:id:taiga006:20191029224802p:plainRGB←→HSB相互変換【Windowsプログラミング研究所】さんより)

上の記事にもある通り、注目すべきはそのHSVの最大・最小値です。 その最大・最小値はHueの値によって計算方法が変わります。それは上の円を見てもらえればわかると思います。円をぐるっと回っていく中で増減しているRGBの対象がかわっていくのがわかります。それが具体的には60度ずつで変わっていきます。これに対応する形で最大・最小値も変わっていくことになります。このパターン分けがあるせいで計算が難しくなっています。

さてこのへんで座学は飽きてきました。というか7割位理解できた気がします。この「60度ごと」鍵となっていて最初の関数の t.xyz が出ていることがわかってきたのであとは愚直なコードを書いてみて比較することにします。

できました。

vec3 hsv(float h, float s, float v){
    float r = v, g = v, b = v;

    if (s == 0) {
        return vec3(r,g,b);
    }

    h *= 6.; // 0.0 ~ 1.0スケールのものを360を6分割した60度ごとに場合分けする

    float i = floor(h); // 60度刻みのどこに属するか
    float f = h - i; // その範囲での割合

    if (i == 0) { // 0 ~ 60度
     // r = v; (MAX)
        g *= 1 - s * (1 - f);
        b *= 1 - s;
    }
    else if (i == 1) { // 60 ~ 120度
        r *= 1 - s * f;
     // g = v; (MAX)
        b *= 1 - s;
    }
    else if (i == 2) { // 120 ~ 180度
        r *= 1 - s;
     // g = v; (MAX)
        b *= 1 - s * (1 - f);
    }
    else if (i == 3) { // 180 ~ 240度
        r *= 1 - s;
        g *= 1 - s * f;
     // b = v; (MAX)
    }
    else if (i == 4) { // 210 ~ 300度
        r *= 1 - s * (1- f);
        g *= 1 - s;
     // b = v; (MAX)
    }
    else if (i == 5) { // 300 ~ 360度
     // r = v; (MAX)
        g *= 1 - s;
        b *= 1 - s * f;
    }
    return vec3(r,g,b);
}

これを使った結果と先程の便利関数を使った結果を比較してみます。

f:id:taiga006:20191029235918g:plain

(左:便利関数、右:愚直関数)

同じ結果と言って差し支えないと思います。

完全に理解したわけではないですが「完全に理解した」レベルにはなったので今回はこれで良しとします。