ReactとThree.jsで、ポストプロセスとカスタムシェーダーを扱ってみました。
シェーダー自体は触っていましたが、Webで組むのはほぼ初めてだったので、そのときの実装を残しています。
中心にあるのは、@react-three/postprocessing のビルトイン利用と、postprocessing の Effect を継承したワールドノーマル可視化です。
環境
- 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でパラメータをいじれます。

ビルトインエフェクト
基本は 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コンポーネントでラップする
forwardRef と useContext(EffectComposerContext) で normalPass と camera を取り、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 を忘れずに追加します。

実装時に意識したのは次です。
Effectはnew Effect(name, fragmentShader, { uniforms, blendFunction, attributes })mainImageのシグネチャは(inputColor, uv, outputColor)- NormalPassを入れたうえで、
EffectComposerContextのnormalPassを渡す
ハマったポイント
View Space → World Space
カメラの viewMatrix を使って法線をワールド空間へ変換します。
vec4 worldNormal = vec4(viewNormal, 1.0) * viewMatrix;React 19で動かないビルトイン
Godrays、Lensflare、FXAAなどはReact 19では正しく動かなかったため、今回のデモでは省いています。
SSAOまわり
SSAOは、パラメータを更新してもリアルタイムに反映されない挙動があり、しかも重いです。
使うならn8aoの方が扱いやすかったです。
テスターで試せるエフェクト
詳細な見た目の比較はテスター用ページ側に寄せています。
実際に試せるエフェクトを、画像と一緒に一覧で残します。
SMAA
絶妙な違いですが、有無で見た目が変わります。


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

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

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

Bloom(ブルーム)

Chromatic Aberration(色収差)

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

RGB Split(色収差)

Kaleidoscope(万華鏡エフェクト)
万華鏡のような見た目になります。

Fractal Noise
ノイズを使って歪みを出せます。

Glitch
壊れたモニターのような表現に使えます。

Pixelation(ドット化)
ドット風のモザイクです。

Dot Screen
漫画やアメコミのような見た目になります。

Grid

Scanline

Outline
選択したオブジェクトだけにアウトラインを付けたり、隠れていても見えるようにしたりするUX/UI向けのエフェクトです。
このデモでは固定したオブジェクトを渡しています。

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

Sepia

Brightness Contrast

Color Dot

Average Color

Color Shift

Tilt Shift

Tilt Shift 2

Water

View Depth Visualization(デプスバッファ可視化)
カメラからの距離を白黒で可視化します。
近いほど黒く、遠いほど白く表示されます。

Simple Check Normal(法線ベクトル可視化)
法線を確認するためのデバッグ表示です。
ビューノーマルとワールド空間のノーマルを切り替えられます。

Noise
雰囲気を加えたいときに便利です。

Vignette

Tonemap

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

Asciiエフェクト
文字を使ったサイバー風の表現です。

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

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