Three.js/シェーダー(GLSL)その1

目次
  1. シェーダーの概要
  2. GLSLの基本
  3. 最初のカスタムシェーダー
  4. オブジェクトの移動・テクスチャの適応
  5. 平面上に様々なパターンを描く
  6. ノイズ(Perlin Noise)
  7. Shadertoy

シェーダーの概要

シェーダーはGPU上で実行されるGLSL(プログラミング言語)で記述されます

頂点シェーダーとフラグメントシェーダー
ジオメトリの各頂点を配置し、各フラグメントを色付けするために使用される
*フラグメント(fragment)とは、WebGLにおいて描画対象のピクセルのことを指します
フラグメントという用語を使うのは、各フラグメントが必ずしも画面のピクセルに対応しているとは限らないためです

  • 頂点シェーダー(Vertex Shader): 頂点をレンダリング空間に配置
    3Dジオメトリの各頂点を2Dのキャンバスにどのように描画するかを決定する役割を持っています
  • フラグメントシェーダー(Fragment Shader): 各フラグメントを色付けする
    同じ色で全てのフラグメントを塗るような単純な指示から、ライトの位置などのデータを元にフェイスが光源の前にどれだけあるかに応じてフラグメントを塗るような複雑なものまで様々です

データを読み込む「→」頂点シェーダー「→」フラグメントシェーダーの順に実行されます

GLSLの基本

  • GPU上で実行されるため、コンソールログのような機能はありません
  • 命令文の終わりにはセミコロンが必須
  • 型付け言語です(変数を宣言する際には、その型を指定が必須)

主な変数の

  • float: 単精度浮動小数点数
  • int: 整数
  • bool: 真偽値(true または false)
  • vec2: 2次元ベクトル
  • vec3: 3次元ベクトル
  • vec4: 4次元ベクトル
  • mat2: 2×2の行列
  • mat3: 3×3の行列
  • mat4: 4×4の行列
  • sampler2D: 2Dテクスチャを貼り付ける
  • samplerCube: キューブマップテクスチャを貼り付ける

修飾子(qualifiers):変数に特定の特性や振る舞いを付与するために使用されます

  • attribute:頂点シェーダーでのみ使用される
    各頂点に固有のデータ(位置、法線、テクスチャ座標など)を格納する
    レンダリングの各呼び出し間で異なる値を持ち、頂点バッファから提供される
  • uniform:頂点シェーダーとフラグメントシェーダーの両方で使用される
    一度設定すると、全ての頂点とフラグメントで共通のデータ(マテリアルの色、光源の位置、変換行列など)を格納する
    レンダリングの各呼び出し間で一定の値を保持する
  • varying:頂点シェーダーからフラグメントシェーダーへデータを渡すために使用
    頂点の色や法線(Normals)のデータがこれに該当する
  • const:定数値を格納するために使用される
    宣言時に初期化され、その後は変更できない
  • in / out:GLSLの新しいバージョンでは、attribute と varying はそれぞれ in と out に置き換えられている
    in 修飾子は、シェーダーへの入力データを表す
    out 修飾子は、シェーダーからの出力データを表す

Swizzle(スウィズル): ベクトルやマトリックスの要素を入れ替えたり、複製したりする機能です

vec4 color = vec4(1.0, 0.5, 0.2, 1.0); 

// r成分のみのベクトル
vec4 red = color.rrrr; 

// gb成分のみのベクトル
vec2 green_blue = color.gb;

関数の定義
戻り値の型を指定した後、関数名を記述し、引数を括弧内に定義します
*何も返さない場合は void 型を使用します
*パラメータ: 関数に渡す引数にも型を指定します
例えば float add(float a, float b)

// xの絶対値を返す関数
float abs(float x) {
    if (x < 0.0) {
        return -x;
    } else {
        return x;
    }
}

