見出し画像

正弦波を重ねるだけでなんかいい感じのアニメーションをつくりたい

まずは掲題の通り「なんかいい感じのアニメーション」を貼っときます。これのコードをさらすのが主目的のnoteです。

古き良き時代(?)のスクリーンセーバーのようなこちらは、位相と振幅の異なる正弦波(sin)を重ね合わせただけで生成しています。以下はこのコードの公開&解説です。Shadertoyにコードが置いてあるので、色をいじって遊びたい方はどうぞ。

遊び場 → https://www.shadertoy.com/view/3dB3DR

目次

### コード
+ コード全文
### 解説
+ main関数
    - 座標系について
    - plot関数
+ 背景色
+ 正弦波の重ね合わせ
    - 正弦波をfor文で重ねる
    - 位置をずらしたい
    - 位相をずらしたい
    - 振幅をずらしたい
    - アニメーションさせたい


コード全文

Processingとかではなく、素のOpenGLをVedaで動かしています。

precision mediump float;
uniform vec2 resolution;
uniform float time;

#define M_PI 3.1415926535897932384626433832795

/* plot関数 */
float plot(vec2 st, float pct){
  return  smoothstep( pct-0.01, pct, st.y) -
          smoothstep( pct, pct+0.01, st.y);
}


/* 乱数 その1 */
float n11(vec2 uv) {
  return fract(uv.x*5764.287135 + uv.y*6453.54181) ;
}
/* 乱数 その2 */
float n12(float n) {
  return fract(n*5764.287135 + n*6453.54181) ;
}

/* 正弦波を重ね合わせる */
vec3 multisin(vec2 uv, float n ) {
  //float i = 1.;
  vec3 col = vec3(0.) ;
  float t = time ;
  float alpha = 7494.680418;
  float y = 0.;
  const float s = 1.0/10.;
  float fadeio = abs(cos(t*0.20)*cos(t*0.20)*cos(t*0.20)) ;
  vec3 basecol = vec3(0.55,0,0.6) ;

  for(float i=0.; i < 1. ; i+=s) {
    y = 0.33*exp(i*1.5*n12(i))
        *sin( uv.x/n*10. + t*0.3*i*0.5 + n12(n)
         - t*0.2*fract(i*40393.02745) + M_PI*float(int(i/s)/4) )  ;

    col += plot(vec2(uv.x, uv.y+i*0.1), y) 
          *(basecol + vec3(0,i/5.*n*.5 ,n*.075) ) 
          *fadeio ;
  }
  return col;

}

/* 背景色を塗る関数 */
vec3 bgcolor(vec2 uv) {
  float m1 = 0.;
  m1 = clamp(.0, 0.5, uv.y*uv.y );
  return vec3(0.,0., m1);
}

void main () {
  vec2 uv = gl_FragCoord.xy / resolution.xy ;
  uv -= .5;
  float asp = resolution.x/resolution.y;
  uv.x *= asp;

  vec3 col =  vec3(0.);
  col += bgcolor(uv);

  col += multisin(uv, 8. );

  gl_FragColor = vec4(col, 1.) ;
}

解説

main関数

1.  座標系を変換する
2. 背景色を塗る
3. 線を書く関数を呼び出す
4. ピクセルに色を設定する
void main () {
/* 1.座標系を変換する */
  vec2 uv = gl_FragCoord.xy / resolution.xy ;
  uv -= .5;
  float asp = resolution.x/resolution.y;
  uv.x *= asp;

/* 2.背景色を塗る */
  vec3 col =  vec3(0.);
  col += bgcolor(uv);

/* 3.線を書く関数を呼び出す */
  col += multisin(uv, 8. );

/* 4.ピクセルに色を設定する*/
  gl_FragColor = vec4(col, 1.) ;
}

座標系について

GLSLの座標は通常左下原点になりますが、絵を描きやすくするため、キャンバス中心を原点にしています。また、キャンバスサイズ(解像度)に依存しないよう、座標を解像度で正規化します。

毎回書くのは面倒なので、スニペットにしておきたいです。

/* 
    座標変換のスニペット 
    x = [-0.5, 0.5]*アスペクト比
    y = [-0.5, 0.5] 
*/
  vec2 uv = gl_FragCoord.xy / resolution.xy ; // 座標を解像度で正規化
  uv -= .5; // 原点を移動
  float asp = resolution.x/resolution.y; // アスペクト比を計算
  uv.x *= asp; //x方向にアスペクト比を掛ける

このへんはDrive Home チュートリアルで学びました。(英語は何言ってるか分かんないけど、ひたすら画面を見ながらリアルタイム写経。)

背景色

青〜藍色のグラデーションを背景に設定します。何色でもいいんだけど、下地があったほうがかっこいいという信念に依る。

/* 背景色を塗る関数 */
vec3 bgcolor(vec2 uv) {
  float m1 = 0.;
  m1 = clamp(.0, 0.5, uv.y*uv.y );
  return vec3(0.,0., m1);
}

clamp関数の使い方がおかしい事にnoteを描きながら気が付いた…。 この関数でやりたい事は「青色を0〜0.5の範囲に収める」なのですが、clamp関数の引数は (x, min,max) の順に指定するため、本来は clamp(uv.y*uv.y, .0, 0.5) と書くのが正しいです。

いずれにせよ、上のコードを実行すると下図のようになります。(モニタによってはほぼ真っ黒に見えるかも…)


