🎥

Three.js - Post Processing

2024/05/09に公開

基本

仕組み

  • canvas ではなく render target と呼ばれるものに対してテクスチャを描画する
    • render target は Three.js 特有の用語で、多くの場合 buffer と呼ばれる
  • このテクスチャはカメラに相対する平面に適用され、画面全体を覆う
  • この平面に適当なシェーダーをもつマテリアルが適用されることで、 post-processing の効果(Three.js では passes と呼ぶ)が与えられる
  • 複数の passes を適用するとき、ひとつの render target のテクスチャへの読み書きは同時にはできないので、2つの render target を交互に使って描画する
    • これを ping pong buffering という
  • 最後の pass を適用したテクスチャは render target ではなく canvas に描画され、画面に表示される

EffectComposer

import {
  DotScreenPass,
  EffectComposer,
  GLTFLoader,
  OrbitControls,
  RenderPass,
} from "three/examples/jsm/Addons.js"

// ...

// Renderer
const renderer = new THREE.WebGLRenderer({ canvas })
renderer.setSize(width, height)    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

// Post processing
const effectComposer = new EffectComposer(renderer)
effectComposer.setSize(width, height)    effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

const renderPass = new RenderPass(scene, camera)
const dotScreenPass = new DotScreenPass()

effectComposer.addPass(renderPass)
effectComposer.addPass(dotScreenPass)

const tick = () => {
  effectComposer.render() // renderer ではなく effectComposer
  requestAnimationFrame(tick)
}
tick()

Passes

RenderPass

  • renderer の出力を最初の入力とする
  • 多くの場合この pass から始まる
const renderPass = new RenderPass(scene, camera)

DotScreenPass

  • 白黒のラスタライズ
const dotScreenPass = new DotScreenPass()

GlitchPass

  • 画面が荒れる
  • goWildでずっと荒れる
const glitchPass = new GlitchPass()
glitchPass.goWild = true

ShaderPass

  • ShaderPassにシェーダーを渡すことで実現する pass もある

RGBShiftPass

const rgbShiftPass = new ShaderPass(RGBShiftShader)

GammaCorrectionPass

  • カラースペースを SRGB にする
    • レンダラーのカラースペースは render target には適用されない
  • ガンマ色補正について
  • 最後の方に適用すること!
    • でもアンチエイリアスの pass よりは前
const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader)

UnrealBloomPass

  • グレアやライトセーバーっぽい輝き(Light glow)を与える
  • パラメータ
    • strength: 強さ
    • radius: 輝きがどのくらい遠くまで拡散するか
    • threshold: どのくらいの明るさから glow し始めるか
const unrealBloomPass = new UnrealBloomPass()
unrealBloomPass.strength = 0.3
unrealBloomPass.radius = 1
unrealBloomPass.threshold = 0.6

カスタム pass を作る

  • カスタム pass = カスタムシェーダーを ShaderPass にわたしたもの

TintPass

  • 色調補正シェーダー
  • uniformとしてその時点での render target がもつテクスチャがtDiffuseに入る
    • 値は自動的に入るので、最初はnullでいい
  • uTintは RGB 各色どれだけ補正するかの値をもつベクトルだが、再利用性のためにシェーダーの内部では値を設定しない
  • vUvvaryingとして与えられる
  • ピクセルのrgbavec4 color = texture2D(tDiffuse, vUv);で取り出せる
const TintShader = {
  uniforms: {
    tDiffuse: { value: null },
    uTint: { value: null },
  },
  vertexShader: `
    varying vec2 vUv;

    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

      vUv = uv;
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform vec3 uTint;
    varying vec2 vUv;

    void main() {
        vec4 color = texture2D(tDiffuse, vUv);
        color.rgb += uTint;

        gl_FragColor = color;
    }
  `,
}

const tintPass = new ShaderPass(TintShader)
tintPass.material.uniforms.uTint.value = new THREE.Vector3()
effectComposer.addPass(tintPass)

gui.add(tintPass.material.uniforms.uTint.value, 'x').min(- 1).max(1).step(0.001).name('red')
gui.add(tintPass.material.uniforms.uTint.value, 'y').min(- 1).max(1).step(0.001).name('green')
gui.add(tintPass.material.uniforms.uTint.value, 'z').min(- 1).max(1).step(0.001).name('blue')

DisplacementPass

  • サインカーブで波打つようなエフェクト
  • UV を移動させて、移動先のピクセルを取ってくるイメージ
const DisplacementShader = {
  uniforms: {
    tDiffuse: { value: null },
  },
  vertexShader: `
      varying vec2 vUv;

      void main() {
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

          vUv = uv;
      }
  `,
  fragmentShader: `
      uniform sampler2D tDiffuse;
      uniform float uTime;
      varying vec2 vUv;

      void main() {
          vec2 newUv = vec2(
              vUv.x,
              vUv.y + sin(vUv.x * 10.0) * 0.1
          );
          vec4 color = texture2D(tDiffuse, newUv);

          gl_FragColor = color;
      }
  `,
}
const displacementPass = new ShaderPass(DisplacementShader)
effectComposer.addPass(displacementPass)

InterfacePass

  • ガラス越し?に見ているかのようなエフェクト
  • uNormalMapテクスチャを貼り付ける
  • 別の方法があるっぽい
const InterfaceShader = {
  uniforms: {
    tDiffuse: { value: null },
    uNormalMap: { value: null },
  },
  vertexShader: `
      varying vec2 vUv;

      void main() {
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

          vUv = uv;
      }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform float uTime;
    uniform sampler2D uNormalMap;

    varying vec2 vUv;

    void main() {
      vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0;
      vec2 newUv = vUv + normalColor.xy * 0.1;
      vec4 color = texture2D(tDiffuse, newUv);

      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 interfacePass = new ShaderPass(InterfaceShader)
interfacePass.material.uniforms.uNormalMap.value = textureLoader.load("/textures/interfaceNormalMap.png")
effectComposer.addPass(interfacePass)

アンチエイリアス

  • EffectComposer はデフォルトのアンチエイリアスをサポートしていないので、他の方法でやる必要がある

WebGLRenderTarget をカスタマイズする方法

  • EffectComposer が使用する WebGLRenderTarget を書き換える
  • WebGL2 をサポートしていないブラウザでは適用されないが、そんなブラウザはもうほとんどないので気にしなくて良さそう
  • ブラウザがサポートしていない場合は単に無視されるだけなので、本当にデメリットなし
const renderTarget = new THREE.WebGLRenderTarget(
  undefined, // setSize で設定するのでなんでもいい
  undefined, // setSize で設定するのでなんでもいい
  {
    samples: renderer.getPixelRatio() === 1 ? 2 : 0,
  },
)
const effectComposer = new EffectComposer(renderer, renderTarget)

アンチエイリアスの pass を適用する方法

  • パフォーマンスが落ち、結果も少し異なる
  • Pass いろいろ
    • FXAA: パフォーマンス◎ 質△
    • SMAA: パフォーマンス◯ 質◯
    • SSAA: パフォーマンス△ 質◎
    • 他にもある
  • 最後の方に適用すること!
    • GammaCorrectionPass よりも!
  • WebGL2 をサポートしていない場合だけ有効にする
if(renderer.getPixelRatio() === 1 && !renderer.capabilities.isWebGL2) {
  const smaaPass = new SMAAPass()
  effectComposer.addPass(smaaPass)
}
  • ちなみに WebGL1 での挙動は renderer を WebGL1Renderer に差し替えることで確認できる
const renderer = new WebGL1Renderer({
  // ...
})

Discussion