glslシェーダで曲線を描画してみる

概要

OpenGLを調べたりしていてシェーダプログラミングも面白そうだったため下のページなどで少し勉強していました。

The Book of Shaders

シェーダプログラミング勉強中でまだあまり難しいところはわかってません。

glslのフラグメントシェーダで三次関数の曲線を描画してみるという簡単なところを試してみました。

フラグメントシェーダ

バーテックスシェーダとフラグメントシェーダというのが基本的なシェーダでwebglでも使用できます。

使ったことはありませんがこの他にジオメトリシェーダなどもあるようです。

フラグメントシェーダはピクセルシェーダとも呼ばれていてピクセル単位の描画を行います。

フラグメントシェーダではバーテックスシェーダから渡されたパラメーターやユニフォームパラメーターと スクリーン上の座標を入力として受け取り描画する色を決定して出力するような流れのようです。

描画方法

The Book of Shaders: Shaping functions

基本的にこのページと同じような方法でやってみました。

曲線の式を決めてそれぞれのピクセルが曲線の上にあるか下にあるかで色を変えると描画できそうです。

{y = a x^{3} + b x^{2} + c x + d}

三次関数では係数が4つあるのでこれを決める必要があります。

ランダムな三次関数を生成してスムーズに接続させるために 境界での{y}座標と傾き{\frac{\mathrm{d}y}{\mathrm{d}x}}をランダムに決めると条件式が四つできて ちょうど三次関数の係数が求まります。

Maximaを使うとこういった連立方程式も解けるためシェーダのプログラミングにも使えるかもしれません。 Ubuntu環境ではaptでインストールして簡単に使うことができました。

Maxima/具体的な使い方 - Wikibooks

下のような感じで解いてくれます。

y(x) := a * x^3 + b * x^2 + c * x + d;
g(x) := diff(y(x), x);
solve([
y0 = subst(0, x, y(x)),
y1 = subst(1, x, y(x)),
g0 = subst(0, x, g(x)),
g1 = subst(1, x, g(x))
],[a, b, c, d]);

Maximaの結果

(%i5) solve([y0 = subst(0,x,y(x)),y1 = subst(1,x,y(x)),g0 = subst(0,x,g(x)),
             g1 = subst(1,x,g(x))],[a,b,c,d])
(%o5) [[a = (-2*y1)+2*y0+g1+g0,b = 3*y1-3*y0-g1-2*g0,c = g0,d = y0]]

シェーダープログラムは下のようになりました。

float random(vec2 st) {
    return 2.0 * (fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123) - 0.5);
}

float cubic(float x, float e)
{
    x = x / e;
    float i = floor(x);
    float f = fract(x);
    float a = 10.0;
    float p = random(vec2(i)) * a;
    float q = random(vec2(i + 1.0)) * a;
    float y = (((p + q) * f - (2.0 * p + q)) * f + p) * f;
    return y;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    float unit = iResolution.x / 10.0;
    vec2 o = iResolution.xy * 0.5;
    float height = unit * 0.5;
    float y = cubic(fragCoord.x, unit) * height + o.y;
    vec4 c = (fragCoord.y <= y) ? vec4(0.0, 0.0, 0.0, 1.0) : vec4(1.0, 1.0, 1.0, 1.0);
    fragColor = c;
}

出力はこのような感じでした。

f:id:tkaaad97:20180225170654p:plain

円周上に描画してみる

上と同じことを円周上で描画してみました。インクが飛び散ったような見た目になりました。

f:id:tkaaad97:20180225170659p:plain

const float pi = 3.1415926535897932384626433832795;
const float epsilon = 1.0E-3;

float random(vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

float atan2(in float y, in float x)
{
    return abs(x) < epsilon ? sign(y) * pi * 0.5 : atan(y, x);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    float radius = iResolution.y * 0.5;
    float frequency = 20.0;
    float waveHeight = radius * 0.1;
    vec2 v = fragCoord.xy - (0.5 * iResolution.xy);
    float a = atan2(v.y, v.x);
    a = (a < 0.0) ? a + pi * 2.0 : a;
    a = a * frequency;
    float b = 0.5 * a / pi;
    float index = floor(b);
    float stepHeight = radius * 0.2;
    float startStep = random(vec2(index));
    float endStep = random(vec2((index + 1.0 >= frequency - epsilon) ? 0.0 : index + 1.0));
    float r = radius + waveHeight * sin(a) + stepHeight * mix(startStep, endStep, smoothstep(0.0, 1.0, fract(b)));
    float inside = (length(v) <= r) ? 1.0 : 0.0;
    fragColor = mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 1.0), inside);
}

まとめ

ランダムな三次関数の曲線を描画してみました。 例えばインクのデカールのような効果にも使えるかもしれません。 同じような方法でランダムな曲面を生成してスムーズにつなげて描画してみようと考えてたんですが なかなか難しくて時間がかかりそうだったためできたら別記事にしたいと思います。