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

目次
  1. ポストプロセッシング
  2. シェーダーを使ってパーティクルをアニメーションする

ポストプロセッシング

ポストプロセッシングは最終的な画像(レンダリング)にエフェクトを加えることです。

  1. ポストプロセッシングでは、レンダーターゲットでレンダリングした「テクスチャ」が生成されます
  2. このレンダーターゲットから得られた「テクスチャ」は、カメラに向かって配置され、ビューポート全体をカバーする「プレーン(平面)」に適用されます
  3. この「プレーン(平面)」は、ポストプロセッシング効果を適用するための特別なフラグメントシェーダーを含むマテリアルを使用します

Three.jsでは、これらの「エフェクト」を「パス」と呼びます
「複数のパス」を組み合わせることで、複雑なエフェクトを作成でき、最終的なパスはキャンバスに直接描画され、ユーザーに最終結果が表示されます

  • レンダーターゲットは、画像やテクスチャを一時的に保持するための容器のようなものです
    WebGLやThree.jsでの3Dレンダリングでは、通常、画像は最終的にキャンバス描画されます
    しかし、ポストプロセッシングでは、直接キャンバスに描画する代わりに、一時的なレンダーターゲットに描画します
    これにより、追加の処理や効果を適用する前に画像を保存し、操作することができます
  • ピンポンバッファリングは、ポストプロセッシングの「複数のパス」を効果的に連携させるための手法です
    一つのレンダーターゲットに描画する間、もう一つのレンダーターゲットからテクスチャを取得し、そのテクスチャに対して処理を行います
    これにより、一連のポストプロセッシング効果(パス)を順番に適用できます
    レンダリングプロセス中に、一つのレンダーターゲットに描画しながら、そのレンダーターゲットのテクスチャを読み取ることはできないそのため、2つのレンダーターゲットが必要です

https://threejs.org/docs/#examples/en/postprocessing/EffectComposer

EffectComposer:ポストプロセッシングのプロセスを管理するためのクラスです
RenderPass:ポストプロセッシングチェーン内で最初に実行されるパスで、主な役割は、シーンとカメラを使って初期のレンダリングを行います
*EffectComposerはエフェクトの適用を制御、RenderPassは最初のレンダリングを提供
その出力を後続のポストプロセッシングパスに渡します
*インポート必要

DotScreenPassで画像にドットスクリーン効果を適用するエフェクトを適応

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
//画像にドットスクリーン効果を適用するエフェクト
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'

//Post processing
//EffectComposer
const effectComposer = new EffectComposer(renderer)
effectComposer.setSize(sizes.width, sizes.height)
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
//RenderPass
const renderPass = new RenderPass(scene, camera)
effectComposer.addPass(renderPass)
//DotScreenPassを適応
const dotScreenPass = new DotScreenPass()
/*
dotScreenPass.enabled = false
でパスを無効にできる 
*/
effectComposer.addPass(dotScreenPass)

//アニメーションの関数内では
const アニメーション = () =>{
    // ...
    //置き換える
    // renderer.render(scene, camera)
    effectComposer.render()
    // ...
}

//EffectComposerとそのパスは、ウィンドウのサイズが変更されたときリサイズする

window.addEventListener('resize', () =>{
   // ...
      // EffectComposerの更新
    effectComposer.setSize(window.innerWidth, window.innerHeight);
    effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  // ...
})

GlitchPass
画面のグリッチ(乱れ)を模倣するエフェクト
デジタル的なエラー、歪み、乱れを表現し、映画やビデオゲームでよく見られるスタイル

import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
// ...
const glitchPass = new GlitchPass()
//glitchPass.goWild = true //ノンストップ 
effectComposer.addPass(glitchPass)

UnrealBloomPass
ブルーム効果(光の輝き)は、光源や反射面からの光が周囲に広がり、光沢感や輝きを演出
strength強度: ブルームの強度を指定します。
radius半径: ブルームの広がり具合を指定します。
thresholdしきい値: ブルームを適用する明るさのしきい値を指定します

import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

// ...
const unrealBloomPass = new UnrealBloomPass()
unrealBloomPass.strength = 0.5
unrealBloomPass.radius = 0.5
unrealBloomPass.threshold = 0.5
effectComposer.addPass(unrealBloomPass)

RenderPixelatedPass
特定のピクセルサイズでレンダリング

import { RenderPixelatedPass } from 'three/addons/postprocessing/RenderPixelatedPass.js';

