シェーダ・ポストプロセス
標準マテリアルでは届かない表現に行くための脱出口。ShaderMaterial で自作シェーダ、 EffectComposer(@react-three/postprocessing)でレンダリング後の画面処理(Bloom 等)。
シェーダの基本
シェーダは頂点シェーダ(vertex)とフラグメントシェーダ(fragment)の2段構成。 基本概念は Three.js のシェーダ と同じ。R3F では JSX の中に書く。
<shaderMaterial> を直接書く
最もシンプル。uniforms / vertexShader / fragmentShader を props で渡す。
function Wave() {
const ref = useRef<THREE.ShaderMaterial>(null!)
useFrame(({ clock }) => {
if (ref.current) ref.current.uniforms.uTime.value = clock.getElapsedTime()
})
return (
<mesh>
<planeGeometry args={[2, 2, 64, 64]} />
<shaderMaterial
ref={ref}
uniforms={{
uTime: { value: 0 },
uColor: { value: new THREE.Color("#7dd3fc") },
}}
vertexShader={`
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
pos.z += sin(pos.x * 4.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`}
fragmentShader={`
uniform vec3 uColor;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(uColor * vUv.y, 1.0);
}
`}
/>
</mesh>
)
}
uniforms={{ ... }} を毎レンダーで新規生成すると、マテリアルが毎回作り直される恐れあり。
useMemo で固定するか、shaderMaterial ヘルパー(後述)を使う。
drei の shaderMaterial ヘルパー(推奨)
カスタムシェーダを JSX タグ化する強力なヘルパー。 uniforms が型推論される、メソッドが生える、ホットリロードに強い。
import { extend } from "@react-three/fiber"
import { shaderMaterial } from "@react-three/drei"
import * as THREE from "three"
const WaveMaterial = shaderMaterial(
// uniforms
{ uTime: 0, uColor: new THREE.Color("#7dd3fc") },
// vertex
/* glsl */ `
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
pos.z += sin(pos.x * 4.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
// fragment
/* glsl */ `
uniform vec3 uColor;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(uColor * vUv.y, 1.0);
}
`,
)
extend({ WaveMaterial })
declare module "@react-three/fiber" {
interface ThreeElements {
waveMaterial: any
}
}
function Wave() {
const ref = useRef<any>(null!)
useFrame((_, dt) => { if (ref.current) ref.current.uTime += dt })
return (
<mesh>
<planeGeometry args={[2, 2, 64, 64]} />
<waveMaterial ref={ref} uColor="hotpink" />
</mesh>
)
}
ポイント:
- uniforms はそのままprops として渡せる(
uColor="hotpink") - ref からuniforms に直接アクセスできる(
ref.current.uTime) extendで JSX 要素として登録
onBeforeCompile — 既存マテリアルに「足す」
MeshStandardMaterial の挙動を全部書き直したくないけど、少しだけシェーダを差し込みたい時。
ライティング・影・PBR を全部維持したまま頂点を歪めたり、色を加工したりできる。
function WavyStandard() {
const ref = useRef<THREE.MeshStandardMaterial>(null!)
const uniforms = useMemo(() => ({ uTime: { value: 0 } }), [])
useFrame(({ clock }) => {
uniforms.uTime.value = clock.getElapsedTime()
})
return (
<mesh>
<sphereGeometry args={[1, 64, 64]} />
<meshStandardMaterial
ref={ref}
color="#7dd3fc"
onBeforeCompile={(shader) => {
shader.uniforms.uTime = uniforms.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;`
)
}}
/>
</mesh>
)
}
テクスチャを uniform で渡す
const tex = useTexture("/wall.jpg")
const Mat = shaderMaterial(
{ uTex: null, uTime: 0 },
vertexShader,
/* glsl */ `
uniform sampler2D uTex;
uniform float uTime;
varying vec2 vUv;
void main() {
vec2 uv = vUv + vec2(uTime * 0.05, 0.0);
gl_FragColor = texture2D(uTex, uv);
}
`,
)
extend({ Mat })
<mesh>
<planeGeometry args={[2, 2]} />
<mat uTex={tex} />
</mesh>
頂点シェーダで attribute を使う
パーティクルや個別頂点情報を使う時:
function Particles({ count = 5000 }) {
const positions = useMemo(() => {
const arr = new Float32Array(count * 3)
for (let i = 0; i < count; i++) {
arr[i*3+0] = (Math.random() - 0.5) * 10
arr[i*3+1] = (Math.random() - 0.5) * 10
arr[i*3+2] = (Math.random() - 0.5) * 10
}
return arr
}, [count])
return (
<points>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
</bufferGeometry>
<shaderMaterial
vertexShader={`
void main() {
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mv;
gl_PointSize = 200.0 / -mv.z;
}
`}
fragmentShader={`
void main() {
float d = length(gl_PointCoord - vec2(0.5));
if (d > 0.5) discard;
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0 - d * 2.0);
}
`}
transparent
depthWrite={false}
/>
</points>
)
}
フルスクリーンシェーダ
画面全体に対する shader 遊び(procedural background や glow effect)。
function FullscreenShader() {
return (
<mesh position={[0, 0, -10]}>
<planeGeometry args={[20, 20]} />
<shaderMaterial
vertexShader={`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`}
fragmentShader={`
uniform float uTime;
varying vec2 vUv;
void main() {
vec2 uv = vUv * 2.0 - 1.0;
float d = length(uv) - 0.5 + sin(uTime) * 0.1;
vec3 color = mix(vec3(0.1), vec3(0.5, 0.8, 1.0), step(d, 0.0));
gl_FragColor = vec4(color, 1.0);
}
`}
uniforms={{ uTime: { value: 0 } }}
/>
</mesh>
)
}
ポストプロセス: @react-three/postprocessing
画面全体にエフェクト(Bloom / DoF / SSAO / Outline 等)。
Three.js の EffectComposer よりパフォーマンスがよい postprocessing パッケージを React でラップしたもの。
npm install @react-three/postprocessing postprocessing
基本
import { EffectComposer, Bloom, Vignette, DepthOfField } from "@react-three/postprocessing"
<Canvas>
<Scene />
<EffectComposer>
<Bloom intensity={0.6} luminanceThreshold={0.85} mipmapBlur />
<DepthOfField focusDistance={0.01} focalLength={0.05} bokehScale={3} />
<Vignette eskil={false} offset={0.1} darkness={0.6} />
</EffectComposer>
</Canvas>
主要な Effect
| Effect | 用途 |
|---|---|
<Bloom /> | 明るい部分を滲ませる(ネオン・発光) |
<DepthOfField /> | 被写界深度(ボケ) |
<Vignette /> | 周辺減光 |
<Noise /> | フィルムノイズ |
<ChromaticAberration /> | 色収差 |
<Glitch /> | グリッチ |
<Pixelation /> | ピクセル化 |
<ToneMapping /> | トーンマッピング差し替え |
<SSAO /> | 環境遮蔽(陰影) |
<SSR /> | スクリーンスペース反射 |
<Outline /> | 選択中のオブジェクトに縁取り |
<Selection /> + <Select /> | Outline ターゲット選択 |
<LUT /> | 3D LUT でカラーグレーディング |
<FXAA /> / <SMAA /> | アンチエイリアス |
<HueSaturation /> | 色相・彩度 |
<BrightnessContrast /> | 明度・コントラスト |
Outline でホバー
import { EffectComposer, Outline, Selection, Select } from "@react-three/postprocessing"
<EffectComposer autoClear={false}>
<Selection>
<Select enabled>
<HoverableObject />
</Select>
<Outline edgeStrength={5} pulseSpeed={0} visibleEdgeColor={0xffffff} />
</Selection>
</EffectComposer>
Bloom を選択的に当てる(Selective Bloom)
特定のオブジェクトだけ光らせたい時のテクニック。 emissive がしきい値を超えたものだけ Bloom する設定:
<EffectComposer>
<Bloom
intensity={1.5}
luminanceThreshold={1} {/* これより明るいものだけ滲む */}
luminanceSmoothing={0.025}
mipmapBlur
/>
</EffectComposer>
{/* 光らせたいメッシュは emissive を 1 超に */}
<mesh>
<sphereGeometry />
<meshStandardMaterial color="#000" emissive="#7dd3fc" emissiveIntensity={3} />
</mesh>
自作 ShaderPass を Effect として登録
自前 fragment shader を 1 つの post effect として乗せたい時。
import { Effect } from "postprocessing"
import { wrapEffect } from "@react-three/postprocessing"
class GrayscaleEffect extends Effect {
constructor() {
super(
"GrayscaleEffect",
/* glsl */ `
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
float l = dot(inputColor.rgb, vec3(0.299, 0.587, 0.114));
outputColor = vec4(vec3(l), inputColor.a);
}
`,
)
}
}
const Grayscale = wrapEffect(GrayscaleEffect)
<EffectComposer>
<Grayscale />
</EffectComposer>
RenderTarget(FBO)パターン
画面以外のテクスチャにレンダリングして、それを別 mesh に貼る。ミニマップ・反射・ピックアップ画面等。
import { useFBO } from "@react-three/drei"
function PortalView() {
const target = useFBO(512, 512)
const { scene, camera, gl } = useThree()
const otherCamera = useRef<THREE.PerspectiveCamera>(null!)
useFrame(() => {
gl.setRenderTarget(target)
gl.render(scene, otherCamera.current)
gl.setRenderTarget(null)
})
return (
<>
<perspectiveCamera ref={otherCamera} position={[5, 5, 5]} />
<mesh position={[0, 1, 0]}>
<planeGeometry args={[2, 2]} />
<meshBasicMaterial map={target.texture} />
</mesh>
</>
)
}
シェーダの GLSL ヒント
- Three.js が自動で渡す uniforms:
modelMatrix/viewMatrix/projectionMatrix/modelViewMatrix/normalMatrix/cameraPosition - 頂点 attribute:
position/normal/uv/color - varyings はvertex から fragment へ補間されて渡す
- fragment では
discard;でそのピクセルを描画しない - テクスチャは
uniform sampler2D uTex; texture2D(uTex, uv)
シェーダ学習リソース
- The Book of Shaders — フラグメントシェーダの教科書(無料)
- Shadertoy — シェーダの作品集
- Three.js Journey — Three.js + シェーダの体系的なコース
- drei の shaderMaterial 例
Three.js は WebGPURenderer と TSL(Three Shading Language)を持っている。
GLSL ではなく JS で書ける式ベースの shader DSL で、WebGL/WebGPU 両対応。R3F でも実験的に使える。
まだ動向が動いているので本番採用は機能をピンポイントで確認してから。