リンクをコピーしました

ReactとThree.jsで、ポストプロセスとカスタムシェーダーを扱ってみました。
シェーダー自体は触っていましたが、Webで組むのはほぼ初めてだったので、そのときの実装を残しています。

中心にあるのは、@react-three/postprocessing のビルトイン利用と、postprocessingEffect を継承したワールドノーマル可視化です。

環境

  • 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
git clone https://github.com/testkun08080/react-postprocess-tester.git
cd react-postprocess-tester
npm install
npm run dev

ブラウザで http://localhost:5173 を開き、Levaでパラメータをいじれます。

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

ビルトインエフェクト

基本は EffectComposer の中に置くだけです。

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でパラメータをバインドすると調整しやすいです。


カスタムシェーダー:ワールドノーマル可視化

個人的に、ワールド空間へ変換したノーマルを見たかったので、Effect を継承して法線バッファを読むシェーダーを追加しました。

1. Effectクラスを継承する

// SimpleCheckNormalEffect.jsx
import { Effect } from "postprocessing";
import { Uniform, Matrix4 } from "three";
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 を渡す

ハマったポイント

View Space → World Space

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

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

React 19で動かないビルトイン

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

SSAOまわり

SSAOは、パラメータを更新してもリアルタイムに反映されない挙動があり、しかも重いです。
使うならn8aoの方が扱いやすかったです。


テスターで試せるエフェクト

詳細な見た目の比較はテスター用ページ側に寄せています。
実際に試せるエフェクトを、画像と一緒に一覧で残します。

SMAA

絶妙な違いですが、有無で見た目が変わります。

SMAA(OFF)
SMAA(ON)

Auto Focus

マニュアル操作のほか、マウスでも試せます。

Auto Focus

SSAO

パラメーターを更新してもリアルタイムに反映されない挙動があり、かなり重いです。

SSAO

n8ao(SSAO)

SSAOを使うなら、個人的にはn8aoの方が扱いやすかったです。

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

アセットはn8aoのリポジトリなどを参照しました。
LUTテクスチャのインポートには少し手こずりました。

LUT

Asciiエフェクト

文字を使ったサイバー風の表現です。

Ascii

シーン設定

背景の表示やライトの色など、簡単なシーン設定も変更できます。

シーン設定

まとめ

@react-three/postprocessingpostprocessing を使うと、ビルトインのポストエフェクトに加えて、Effect 継承でカスタムGLSLも組めます。
カスタム側は少し手間がかかりますが、リポジトリテスターをあわせて見ると追いやすいと思います。

関連リンク

作者が作ったツール