WebGL & Three.js

【GLSL】glslifyのノイズ(noise)の使い方とデモ

GLSLでノイズ(noise)を使う方法を、glslifyとglsl-noiseのシンプレックスノイズで解説します。2D・3Dノイズの読み込み、スケール・移動・波・時間変化の書き方、画像を水面のように揺らすShaderMaterialの実例をデモ付きで紹介します。

この記事の目次
  1. 【GLSL】glslifyのノイズ(noise)のデモ
  2. 【GLSL】glslifyのノイズ(noise)の使い方
  3. glslifyをインストール
  4. 2Dと3Dを読み込む
  5. ノイズを生成する
  6. フラグメントシェーダのコード
  7. 通常の2Dノイズを生成
  8. スケールを大きくして細かいノイズを生成
  9. 時間経過と共に細かくなるノイズを生成
  10. 時間経過と共に移動するノイズを生成
  11. X軸方向に移動するノイズを生成
  12. 波のように揺れるノイズを生成
  13. 3Dノイズを使って時間経過と共に模様が変化するエフェクトを生成
  14. 横シマ模様の3Dノイズを生成
  15. 水に浮かんでいるような揺れる画像
  16. ノイズが表示されないときの確認

WebGL(GLSL)でよく使うglslifyのノイズ(noise)の使い方を紹介します。
デモもあるので確認してみてください。

結論から言うと、GLSLでノイズを使う一番手軽な方法は、glsl-noiseパッケージのシンプレックスノイズをglslifyの#pragma glslifyで読み込み、noise2(vUv * スケール)のようにUV座標へ掛け算した値を渡すことです。戻り値はおよそ-1〜1の浮動小数なので、色に使うときはn * 0.5 + 0.5で0〜1へ寄せるか、この記事のように負の値は0として扱います。時間で変化させたいときは3Dノイズの第3引数に経過時間を渡します。

【GLSL】glslifyのノイズ(noise)のデモ







スポンサーリンク

【GLSL】glslifyのノイズ(noise)の使い方

glslifyをインストール

まずはglslifyと、ノイズ関数本体のglsl-noiseをインストールします。
glslifyのGithub

npm install -D glslify
npm install glsl-noise

そしてpackage.jsonに以下のように入っていればOKです。

  "dependencies": {
    ...
    "glslify": "^7.1.1",
    ...
    "three": "^0.152.0"
  },

バージョンは執筆時点の例です。#pragma glslifyはGLSLの標準構文ではなく、ビルド時にglslifyが展開する命令なので、バンドラー側にglslify用のプラグイン(Viteならvite-plugin-glslifyなど)を入れてシェーダーファイルを通す必要があります。プラグインなしで#pragmaが残ったままだとコンパイルエラーになります。

2Dと3Dを読み込む

// 2Dと3Dのシンプレックスノイズ関数をインポート
#pragma glslify: noise2 = require(glsl-noise/simplex/2d);
#pragma glslify: noise3 = require(glsl-noise/simplex/3d);

やりたいことに合わせて、2Dもしくは3Dを上のように読み込みます。
X軸とY軸のみを扱う場合は、2dの方を使います。
X軸とY軸に加えて、時間などの経過で模様を変化させたいときは3dを使います。
vUvのような頂点シェーダーから受け取る値と、uTickのようにJavaScriptから渡す値の違いはGLSLのvaryingとuniformの挙動と使い方で解説しています。

ノイズを生成する

あとは、以下のようにするだけでノイズを生成できます。

  float n = noise2(vUv);

基本のコードについて知りたい場合は、【WebGL&Three.js入門】Three.jsの一番シンプルなサンプルコードのチュートリアルの解説にて詳しく解説しているので参考にしてみてください。
今回はglslifyのノイズ(noise)に関係しているところを解説します。

フラグメントシェーダのコード

ノイズ関数を使用して動的なビジュアルエフェクトを生成するフラグメントシェーダーのコードです。

// 浮動小数点数の精度を中程度に設定
precision mediump float;