ネイティブ関数の例

  • mix(x, y, a):二つの値 x と y を a で線形補間します
  • clamp(x, minVal, maxVal):値 x を minVal と maxVal の範囲に制限します
  • dot(x, y): 二つのベクトル x と y のドット積を計算します
  • cross(x, y): 二つのベクトル x と y のクロス積を計算します
  • length(x):ベクトル x の長さを計算します
  • normalize(x):ベクトル x を正規化します
  • step(edge, x):x が edge 以上の場合は 1 を、そうでない場合は 0 を返します

最初のカスタムシェーダー

three.jsでシェーダーを作成するには、特定のマテリアル(ShaderMaterial または RawShadermaterial)を作成する必要があります

RawShaderMaterialとShaderMaterialについて

  • RawShaderMaterialはシェーダーコードに何も追加されません
  • ShaderMaterialはprojectionMatrix、viewMatrix、modelMatrix、position、uvなどの一般的なuniformとattribute、精度設定は、自動的にシェーダーコードに追加されているので記述する必要がない

平面ジオメトリ(THREE.PlaneGeometry)にカスタムシェーダー(THREE.RawShaderMaterial)を適用を使って赤い四角形を描画

// 1x1サイズの平面ジオメトリを作成
const geometry = new THREE.PlaneGeometry(1, 1);
// シェーダーマテリアルを作成
const material = new THREE.RawShaderMaterial({
// 頂点シェーダーの定義   
 vertexShader: `
    precision mediump float; // 浮動小数点の精度を指定
    uniform mat4 projectionMatrix; // 投影行列 
    uniform mat4 modelViewMatrix; // モデルビュー行列
    attribute vec3 position; // 頂点の位置
    void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);  // 頂点位置の計算  
    }
     `,
// フラグメントシェーダーの定義 
 fragmentShader: `
    precision mediump float; // 浮動小数点の精度を指定
    void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
    `
});

// ジオメトリとマテリアルを使ってメッシュを作成
const plane = new THREE.Mesh(geometry, material);
  • projectionMatrix(投影行列):3Dの世界を2Dのキャンバス上にどのように投影するか(どこからどこまでを表示するか)
  • modelMatrix(モデル行列):オブジェクトの位置、回転、スケールを調整します
  • viewMatrix(ビュー行列):これはカメラ自体の指示です。カメラがどこにあるのか、どの方向を向いているのかを指定します
  • modelViewMatrix(モデルビュー行列):物体とカメラの関係を設定します
    要するにmodelViewMatrix = viewMatrix * modelMatrix

precision mediump floatについて:データの精度レベルを決めます(mediump は中精度)

void main() :これはシェーダーのメイン関数で、自動的に呼び出されます

ShaderMaterialでは下記コードなどは自動的に追加されるので不要

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
precision mediump float;
//*ShaderMaterialを使う場合
const geometry = new THREE.PlaneGeometry(1, 1); 

const material = new THREE.ShaderMaterial({
  vertexShader: `
  void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 
    }
`,
  fragmentShader: `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  
  }
`
});

const plane = new THREE.Mesh(geometry, material);

「RawShaderMaterial」「ShaderMaterial」のプロパティについて

  • wireframe、side、transparent、flatShadingなど、他のマテリアルの一般的なプロパティが利用可能ですが、map(テクスチャマッピング)、alphaMap(透明度マップ)、opacity(不透明度)、color(色)などのプロパティはシェーダー内で実装する必要があります(GLSLコードを自分で書く)
  • uniforms」プロパティは、シェーダーで定義されたuniform変数に値(グローバル変数)を設定するために使用(シェーダーの実行中に変更されない定数値)
    頂点シェーダーとフラグメントシェーダーの両方で参照可能
  • attributes」プロパティは、シェーダーで定義されたattribute変数と頂点データを紐づけるために使用(頂点ごとに異なる値)
    頂点シェーダーでのみ参照可能
    *ShaderMaterialを使う場合は自分でattribute変数を定義する必要がなく、シンプルにattributesプロパティに頂点属性名を指定するだけでそれが頂点シェーダー内でattribute変数として紐づけられます
