2
3

小さなGPUのパワーエンジニアリング。4層の異なるスケールのパーリンノイズを重ねて、流動的な乱流のような動き。

Last updated at Posted at 2024-09-11

前回のあらすじ。CPU+GPU

4層の異なるスケールのパーリンノイズを重ねて、流動的かつ乱流のような動き 100% GPU計算。

WebGLを使用して高速にGPUで計算してます。前回よりも高解像度でスムースです。

スクリーンショット 2024-09-12 044414.png

スクリーンショット 2024-09-12 044429.png

image.png

乱流のような動きを表現するには、ノイズのスケールや時間の変化を複雑にし、ノイズの層を追加することで、より流体的な乱流効果を生み出すことができます。複数の異なるスケールのノイズを組み合わせて乱流のような動きとしています。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Perlin Noise with WebGL</title>
  <style>
    body, html {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background-color: black;
    }
    canvas {
      display: block;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script type="text/javascript">
    const canvas = document.getElementById('canvas');
    const gl = canvas.getContext('webgl');

    // キャンバスサイズの設定
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // ウィンドウリサイズ時にキャンバスを再設定
    window.addEventListener('resize', () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    });

    // 頂点シェーダーコード
    const vertexShaderSource = `
      attribute vec2 a_position;
      varying vec2 v_uv;
      void main() {
        // 位置を0〜1の範囲にマッピング
        v_uv = a_position * 0.5 + 0.5;
        gl_Position = vec4(a_position, 0.0, 1.0);
      }
    `;

    // フラグメントシェーダーコード
    const fragmentShaderSource = `
      precision highp float;
      varying vec2 v_uv;
      uniform float u_time;

      // パーリンノイズの補助関数(フェード関数)
      float fade(float t) {
        return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
      }

      // 線形補間関数
      float lerp(float t, float a, float b) {
        return a + t * (b - a);
      }

      // ランダムなベクトル生成
      vec2 random2(vec2 st) {
        st = vec2(dot(st, vec2(127.1, 311.7)),
                  dot(st, vec2(269.5, 183.3)));
        return -1.0 + 2.0 * fract(sin(st) * 43758.5453123);
      }

      // パーリンノイズ生成関数
      float perlin(vec2 st) {
        vec2 i = floor(st);
        vec2 f = fract(st);

        vec2 u = f * f * (3.0 - 2.0 * f);

        return lerp(u.y, lerp(u.x, dot(random2(i), f - vec2(0.0, 0.0)),
                              dot(random2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0))),
                    lerp(u.x, dot(random2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
                              dot(random2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0))));
      }

      // 複数のノイズ層を組み合わせて乱流効果を出す関数
      float turbulence(vec2 st, float time) {
        float value = 0.0;
        float scale = 1.0;
        float amplitude = 1.0;

        // 4層のパーリンノイズを重ねて複雑な動きを作る
        for (int i = 0; i < 4; i++) {
          value += amplitude * abs(perlin(st * scale + time));
          scale *= 2.0;
          amplitude *= 0.5;
        }

        return value;
      }

      void main() {
        // uv座標をスケーリングして乱流っぽい動きを付ける
        vec2 uv = v_uv * 5.0;

        // 時間を加味した乱流ノイズ
        float noise = turbulence(uv, u_time * 0.3);

        // 赤色にノイズを適用して色を設定
        vec3 color = vec3(noise * 1.0, noise * 0.0, noise * 0.0); // 赤色

        // フラグメントの色を設定
        gl_FragColor = vec4(color, 1.0);
      }
    `;

    // シェーダー作成関数
    function createShader(gl, type, source) {
      const shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
      }
      return shader;
    }

    // プログラム作成関数
    function createProgram(gl, vertexShader, fragmentShader) {
      const program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error(gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
        return null;
      }
      return program;
    }

    // 頂点とフラグメントシェーダーの作成
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
    const program = createProgram(gl, vertexShader, fragmentShader);

    // 頂点位置のバッファを作成
    const positionLocation = gl.getAttribLocation(program, "a_position");

    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
      -1, -1,
      1, -1,
      -1, 1,
      1, 1,
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    // 時間の uniform 位置を取得
    const timeLocation = gl.getUniformLocation(program, "u_time");

    // シェーダープログラムの使用
    gl.useProgram(program);
    gl.enableVertexAttribArray(positionLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // レンダリング関数
    function render(time) {
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      gl.clear(gl.COLOR_BUFFER_BIT);

      // 時間を uniform に渡す
      gl.uniform1f(timeLocation, time * 0.001); // ミリ秒を秒に変換

      // 四角形を描画
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      requestAnimationFrame(render);
    }

    render(); // レンダリングを開始
  </script>
</body>
</html>

参考。

シェーダーコードの部分について、頂点シェーダーとフラグメントシェーダーを詳しく解説します。WebGLのシェーダーは、GPUで並列計算を行い、グラフィックスを描画するプログラムです。以下は、このコードに基づいた説明です。

  1. 頂点シェーダー (vertexShaderSource)

attribute vec2 a_position;
varying vec2 v_uv;
void main() {
  // 位置を0〜1の範囲にマッピング
  v_uv = a_position * 0.5 + 0.5;
  gl_Position = vec4(a_position, 0.0, 1.0);
}

解説:
attribute vec2 a_position;
これは頂点の位置を示す属性(attribute)です。vec2型で、2D空間の座標(x, y)を保持しています。この属性は、各頂点ごとに異なる値が与えられます。

varying vec2 v_uv;
varyingは、頂点シェーダーからフラグメントシェーダーに値を渡すための変数です。v_uvは後でフラグメントシェーダーで使用するテクスチャ座標として機能します。ここでは、頂点シェーダーで計算されたv_uvの値が、各ピクセルで補間され、フラグメントシェーダーで使用されます。

v_uv = a_position * 0.5 + 0.5;
これは、-1〜1の範囲にある頂点座標を、0〜1の範囲にマッピングしています。a_positionが-1だとv_uvは0に、1だと1になります。これによって、WebGLのNDC(Normalized Device Coordinates: 正規化されたデバイス座標)空間から、テクスチャや色のマッピングに使うUV座標空間に変換しています。

gl_Position = vec4(a_position, 0.0, 1.0);
これは、現在処理している頂点の最終的な位置を指定する部分です。a_positionは2D座標なので、vec4型に変換し、Z座標は0、W座標は1としています。これにより、2D空間の頂点がスクリーン上に描画されます。

  1. フラグメントシェーダー (fragmentShaderSource)

precision highp float;
varying vec2 v_uv;
uniform float u_time;

// パーリンノイズの補助関数(フェード関数)
float fade(float t) {
  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

// 線形補間関数
float lerp(float t, float a, float b) {
  return a + t * (b - a);
}

// ランダムなベクトル生成
vec2 random2(vec2 st) {
  st = vec2(dot(st, vec2(127.1, 311.7)),
            dot(st, vec2(269.5, 183.3)));
  return -1.0 + 2.0 * fract(sin(st) * 43758.5453123);
}

// パーリンノイズ生成関数
float perlin(vec2 st) {
  vec2 i = floor(st);
  vec2 f = fract(st);

  vec2 u = f * f * (3.0 - 2.0 * f);

  return lerp(u.y, lerp(u.x, dot(random2(i), f - vec2(0.0, 0.0)),
                        dot(random2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0))),
              lerp(u.x, dot(random2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
                        dot(random2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0))));
}

// 複数のノイズ層を組み合わせて乱流効果を出す関数
float turbulence(vec2 st, float time) {
  float value = 0.0;
  float scale = 1.0;
  float amplitude = 1.0;

  // 4層のパーリンノイズを重ねて複雑な動きを作る
  for (int i = 0; i < 4; i++) {
    value += amplitude * abs(perlin(st * scale + time));
    scale *= 2.0;
    amplitude *= 0.5;
  }

  return value;
}

void main() {
  // uv座標をスケーリングして乱流っぽい動きを付ける
  vec2 uv = v_uv * 5.0;

  // 時間を加味した乱流ノイズ
  float noise = turbulence(uv, u_time * 0.1);

  // 赤色にノイズを適用して色を設定
  vec3 color = vec3(noise * 1.0, noise * 0.0, noise * 0.0); // 赤色

  // フラグメントの色を設定
  gl_FragColor = vec4(color, 1.0);
}

解説:
precision highp float;
highpは高精度の浮動小数点数を使用することを宣言しています。特にフラグメントシェーダーでは、高精度が必要になることが多いため、これを指定しています。

varying vec2 v_uv;
頂点シェーダーから渡されたUV座標です。この座標を元に、ノイズを計算します。

uniform float u_time;
時間を表すuniform変数です。この値はJavaScript側からシェーダーに渡され、アニメーションや時間変化の効果を生み出します。

ノイズ生成関連の関数:
fade関数:
パーリンノイズの補助関数で、スムーズな補間を行うためのカーブを生成します。値tの3次関数と複数の定数を用いて計算されます。

lerp関数:
線形補間関数です。2つの値aとbの間を、補間値tに基づいて補完します。

random2関数:
2次元ベクトルstに基づいてランダムなベクトルを生成します。これは、パーリンノイズのグラディエントベクトルの生成に使用されます。

perlin関数:
パーリンノイズの計算を行います。まず、グリッドのセルを決定し、その周辺の点から距離を計算し、補間して最終的なノイズ値を得ます。

複雑なノイズ(乱流):
turbulence関数:
複数の異なるスケールのパーリンノイズを重ねて乱流のような複雑なノイズを生成します。スケールを倍増させ、振幅を半分に減少させることで、異なるスケールでのノイズを積み上げていきます。これにより、細かいディテールの多いノイズが生成されます。
フラグメントシェーダーのメイン処理:
uv座標のスケーリング:
v_uvを5倍にして、ノイズの詳細を大きくします。これにより、より細かい乱流パターンが描画されます。

turbulence関数によるノイズ計算:
turbulence関数を用いて、時間に基づくノイズの変化を生成します。これにより、時間とともに動くパターンが作られます。

赤色の適用:
計算されたノイズ値を赤色の強度として使用し、赤色の動く模様が描画されます。

gl_FragColor:
フラグメントの最終的な色を設定します。

参考2。シェーダープログラムとは。

例 三角形の描画プロセス

頂点シェーダーでの計算:

入力: 三角形の3つの頂点の位置(各頂点はベクトルで表現されます)。
処理: 各頂点の位置をカメラの視点や投影に基づいて画面上の位置に変換するために、行列計算を行います。
出力: 画面上の位置や、色、テクスチャ座標などの属性をフラグメントシェーダーに渡します。

ラスタライズ:

頂点シェーダーで計算された頂点の位置に基づいて、三角形の内部の全てのピクセルを決定します。これを「ラスタライズ」と呼びます。

フラグメントシェーダーでの計算:

入力: 頂点シェーダーから渡された属性(テクスチャ座標、色など)。
処理: 各ピクセルの色を計算します。例えば、色のグラデーション、テクスチャの適用、その他のエフェクト(回転、スケーリングなど)を実行します。
出力: 最終的なピクセルの色として、画面に表示される結果を生成します。

行列計算の役割

頂点シェーダーでの行列計算:

モデル変換行列: オブジェクトの位置、回転、スケーリングを設定します。
ビュー行列: カメラの位置や視点を設定します。
プロジェクション行列: 3Dシーンを2D画面に投影するための変換を行います。
これらの行列を掛け合わせて、最終的な頂点の位置を計算します。

フラグメントシェーダーでの処理:

行列計算は通常、頂点シェーダーで行いますが、フラグメントシェーダーでもテクスチャ座標の変換や色の補正などに使用されることがあります。
効果の計算
色のグラデーション: フラグメントシェーダーで、頂点から渡された色を補間することで実現します。
回転や変形: 頂点シェーダーで行列を用いて行います。例えば、回転行列を使ってオブジェクトを回転させます。

シェーダーはこのように、頂点シェーダーでの計算とフラグメントシェーダーでの計算を組み合わせて、視覚的な効果をリアルタイムで描画するための強力なツールです。

参考3。GLSL(OpenGL Shading Language)の、様々な組み込み関数とユーティリティ関数

GLSL(OpenGL Shading Language)には、様々な組み込み関数とユーティリティ関数が用意されています。これらの関数は、シェーダー内での計算やデータ処理を助けるために設計されています。以下に、GLSLの主要な関数とそのカテゴリを示します。

数学関数

算術関数

abs(x): 絶対値
sign(x): 符号
min(x, y): 最小値
max(x, y): 最大値
clamp(x, minVal, maxVal): 値を範囲内に制限
mix(x, y, a): 線形補間

三角関数

sin(x): サイン
cos(x): コサイン
tan(x): タンジェント
asin(x): アークサイン
acos(x): アークコサイン
atan(x): アークタンジェント
atan(y, x): 2変数アークタンジェント

指数・対数関数

pow(x, y): 指数
exp(x): 指数関数
log(x): 自然対数
log2(x): 2を底とする対数
sqrt(x): 平方根
inversesqrt(x): 逆平方根

絶対値・浮動小数点関数

float(x): 浮動小数点数への変換
int(x): 整数への変換
round(x): 四捨五入
floor(x): 小数点以下切り捨て
ceil(x): 小数点以下切り上げ

ベクトル・行列関数

ベクトル関数

length(x): ベクトルの長さ
normalize(x): ベクトルの正規化
dot(x, y): ドット積
cross(x, y): クロス積
reflect(I, N): 反射ベクトル
refract(I, N, eta): 屈折ベクトル

行列関数

transpose(x): 行列の転置
inverse(x): 行列の逆行列
determinant(x): 行列の行列式

テクスチャ関数

サンプリング関数

texture2D(sampler2D, coord): 2Dテクスチャサンプリング
texture3D(sampler3D, coord): 3Dテクスチャサンプリング
textureCube(samplerCube, coord): キューブマップテクスチャサンプリング

テクスチャ関連

textureSize(sampler2D, level): テクスチャのサイズ取得
texelFetch(sampler2D, P, lod): 指定されたレベルのテクセル取得

数値演算関数

浮動小数点数演算

mod(x, y): 剰余
fmod(x, y): 浮動小数点剰余
fract(x): 小数部分
trunc(x): 切り捨て整数

ビット操作関数(GLSL 4.0以降)

bitCount(x): ビット数
bitFieldExtract(x, offset, count): ビットフィールド抽出
bitFieldInsert(x, insert, offset, count): ビットフィールド挿入
bitReverse(x): ビット反転

その他の関数

制御関数

if(condition) { ... }: 条件付き実行
for(init; condition; increment) { ... }: ループ
while(condition) { ... }: 繰り返し

switch(expression) { case value: ... break; ... default: ... }: スイッチ文
discard;: 現在のフラグメントを描画しない

エラー処理

assert(condition);: 条件が満たされない場合にエラーを発生させる(GLSL 4.30以降)

関数の定義

void functionName(params) { ... }: GLSLでは、シェーダー内で関数を定義し、呼び出すことができます。引数や戻り値の型を指定し、複雑な処理を分割して管理することができます。

シェーダー内での利用
シェーダー内でこれらの関数を利用することで、さまざまなグラフィックス処理を効率的に行うことができます。例えば、頂点シェーダーでは、頂点の位置計算や変換を行い、フラグメントシェーダーでは、ピクセルごとの色計算やテクスチャサンプリングを行います。

GLSLコードのサンプル

以下に、いくつかのGLSLの関数を使ったシンプルなシェーダーの例を示します。

頂点シェーダーのサンプル


#version 300 es
layout(location = 0) in vec4 a_position; // 頂点位置
layout(location = 1) in vec4 a_color;    // 頂点カラー

out vec4 v_color; // フラグメントシェーダーへの出力

void main() {
    gl_Position = a_position; // 頂点の位置を設定
    v_color = a_color;       // 頂点カラーをフラグメントシェーダーへ渡す
}

フラグメントシェーダーのサンプル


#version 300 es
precision mediump float; // 浮動小数点の精度指定

in vec4 v_color; // 頂点シェーダーからの入力

out vec4 fragColor; // 最終的なフラグメント色

void main() {
    fragColor = v_color; // 頂点カラーをそのまま出力
}

まとめ
GLSLの組み込み関数は、グラフィックスシェーダーでの計算や処理を効率的に行うための強力なツールです。

2
3
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
2
3