正弦波の重ね合わせ

### やりたいこと
1. 正弦波をfor文で重ねる
2. 位置をずらして重ねる
3. 振幅をずらして重ねる
4. 位相をずらして重ねる
5. アニメーションさせたい

コードは以下になります。

/* 乱数 その1 */
float n11(vec2 uv) {
  return fract(uv.x*5764.287135 + uv.y*6453.54181) ;
}
/* 乱数 その2 */
float n12(float n) {
  return fract(n*5764.287135 + n*6453.54181) ;
}

/* 正弦波を重ね合わせる */
vec3 multisin(vec2 uv, float n ) {
  //float i = 1.;
  vec3 col = vec3(0.) ;
  float t = time ;
  float alpha = 7494.680418;
  float y = 0.;
  const float s = 1.0/10.;
  float fadeio = abs(cos(t*0.20)*cos(t*0.20)*cos(t*0.20)) ;
  vec3 basecol = vec3(0.55,0,0.6) ;

  for(float i=0.; i < 1. ; i+=s) {

  /* 描画する曲線の関数を定義 */
    y = 0.33*exp(i*1.5*n12(i))
        *sin( uv.x/n*10. + t*0.3*i*0.5 + n12(n)
         - t*0.2*fract(i*40393.02745) + M_PI*float(int(i/s)/4) )  ;

  /* 重ね合わせ */
    col += plot(vec2(uv.x, uv.y+i*0.1), y) 
          *(basecol + vec3(0,i/5.*n*.5 ,n*.075) ) 
          *fadeio ;
  }
  return col;

}

0. plot関数

毎回座標を計算しながらstep関数を書くのはしんどいので、任意の曲線を引くための関数を定義しておきます。これもスニペットにしておきたいやつ。

// stは座標、pctは描きたい曲線。y=f(x)の形で書いておき、 plot(st, y) とする。
float plot(vec2 st, float pct){
  return  smoothstep( pct-0.01, pct, st.y) -
          smoothstep( pct, pct+0.01, st.y);
}

1. 正弦波をfor文で重ねる

for文はC言語と同じ書き方。上記のコードでは、ループの変数 i をインクリメントするときに、定数(const)を定義しています。ループ内でも再利用しやすくするため。これも前掲のDrive Home チュートリアルを参考にしています。

y= hogehoge の箇所で曲線の関数を記述し、次の col += hogehoge の行で"重ね合わせ"をしています。

  /* 描画する曲線の関数を定義 */
    y = f(x); // 解説は後回し)

  /* 重ね合わせ */
    col += plot(vec2(uv.x, uv.y+i*0.1), y) 
          *(basecol + vec3(0,i/5.*n*.5 ,n*.075) ) 
          *fadeio ;

2. 位置をずらして重ねる

重ね合わせの部分(col += hogehoge)は、1行目のplot関数で描画する曲線の位置と種類を指定しています。第1引数に座標のuvをそのまま渡さず、ループ回数に合わせてy座標をずらしています。重ね合わせ方にランダム性を持たせるための工夫、その1。

2行目 (basecol + vec3(0,i/5.*n*.5 ,n*.075) ) は色を設定する行。basecolで大雑把な色合いを決めた上で、ループ回数ごとに色味を適当に足し、線ごとの色を変えています。

3. 振幅をずらして重ねる

    y = 0.33*exp(i*1.5*n12(i))
        *sin( uv.x/n*10. + t*0.3*i*0.5 + n12(n)
         - t*0.2*fract(i*40393.02745) + M_PI*float(int(i/s)/4) )  ;

上記コードの1行目。sin関数をループごとに適当倍して線をずらしています。expを使うのは"なんとなく"。単調増加なら何でも扱いやすいとは思います。なお、途中で出現する n12 は 0〜1の間の数値を何か返す関数。randomだと思ってください。

4. 位相をずらして重ねる

ループごとにずらし方を固定しておいた方が綺麗な絵になりそうだったので、ループ1回ごとに 45度ずつ固定でずらす意図で、一番最後の項 M_PI*float(int(i/s)/4) ) を導入しています。
 {α_n: n=1,2,3,..}={ 0, π/4, π/2, π, ... }

5. アニメーションさせたい

sin関数の内側にユニフォーム変数 time を含めると、良い具合に波がうねうねします(雑)。適当な倍率をかけるとアニメーション速度を調整できるので、描画された絵を見ながら微調整。0.1倍のオーダーだと体感的にちょうどいい速度になる印象。

ちなみに、消えたり現れたりするのは y= sin(hogehoge) ではなく、重ね合わせの際にまとめて対応しています。

float fadeio = abs(cos(t*0.20)*cos(t*0.20)*cos(t*0.20)) ;

/* 重ね合わせ */
    col += plot(vec2(uv.x, uv.y+i*0.1), y) 
          *(basecol + vec3(0,i/5.*n*.5 ,n*.075) ) 
          *fadeio ;

y=abs(cos(x)^3) の関数は下図のような出力になります。cosの3乗は[-1,1]の範囲になるため、常にプラスに振るよう絶対値を取っています。

最終的にうまく行くと、以下のようなアニメーションになります。興味があれば適当に色味や周期を変えて遊んでみてください。

遊びたい方はこちら↓↓


面白かった、ためになったと感じた方にサポートいただけると励みになります。いつか展示会やイベントをやるための積立金として、あるいは飢死直前金欠時のライフラインとして使わせていただきます。