const material = new THREE.RawShaderMaterial({
    vertexShader: `
      `,
    fragmentShader: ` 
     `,
    wireframe: true,
    uniforms:{
        uFrequency: { value: 10.0 },
    },
//position, normal, uvというattribute変数と頂点データを紐づけています
    attributes: ['position', 'normal', 'uv']
})

シェーダーファイル(.glsl)の分離について
*JavaScriptで.glslファイルを直接インポートしてプレーンテキストとして表示される必要がありますが、プロジェクトがそのようなファイル形式をどう扱うかを知らないためエラーが発生します

Viteには.glslファイルをサポートするために「vite-plugin-glsl」をインストールします

npm install vite-plugin-glsl
//vite.config.jsファイル
import glsl from 'vite-plugin-glsl'

export default {
    // pluginsに追加
    plugins:[glsl()]
}

オブジェクトの移動・テクスチャの適応

平面を波の形に描画する(頂点シェーダー)
座標変換:vec4(position, 1.0): 3Dの頂点座標(vec3型)を4Dの座標(vec4型)に変換します
行列が掛け算さて頂点の最終的な位置を表します
gl_Positionは、頂点シェーダーの中で使用される各頂点の最終的な位置を表すvec4型の組み込み変数
4次元の座標(x, y, z, w)を持っています
頂点シェーダー内で値を割り当てない場合、デフォルトは(0.0, 0.0, 0.0, 1.0)です

//modelPosition、viewPosition、projectedPositionという変数を用意して変数を分ける
//モデル座標系からビュー座標系、そして投影座標系への変換を順に表してコードを読みやすくする
//gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

 vec4 modelPosition = modelMatrix * vec4(position, 1.0);
 vec4 viewPosition = viewMatrix * modelPosition;
 vec4 projectedPosition = projectionMatrix * viewPosition;
 gl_Position = projectedPosition;
  uniform mat4 projectionMatrix;
  uniform mat4 viewMatrix;
  uniform mat4 modelMatrix; 
  attribute vec3 position;
  void main(){
  //modelMatrixを変更することで、オブジェクトの位置を変えることができます
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
   //オブジェクトに波形のような動きを加える
   //横方向の波:z座標(高さ)を、その頂点のx座標(横位置)に基づいて調整 
   //* 10.0で波の周波数を上げる
  //* 0.1で振り幅を小さくする
    modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
   }

「uniforms」プロパティで管理することにより、グローバル変数としてシェーダー内外からアクセスと変更ができる
頂点シェーダーとフラグメントシェーダーで値を共有でき、JavaScriptからも値の動的な更新が可能になりま

// JavaScript
const material = new THREE.ShaderMaterial({
   //略
    uniforms: {
       uFrequency:{value: 10.0}, 
    }
})

//頂点シェーダー内
uniform float uFrequency;
 void main(){
    //
    modelPosition.z += sin(modelPosition.x * uFrequency) * 0.1;
    //
   }

アニメーションするために、経過時間を管理するutimeをuniformsに追加
値を頂点シェーダーのsin(…) 関数内で使用

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        //
        uTime: { value: 0 }
    }
})

const clock = new THREE.Clock()
const tick = () =>{
    const elapsedTime = clock.getElapsedTime()
    //マテリアルの更新
    material.uniforms.uTime.value = elapsedTime
    // ...
}
// ...
uniform float uTime;

void main(){
    // ...
    modelPosition.z += sin(modelPosition.x * uFrequency.x + uTime) * 0.1;
    // ...
}

オブジェクトの色を変える(フラグメントシェーダー)
gl_FragColor は、フラグメントシェーダーで使用される組み込みvec4型の変数
RGB色とアルファ値を指定できます
*アルファ値を 1.0 未満に設定したい場合は、「RawShaderMaterial」「ShaderMaterial」のプロパティで transparent: trueに設定する必要がある

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
//uniformsプロパティでuColorとして管理する
// JavaScript
const material = new THREE.ShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms: {
        uColor: { value: new THREE.Color('red') }, 
    }
})

// フラグメントシェーダー内
uniform vec3 uColor; 

void main() {
// uColorを参照できる
  gl_FragColor = vec4(uColor, 1.0);
}

