0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポストエフェクト祭り。React/ポストプロセスを触ってカスタムシェーダーの作り方をざっくり理解してみよう

Posted at

はじめに

もう九月か。。。
独り立ちして約1年ぐらい経とうとしている中、ポートフォリオページがまだUnder Maintenanceなので、
重い腰を上げて、ポートフォリオページを作成することにしました。
そこで、大好きなシェーダーをポートフォリオにもモリモリ使おうと思い立ったのがきっかけです。
(ゲームだとそうは行かないので)

この記事では以下モジュール

で用意されている一般的なポストエフェクトの実装と、カスタムシェーダーを使ったワールドノーマルを表示を行ってみます。
また、一般的なカスタムシェーダーをClaudeに手伝っておもらったので、載せておきます。

少し目次は長いですけど、こんなのもあるのかぁ程度にサンプルとしてみて使っていただけたらと思います。

レポ

テスター用ページ

開発環境

  • macOS Sequoia 15.5
  • VsCode
  • Node.js 20+
  • npm

使用技術スタック

コアライブラリ

  • React 19.1.1 - UIフレームワーク
  • Vite 7.1.2 - ビルドツール
  • Three.js 0.179.1 - 3Dレンダリングエンジン
  • @react-three/fiber 9.3.0 - ReactとThree.jsのブリッジ
  • @react-three/drei 10.7.4 - React Three Fiberヘルパー集
  • @react-three/postprocessing 3.0.4 - ポストプロセスReactラッパー
  • postprocessing 6.37.7 - ポストプロセスエフェクトライブラリ

UI・ツール

  • TailwindCSS 4.1.12 - スタイリング
  • Leva 0.10.0 - リアルタイムパラメーター調整UI

プロジェクト構造

react-postprocess-tester/
├── src/
│   ├── main.jsx                      # エントリーポイント
│   ├── App.jsx                       # ルーター設定
│   ├── components/
│   │   ├── Navigation.jsx            # ナビゲーション
│   │   ├── Scene.jsx                 # 3Dシーン設定
│   │   └── postprocessing/           # ポストプロセスエフェクト
│   │       ├── PostEffects.jsx       # エフェクトコントローラー
│   │       ├── WaveEffect.jsx        # 波状歪みエフェクト
│   │       ├── RGBSplitEffect.jsx    # 色収差エフェクト
│   │       ├── KaleidoscopeEffect.jsx # 万華鏡エフェクト
│   │       ├── ColorShiftEffect.jsx   # HSV色変換
│   │       ├── FractalNoiseEffect.jsx # フラクタルノイズ
│   │       ├── EdgeOutlineEffect.jsx  # エッジ検出
│   │       ├── LensFlareEffect.jsx    # レンズフレア
│   │       ├── ViewDepthVisualization.jsx    # デプスバッファ可視化
│   │       ├── SimpleCheckNormalEffect.jsx   # 法線ベクトル可視化
│   │       └── shaders/              # GLSLシェーダーファイル
│   │           ├── wave.glsl
│   │           ├── rgbSplit.glsl
│   │           ├── kaleidoscope.glsl
│   │           ├── colorShift.glsl
│   │           ├── fractalNoise.glsl
│   │           ├── edgeOutline.glsl
│   │           ├── lensFlare.glsl
│   │           ├── viewDepth.glsl
│   │           ├── worldNormal.glsl
│   │           └── index.js          # シェーダーエクスポート
│   └── pages/
│       └── PostEffectsSample.jsx     # メインデモページ
├── package.json
├── vite.config.js
└── tailwind.config.js

セットアップ

1. プロジェクトのクローン

git clone https://github.com/testkun08080/react-postprocess-tester.git
cd react-postprocess-tester

2. 依存関係のインストール

npm install

3. 開発サーバー起動

npm run dev

ブラウザで http://localhost:5173 にアクセスできるはずです。

サンプル画面
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でパラメーター調整

Levaを使ってリアルタイムにパラメーターを調整できるようにします

import { useControls } from "leva";

