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

目次
  1. シェーダーの概要
  2. GLSLの基本
  3. 最初のカスタムシェーダー(平面ジオメトリ)
  4. 頂点を動的に移動
  5. 色を動的に変える
  6. テクスチャ
  7. 平面ジオメトリでの実装例
    1. 揺れる旗(旗のテクスチャ)
    2. 波(ノイズ)
    3. 湯気(ノイズテクスチャ)
  8. 平面上に様々なパターンを描く
  9. 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:頂点シェーダーでのみ使用される
    各頂点に固有のデータ(位置position、法線normal、テクスチャ座標uvなど)を格納する
    レンダリングの各呼び出し間で異なる値を持ち、頂点バッファから提供される
  • uniform:頂点シェーダーとフラグメントシェーダーの両方で使用される
    一度設定すると、全ての頂点とフラグメントで共通のデータを格納する
    レンダリングの各呼び出し間で一定の値を保持する
  • varying:頂点シェーダーからフラグメントシェーダーへデータを渡すために使用
    頂点の色や法線(Normals)のデータがこれに該当する
  • const:定数値を格納するために使用される
    宣言時に初期化され、その後は変更できない
  • in / out:GLSLの新しいバージョンでは、attribute と varying はそれぞれ in と out に置き換えられている
    in 修飾子は、シェーダーへの入力データを表す
    out 修飾子は、シェーダーからの出力データを表す

Swizzle(スウィズル): ベクトルの要素を簡単に操作するための機能

vec4 myVec = vec4(1.0, 2.0, 3.0, 4.0);

//ベクトルの要素にアクセス
float x = myVec.x; // x は 1.0
float y = myVec.y; // y は 2.0

//新しいベクトルを作成
vec2 newVec = myVec.xy; // newVec は (1.0, 2.0)
vec3 newVec2 = myVec.zyx; // newVec2 は (3.0, 2.0, 1.0)

//同じ要素を複数回使う
vec3 repeatVec = myVec.xxx; // repeatVec は (1.0, 1.0, 1.0)

関数の定義
戻り値の型を指定した後、関数名を記述し、引数を括弧内に定義します
*何も返さない場合は 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 の範囲に制限します
  • length(x):ベクトル x の長さを計算します
  • normalize(x):ベクトル x を正規化します
  • step(edge, x):x が edge 以上の場合は 1 を、そうでない場合は 0 を返します
  • dot(x, y): 二つのベクトル x と y のドット積を計算します
  • cross(x, y): 二つのベクトル x と y のクロス積を計算します

GLSLでのベクトル演算は、ベクトル演算子(+, -, *, dot(), cross())を使用して直接を行う

備考:glslでは組み込みの乱数生成関数が提供されていません
ランダムな効果を実現するためには、数学的なトリックを使用して疑似ランダム値を生成する関数が必要です

//0.0以上1.0未満の範囲でランダムな値を得る

float randomFloat(float value) {
    return fract(sin(value * 12.9898) * 43758.5453123);
}

float random2D(vec2 value){
    return fract(sin(dot(value, vec2(12.9898,78.233))) * 43758.5453123);
}

float random3D(vec3 value) {
    return fract(sin(dot(value, vec3(12.9898, 78.233, 45.164))) * 43758.5453123);
}

定数の定義
*定数を定義する一般的な方法は #define を使う

#define PI 3.14159

include ディレクティブ
ファイルやコード断片を現在のシェーダーコードに取り込むために使用されます

Three.jsでは、多くのシェーダーチャンク(再利用可能な、特定の計算や効果を実装した小さなコード片)が提供されています
includeディレクティブを使用してシェーダーチャンクを現在のシェーダーに挿入し、必要な機能を追加することができます

#include <colorspace_fragment> //色空間変換を行う
#include <tonemapping_fragment> //トーンマッピングを行う

最初のカスタムシェーダー(平面ジオメトリ)