テクスチャをオブジェクトに適用

テクスチャを読み込み、シェーダーに渡す

const texture = textureLoader.load('/texture.jpg')
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
const material = new THREE.RawShaderMaterial({
    // ...
    uniforms:
    {
        // ...
        uTexture: { value: texture}
    }
})

テクスチャを正しくマッピングするためには、UV座標をフラグメントシェーダーに渡す必要があります
Three.js の標準的なジオメトリは通常、UV 座標を含んでいます(geometry.attributes.uvで確認できます)
console.log(geometry.attributes.uv)
ジオメトリに含まれる UV座標は頂点シェーダーに存在する(attribute vec2 uv;)
*ShaderMaterialの場合は宣言不要
頂点シェーダーからフラグメントシェーダーへvarying変数を介して伝達する

// 頂点シェーダーでuv属性を定義(ShaderMaterialの場合は宣言不要)し、
attribute vec2 uv;
//varying変数vUvを通じてフラグメントシェーダーにUV座標を伝達
varying vec2 vUv;
void main()
{
    // ...

    vUv = uv;
}


フラグメントシェーダー内でsampler2D型の変数を宣言し、テクスチャを参照する

  • texture2D関数を使用して、指定されたUV座標におけるテクスチャの色を取得
    *texture2D関数
    1番目のパラメータはテクスチャ (uTexture)
    2番目のパラメータはUV座標
    戻り値はvec4形式(RGBA)
uniform sampler2D uTexture;

varying vec2 vUv;

void main()
{
    vec4 textureColor = texture2D(uTexture, vUv);
    gl_FragColor = textureColor;
}

波形の動きに合わせて影をつける
*modelPosition.z(高さ)が変化することにより、その部分のvElevationも変化し、テクスチャの色がその部分で明るくなったり暗くなったりする
頂点シェーダーで変数(elevation)を用意してフラグメントシェーダーへ送って使えるようにする

  • モデルの位置(modelPosition)を使って風による高さの変動(elevation)を計算します
  • 頂点シェーダーからフラグメントシェーダーへのデータ伝送
    varying変数vElevationを使用して、計算された高さの情報をフラグメントシェーダーに送くる
  • フラグメントシェーダーでは、テクスチャの色(textureColor)にvElevationを使って明るさの変化を加える
//頂点シェーダー
varying float vElevation;

void main() {
    float elevation = sin(modelPosition.x * uFrequency.x + uTime) * 0.1;
    modelPosition.z += elevation;
    // ...
    vElevation = elevation;
}

//フラグメントシェーダー
varying float vElevation;

void main() {
    vec4 textureColor = texture2D(uTexture, vUv);
   //テクスチャのRGB値を動的に変化させる
    textureColor.rgb *= vElevation * 2.0 + 0.5; 
    gl_FragColor = textureColor;
}

平面上に様々なパターンを描く

*UV座標を利用してパターンを作成する

UV座標を送信
1:頂点シェーダーでUV座標を取得
これはattribute vec2 uv;のように定義されますが、ShaderMaterialを使用している場合、このコードは自動的に追加されます
2:頂点シェーダーからフラグメントシェーダーへvaryingを使用してUV座標を送る

//attribute vec2 uv; //ShaderMaterialを使用している場合不要
varying vec2 vUv;

void main()
{
    // ...

    vUv = uv;
}
varying vec2 vUv;

void main()
{
//vUvの2つの成分が赤と緑の色成分に使われています。つまり、UV座標のU値が赤の強度を、V値が緑の強度を表します。
//青の成分は直接1.0(最大値)に設定されています。これは青色が最大強度であることを意味します。
//最後の成分はアルファ(透明度)ですが、このコードでは指定されていないため、デフォルトの1.0(完全不透明)が使われます。
//結果として、このシェーダーはUV座標(0~1)に基づいて変化する赤と緑の強度を持ち、青は常に最大強度で、不透明な色のパターンを生成します
    gl_FragColor = vec4(vUv, 1.0, 1.0);
}

