概要
Shaderの勉強を始めて、ある程度わかったことを以下の記事にまとめました。
本記事では、じゃあ実際にReact Three FiberでShaderをどのように使うのかについてまとめました。
ただ、すべての使用ケースをまとめると長くなるので、Post-processingにShaderを適用するケースに絞ってまとめています。
Three.jsでShaderを使用するケース
Three.jsでは、以下のケースでShaderを使用します。(私が把握している限り)
※ Material Shadingは、勝手に呼んでいるだけで正式な名称ではないです。
-
**Material Shading**は、MeshのMaterialに対してShaderを適用します。ただ、Materialとは言っても、Vertex Shaderで頂点座標を変更することもできます。
-
Post-processingは、レンダリングされた後のシーン全体にShaderを適用します。
本記事では、Post-processingの実装方法をまとめています。
さらに、Post-processingでは、3パターンの実装方法があります。
- Pass:Built-in Passを使用する方法
- Shader:Built-in Shaderを使用する方法
- Custom Shader:自分で作成したShaderを使用する方法
Material Shadingについて
Built-in Passを使用した実装
Built-in Passは、Three.js側に組み込まれているEffect Passです。内部的には、Built-in Shader
をいい感じにチューニングして使いやすくしています。
そのため、簡単に使用することができます。
Built-in Passの一覧
Passを使用したサンプルの一覧
React Three Fiberでは、Post-processingは**React Postprocessing(@react-three/postprocessing)**としてパッケージ化されています。
Three.jsで用意されているPassとほぼ同じものがBuilt-inとして用意されていますが、若干ことなります。(おそらく内部で使用しているShaderは同じで、チューニングの仕方が少し違う感じなんだと思います)
こちらも使用は簡単にできます。サンプルはドキュメントに載っているものと載っていないものがあります。載っていないものは、Three.jsの方のサンプルを見るのが良いでしょう。
ドキュメント
成果物
コード
import { Stats, OrbitControls, Environment } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Suspense, VFC } from 'react';
import { Model } from './Model';
import { PassEffects } from './component_design/PassEffects';
import { PrimitiveEffects } from './component_design/PrimitiveEffects';
import { ExtendNativeEffects } from './component_design/ExtendNativeEffects';
import { NativeEffects } from './component_design/NativeEffects';
export const App: VFC = () => {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Canvas
camera={{
position: [0, 0, 5],
fov: 50,
aspect: window.innerWidth / window.innerHeight,
near: 0.1,
far: 2000
}}
dpr={window.devicePixelRatio}
shadows>
{/* canvas color */}
<color attach="background" args={['#1e1e1e']} />
{/* fps */}
<Stats />
{/* camera controller */}
<OrbitControls attach="orbitControls" />
{/* objects */}
<Suspense fallback={null}>
<Environment preset="forest" background />
<Model />
</Suspense>
{/* Effects */}
<PassEffects />
{/* <PrimitiveEffects /> */}
{/* <ExtendNativeEffects /> */}
{/* <NativeEffects /> */}
</Canvas>
</div>
);
};
import { useControls } from 'leva';
import React, { VFC } from 'react';
import { EffectComposer, Scanline } from '@react-three/postprocessing';
export const PassEffects: VFC = () => {
const datas = useControls('Scanline', {
enabled: true,
density: { value: 1.25, min: 0, max: 10, step: 0.01 }
});
return (
<EffectComposer>
<>{datas.enabled && <Scanline density={datas.density} />}</>
</EffectComposer>
);
};
App.tsx
では、シーンを作成しています。{/* Effects */}
で、今後説明するEffectコンポーネントを切り替えています。
PassEffects.tsx
では、Post-processingをシーンに適用しています。Post-processingを使用する場合、<EffectComposer>
でPassを囲う必要があります。
<EffectComposer>
<>{datas.enabled && <Scanline density={datas.density} />}</>
</EffectComposer>
LEVAコントローラー
値のコントローラーには、**LEVA**を使用しています。使い方については、以下の記事を参考にしてください。
primitiveなPassを使う
React Postprocessingは内部的には、postprocessing(Three.jsのpostprocessingをパッケージ化したもの)を使用しています。
ただし、postprocessingで実装されているすべてのPassが、React Postprocessingでサポートされているわけではないようです。
postprocessingで実装されているBuilt-in Pass
React Postprocessing側で実装されていない、postprocessingのPassを使用したい場合は、**Custom effects**を使用します。
import { useControls } from 'leva';
import { PixelationEffect } from 'postprocessing';
import React, { forwardRef, useMemo, VFC } from 'react';
import { EffectComposer } from '@react-three/postprocessing';
export const PrimitiveEffects: VFC = () => {
const datas = useControls('Pixelation', {
enabled: true,
granularity: { value: 20, min: 0, max: 50, step: 1 }
});
return (
<EffectComposer>
<Pixelation enabled={datas.enabled} granularity={datas.granularity} />
</EffectComposer>
);
};
// ----------------------------------------------
type PixelationProps = {
enabled?: boolean;
granularity?: number;
};
const Pixelation: VFC<PixelationProps> = forwardRef((props, ref) => {
const { enabled = true, granularity = 20 } = props;
const effect = useMemo(() => new PixelationEffect(granularity), [
granularity
]);
return (
<>{enabled && <primitive ref={ref} object={effect} dispose={null} />}</>
);
});
postprocessingのPassを使用する場合、注意する点として、postprocessingはTypeScriptに対応していません。
また、型定義ファイルもnpmに公開されていません。
ただ、型定義ファイルそのものを見つけ【成果物】ではそれを使用しているので、そちらを参考にしてください。
src/types/postprocessing.d.ts
GodRays Effectを使用したサンプル
Built-in Shader・Custom Shaderを使用した実装
Built-in Passは、内部的にBuilt-in Shaderを使用して、いい感じにパラメーターを調整してくれていますが、Built-in Shaderをそのまま使うこともできます。
Three.jsで実装されているBuilt-in Shader
この実装では、Three.jsに組み込まれているBuilt-in Shaderと、自分で用意したShader、Three.jsに組み込まれているBuilt-in Passを使う方法を紹介します。
Three.jsのサンプルの方が、React Three Fiberのサンプルより圧倒的に多く、Effect PassもThree.jsに組み込まれているものをそのまま使いたいというケースが多いと思います。
ドキュメント
コード
import { useControls } from 'leva';
import React, { useEffect, useRef, VFC } from 'react';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass';
import { PixelShader } from 'three/examples/jsm/shaders/PixelShader';
import { extend, useFrame, useThree } from '@react-three/fiber';
import { blurShader } from '../shader/blur';
extend({ EffectComposer, RenderPass, ShaderPass, GlitchPass });
export const ExtendNativeEffects: VFC = () => {
const { gl, scene, camera, size } = useThree();
const composerRef = useRef<EffectComposer>(null);
const glitch_datas = useControls('Glitch', {
enabled: false,
goWild: false
});
const pixel_datas = useControls('Pixel', {
enabled: true,
pixelSize: { value: 10, min: 1, max: 30, step: 1 }
});
const blur_datas = useControls('Blur', {
enabled: true,
blurSize: { value: 8, min: 0, max: 20, step: 1 }
});
useEffect(() => {
composerRef.current!.setSize(size.width, size.height);
}, [size]);
useFrame(() => {
composerRef.current!.render();
}, 1);
return (
<effectComposer ref={composerRef} args={[gl]}>
<renderPass attachArray="passes" args={[scene, camera]} />
{/* built-in pass */}
<glitchPass
attachArray="passes"
enabled={glitch_datas.enabled}
goWild={glitch_datas.goWild}
/>
{/* built-in shader */}
<shaderPass
attachArray="passes"
args={[PixelShader]}
enabled={pixel_datas.enabled}
uniforms-resolution-value={[window.innerWidth, window.innerHeight]}
uniforms-pixelSize-value={pixel_datas.pixelSize}
/>
{/* custom shader */}
<shaderPass
attachArray="passes"
args={[blurShader]}
enabled={blur_datas.enabled}
uniforms-blurSize-value={blur_datas.blurSize}
/>
</effectComposer>
);
};
import * as THREE from 'three';
export const blurShader = {
uniforms: {
tDiffuse: { value: null },
resolution: {
value: new THREE.Vector2(
window.innerWidth,
window.innerHeight
).multiplyScalar(window.devicePixelRatio)
},
blurSize: { value: 8 }
},
vertexShader: `
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec2 resolution;
uniform float blurSize;
varying vec2 v_uv;
vec4 blur(sampler2D tex) {
const float PI2 = 6.28318530718; // PI * 2
const float directions = 16.0;
const float quality = 3.0;
vec2 radius = blurSize / resolution;
// normalized pixel coordinates (0-1)
// vec2 uv = gl_FragCoord.xy / resolution;
vec2 uv = v_uv;
// pixel color
vec4 color = texture2D(tDiffuse, uv);
int count = 1;
for (float theta = 0.0; theta < PI2; theta += PI2 / directions) {
vec2 dir = vec2(cos(theta), sin(theta)) * radius;
for (float i = 1.0 / quality; i <= 1.0; i += 1.0 / quality) {
color += texture2D(tex, uv + dir * i);
count++;
}
}
color /= float(count);
return color;
}
void main() {
gl_FragColor = blur(tDiffuse);
}
`
};
React Three Fiberのextendを使用することで、ネイティブ(Three.js)のEffectcomposer
やShaderPass
、Built-in Pass(GlitchPass)
を、そのままコンポーネントとして扱うことができます。
extend({ EffectComposer, RenderPass, ShaderPass, GlitchPass });
custom shader
自分で作成したShaderを割り当てるには、以下のように記述します。
<shaderPass
attachArray="passes"
args={[blurShader]}
enabled={blur_datas.enabled}
uniforms-blurSize-value={blur_datas.blurSize}
/>
- argsには、自分で作成したShader(blurShader)を割り当てます。オブジェクト構造は、
blur.ts
を参照してください。 -
shaderPass
やBuilt-in Pass(GlitchPass)
は、Passクラスを継承していて、Effectのオンオフができるenabledが使用できます。 - uniform変数に渡す値は、
uniforms-変数名-value
の形式で指定できます。
built-in shader
built-in shaderも内部的には、blurShaderと同じオブジェクト構造をしているので、shaderPassに渡すことができます。
<shaderPass
attachArray="passes"
args={[PixelShader]}
enabled={pixel_datas.enabled}
uniforms-resolution-value={[window.innerWidth, window.innerHeight]}
uniforms-pixelSize-value={pixel_datas.pixelSize}
/>
built-in pass
Built-in Pass(GlitchPass)を使用するためには、型定義を行う必要があります。
EffectComposer、RenderPass、ShaderPassの型定義に関しては、**Drei(@react-three/drei)**をインストールすることで導入することができます。
これらの型定義を参考にして、Built-in Pass(GlitchPass)の型定義ファイルを作成します。(余談ですが、interfaceの宣言のマージってこういうところで生きてくるんですね...)
import { GlitchPass } from 'three-stdlib';
import { ReactThreeFiber } from '@react-three/fiber';
declare global {
namespace JSX {
interface IntrinsicElements {
glitchPass: ReactThreeFiber.Node<GlitchPass, typeof GlitchPass>;
}
}
}
あとは、tsconfig.json
でtypesフォルダを参照してあげればOKです。
{
"include": [
"./src/**/*"
],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"lib": [
"dom",
"es2015"
],
"jsx": "react-jsx"
},
"types": [
"types"
]
}
Built-in Pass(GlitchPass)は、Built-in Shaderをいい感じにしたものなので、直接uniformで値を渡すわけではありません。
どんな引数がとれるかは、Built-in Pass(GlitchPass)の中身を見るか、サンプルを参考にして確認しましょう。
<glitchPass
attachArray="passes"
enabled={glitch_datas.enabled}
goWild={glitch_datas.goWild}
/>
参考にさせて頂いたアプリケーション
React Postprocessingを使用したCustom Shaderの実装は...
Custom Shaderを使ったPost-processingは、React PostprocessingのCustom effectsを使用することで実装することもできます。
が、実装がそこそこ複雑なのと、独自のGLSLの関数(mainUv、mainImage)などを実装する必要があり、Shaderを始めたばかりの自分には混乱するだけだったので、調べるのをやめました。(Custom Shaderの実装方法が他にもあったので)
mainUv、mainImageについてはドキュメントを探しても見つかりませんでした。ソースコード内だけでその存在が確認できました。
mainUv・mainImageなどを解釈しているソースコード
サンプルが欲しい方は、以下を参照してください。
まとめ
React Three Fiberを使用する場合、React Postprocessingパッケージを利用することでBuilt-in Passを簡単に実装することができます。(ただし、Three.jsのBuilt-in Passとは少し異なります)
自身で作成したShaderや、Three.jsのBuilt-in Pass、Three.jsのBuilt-in Shaderを使用したい場合は、extendを使って実装できます。
Shader勉強するにあたって、その導入方法から取り掛かったのですが、思いのほか調べることが多かったです。
ネイティブ(Three.js)の情報を、じゃあラッパーパッケージ(React Three Fiber)ではどう使うのか?ということも調べる必要があるのは、ラッパーパッケージを使う宿命ですが...