リンクをコピーしました

ReactThree.js で、ポストプロセス(ポストエフェクト)カスタムシェーダー を扱う方法をまとめてみました。

僕はシェーダーを扱っていましたが、ウェブで扱うのはほぼ初めてだったのでまとめてみました。

@react-three/postprocessing のビルトインエフェクトの使い方と、
postprocessingEffect クラスを継承したカスタムシェーダー(ワールドノーマル可視化)の実装までを紹介します。

デモ用リポジトリとテスター用ページへのリンクも掲載しています。(お気軽に遊んでみてください)

この記事でわかること

  • @react-three/postprocessing でビルトインエフェクトを使う方法
  • カスタムシェーダーの 4 ステップ(Effect 継承 → React でラップ → GLSL 記述 → EffectComposer に追加)
  • NormalPassEffectComposerContext の役割
  • ハマりやすいポイント(View Space → World Space の変換、React 19 で動かないビルトインなど)

リポジトリ・デモ

開発環境・技術スタック

  • macOS Sequoia 15.5 / VS Code / Node.js 20+ / npm
  • React 19.1.1, Vite 7.1.2, Three.js 0.179.1
  • @react-three/fiber, @react-three/drei, @react-three/postprocessing, postprocessing
  • TailwindCSS 4, Leva(パラメータ調整 UI)

セットアップ

git clone https://github.com/testkun08080/react-postprocess-tester.git
cd react-postprocess-tester
npm install
npm run dev

ブラウザで http://localhost:5173 にアクセスします。
Leva でエフェクトをリアルタイムに調整したり出来ると思います。

サンプル画面(Levaでエフェクトを調整)

ビルトインエフェクトの使い方

@react-three/postprocessing には 20 種類以上のビルトインエフェクトがあります。

基本的な使い方

import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";

function PostEffects() {
  return (
    <EffectComposer>
      <Bloom intensity={1.0} luminanceThreshold={0.9} />
      <Vignette offset={0.5} darkness={0.5} />
    </EffectComposer>
  );
}

主なビルトイン: Bloom, DepthOfField, ChromaticAberration, Glitch, Vignette, SSAO / N8AO, ToneMapping などです。
Leva でパラメータをバインドすると調整しやすくなります。


カスタムシェーダーの作り方(ワールドノーマル可視化)

個人的にワールド変換されたノーマルマップをしようしたかったので、
postprocessingEffect を継承し、法線バッファを読んでワールドノーマルを可視化するカスタムシェーダーを追加します。

1. Effect クラスを継承したクラスを作成

// SimpleCheckNormalEffect.jsx
import { Effect } from "postprocessing";
import { Uniform, Matrix4 } from "three";
import { EffectComposerContext } from "@react-three/postprocessing";
import { checkNormalShader } from "./shaders/index.js";

class SimpleCheckNormalEffectImpl extends Effect {
  constructor({ normalBuffer, mode = 0, useWorldSpace = true }) {
    super("SimpleCheckNormalEffect", checkNormalShader, {
      uniforms: new Map([
        ["normalBuffer", new Uniform(normalBuffer)],
        ["uMode", new Uniform(mode)],
        ["uUseWorldSpace", new Uniform(useWorldSpace)],
        ["cameraMatrixWorld", new Uniform(new Matrix4())],
        ["viewMatrix", new Uniform(new Matrix4())],
        // ...
      ]),
    });
  }
  set mode(value) {
    this.uniforms.get("uMode").value = value;
  }
  set useWorldSpace(value) {
    this.uniforms.get("uUseWorldSpace").value = value;
  }
  // camera matrix の setter も同様に定義
}

2. React コンポーネントでラップ

forwardRefuseContext(EffectComposerContext)normalPasscamera を取得し、useMemo で Effect インスタンスを作成。
useEffect でカメラ行列や props を渡します。

export const SimpleCheckNormalEffect = forwardRef((props, ref) => {
  const { normalPass, camera } = useContext(EffectComposerContext);
  const effect = useMemo(
    () =>
      new SimpleCheckNormalEffectImpl({
        normalBuffer: normalPass?.texture,
        ...props,
      }),
    [normalPass, props],
  );
  useEffect(() => {
    if (camera) {
      effect.cameraMatrixWorld = camera.matrixWorld;
      effect.viewMatrix = camera.matrixWorldInverse;
      effect.projectionMatrix = camera.projectionMatrix;
      effect.inverseProjectionMatrix = camera.projectionMatrixInverse;
    }
  }, [effect, camera]);
  return <primitive ref={ref} object={effect} dispose={null} />;
});

3. GLSL で mainImage を記述

postprocessing では mainImage(inputColor, uv, outputColor) がピクセルごとに呼ばれます。
法線バッファを読んで View Space → World Space に変換し、法線を色で可視化します。

