LoginSignup
26
17

More than 5 years have passed since last update.

2Dの小技 動くお絵かき

Last updated at Posted at 2016-12-07

webGLだとネタは全く無いが、GLSLだと探せばあります。なので参加させてもらいました。よろしくお願いします。

2Dでお絵かき

2Dでお絵かきするなら直線と円だけでも、それなりのモノが描けます。それにアニメーションがプラスされれば、更に楽しい。3Dとか曲線とかは捨て置き、直線と円を使っての小技を書いてみます。

まずは2Dの距離関数

float circle(vec2 p, float r) // 線円
{
    return abs(length(p) - r);
}

float line(vec2 p, vec2 a, vec2 b) // 直線
{
    vec2 pa = p - a, ba = b - a;
    float t = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0);
    return length( pa - ba*t );
}

この2つは線の太さ情報を入力しません。単純に距離を求める関数です。理由は後でわかると思います。円は線として使うので abs()を使ってます。又これには中心点の位置情報は引数に入っていません。でも直線の方は位置情報が入っています。整合性が欲しいなら


float circle(vec2 p, float r, vec2 a)
{
    return abs(length(p - a) - r);
}

ですかね。この辺りは慣れの問題です。

本題に入る前にGLSLのついて

for文の中では次回に使うパラメーターを変える事ができたり、前回のデータを参照できたりと柔軟に対応できるが、GLSLは並列処理なので、そんな事はできません。その為に無駄な記述が必要になります。無駄な記述というのは

y = function1(x) * 0 + function2(x) * 1 // これは説明用のイメージです

これのfunction1(x) * 0 は無駄である。でも、ある時は0が1になることもあるので毎回わざわざ0を乗算して加算する。効率が悪そうだがGPUが速いので、それをカバーしてくれる。四角四面に仕事をする感じですかね。もしかして「無用の用」なのかも。この辺りの感覚が並列処理独特なんだと思ってます。わかりづらいですけど。なんとなく腑に落ちてくれる事を祈ってます。

円を書いてみます。

では、円を書いてみます。
その為には、引数である座標が必要です。


vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / resolution.y;

これで座標が取得できました。これを距離関数に入れれば距離が取得できます。

線を描くことは背景と違う色にする事。当たり前ですけど、これも大事な要素です。なので背景色を決めます。

vec3 col = vec3(1); // ちなみに vec3()の引数にintも可能です。

背景にグラデーションを入れたかったら


vec3 col = vec3(1) * p.y * p.y;

という感じで座標pを使って色んな事ができます。

線を描くとは「座標と線の距離が例えば0.1以下の時に線の色で背景の色を塗り替える」という処理をする事。
プログラムで書くと


vec3 col = vec3(1);
if (circle(p, 0.8) < 0.1) {col = vec3(0);}

になります。単純な仕組みです。

でも、これではスマートではないのでstep()とmix()を使います。
参考: 条件分岐のためにstep関数を使う時の考え方をまとめてみた - Qiita


vec3 col = vec3(1);
col = mix(vec3(0), col, step(0.1, circle(p, 0.8)));

こう書きます。
このままでは、シャギるのでsmoothstep()を使います。


vec3 col = vec3(1);
col = mix(vec3(0), col, smoothstep(0.1, 0.12, circle(p, 0.8)));

線の太さとかグラデーションはsmoothstepの第1と第2の引数で調整します。これが距離関数に太さ情報を入れなかった理由でもあります。あとは、このmix()とmoothstep()のパターンを羅列するだけです。羅列して書いていくとmix()の引数の第1と第2を入れ替えたくなります。その時は


col = mix(col, vec3(0), 1.0 - smoothstep(0.1, 0.12, circle(p, 0.8)));
col = mix(col, vec3(0), smoothstep(0.12, 0.1, circle(p, 0.8)));

この2種類の方法を、お好みでどうぞ。
2番目の方法はsmoothstepの引数の順序が入れ替わってます。

最後に


gl_FragColor = vec4(col, 1.0);

これが、お絵かきの基本形(たぶん)です。

回転行列を使ってみる。


mat2 rotate(float a)
{
    float s = sin(a), c = cos(a);
    return mat2(c, s, -s, c);
}

これは行列なので単純に座標を掛けるだけです。


p = rotate(radians(90.0)) * p;

こんな感じで使います。
rotate()の引数にtimeを使えばアニメーションできます。
ちなみに掛ける順番を入れ替えて


p = p * rotate(radians(90.0));

とか


p *= rotate(radians(90.0));

にすればrotate()は逆行列扱いになります。
方向はどうでもいい時は2番目を使えば記述は少なくて済みます。
この行列で処理した座標を距離関数に入れれば回転した状態が見れます。

次は補間です。