X座標に基づいて変化するグレースケールのグラデーション
X座標が0の場合、色は黒(0,0,0)になり、X座標が1の場合、色は白(1,1,1)
*strength という新しい変数を作成しそれにUV座標のX成分を代入
float strength = vUv.yにするとY軸に沿って変化

//vec4(vUv.x, vUv.x, vUv.x, 1.0);
float strength = vUv.x;
//Y軸に沿って変化
//float strength = vUv.y;
//反転
//float strength = 1.0 - vUv.y;
//急激な変化
//float strength = vUv.y * 10.0;
gl_FragColor = vec4(vec3(strength), 1.0);

mod(vUv.y * 10.0, 1.0);
vUv.y * 10.0により急激に0〜1の間を変化、剰余演算により1.0に到達する前に「リセット」され、特定の範囲内で繰り返します
*一般的なプログラミング言語では剰余演算として%を使いますが、GLSLではmod()関数を使う必要があります
mod()関数で値を特定の範囲に収めることができます

 float strength = mod(vUv.y * 10.0, 1.0);
 gl_FragColor = vec4(vec3(strength), 1.0);

step(edge, x)関数
xがedgeより小さい場合は0.0を、edgeより大きい場合は1.0を返します

float strength = mod(vUv.y * 10.0, 1.0);
strength = step(0.5, strength);

X軸とY軸のどちらか一方のみが閾値を超えた場合にstrengthが1.0になり、その結果として白い領域が現れることになります。両方の軸が閾値を超える場所では、加算によりstrengthは2.0(最終的には1.0にクランプされます)になるため、黒い領域が現れます

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));

乗算を使用すると、X軸とY軸の両方で閾値を超える交差点のみを強調表示することができます
1.0 * 1.0 = 1.0
1.0 * 0.0 = 0.0 または 0.0 * 1.0 = 0.0なので

 float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
 strength *= step(0.8, mod(vUv.y * 10.0, 1.0));

リングや放射状のグラデーションなど、中心からの距離に基づいた効果を生成する
例えば:vUv.x – 0.5とすることで、0~1の範囲を-0.5~0.5に変更
abs(vUv.x – 0.5)で絶対値(正の値)0.0~0.5を返します

加算
vUv.xとvUv.yが両方とも0.5に近い場合からの距離に応じたグラデーションが生成されます
中心に近いほど値は小さく、端に近いほど値は大きくなります

 float strength = abs(vUv.x - 0.5)+abs(vUv.y - 0.5);

乗算
いずれかの値がゼロの場合、結果もゼロになるので他方の値の影響を完全に「遮断」する

float strength = abs(vUv.x - 0.5)*abs(vUv.y - 0.5);

XとYを比べて最大値をstrengthに割り当てる

float strength = max(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

XとYを比べて最小値をstrengthに割り当てる

float strength = min(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

水平方向に10の異なる段階を持つパターンが作成
*10で割ることで、値を再び0から1の範囲に戻す(正規化)
vUv.xの値に基づいて10等分された区間が生成されます。各区間は0.0, 0.1, 0.2, …, 0.9の値を取る

float strength = floor(vUv.x * 10.0) / 10.0;

length(vUv)は、ベクトルvUv(テクスチャ座標)の長さ、つまり原点(0.0, 0.0)からオフセットの適用(vUv – 0.5)で、テクスチャ座標を中心(0.5, 0.5)からの相対座標に変換
またはdistance(vUv, vec2(0.5))を使って、vUvの座標と中心点(0.5, 0.5)との距離を計算

float strength = length(vUv-0.5);
//または
float strength = distance(vUv, vec2(0.5));

中心からの距離に反比例して強度を増加させる
*距離が小さいほど、つまり中心に近いほど、強度は大きくなり、
距離が大きいほど、つまり中心から遠いほど、強度は小さくる

float strength = 0.015 / distance(vUv, vec2(0.5));

原点を中心とする円状のパターンに従って、vUvに基づいて0-1の間の角度値を生成
atan()関数:xとyの値から角度を計算する

#define PI 3.1415926535897932384626433832795

//角度の範囲を0.0から1.0に調整
//atan()関数の戻り値は、-π から π までの範囲
//angle / PI は -1 から 1 の範囲
//PIの2倍で割ることで、-0.5から0.5の範囲
float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (PI * 2.0) + 0.5;
float strength = angle;

UV座標を使ってグラデーションの色を生成し、それをフラグメントカラーとして使用
mix関数は、2つの値をブレンドするために使われ
2つの入力値と、それらの間でのミックス比率を決定する浮動小数点数の3つの引数を取ります
*strength変数を0.0から1.0の範囲にクランプすることで交差点での色の過剰な輝きがなくなります

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));
strength = clamp(strength, 0.0, 1.0);