void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
  vec3 viewSpaceNormal = texture2D(normalBuffer, uv).xyz * 2.0 - 1.0;
  vec3 normal = uUseWorldSpace ? viewToWorldNormal(viewSpaceNormal) : viewSpaceNormal;
  vec3 normalColor = visualizeNormal(normal, uMode);
  outputColor = vec4(normalColor, inputColor.a);
}

4. EffectComposer に組み込む

<EffectComposer>
  <NormalPass />
  {worldNormalControls.enabled && (
    <SimpleCheckNormalEffect
      mode={worldNormalControls.mode}
      useWorldSpace={worldNormalControls.useWorldSpace}
    />
  )}
</EffectComposer>

法線バッファを使う場合は NormalPass を忘れずに追加します。

ワールドノーマル可視化の表示例


カスタムエフェクト実装のポイント

  • Effectnew Effect(name, fragmentShader, { uniforms, blendFunction, attributes }) で作成。
  • mainImage のシグネチャは (inputColor, uv, outputColor)
  • NormalPass を入れたうえで、EffectComposerContextnormalPass をカスタムエフェクトに渡す。

シェーダー一覧(サンプル)

このプロジェクトでは次のようなビルトイン・カスタムを試せます。スライダーでの比較はテスター用ページで確認してください。以下は元記事(Zenn)と同じく、エフェクトごとに小分けした一覧です(画像はローカル src/assets/react-postshader/ を参照)。

SMAA

絶妙な違いですけど、やっぱ有無では違いますね。

SMAA(OFF)

SMAA(ON)

Auto Focus

マニュアルでもマウスで試すことも可能です。

Auto Focus

SSAO

パラメーターを更新してもリアルタイムに反映されないバグがあります。そして、激重。

SSAO

n8ao (SSAO)

SSAO 使うなら、こちらを推奨します。使いやすいし、バグはないかなと思います。ソースレポ

n8ao

Bloom(ブルーム)

Bloom

Chromatic Aberration(色収差)

Chromatic Aberration

Wave Distortion(波状歪みエフェクト)

Wave Distortion

RGB Split(色収差)

RGB Split

Kaleidoscope(万華鏡エフェクト)

万華鏡っぽいやつです。

Kaleidoscope

Fractal Noise

定番ノイズで歪みを出せるやつです。

Fractal Noise

Glitch

壊れたモニターでよく使われるやつです。

Glitch

Pixelation(ドット化)

ドット風モザイクです。

Pixelation

Dot Screen

漫画・アメコミ風です。

Dot Screen

Grid

Grid

Scanline

Scanline

Outline

選択しているオブジェクトのみにアウトラインをつけたり、隠れていても見えるようにする UX/UI 用エフェクトです。(※内部では固定したオブジェクトを渡しています)

Outline

Edge Outline(エッジ検出エフェクト)

深度バッファと法線バッファを使用してエッジを検出し、アウトラインを描画します。トゥーンシェーディングやセル画風の表現に使えます。

Edge Outline

Sepia

Sepia

Brightness Contrast

Brightness Contrast

Color Dot

Color Dot

Average Color

Average Color

Color Shift

Color Shift

Tilt Shift

Tilt Shift

Tilt Shift 2

Tilt Shift 2

Water

Water

View Depth Visualization(デプスバッファ可視化)

カメラからの距離(深度)を白黒で視覚化します。デバッグやアート表現に有用で、近いほど黒く、遠いほど白く表示されます。

View Depth Visualization

Simple Check Normal(法線ベクトル可視化)

ノーマル可視化用のデバッグ用です。ビューノーマルと、ワールド空間用のノーマルを切り替えて見れます。

Simple Check Normal

Noise

雰囲気を与えるのにノイズは便利です。

Noise

Vignette

Vignette

Tonemap

Tonemap

LUT

アセットは引用元などから。地味に LUT テクスチャのインポートに手こずりました。

LUT

Ascii エフェクト

文字を使ってサイバーっぽくするやつです。

Ascii

シーン設定について

背景の非表示やライトの色などの簡易設定ができます。

シーン設定


ハマったポイント

1. View Space → World Space の変換

カメラの viewMatrix を使って法線をワールド空間に変換します。

vec4 worldNormal = vec4(viewNormal, 1.0) * viewMatrix;

2. React 19 で一部ビルトインが動作しない

Godrays, Lensflare, FXAA などは React 19 では正しく動かなかったため、今回のデモでは省いています。


まとめ

  • @react-three/postprocessingpostprocessing を使うと、ビルトインで 20 種類以上のポストエフェクトを手軽に使え、Effect を継承すれば カスタム GLSL シェーダー も組み込めます。
  • カスタムシェーダーは少し手間がかかりますが、このリポジトリのコードとテスター用ページをあわせて参考にして頂ければと思います。

関連リンク