three.jsでシェーダーを作成するには
RawShaderMaterialまたはShaderMaterialを作成する必要があります

平面ジオメトリ(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);

RawShaderMaterialとShaderMaterialについて

  • RawShaderMaterialはシェーダーコードに何も追加されません、明示的な宣言が必要です
  • ShaderMaterialを使用する場合、Three.jsは
    標準的なGLSLシェーダー変数(projectionMatrix、modelViewMatrix、normalMatrixなど)
    一部のattribute(position、normal、uvなど)を自動的に提供します
    そのため、これらの変数と属性については、GLSLコード内での明示的な宣言が不要です

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

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position; //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);

Three.jsでは、シェーダーに渡される「組み込みのuniform変数」の例
*これらの変数は、レンダリングの際にThree.jsによって自動的に更新され、シェーダーが3Dシーンの状態に基づいて計算を行うための情報を提供します

  • modelMatrix(モデル行列):モデルのローカル座標(モデル自身の位置やスケール、回転)からワールド座標を計算するための変換行列
  • viewMatrix(ビュー行列):ワールド座標からカメラ空間の座標を計算するための変換行列
    モデルがどの位置からカメラに映るかが決まります
  • modelViewMatrix(モデルビュー行列):モデルのローカル座標から直接カメラ空間までを計算する変換行列
    要するにmodelViewMatrix = viewMatrix * modelMatrix
  • projectionMatrix(投影行列):カメラ空間での3D座標を2Dスクリーン座標に投影を計算するための変換行列
    これでモデルが最終的にスクリーン上にどのように見えるかが決まります(遠近感を反映したり、視野の範囲を制限したりします)
  • cameraPosition:カメラ自体の位置であり、ワールド空間におけるカメラの位置

備考:同次座標(Homogeneous coordinates)について
通常の3次元空間では、1つの点は (x, y, z) のように3つの値で表されます
しかし、これを4つの要素を持つ同次座標 (x, y, z, w) に拡張、このw要素が同次座標の特徴で次のような意味を持ちます
*vec4(position, 1.0)

  • w = 1.0の場合
    通常の座標と同じで、位置ベクトルとして扱われます。
    このとき (x, y, z, 1) は点を表し、変換の際には平行移動、回転、スケールの全てが適用されます
  • w = 0.0の場合
    方向ベクトルとして扱われ、位置の情報を持ちません(位置的な移動には影響されません)
    このとき (x, y, z, 0) は方向ベクトル(法線)を表します
    平行移動は無視され、回転とスケールのみが適用されます

precision mediump floatについて:浮動小数点数の精度を指定(mediump は中精度)

  • lowp:低精度
  • mediump:中精度
  • highp:高精度 *一部のデバイスではhighpがサポートされていない場合がある

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

備考gl_で始まる変数は組み込み変数として定義されており、シェーダープログラム内で特別な目的に使われます

  • 頂点シェーダーで使われる組み込み変数
    • gl_Position:全ての頂点シェーダーが出力するべき最終的な頂点の位置
    • gl_PointSize:点(ポイント)が描画される際の画面上での大きさ
  • フラグメントシェーダーで使われる組み込み変数
    • gl_FragCoord:処理中のフラグメントのウィンドウ座標系での位置
      *Z値は深度テストに使用されます
    • gl_FrontFacing:処理中のフラグメントがポリゴンの表面に属しているかどうかを示すブール値
    • gl_FragDepth:フラグメントの深度値をオーバーライドするのに使用、デフォルトではgl_FragCoord.zが使われる

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

  • wireframe、side、transparent、flatShadingなど、他のマテリアルの一般的なプロパティが利用可能ですが、map(テクスチャマッピング)、alphaMap(透明度マップ)、opacity(不透明度)、color(色)などのプロパティはシェーダー内で実装する必要があります(GLSLコードを自分で書く)
  • uniforms」プロパティ
    シェーダーで定義された「uniform変数」に値(グローバル変数)を設定するために使用(シェーダーの実行中に変更されない定数値)
    頂点シェーダーとフラグメントシェーダーの両方で参照可能
  • attributes」プロパティ
    シェーダー内で定義された「attribute変数」にJavaScriptから頂点データを紐づけるために使用されます
    Three.jsは自動的にバッファジオメトリの属性(頂点の位置、法線、色、テクスチャ座標など)をシェーダーのattribute変数にバインドします
    Three.js の標準的なジオメトリは(BoxGeometry、SphereGeometry、PlaneGeometryなど)基本的なバッファジオメトリの属性(position、normal、uvなど)を含んでいますが、カスタム属性をシェーダーに渡す場合は、BufferGeometryにカスタム属性を追加し、それをシェーダーで参照することが可能です
    *「attribute変数」は頂点シェーダーでのみ使用可能で、読み取り専用
    その値を直接変更することはできませんが、他の変数に代入して利用したり、頂点シェーダーでattribute変数の値を基にして計算した結果をvarying変数に格納してフラグメントシェーダーに渡したりします
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)です

//gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);を
//modelPosition、viewPosition、projectedPositionという変数を用意して
//モデル座標系⇨ビュー座標系⇨投影座標系への変換ステップを表すと
 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);
}

備考
色をvec3の形式で(R, G, B)として表し、各成分を0から1の範囲で指定
CSSのカラーコードは通常、#RRGGBBの形式で記述されそれぞれ16進数で表示
*FFは10進数で255に相当
例としてCSSの#7F7FFFをシェーダーで使いたい場合
16進数の7Fは10進数で127に相当し、FFは255に相当
R: 127 / 255 = 0.498
G: 127 / 255 = 0.498
B: 255 / 255 = 1.0

備考
Three.jsで平面ジオメトリposition、uv、normal属性について

  • position:各頂点の3D空間における位置
    X, Y, Zの3つの座標で表現
  • uv: テクスチャマッピングのための2次元座標
    テクスチャをジオメトリに貼り付ける位置を指定
  • normal:法線ベクトルは、面に対して垂直に伸びるベクトル
    X, Y, Zの3つの座標で表現
    平面の場合、通常の法線ベクトルはZ軸方向に向かう

テクスチャ

テクスチャを適用

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

  • value」は、実際にシェーダーに渡されるデータを指定するプロパティです(THREE.Uniformインスタンスを作成する前にデータ(texture)を渡す方法)
  • 「new THREE.Uniform」は、シェーダーにデータを渡すためのuniform変数を作成します
    ここで作成されたUniformインスタンスは、渡されるデータ(texture)を含みます
const texture = textureLoader.load('/texture.jpg')
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
const material = new THREE.RawShaderMaterial({
    // ...
    uniforms:{
        // ...
        uTexture: { value: texture}
     //または、uTexture: new THREE.Uniform(texture)
    }
})

テクスチャを正しくマッピングするためには、UV座標をフラグメントシェーダーに渡す必要があります
console.log(geometry.attributes.uv)
*Three.js の標準的なジオメトリは通常、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;
    elevation += sin(modelPosition.y * uFrequency.y - 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;
}

波(ノイズ)

ノイズ(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);

湯気(ノイズテクスチャ)

この画像で湯気を表現する

smoothstep関数は2つのエッジ値(edge0とedge1)と、それらの間で補間するための値(x)を引数に取り、特定の範囲内(2つのエッジ値)を滑らかに補間(滑らかに遷移させる)するために使用されます

smoothstep(edge0, edge1, x);
  • xの値がedge0より小さい場合、smoothstepは0を返します
  • xの値がedge1より大きい場合、1を返します
  • xがedge0とedge1の間にある場合、smoothstepは0から1までの間で滑らかに変化する値を計算し、その結果を返します

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

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つの成分が赤Rと緑Gに使われてる、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)