const bloomControls = useControls("Bloom", {
  enabled: false,
  intensity: { value: 1.0, min: 0, max: 3, step: 0.01 },
  luminanceThreshold: { value: 0.9, min: 0, max: 1, step: 0.01 },
});

カスタムシェーダーの作り方

ここからが本題です。カスタムシェーダーを作ってワールドノーマルを可視化してみます。

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

postprocessingライブラリのEffectクラスを継承します。

// SimpleCheckNormalEffect.jsx
import { forwardRef, useMemo, useEffect, useContext } from "react";
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())],
        ["projectionMatrix", new Uniform(new Matrix4())],
        ["inverseProjectionMatrix", new Uniform(new Matrix4())],
      ]),
    });
  }

  // Setter for mode
  set mode(value) {
    this.uniforms.get("uMode").value = value;
  }

  set useWorldSpace(value) {
    this.uniforms.get("uUseWorldSpace").value = value;
  }

  // Camera matrix setters
  set cameraMatrixWorld(matrix) {
    this.uniforms.get("cameraMatrixWorld").value = matrix;
  }

  set viewMatrix(matrix) {
    this.uniforms.get("viewMatrix").value = matrix;
  }

  set projectionMatrix(matrix) {
    this.uniforms.get("projectionMatrix").value.copy(matrix);
  }

  set inverseProjectionMatrix(matrix) {
    this.uniforms.get("inverseProjectionMatrix").value.copy(matrix);
  }
}

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

React Three Fiberと統合するためにforwardRefを使います。

export const SimpleCheckNormalEffect = forwardRef((props, ref) => {
  const { normalPass, camera } = useContext(EffectComposerContext);

  const effect = useMemo(
    () =>
      new SimpleCheckNormalEffectImpl({
        normalBuffer: normalPass?.texture,
        ...props,
      }),
    [normalPass, props]
  );

  // Update camera matrices
  useEffect(() => {
    if (camera) {
      effect.cameraMatrixWorld = camera.matrixWorld;
      effect.viewMatrix = camera.matrixWorldInverse;
      effect.projectionMatrix = camera.projectionMatrix;
      effect.inverseProjectionMatrix = camera.projectionMatrixInverse;
    }
  }, [effect, camera]);

  // Update other properties
  useEffect(() => {
    if (props.mode !== undefined) effect.mode = props.mode;
    if (props.useWorldSpace !== undefined)
      effect.useWorldSpace = props.useWorldSpace;
  }, [effect, props]);

  return <primitive ref={ref} object={effect} dispose={null} />;
});

3. GLSLシェーダーを記述

// worldNormal.glsl
uniform sampler2D normalBuffer;
uniform int uMode;
uniform bool uUseWorldSpace;
uniform mat4 cameraMatrixWorld;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 inverseProjectionMatrix;

// Convert view space normal to world space
vec3 viewToWorldNormal(vec3 viewNormal) {
  vec4 worldNormal = vec4(viewNormal, 1.0) * viewMatrix;
  return worldNormal.xyz;
}

// Normal visualization modes
vec3 visualizeNormal(vec3 normal, int mode) {
  vec3 color;
  vec3 n = normalize(normal);

  if (mode == 0) {
    // Normal RGB mode
    color = n * 0.5 + 0.5; // Remap from [-1,1] to [0,1]
  } else if (mode == 1) {
    // Red channel only (X component)
    float x = n.x * 0.5 + 0.5;
    color = vec3(x, 0.0, 0.0);
  } else if (mode == 2) {
    // Green channel only (Y component)
    float y = n.y * 0.5 + 0.5;
    color = vec3(0.0, y, 0.0);
  } else {
    // Blue channel only (Z component)
    float z = n.z * 0.5 + 0.5;
    color = vec3(0.0, 0.0, z);
  }

  return color;
}

void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
  // Read normal from normal buffer (view space)
  vec3 viewSpaceNormal = texture2D(normalBuffer, uv).xyz;
  viewSpaceNormal = viewSpaceNormal * 2.0 - 1.0; // Remap to [-1,1]

  // Choose between view space and world space
  vec3 normal;
  if (uUseWorldSpace) {
    normal = viewToWorldNormal(viewSpaceNormal);
  } else {
    normal = viewSpaceNormal;
  }

  // Generate visualization color
  vec3 normalColor = visualizeNormal(normal, uMode);

  outputColor = vec4(normalColor, inputColor.a);
}

