飴屋

WebGL/シェーダーマテリアルその2

時間経過とともにマテリアルが変化

前回はシェーダーマテリアルの基礎的な話に終始してしまったので、もうちょっと面白いことをやりましょう。世の中のWebGL作例を眺めているとボーっといつまでも見てられるような、複雑な変化を繰り広げ続けるものがありますが、あれはどういう仕組みなんでしょう。あれをやりましょう。複雑な変化は乱数を使った表現なんだろうなと察しはつきますが、それが連続したアニメーションになる仕組みが気になりますね。

乱数、つまりノイズを使っていい感じの連続する数値を取得する方法でよく名前を聞くのはパーリンノイズと呼ばれているものでしょうか。少ないリソースでイイ感じの粒度を持った疑似乱数としていろんなところで利用されています。XYZ座標の空間位置と時間位置を指定すれば一意の数値も取得できて、とても気軽に使えます。このパーリンノイズの改良版・シンプレックスノイズというのを使ってみようと思います。

まずシェーダー・・・フラグメントシェーダー内でノイズを発生させるために、ノイズの種(シード値)になる「時間」を渡してあげる必要があります。ページを開いたときの時刻(startTime)と描画時の時刻の差をとって、経過時間(elapsed)をuniformsとしてシェーダーに渡します。uniformsは前回出てきたけど使わなかったやつです。シェーダーが参照できる変数の一つです。変数の型は「f」とあるからfloat値でしょうね。この値を描画の度に経過時間で更新します。

uniformsにはもう一つ、カンバスの解像度の譲歩を渡してあげておきます。変数の型は「v2」で、二値のベクトルです。GLSL的にはvec2です。ここにカンバスの幅と高さの情報を入れておきます。

const startTime = Date.now();
const uniforms = {
  time: { type: "f", value: 1.0 },
  resolution: { type: "v2", value: new THREE.Vector2() }
};
uniforms.resolution.value.x = width;
uniforms.resolution.value.y = height;

function draw() {
  let elapsed = (Date.now() - startTime) / 1000;
  uniforms.time.value = elapsed;
  renderer.render(scene, camera);
  requestAnimationFrame(draw);
}

続いて、フラグメントシェーダーを編集します。前回とにかく赤いピクセルを返しただけのシェーダーを面白くしていく作業ですね。まず、uniform値のtimeとresolutionを使うよって先頭で宣言しておきます。

uniform float time;
uniform vec2 resolution;

下にシンプレックスノイズを作る関数を書きます。ちなみにこのノイズの仕組みを私はちゃんと把握しているわけではありません。いくつかソースを参考に見ながら、基本コピペです。何がどうなったらこんな数式を思いつくんでしょう。snoise関数はvec2つまり二値のベクトル値を引数をとって一つ乱数を返すってことでいいのかな。例えば、カンバスのXY座標をsnoise関数に渡すと・・・何が返ってくるんだろう。まぁ、使い勝手を考えたら0.0-1.0の範囲の乱数が返ってくるんだろうな。引数にtime値を混ぜ込むと時間変化を表現できそうな気がしてきます。

vec3 mod289(vec3 x){
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x){
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x){
  return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v){
  const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
  vec2 i = floor(v + dot(v, C.yy));
  vec2 x0 = v - i + dot(i, C.xx);
  vec2 i1;
  i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
  vec4 x12 = x0.xyxy + C.xxzz;
  x12.xy -= i1;
  i = mod289(i);
  vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 ));
  vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
  m = m*m ;
  m = m*m ;
  vec3 x = 2.0 * fract(p * C.www) - 1.0;
  vec3 h = abs(x) - 0.5;
  vec3 ox = floor(x + 0.5);
  vec3 a0 = x - ox;
  m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
  vec3 g;
  g.x  = a0.x  * x0.x  + h.x  * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  return 130.0 * dot(m, g);
}

GLSLをざっくりとしかわかってないですが、どんな関数があるのか少しずつ覚えましょう。関数名でなんとなくわかりますけどね。

abs(x)
xの絶対値を返す
max(x,y)
引数の最大値を返す
floor(x)
x以下の最大の整数を返す(xが正の数なら整数部分を返すと覚えてもいい)
fract(x)
x-floor(x)を返す(xが正の数なら小数部分を返すと覚えてもいい)
dot(x,y)
ベクトルx,yの内積をfloatで返す

ノイズを含んだピクセルの色を出力するフラグメントシェーダーの本体(main)を作ってみました。colorに背景色の暗い青を設定し、ノイズ値に応じてオレンジ色っぽいがつくような調整になっています。

void main(){
  vec3 color = vec3(17.0 / 255.0, 34.0 / 255.0 , 52.0 / 255.0);
  vec3 subtract_color = color;
  vec2 vel = vec2(time, time);
  vec2 st = gl_FragCoord.xy/resolution.xy;
  vec2 pos = vec2(st*1.0);
  float val = snoise(pos-vel)*.25+.25;
  color += vec3(smoothstep(.3, .55, val)) * (vec3(209.0 / 255.0, 109.0 / 255.0, 86.0 / 255.0) - subtract_color);
  gl_FragColor = vec4(color, 1.0);
}

vel変数のあたりが時刻を使った乱数の種になっています時間の経過に伴ってどんどん大きくなり、カンバスのXY座標の増加方向に模様が移動していくのが確認できます。vel値を半分にすると移動スピードも半減するのでスピード調節に使えそうです。
smoothstep関数が雲のようなノイズ模様の輪郭のぼんやり感を決定してくれます。定義域が引数1と引数2の間のときに値域が0.0から1.0にスムーズに変わるような関数を作って、引数3をその関数に渡した返り値を取得できる・・・という関数ですが、口で便利さを説明するのが難しいですね。

uniformに渡していたresolution値でgl_FragCoordを割って、描画中のピクセルの位置を0.0-1.0の範囲で画面中のどの辺にあるか計算しています。(st)これは時間値とともに今後も頻出するでしょうね。

ノイズをsmoothstep関数で成形したり、複数のノイズを足したり引いたりして望む出力を探っていく作業になるんでしょうね。