// ...
const renderPixelatedPass = new RenderPixelatedPass( 10, scene, camera );
effectComposer.addPass( renderPixelatedPass );

ポストプロセッシングを適用した後、レンダリングされたシーンが暗くなってしまうことがあります
*レンダーターゲットでレンダリングされるため、色空間が同じ方法でサポートされない
解決策:GammaCorrectionShaderを最後のパスとして追加する
このシェーダーは色空間をSRGBに変換し、色を補正する

import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.js';

// ...

const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
effectComposer.addPass(gammaCorrectionPass);

アンチエイリアシング:エッジがジャギー(刻み目のようになる現象)に見える問題を解決する
*高品質ほどパフォーマンスは悪くなります
注意:GammaCorrectionShaderより後に設定する(エフェクトが色空間の調整後の画像に対して最適な設定をするため)

  • SSAA :最高品質のアンチエイリアシング
  • SMAA:FXAAよりも高品質
    エッジ検出と局所的な処理により、より洗練されたエッジを提供します
  • TAA:複数のフレームを統合してエッジのジャギーを減らします
    結果は比較的良いが、動きのあるシーンではボケやゴースト現象が生じることがる
  • FXAA :場合によっては少しボケた印象になります。
    画像全体にフィルタを適用することでエッジを滑らかにする
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';

// ...

const smaaPass = new SMAAPass();
effectComposer.addPass(smaaPass);

RGBShiftエフェクト
赤(R)緑(G)青(B)の色成分をそれぞれ少しずつずらすことで、特有の視覚的な歪みを生み出すエフェクト
適用手順
RGBShiftShader:RGBShiftエフェクトのためのシェーダー
ShaderPass:このシェーダーをポストプロセッシングパイプラインに統合するために使用

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js'

// ...

const rgbShiftPass = new ShaderPass(RGBShiftShader)
effectComposer.addPass(rgbShiftPass)

独自のシェーダーパスを適応する

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'