余談ですがパラメトリック関数は、絶望的に距離関数との相性が悪いです。以前うっかり、こいつに挑み討ち死にしました。そんなパラメトリック関数ですが、補間で使うとゴキゲンになれます。GLSLに用意されている mix() を使ってみます。
とりあえずサンプル。


float t = fract(time);
p -= mix(vec2(-1,0), vec2(1,0), t);
vec3 col = vec3(1);
col = mix(col, vec3(0), 1.0 - smoothstep(0.1, 0.12, circle(p, 0.8)));

これは、円が等速移動していきます。
これでは、つまらないので


p -= mix(vec2(-1,0), vec2(1,0), sin(t * radians(90.0)));

とか


p -= mix(vec2(-1,0), vec2(1,0), pow(sin(t * radians(90.0)), 0.3));

とか


p -= mix(vec2(-1,0), vec2(1,0), 1.0 - pow(cos(t * radians(90.0)), 0.3));

とかしてみてください。いい動きしてますよ。pow()の第2引数を変えると加速度が変わります。他にもありそうだけど持ちネタは全部です。この例は移動ですけど、大きさとか回転とか色とかにも応用できます。

もう一丁いきます。

スケジュール管理みたいなやつ。これは、補間と絡めて使います。

時間の数値をclamp()を使って2秒後に3秒かけて0から1にする。2秒より前は0で、5秒から後は1になってます。それを補間のt値(0.0<=t<=1.0)に代入する。


float t = clamp(time - 2.0, 0.0, 3.0) / 3.0;
p -= mix(vec2(-1,0), vec2(1,0), t);

こうすることで、複数のオブジェクトを時間差でアニメーションできます。

おまけ

ついでにベジェの関数も書いときます。でも距離関数としては使えません。小さく区切って直線を描けばベジェ曲線もどきになります。これもvec2のx成分だけ使えば、ちょっと冗長だけど補間にも使えます。


vec2 bezier(in vec2 a, in vec2 b, in vec2 c, in float t)
{
  return mix(mix(a, b, t), mix(b, c, t), t);
}

最後に

他にも折り畳みとかマスクとかノイズを混ぜたりとか曲線も面倒だけど出来ます。距離関数をmin()を使って一まとめにもできるし、mix()を使ってモーフィングとかも面白いし、mat3とかmat4を使って擬似3Dなんってのもありだし、意外と2Dでも遊べる要素がありますね。shadertoyをみてると、結構2Dのスキルが落ちているんで盗んでみてもいいんじゃないですかね。
今回、記事を書いたおかげで思考が整理されてスッキリしました。こうやって整理してみれば、大した事はやってません。数値を正規化して、それを使って処理を羅列してるだけ。意外とシンプルでした。だからGPUは速いんですかね。

今回書いた小技を使ったサンプルを作っておきました。

//Fragment Shader Source

precision mediump float;

uniform vec2 resolution;
uniform float time;

mat2 rotate(in float a)
{
    float s = sin(a), c = cos(a);
    return mat2(c, s, -s, c);
}

float circle(in vec2 p, in float r)
{
    return abs(length(p) - r);
}

float line(in vec2 p, in vec2 a, in vec2 b)
{
    vec2 pa = p - a;
    vec2 ba = b - a;
    float h = clamp(dot(pa, ba)/dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

float ezing(in float t)
{
    //return t;  
    //return t * t * (3.0 - 2.0 * t);
    //return sin(t * radians(90.0));
    //return 1.0 - pow(cos(t * radians(90.0)), 0.5);
    return pow(sin(t * radians(90.0)), 0.3);
}

float scene(in float t, in float w, in float s)
{
    return clamp(t - w, 0.0, s) / s;  
}

void main() {
    vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / resolution.y;
    p *= 1.5;
    float t = mod(time,6.0);
    float a, b;     
    vec3 line_col =  vec3(0.2, 0.4, 0.1); 
    vec3 col = vec3(0.05, 0.07, 0.15) * p.y * p.y;
    p *= rotate(radians(45.) * ezing(scene(t, 3.0, 1.0)));  
    a =  ezing(scene(t, 0.0, 1.0)); 
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, line(p, vec2(-a, 0), vec2(a, 0))));  
    a =  ezing(scene(t, 2.0, 1.0)); 
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, line(p, vec2(0, -a), vec2(0, a))));  
    a = ezing(scene(t, 1.0, 1.0));
    b = ezing(scene(t, 4.0, 2.0));
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, circle(p - vec2( b, 0), 0.5 * a)));  
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, circle(p - vec2(-b, 0), 0.5 * a)));  
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, circle(p - vec2(0,  b), 0.5 * a)));  
    col = mix(col, line_col, 1.0 - smoothstep(0.05, 0.052, circle(p - vec2(0, -b), 0.5 * a)));    
    gl_FragColor = vec4(col, 1.0);
}
26
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
17