16
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React Three Fiber】Post-processingにShaderを適用する

Last updated at Posted at 2021-12-14

概要

Shaderの勉強を始めて、ある程度わかったことを以下の記事にまとめました。

本記事では、じゃあ実際にReact Three FiberでShaderをどのように使うのかについてまとめました。
ただ、すべての使用ケースをまとめると長くなるので、Post-processingにShaderを適用するケースに絞ってまとめています。

Three.jsでShaderを使用するケース

Three.jsでは、以下のケースでShaderを使用します。(私が把握している限り)
shader.png
※ 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の方のサンプルを見るのが良いでしょう。

ドキュメント

成果物

コード

App.tsx
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>
	);
};
PassEffects.tsx
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を囲う必要があります。

.tsx
<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**を使用します。

PrimitiveEffects.tsx
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と、自分で用意したShaderThree.jsに組み込まれているBuilt-in Passを使う方法を紹介します。

Three.jsのサンプルの方が、React Three Fiberのサンプルより圧倒的に多く、Effect PassもThree.jsに組み込まれているものをそのまま使いたいというケースが多いと思います。

ドキュメント

コード

ExtendNativeEffects.tsx
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>
	);
};
shader/blur.ts
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)のEffectcomposerShaderPassBuilt-in Pass(GlitchPass)を、そのままコンポーネントとして扱うことができます。

.tsx
extend({ EffectComposer, RenderPass, ShaderPass, GlitchPass });

custom shader

自分で作成したShaderを割り当てるには、以下のように記述します。

.tsx
<shaderPass
	attachArray="passes"
	args={[blurShader]}
	enabled={blur_datas.enabled}
	uniforms-blurSize-value={blur_datas.blurSize}
/>
  • argsには、自分で作成したShader(blurShader)を割り当てます。オブジェクト構造は、blur.tsを参照してください。
  • shaderPassBuilt-in Pass(GlitchPass)は、Passクラスを継承していて、Effectのオンオフができるenabledが使用できます。
  • uniform変数に渡す値は、uniforms-変数名-valueの形式で指定できます。

built-in shader

built-in shaderも内部的には、blurShaderと同じオブジェクト構造をしているので、shaderPassに渡すことができます。

.tsx
<shaderPass
	attachArray="passes"
	args={[PixelShader]}
	enabled={pixel_datas.enabled}
	uniforms-resolution-value={[window.innerWidth, window.innerHeight]}
	uniforms-pixelSize-value={pixel_datas.pixelSize}
/>

PixelShader

built-in pass

Built-in Pass(GlitchPass)を使用するためには、型定義を行う必要があります。

EffectComposer、RenderPass、ShaderPassの型定義に関しては、**Drei(@react-three/drei)**をインストールすることで導入することができます。
スクリーンショット 2021-12-15 014728.png

これらの型定義を参考にして、Built-in Pass(GlitchPass)の型定義ファイルを作成します。(余談ですが、interfaceの宣言のマージってこういうところで生きてくるんですね...:writing_hand:

types/built_in_effects.d.ts
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です。

tsconfig.json
{
    "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)の中身を見るか、サンプルを参考にして確認しましょう。

.tsx
<glitchPass
	attachArray="passes"
	enabled={glitch_datas.enabled}
	goWild={glitch_datas.goWild}
/>

参考にさせて頂いたアプリケーション

React Postprocessingを使用したCustom Shaderの実装は...:rolling_eyes:

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)ではどう使うのか?ということも調べる必要があるのは、ラッパーパッケージを使う宿命ですが...

16
4
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
16
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?