// 2Dと3Dのシンプレックスノイズ関数をインポート
#pragma glslify: noise2 = require(glsl-noise/simplex/2d);
#pragma glslify: noise3 = require(glsl-noise/simplex/3d);

// テクスチャ座標をバーテックスシェーダーから受け取る
varying vec2 vUv;

// ユニフォーム変数として時間を受け取る
uniform float uTick;

void main() {
  // ノイズのスケールを定義
  const float SIZE = 10.0;

  // ノイズの動きのスピードを定義
  const float SPEED = 0.02;

  // 通常の2Dノイズを生成
  float n = noise2(vUv);

  // スケールを大きくして細かいノイズを生成
  n = noise2(vUv * SIZE);

  // 時間経過と共に細かくなるノイズを生成
  n = noise2(vUv * uTick * SPEED);

  // 時間経過と共に移動するノイズを生成
  n = noise2(vUv * SIZE - uTick * SPEED);

  // X軸方向に移動するノイズを生成
  n = noise2(vec2(vUv.x * SIZE + uTick * SPEED, vUv.y));

  // 波のように揺れるノイズを生成
  n = noise2(vec2(vUv.x * 80.0 - sin(vUv.y + uTick / 80.0), vUv.y * SIZE - sin(vUv.x + uTick / 80.0)));

  // 3Dノイズを使って時間経過と共に模様が変化するエフェクトを生成
  n = noise3(vec3(vUv * SIZE, uTick * SPEED));

  // 横縞模様の3Dノイズを生成
  n = noise3(vec3(vUv.x * SIZE * 2.0, vUv.y * SIZE * 7.0, uTick * SPEED));

  // 最終的なフラグメントの色を設定(青緑のグラデーション)
  gl_FragColor = vec4(0.0, n, n, 1.);
}

vUv変数はテクスチャ座標を表し、uTick変数は時間を表しています。
ノイズ関数の出力値nは、最終的に色の青緑成分に使用され、動的なビジュアルパターンを生成しています。
上の場合、ノイズ関数の出力値nをgl_FragColorの緑と青の部分にだけ入れて、青緑色になるようにしています。
なお、上のコードはnを何度も上書きしているので、実際に描画されるのは最後の「横縞模様の3Dノイズ」だけです。試すときは使いたい1行だけ残してください。

それぞれのケースでみてみましょう。

通常の2Dノイズを生成

noise2(vUv)で生成した通常の2Dノイズ

float n = noise2(vUv);

上のようなシンプルなコードで生成できます。
通常のノイズは割と大きいです。

スケールを大きくして細かいノイズを生成

UV座標に10を掛けて細かくした2Dノイズ

    const float SIZE = 10.0;
    n = noise2(vUv * SIZE);

ノイズのサイズを細かくしたい場合、UV座標に掛け算してあげると粒子が細かくなります。
10をかけると上のようになり、100をかけると下のようになります。
UV座標に100を掛けてさらに細かくした2Dノイズ

時間経過と共に細かくなるノイズを生成

  n = noise2(vUv * uTick * SPEED);

時間経過と共に変化させるには、uTickにスピードをかけて調整してあげます。
ここから詳しい見た目はデモを参照した方がわかりやすいです。

時間経過と共に移動するノイズを生成

  n = noise2(vUv * SIZE + uTick * SPEED);

移動はuTickを足したり引いたりして調整できます。

X軸方向に移動するノイズを生成

  n = noise2(vec2(vUv.x * SIZE - uTick * SPEED, vUv.y));

X軸だけ移動させたい場合は、vUvのX軸だけに上の処理を追加すればOKです。
Y軸でやりたい場合も同様です。
移動の向きを変えたいときは+と-を変えればいいです。

波のように揺れるノイズを生成

  n = noise2(vec2(vUv.x * 80.0 - sin(vUv.y + uTick / 80.0), vUv.y * SIZE - sin(vUv.x + uTick / 80.0)));