ポイント:グレースケールの場合、その画像のR(赤)、G(緑)、B(青)の各成分は同じ値

gl_FragColor = vec4(vUv.x, vUv.x, vUv.x, 1.0);

//strengthという新しい変数を作成しそれにUV座標のX成分を代入
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);
mod() 関数 :第一引数を第二引数で割った余りを返す
*fract() 関数:常に1.0で割った余りを返す
y座標を10倍してuv座標を拡大して、パターンの密度を増やし
拡大した座標を1.0で割った余りを計算
結果は0から1までの値を周期的に繰り返す

*一般的なプログラミング言語では剰余演算として%を使いますが、GLSLでは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軸のどちらか一方のみが閾値を超えた場合に線上では1、線の外では0、加算によりグリッドの交差点ではstrengthは2.0(最終的には自動的に1.0にクランプされます)になります
*通常GLSLの色の処理では値が自動的に[0, 1]の範囲にクランプされます

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));

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を使う
https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83


Shadertoy

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

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

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

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

Shadertoy:mainImage関数
出力色はfragColor に設定
 *fragCoord は画面上のピクセルの座標
フラグメントシェーダー:main関数
出力色は gl_FragColor に設定

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

// フラグメントシェーダーのコード
void main() {
    // 色の計算
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 赤色
}

スクリーンスペースでの描画
カメラやモデルの位置に関係なくスクリーン(canvas要素)に対して直接描画する
*特別なカメラ設定は不要、最低限のカメラを設定してシーンに追加
*canvas要素のアスペクト比によって変形
*THREE.PlaneGeometry(2, 2)でcanvas要素の全体をカバー

// シーンの作成
const scene = new THREE.Scene();

// 平面ジオメトリの作成
const geometry = new THREE.PlaneGeometry(2, 2);

// シェーダーマテリアルの作成
const material = new THREE.ShaderMaterial({
  vertexShader: `
    varying vec2 vUv;
    void main() {
        gl_Position = vec4(position, 1.0);
        vUv = uv;
    }
  `,
  fragmentShader: `
    uniform vec2 uResolution;
    varying vec2 vUv;
    void main() {
        vec2 uv = vec2(uResolution.x / uResolution.y, 1.0);
        gl_FragColor = vec4(uv, 0.0, 1.0);
    }
  `,
  uniforms: {
    uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
  }
});

// メッシュの作成とシーンへの追加
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// カメラの作成
const camera = new THREE.Camera();
scene.add(camera);

window.addEventListener('resize', () =>{
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

const tick = () =>{
    renderer.render(scene, camera)
    window.requestAnimationFrame(tick)
}
tick()

頂点シェーダー
gl_Position = vec4(position, 1.0);
この場合はモデルの位置やカメラの影響を受けず、単純なスクリーンスペースでの描画

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

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

カメラやモデルの位置を考慮した描画の場合
*projectionMatrixやmodelViewMatrixを使用してgl_Positionを計算
*フラグメントシェーダーではvUv-1.0 から 1.0 の範囲にマッピングするだけでいい
vec2 uv = vUv * 2.0 - 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) * vec2(uResolution.x / uResolution.y, 1.0);
  /*
  uv が fract関数とオフセットによって変更された後の値ではなく、
  元のuvの値をpalette関数に渡すためにコピーする
  画面全体で連続した色の変化を生成する
  */
  vec2 uv0 = uv;
/*
fract関数はその浮動小数点数の小数部分のみを返します
スケールアップ(* 2.0この数字が大きいほど多く分割される)にfract関数を適用し0.5 を減算することで、範囲を [-0.5, 0.5] に変更
*/
    uv = fract(uv * 2.0) - 0.5;
    float d = length(uv);
/*
col 変数は、色情報を保持するための vec3 ベクトルで、この場合は赤色の最大値1.0
*/
//vec3 col =vec3(1.0,0.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(uResolution.x / uResolution.y, 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 で累乗した値を返します