// TintShader: 画像に特定の効果(この場合はティント)を適用するためのシェーダー定義
const TintShader = {
    // uniforms: シェーダーに渡されるグローバルな変数
    uniforms:{
        tDiffuse: { value: null } // 元の画像テクスチャ用
        uTint: { value: null } //vec3型でRGB値を加算するため
    },
    // vertexShader: 頂点シェーダーのコード
    vertexShader: `
        varying vec2 vUv; // フラグメントシェーダーに渡すためのUV座標
        void main(){
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算

            vUv = uv; // UV座標を設定
        }
    `,
    // fragmentShader: フラグメントシェーダーのコード
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform vec3 uTint; //

        varying vec2 vUv; // 頂点シェーダーから受け取ったUV座標
        void main(){
            vec4 color = texture2D(tDiffuse, vUv); // UV座標に基づいてテクスチャから色を取得
            color.rgb += uTint; //元のテクスチャの色にuTintの値を加算
            gl_FragColor = color; // 取得した色をフラグメントの色として設定
        }
    `
}

// TintShaderをポストプロセッシングパスとして作成
const tintPass = new ShaderPass(TintShader)

//uTint.valueに新しいTHREE.Vector3オブジェクトを設定
tintPass.material.uniforms.uTint.value = new THREE.Vector3(-0.5,0.5,0)

// effectComposerにtintPassを追加
effectComposer.addPass(tintPass)

変位エフェクト
ジオメトリの頂点の位置を変更することで、物体の表面に目に見える変形を作り出す
*GammaCorrectionShaderの後、アンチエイリアシングの前に実装する

// 変位エフェクト用のシェーダー定義
const DisplacementShader = {
    uniforms: {
        tDiffuse: { value: null }, // 元の画像テクスチャ
        uTime: { value: null }     // アニメーション用の時間変数
    },
    vertexShader: `
        varying vec2 vUv; // UV座標をフラグメントシェーダーに渡すための変数

        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算
            vUv = uv; // UV座標を設定
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform float uTime;        // アニメーション用の時間変数
        varying vec2 vUv;           // 頂点シェーダーから受け取ったUV座標

        void main() {
            // 新しいUV座標を計算(Y座標にサイン波を加えて変位させる)
            vec2 newUv = vec2(
                vUv.x,
                vUv.y + sin(vUv.x * 10.0 + uTime) * 0.05
            );
            vec4 color = texture2D(tDiffuse, newUv); // 変位したUV座標でテクスチャから色を取得

            gl_FragColor = color; // フラグメントの色として設定
        }
    `
}

// 変位エフェクト用のパスを作成し、エフェクトコンポーザーに追加
const displacementPass = new ShaderPass(DisplacementShader);
effectComposer.addPass(displacementPass);

// アニメーション時は関数内で
displacementPass.material.uniforms.uTime.value = elapsedTime; // 経過時間をパスの時間変数に設定

ノーマルマップテクスチャを使用して、UV座標に変位を加える

// 変位エフェクト用のシェーダー定義
const NormalMapDisplacementShader = {
    uniforms: {
        tDiffuse: { value: null },       // 元の画像テクスチャ
        uNormalMap: { value: null }      // 変位に使用するノーマルマップテクスチャ
    },
    vertexShader: `
        varying vec2 vUv; // UV座標をフラグメントシェーダーに渡すための変数

        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算
            vUv = uv; // UV座標を設定
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform sampler2D uNormalMap; // 変位に使用するノーマルマップテクスチャ

        varying vec2 vUv; // 頂点シェーダーから受け取ったUV座標

        void main() {
            vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0; // ノーマルマップから色情報を取得し変換
            vec2 newUv = vUv + normalColor.xy * 0.1; // 新しいUV座標を計算
            vec4 color = texture2D(tDiffuse, newUv); // 新しいUV座標でテクスチャから色を取得

            vec3 lightDirection = normalize(vec3(- 1.0, 1.0, 0.0)); // 光の方向
            float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0); // 光の強さを計算
            color.rgb += lightness * 2.0; // 光の強さを色に加算

            gl_FragColor = color; // フラグメントの色として設定
        }
    `
}

// ノーマルマップ変位エフェクトのパスを作成し、エフェクトコンポーザーに追加
const normalMapdisplacementPass = new ShaderPass(NormalMapDisplacementShader);
normalMapdisplacementPass.material.uniforms.uNormalMap.value = textureLoader.load('ノーマルマップのパス');
effectComposer.addPass(normalMapdisplacementPass);

シェーダーを使ってパーティクルをアニメーションする

*パーティクルの各頂点を個別にアニメーション化するのではなく、GPUを使用して頂点シェーダー内で直接アニメーション化する

そもそもパーティクルのマテリアルではPointsMaterialを使いますが、これをShaderMaterialに置き換えます
PointsMaterialの実装は:https://koro-koro.com/threejs-no3/#chapter-8

ShaderMaterialはsizeとsizeAttenuation(カメラからの距離に応じてどのように変化するかを制御)プロパティをサポートしていないので、シェーダー内で実装する必要があります
パーティクルはランダムなサイズを持ち、sizeAttenuationの機能を実装します

// パーティクルのジオメトリを作成
const particlesGeometry = new THREE.BufferGeometry();
const count = 2000; // パーティクルの数

const positions = new Float32Array(count * 3); // 位置データ vec3
const colors = new Float32Array(count * 3); // 色データ vec3
//ランダムなサイズ作成用
const scales = new Float32Array(count * 1); // スケール(サイズ) float

for (let i = 0; i < count * 3; i++) {
  positions[i] = (Math.random() - 0.5) * 2; // ランダムな位置
  colors[i] = Math.random(); // ランダムな色
}

//パーティクルの数だけランダムな値を格納
for (let i = 0; i < count; i++) {
  scales[i] = Math.random(); // ランダムなスケール
}

particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

// ジオメトリに属性に割り当てaScaleとしてカスタム属性をつける
particlesGeometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));

// シェーダーマテリアルの作成
const material = new THREE.ShaderMaterial({
  uniforms: {
//パーティクルのサイズをuniformsに格納
    uSize: { value: 30 }, 
  },
  depthWrite: false, // 深度バッファへの書き込みを無効化(パーティクルが透明であるため)
  blending: THREE.AdditiveBlending, // 加算ブレンディングを使用(光のような効果)
  vertexColors: true, // 頂点色を有効にする
  vertexShader: ``,  // 頂点シェーダーのソースコード
  fragmentShader: ``  // フラグメントシェーダーのソースコード
});

// パーティクルシステムの作成とシーンへの追加
const particles = new THREE.Points(particlesGeometry, material);
scene.add(particles);
uniform float uSize;  

/*
備考:position、color、normalのような一般的な属性は、
ShaderMaterialを使用する際に頂点シェーダーに自動的に含まれます
aScaleのようなカスタム属性を定義してシェーダーに追加する場合は
頂点シェーダー内でこれらのカスタム属性を明示的に宣言する
*/
attribute float aScale;
//フラグメントシェーダーに色情報(vColor)を渡す
varying vec3 vColor;

void main(){
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;
 //ランダムなサイズの実装
  gl_PointSize = uSize * aScale;
  /*
  sizeAttenuationの実装は
  パーティクルの基本サイズに
  1.0 / - viewPosition.zを掛ける
  */
  gl_PointSize *= (1.0 / - viewPosition.z);
  
  //vColorに色情報代入
  vColor = color;
}

デバイスのピクセル比に応じたレンダリング
ピクセル比が高いデバイスほど、同じサイズのパーティクルが小さく表示される
そこで、uniformsに格納したuSizeのvalueにレンダラーのピクセル比を掛けます
*注意:レンダラーのインスタンス化の後にマテリアル定義すること

 const material = new THREE.ShaderMaterial({
     uniforms:{
        uSize: { value: 30 * renderer.getPixelRatio()},
    },  
   //...省略
   })

Three.jsのフラグメントシェーダーでは、gl_PointCoord という特別な組み込み変数を使って、パーティクルのUV座標に直接アクセスできます
gl_PointCoord を使って円盤状、拡散点、光点などのパターンをパーティクルに描画することが可能

  void main(){
      gl_FragColor = vec4(gl_PointCoord, 1.0, 1.0); 
  }
void main(){
    //円盤状
   // float strength = distance(gl_PointCoord, vec2(0.5));
   // strength = step(0.5, strength);
   // strength = 1.0 - strength;
    
    // 拡散点
    float strength = distance(gl_PointCoord, vec2(0.5));
    strength *= 2.0;
    strength = 1.0 - strength;
    
    //光点
    // float strength = distance(gl_PointCoord, vec2(0.5));
    // strength = 1.0 - strength;
    // strength = pow(strength, 10.0);
    
    gl_FragColor = vec4(vec3(strength), 1.0);
  }

頂点シェーダーからフラグメントシェーダーに色情報(vColor)を受け取り
「mix関数」を使用して、黒色とvColorをstrength に基づいて混合したcolorを
gl_FragColorに割り当てる

//vColorを宣言  
varying vec3 vColor;

void main(){
  //...省略
  
  //黒色と頂点の色を混合して最終的な色を生成
    vec3 color = mix(vec3(0.0), vColor, strength);

    gl_FragColor = vec4(color, 1.0);  
  }

頂点シェーダー内でアニメーション
uTimeというユニフォームをTHREE.ShaderMaterialのuniformsに追加
アニメーション関数内でその値を更新
頂点シェーダーで、modelPositionをuTimeを使って動かします

 const material = new THREE.ShaderMaterial({
     uniforms:{
        uSize: { value: 30 * renderer.getPixelRatio()},
        uTime: { value: 0 }
    },  
    //...省略
 })

//アニメーション関数
const clock = new THREE.Clock()
const tick = () =>{
    const elapsedTime = clock.getElapsedTime() 
  // uTimeを更新
    material.uniforms.uTime.value = elapsedTime
  //...省略
}
uniform float uSize;  
//経過時間
uniform float uTime;
 //...省略
void main(){
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);

//アニメーションの実装部分はここから
   float posZ = fract(modelPosition.z + uTime * 0.3); // fract与えられた数値の小数部分
   modelPosition.z += posZ ;   // z軸の位置をずらす
//ここまで

  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;

  //...省略
}

パーティクルは、カメラの視野の前後にランダムに散らばっている
パーティクルの位置を時間経過とともに微妙に変化させる
パーティクルのZ座標は、時間の経過によって連続的に常に0から1の範囲内で変動
実際には限られた範囲内で動いているだけですが、カメラに向かってくるような錯覚のアニメーション


備考:既存のマテリアルを改変することもできる

Three.jsの組み込みマテリアル(この場合はMeshStandardMaterial)は、光源の取り扱い、環境マッピング、物理ベースのレンダリング、テクスチャタイプなど、複雑な機能をゼロから再構築するのは時間がかかるため、既存のマテリアルをベースにThree.jsのフックを使って、シェーダーがコンパイルされる前にカスタムコードを注入することができる

material.onBeforeCompile = (shader) =>{
    console.log(shader)
}

頂点シェーダー(vertexShader)、フラグメントシェーダー(fragmentShader)、ユニフォーム(uniforms)にアクセスし、これらを変更することが可能