vec3 blackColor = vec3(0.0);
vec3 uvColor = vec3(vUv, 1.0);
vec3 mixedColor = mix(blackColor, uvColor, strength);

gl_FragColor = vec4(mixedColor, 1.0);

ノイズ(Perlin Noise)

Perlin Noiseはコンピュータグラフィックスで使用される擬似ランダムなノイズです
自然なテクスチャや風景の生成に利用されており、滑らかでリアルな画像を作り出すことができます。

Perlin Noiseを使う
https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83

//ノイズを生成するためのカスタム関数
vec4 permute(vec4 x){
    return mod(((x*34.0)+1.0)*x, 289.0);
}

vec2 fade(vec2 t){
    return t*t*t*(t*(t*6.0-15.0)+10.0);
}
float cnoise(vec2 P){
    vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
    vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
    Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
    vec4 ix = Pi.xzxz;
    vec4 iy = Pi.yyww;
    vec4 fx = Pf.xzxz;
    vec4 fy = Pf.yyww;
    vec4 i = permute(permute(ix) + iy);
    vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
    vec4 gy = abs(gx) - 0.5;
    vec4 tx = floor(gx + 0.5);
    gx = gx - tx;
    vec2 g00 = vec2(gx.x,gy.x);
    vec2 g10 = vec2(gx.y,gy.y);
    vec2 g01 = vec2(gx.z,gy.z);
    vec2 g11 = vec2(gx.w,gy.w);
    vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
    g00 *= norm.x;
    g01 *= norm.y;
    g10 *= norm.z;
    g11 *= norm.w;
    float n00 = dot(g00, vec2(fx.x, fy.x));
    float n10 = dot(g10, vec2(fx.y, fy.y));
    float n01 = dot(g01, vec2(fx.z, fy.z));
    float n11 = dot(g11, vec2(fx.w, fy.w));
    vec2 fade_xy = fade(Pf.xy);
    vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
    float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
    return 2.3 * n_xy;
}

//cnoise関数を利用
 float strength = cnoise(vUv * 10.0);

Shadertoy

Shadertoyは、ウェブベースのコミュニティとプラットフォームで、GLSL(OpenGL Shading Language)を使用して主にフラグメントシェーダーを使ってビジュアルエフェクトを作成(ピクセルレベルでの画像処理に集中)

こちらのYouTubeのShadertoyで作成されたフラグメントシェーダーのコードをThree.jsで実装してみる

Shadertoyで作成されたフラグメントシェーダーのコードをThree.jsで実装するステップ

  1. 平面ジオメトリの作成
    Three.jsではTHREE.PlaneGeometryを使用して平面ジオメトリを作成します
    Shadertoyでのシェーダーは、通常、2D平面にフラグメントシェーダーを適用することを想定しているため、平面ジオメトリはこの目的に適しています
  2. シェーダーマテリアルの作成
    THREE.ShaderMaterialを使用して、カスタムのシェーダーをマテリアルを組み込みます
  3. ユニフォームの設定
    Shadertoyのシェーダーでは、多くの場合iTime(時間)、iResolution(解像度)、iMouse(マウスの位置)などを利用するために、ユニフォーム変数を定義します
  4. メッシュの作成とシーンへの追加
    作成したジオメトリとシェーダーマテリアルを使用して、THREE.Meshを作成し、それをシーンに追加
  5. レンダリングループの設定
    アニメーションやインタラクティブな要素を持つシェーダーの場合、レンダリングループ内でユニフォーム変数(例えばuTime)を更新する