波の場合は基本的には上と同じですが、三角関数のsineなどを使います。
sineなどの三角関数については、必要な最低限の知識部分のみ【WebGL】GLSLで使う数学(三角関数、ベクトル)を文系エンジニアのために実務でいるところだけ解説で詳しく解説しているので確認してみてください。

3Dノイズを使って時間経過と共に模様が変化するエフェクトを生成

  n = noise3(vec3(vUv * SIZE, uTick * SPEED));

時間経過と共に模様を変化させたい場合は三次元が必要になるので、3Dノイズを使います。
vec3で三次元を指定して、最初の二つはX軸、Y軸、最後の軸は時間としてuTickの値をとれば時間の経過とともに模様が変化するようになります。

横シマ模様の3Dノイズを生成

  n = noise3(vec3(vUv.x * SIZE * 2.0, vUv.y * SIZE * 7.0, uTick * SPEED));

vUvのxとyを分けて記述すると、大きさをx軸とy軸で変えられます。
あとは基本的に上と同じようにしてスピードや大きさを調整します。
縦シマにしたい場合は、xとyの値を入れ替えてあげる(xに大きい数値をかける、yに小さい数値をかける)だけで実現できます。

水に浮かんでいるような揺れる画像

ノイズで揺らす前の元画像
上が元の画像です。
JavaScriptのマテリアルには以下のような記述をします。

  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTex: { value: await loadTex("/imagepath/noise1.jpg") },
      uTick: { value: 0 },
      uNoise: { value: new THREE.Vector2(15, 52) },
    },
    vertexShader,
    fragmentShader,
  });

フラグメントシェーダーでuniformとして値を受け取りたいので、uNoiseというプロパティを用意して、valueには二次元ベクトルを渡します。
そうすると、xとyに指定した値を持つオブジェクトがvalueに入ります。
ShaderMaterialで画像をテクスチャとして渡す部分の基本はThree.jsのShaderMaterialで画像や動画を出力する方法で解説しています。

precision mediump float;

#pragma glslify: noise2 = require(glsl-noise/simplex/2d);
#pragma glslify: noise3 = require(glsl-noise/simplex/3d);

varying vec2 vUv;
uniform sampler2D uTex;
uniform float uTick;
uniform vec2 uNoise;

void main() {
  const float SPEED = 0.02;
  const float DETAIL_LEVEL = 0.007;
  float noise = noise3(vec3(vUv.x * uNoise.x, vUv.y * uNoise.y, uTick * SPEED));

  vec4 tex = texture(uTex, vUv + noise * DETAIL_LEVEL);

  // gl_FragColor = vec4(noise, noise, noise, 1.);
  gl_FragColor = tex;
}

元の画像と横シマ模様の3Dノイズでみせたようなノイズと合わせることでデモのような見た目を実現できます。
noiseを生成するときに、ノイズの大きさとしてuNoiseをvUvに掛け合わせてあげることでノイズの大きさを調整できます。

  vec4 tex = texture(uTex, vUv + noise * DETAIL_LEVEL);

そして、テクスチャ座標をノイズの分だけずらすと、水面のように不思議な感じに揺らせます。
上の例ではノイズを足していますが、引いても逆方向に動いていい感じになります。
画像をどれくらいはっきり見せたいかはDETAIL_LEVELという定数で調整します。値が大きいほど歪みが強くなるので、見ながら適切な値を探してください。

ノイズが表示されないときの確認

  • 真っ黒になる: 戻り値が負の範囲に入っています。n * 0.5 + 0.5で0〜1へ寄せるか、abs(n)を使います
  • 模様が動かない: JavaScript側のrequestAnimationFrameでmaterial.uniforms.uTick.valueを毎フレーム増やしているか確認します
  • #pragmaでコンパイルエラー: glslifyのプラグインがシェーダーファイルを処理していません。拡張子やプラグインの設定を確認します
  • 粒が粗すぎる・細かすぎる: SIZEの値を変えます。掛ける数を大きくするほど細かくなります
スポンサーリンク