シェーダ・ポストプロセス

標準マテリアルでは届かない表現に行くための脱出口。ShaderMaterial で自作シェーダ、 EffectComposer でレンダリング後の画面処理(Bloom 等)。

シェーダの基本

シェーダは頂点シェーダ(vertex)フラグメントシェーダ(fragment)の2段。

  1. vertex shader: 各頂点の位置を gl_Position に書き出す。属性(attribute)を varying として fragment へ渡す。
  2. 頂点が3つ集まると三角形になり、ラスタライズされる(三角形の中の各ピクセルへ補間)。
  3. fragment shader: 各ピクセル の色を gl_FragColor(または out vec4 outColor)に書く。

3 種類の変数

ShaderMaterial

Three.js が共通の uniforms(カメラ行列、モデル行列、ライト等)を自動で渡してくれるRawShaderMaterial はこれを渡さない素のバージョン。

最小の自作 shader
const mat = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },
    uColor: { value: new THREE.Color("#7dd3fc") },
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform float uTime;
    uniform vec3  uColor;
    varying vec2 vUv;
    void main() {
      float wave = sin(vUv.x * 10.0 + uTime) * 0.5 + 0.5;
      gl_FragColor = vec4(uColor * wave, 1.0);
    }
  `,
})

renderer.setAnimationLoop(() => {
  mat.uniforms.uTime.value += 0.016
  renderer.render(scene, camera)
})

Three.js が自動で渡す uniforms の主なもの:

頂点 attribute: position / normal / uv など(自動)。

典型パターン: 画面いっぱいの shader

フルスクリーンでシェーダ遊びをするなら、画面いっぱいの板(PlaneGeometry)に Orthographic カメラで ShaderMaterial を貼るのが定番。

const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
const geom = new THREE.PlaneGeometry(2, 2)
const quad = new THREE.Mesh(geom, mat)
scene.add(quad)

onBeforeCompile — 既存マテリアルに「足す」

MeshStandardMaterial の挙動を全部書き直したくないけど、少しだけシェーダを差し込みたい時。

const mat = new THREE.MeshStandardMaterial({ color: "white" })

mat.userData.uTime = { value: 0 }
mat.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = mat.userData.uTime

  // 既存コードに前付け
  shader.vertexShader = shader.vertexShader.replace(
    "#include <common>",
    `#include <common>
     uniform float uTime;`
  )

  // 頂点を波打たせる
  shader.vertexShader = shader.vertexShader.replace(
    "#include <begin_vertex>",
    `#include <begin_vertex>
     transformed.y += sin(transformed.x * 4.0 + uTime) * 0.1;`
  )
}

renderer.setAnimationLoop(() => {
  mat.userData.uTime.value += 0.016
  renderer.render(scene, camera)
})

chunk 名(#include <...>は Three.js のソース(renderers/shaders/ShaderChunk/)を参考にする。 よく使うのは:

テクスチャを使う

const mat = new THREE.ShaderMaterial({
  uniforms: {
    uTex: { value: texture },
  },
  vertexShader: `varying vec2 vUv; void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }`,
  fragmentShader: `uniform sampler2D uTex; varying vec2 vUv;
  void main() {
    gl_FragColor = texture2D(uTex, vUv);
  }`,
})

頂点カラーや属性を渡す

const colors = new Float32Array(N * 3)
geometry.setAttribute("aColor", new THREE.BufferAttribute(colors, 3))

// shader で
// attribute vec3 aColor;
// varying vec3 vColor;
// void main() { vColor = aColor; ... }

ポストプロセス(EffectComposer

画面全体に対するエフェクト(Bloom、DoF、SSAO、Outline、FXAA 等)。 EffectComposer がレンダラの代わりに走り、パスの連鎖を実行する。

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"
import { RenderPass }     from "three/addons/postprocessing/RenderPass.js"
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"
import { OutputPass }      from "three/addons/postprocessing/OutputPass.js"

const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(new UnrealBloomPass(
  new THREE.Vector2(innerWidth, innerHeight),
  0.6,   // strength
  0.4,   // radius
  0.85,  // threshold
))
composer.addPass(new OutputPass())   // tonemap + sRGB を最終に

renderer.setAnimationLoop(() => {
  composer.render()                  // ← renderer.render の代わり
})

window.addEventListener("resize", () => {
  composer.setSize(innerWidth, innerHeight)
})
最後は OutputPass

ポストプロセスを使う時、最終 pass で OutputPass を入れないと tonemap や sRGB 変換が漏れて色がおかしくなる。

主な addon パス

パス用途
RenderPassシーンを描画する。最初に必須
UnrealBloomPassブルーム(光が滲む)
OutlinePass選択中のオブジェクトを縁取り
SSAOPass / SSAARenderPass環境遮蔽 / アンチエイリアス
BokehPass被写界深度(DoF)
GlitchPassグリッチ演出
FilmPassフィルム調(ノイズ・スキャンライン)
ShaderPass(shader)自作シェーダを 1 パスとして差し込む
OutputPass最終のトーンマップ・sRGB変換(必須)

自作 ShaderPass

import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"

const myShader = {
  uniforms: {
    tDiffuse: { value: null },     // 前パスの出力が自動で入る
    uStrength: { value: 0.5 },
  },
  vertexShader: `varying vec2 vUv;
    void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
  fragmentShader: `uniform sampler2D tDiffuse;
    uniform float uStrength;
    varying vec2 vUv;
    void main() {
      vec4 c = texture2D(tDiffuse, vUv);
      // モノクロ寄せ
      float l = dot(c.rgb, vec3(0.299, 0.587, 0.114));
      gl_FragColor = vec4(mix(c.rgb, vec3(l), uStrength), c.a);
    }`,
}

composer.addPass(new ShaderPass(myShader))

postprocessing パッケージ(高機能)

addons の EffectComposer より高機能で速い実装が postprocessing パッケージにある。 Effect 単位で合成するため、複数エフェクトを 1 パスにマージできて速い。 R3F なら @react-three/postprocessing

レンダーターゲット(FBO)

画面以外の場所(テクスチャ)にレンダリングして、それを別 mesh のテクスチャに使う。 ミニマップ・反射・カスタム post effect等に必須。

const target = new THREE.WebGLRenderTarget(512, 512, {
  colorSpace: THREE.SRGBColorSpace,
})

renderer.setRenderTarget(target)
renderer.render(sceneA, cameraA)
renderer.setRenderTarget(null)            // 元に戻す

// target.texture を別マテリアルの map に
displayMaterial.map = target.texture

シェーダ学習リソース

WebGPU と TSL

最近の Three.js は WebGPURendererTSL(Three Shading Language)を持っている。 GLSL ではなく JS で書ける式ベースの shader DSL で、WebGL/WebGPU 両対応。 まだ実験的な部分があるので、本番採用は機能をピンポイントで確認してから。