import * as THREE from 'three'
import vertexShader from './shaders/vertex.glsl'
import fragmentShader from "./shaders/fragment.glsl";

const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}
// Canvasの設定
const canvas = document.querySelector('canvas.webgl')
// Sceneの作成
const scene = new THREE.Scene()
// 平面ジオメトリの作成 (Shadertoyの2Dエフェクトに適用)
const geometry = new THREE.PlaneGeometry(1,1)
// シェーダーマテリアルの作成 (Shadertoyのシェーダーコードを組み込み)
const material = new THREE.ShaderMaterial({
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  uniforms: {
    uTime: { value: 0 },
    uResolution: {
      value: new THREE.Vector2(sizes.width, sizes.height),
    },
  },
});

// メッシュの作成とシーンへの追加
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
// カメラの設定
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
scene.add(camera)

// レンダラーの設定
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

// 時間の経過によるアニメーションの設定
const clock = new THREE.Clock()
const tick = () =>{
    const elapsedTime = clock.getElapsedTime()
    // ユニフォーム変数の更新
    material.uniforms.uTime.value = elapsedTime
    // レンダリングの実行
    renderer.render(scene, camera)
    window.requestAnimationFrame(tick)
}
tick()

// ウィンドウのリサイズ処理
function handleResize() {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;
  // ユニフォーム変数の更新
  material.uniforms.uResolution.value = new THREE.Vector2(
     sizes.width,
     sizes.height
   );
  camera.aspect = sizes.width / sizes.height
  camera.updateProjectionMatrix();
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

handleResize();
window.addEventListener("resize", handleResize);

頂点シェーダー
Three.jsのTHREE.PlaneGeometryは各頂点にUV座標を持ち、これらの座標はテクスチャマッピングに使われます
頂点シェーダーでは、uvという組み込みの属性を用いてこれらのUV座標を取得し、vUvというvarying変数に割り当てることでフラグメントシェーダーに伝達します

varying vec2 vUv;

void main() {
    vUv = uv;
   //この場合、アスペクト比の調整はフラグメントシェーダーで行う必要があります
    gl_Position = vec4(position, 1.0);
}

フラグメントシェーダー
Shadertoyの mainImage 関数は出力を fragColor に設定するための2つの引数を取ります
fragCoord は画面上のピクセルの座標を表し、これを使って色を計算します
WebGLで標準的に使用される main 関数は、フラグメントシェーダーの出力色を直接 gl_FragColor に設定します

// Shadertoyのコード
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // 色の計算
    fragColor = vec4(1.0, 0.0, 0.0, 1.0); // 赤色
}

// WebGLに変換したコード
void main() {
    // 色の計算
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 赤色
}

フラグメントシェーダーで、uResolutionを使用してウィンドウの縦横のサイズを管理するのは、シェーダーが異なる解像度や画面サイズに対応でき、生成されるビジュアルエフェクトの一貫性を保つためです

vec2 uv = (vUv * 2.0 - 1.0) * vec2(uResolution.x / uResolution.y, 1.0);

varying vec2 vUv;

void main(){
    vUv = uv;
/*
ちなみにgl_Position が変換行列を使用して設定される場合
projectionMatrix はアスペクト比を考慮しているので
フラグメントシェーダーではvUv-1.0 から 1.0 の範囲にマッピングするだけでいい
vec2 uv = vUv * 2.0 - 1.0;
*/
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
uniform vec2 uResolution;
uniform float uTime;
varying vec2 vUv;
/*
色のパレットを生成するためのabcdの値を生成するための参考サイト
http://dev.thi.ng/gradients/
https://iquilezles.org/articles/palettes/
*/
vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.000, 0.333, 0.667);

    return a + b * cos(6.28318 * (c * t + d));
}