4. EffectComposerに組み込む

import { SimpleCheckNormalEffect } from "./SimpleCheckNormalEffect.jsx";

const worldNormalControls = useControls("World Normal", {
  enabled: false,
  mode: { value: 0, min: 0, max: 3, step: 1 },
  useWorldSpace: true,
});

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

ワールドノーマル表示
ワールドノーマルが色として可視化されます


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

Effectクラスの基本構造

new Effect(name, fragmentShader, {
  uniforms: new Map([
    ["uniformName", new Uniform(value)],
  ]),
  blendFunction: BlendFunction.NORMAL,
  attributes: EffectAttribute.CONVOLUTION,
});

mainImage関数

GLSLシェーダーのmainImage関数が、各ピクセルに対して実行されます。

void mainImage(
  const in vec4 inputColor,  // 入力カラー
  const in vec2 uv,          // UV座標 (0.0 ~ 1.0)
  out vec4 outputColor       // 出力カラー
)

NormalPassの利用

法線バッファを使うにはNormalPassを有効にする必要があります。

<EffectComposer>
  <NormalPass />
  {/* Your effects here */}
</EffectComposer>

EffectComposerContextからnormalPassを取得できます。

const { normalPass, camera } = useContext(EffectComposerContext);

シェーダー一覧

このプロジェクトでは以下のカスタムシェーダーを確認できます。
スライドバーで画像比較がここでできればよかったんですけど、厳しいのでテスター用のページで見ていただければと思います。

SMAA

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

SMAA-ena
ON

SMAA-dis
OFF

Auto Focus

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

autofocus

SSAO

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

ssao

n8ao (SSAO)

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

n8ao

[ソースレポ]https://github.com/N8python/n8ao

Bloom(ブルーム)

bloom

Chromatic aberration(色収差)

chromatic

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

Wave Distortion Effect

RGB Split(色収差)

RGB Split Effect

Kaleidoscope(万華鏡エフェクト)

万華鏡っぽいやつ

Kaleidoscope Effect

Fractal Nois

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

Fractal Noise Effect

Glitch

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

glitch

Pixcelation (ドット化)

ドット風モザイク

pixcelation

Dot screen

漫画、アメコミ風

dot-s

Grid

grid

Scanline

scanline

Outline

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

outline

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

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

Edge Outline Effect

Sepia

sepia

Brightess contrast

bright-contrast

Color Dot

bright-contrast

Avarage Color

avarage-color

Avarage Color

avarage-color

Color Shift

color-shift

Tilt shift

tilt-shift

Tilt shift 2

tilt-shift2

Water

water

View Depth

深度可視化用のデバッグ用です

vdepth

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

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

vdepth

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

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

checknormal

Noise

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

checknormal

Vignette

vignette

Tonemap

tonemap

Lut

アセットはここから引用させていただきました。
地味にLUTテクスチャ達のインポートにテコづりました。

lut

Ascii エフェクト

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

ascii


ハマったポイント

1. View Space → World Spaceの変換

カメラのviewMatrixを使って座標変換します。

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

2. 一部ビルトインシェーダーがReact19では正しく動作しない

ビルトインのGodrays, Lensfrare, FXAAなどは正しく動作しなかったので、今回は省いています。

シーン設定について

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


まとめ

色々なすでにビルトインされているものもあって手軽に使えてありがたいです。
サンプルもshdertoyやthree.jsに色々落ちていたりするので、色々遊べます。

カスタムシェーダーもEffectクラスを継承するだけでいいんですが、パスの渡し方とかそこら辺が触ってみないと何とも言えない感じでした。
癖がわかれば、そのあとはスイスイ行けるかなぁと思います。

好評や色々な方が見られるようでしたら、工程をもっと細かく砕いてzennなどの本としてまとめてみようかと思います。

何かミスなどがあれば、コメントください〜
では!


関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?