void main(){
  vec2 uv = vUv * 2.0 - 1.0;
  /*
  uv が fract関数とオフセットによって変更された後の値ではなく、
  元のuvの値をpalette関数に渡すためにコピーする
  画面全体で連続した色の変化を生成する
  */
  vec2 uv0 = uv;
  /*
fract 関数はその浮動小数点数の小数部分のみを返します
スケールアップされた座標にfract関数を適用し元の [0.0, 2.0] の範囲が [0.0, 1.0] の範囲に「折り返され」、タイル状のパターンが生成され
[0.0, 1.0] の範囲の値から 0.5 を減算することで、範囲を [-0.5, 0.5] に変更
*/
    uv = fract(uv * 2.0) - 0.5;
    float d = length(uv);
/*
col 変数は、色情報を保持するための vec3 ベクトルで、この場合は赤色の最大値1.0
*/
//vec3 col =vec3(0.0, 1.0, 0.0);

//palette関数を使って色を計算
//    vec3 col = palette(d + uTime);
vec3 col = palette(length(uv0) + uTime);
/*
d * 8.: 変数 d(画面中心からの距離)に8を乗算することでスケールを調整し、波の密度を変更します。数字が大きいほど、波の数が多くなります
+ uTime: uTime はシェーダーが開始されてからの経過時間を表すユニフォーム変数です。これを式に加えることで、時間の経過に応じて波の位相が変化し、動的なアニメーション効果を作り出します
sin 関数の結果は-1から1の範囲で変動
/ 8.: sin 関数の結果を8で割ってsin関数の結果の振幅を小さくし、結果の範囲を -1/8 から 1/8 に縮小
*/
    d = sin(d * 8. + uTime) / 8.;
    d = abs(d);
/*
割り算の効果: 距離 d で定数を割ることにより、距離が小さい(つまり、画面の中心に近い)ほど結果の値は大きくなる
ビジュアルの影響: この式により、画面中心に近いピクセルはより明るく、画面の端に近いピクセルはより暗くなり光のようなビジュアルを作り出すことができる
*/
    d = 0.02 / d;
//dの値が大きいほど色が暗くなる
    col *= d;

    gl_FragColor = vec4(col, 1.0);
}

複雑なパターンを生成
ループを使用して、複数の異なるスケールのパターンが重なり合うようにする

uniform vec2 uResolution;
uniform float uTime;
varying vec2 vUv;

vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.000, 0.333, 0.667);
    return a + b * cos(6.28318 * (c * t + d));
}

void main() {
    vec2 uv = (vUv * 2.0 - 1.0) ;
    vec2 uv0 = uv;
//finalColorを定義して、col * d を加算し、複数のパターンを重ね合わせて最終色を形成
    vec3 finalColor = vec3(0.0);
//ループを使用して、複数の異なるスケールのパターンが重なり合うようにする
    for (float i = 0.0; i < 4.0; i++) {
        uv = fract(uv * 1.5) - 0.5;
//exp(-length(uv0)) は、距離に基づいて値が急速に0に近づく効果を表現
        float d = length(uv) * exp(-length(uv0));
//ループの各イテレーションで少しずつ異なる色を生成、時間の経過に伴う色の変化を制御
        vec3 col = palette(length(uv0) + i * 0.4 + uTime * 0.4);
        d = sin(d * 8.0 + uTime) / 8.0;
        d = abs(d);
//距離が近いときの値の増加がより顕著になり、遠いときの減少がより強調
        d = pow(0.01 / d, 1.2);
        finalColor += col * d;
    }
    gl_FragColor = vec4(finalColor, 1.0);
}
  • exp関数は自然対数の底(約2.718)を基とする指数関数で、GLSLでは光や影、その他の視覚効果をリアルにシミュレートするために使われる
    例えば、光源からの距離に応じて光の強さが減衰する効果など
    exp(x) はx が増加するにつれて急速に増加する関数
    exp(-x) は x が増加するにつれて急速に0に近づく関数
  • pow関数は、pow(base, exponent) の形で書かれ、base を exponent で累乗した値を返します