【React Three Fiber】Three.jsでShaderを使う準備をする

<!-- ================================================ -->
# 概要
<!-- ================================================ -->
Three.js（React Three Fiber）で**Shader**使い始めるための基本的な情報をまとめました。
Shaderを使いこなせれば、もう一段階上の表現力を手に入れることができます。

![output(video-cutter-js.com).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/1e9e057d-db15-fc24-9533-3c2d6573d470.gif)
<details><summary>コード</summary><div>

```.tsx:
import React, { Suspense, VFC } from 'react';
import * as THREE from 'three';
import { Environment, OrbitControls, Stats } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';

export const ShaderMesh: VFC = () => {
	return (
		<Canvas
			camera={{
				position: [15, 15, 15],
				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" />
			{/* lights */}
			<ambientLight intensity={0.1} />
			<directionalLight position={[20, 20, 20]} castShadow />
			{/* objects */}
			<Suspense fallback={null}>
				<Environment preset="city" />
				<Objects />
			</Suspense>
			{/* helper */}
			<axesHelper />
		</Canvas>
	)
}

const Objects: VFC = () => {
	const material = new THREE.MeshStandardMaterial({ color: '#22a7f2', metalness: 1, roughness: 0.2, wireframe: false })
	material.onBeforeCompile = shader => {
		shader.uniforms.u_time = { value: 0 }
		shader.uniforms.u_radius = { value: 10 }
		// vertex
		shader.vertexShader = vertexShaderDefine + shader.vertexShader
		shader.vertexShader = shader.vertexShader.replace('#include <beginnormal_vertex>', beginnormal_vertex)
		shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', begin_vertex)
		// fragment
		shader.fragmentShader = fragmentShaderDefine + shader.fragmentShader
		shader.fragmentShader = shader.fragmentShader.replace('#include <color_fragment>', color_fragment)

		material.userData.shader = shader
		// debug
		console.log('vertexShader', shader.vertexShader)
		console.log('fragmentShader', shader.fragmentShader)
	}

	const geometry = new THREE.BoxBufferGeometry(10, 10, 10, 12, 12, 12)

	useFrame(({ clock }) => {
		const shader = material.userData.shader
		if (shader) {
			;(shader as THREE.Shader).uniforms.u_time.value = clock.getElapsedTime()
		}
	})

	return <mesh geometry={geometry} material={material} receiveShadow />
}

const vertexShaderDefine = `
uniform float u_time;
uniform float u_radius;
varying vec2 v_uv;
`

const beginnormal_vertex = `
float delta = (sin(u_time) + 1.0) / 2.0;
vec3 objectNormal = delta * normal + (1.0 - delta) * normalize(position);
`

const begin_vertex = `
v_uv = uv;
vec3 v = normalize(position) * u_radius;
vec3 transformed = delta * position + (1.0 - delta) * v;
`

const fragmentShaderDefine = `
uniform float u_time;
varying vec2 v_uv;
`

const color_fragment = `
float rnd = fract(sin(dot(v_uv.xy, vec2(12.9898, 78.233)) + u_time) * 43758.5453123);
float r = (sin(u_time) + 1.0) / 2.0;
float g = (sin(PI * 2.0 / 3.0 + u_time) + 1.0) / 2.0;
float b = (sin(PI * 4.0 / 3.0 + u_time) + 1.0) / 2.0;
vec3 color = vec3(r, g, b) * rnd;
diffuseColor = vec4(color, 1.0);
`
```
</div></details>

:::note warn
私もまだ学習を始めたばかりなので、認識が正しくない情報があるかもしれませんがご了承ください。
:::

<!-- ================================================ -->
# GLSLをざっと知る
<!-- ================================================ -->
**GLSL**とは`OpenGL Shading Language`の略で、WwbGLでShaderを扱うためのC言語ライクなプログラミング言語です。
この記事では、GLSLの基本的な記述方法は記載しませんが、以下のサイトを軽く読んでいただければ概ね理解できると思います。

### wgld.org
まず概要を掴むために、以下のサイトを読むことをおすすめします。
ただ、ものすごく長く（情報量が多く）、Three.jsを対象としているわけでもないので、まずは[頂点バッファの基礎](https://wgld.org/d/webgl/w009.html)まで読むといいと思います。

https://wgld.org/

### The Book of Shaders
多くの記事でGLSLの学習サイトとして取り上げられています。
Shaderを実際に書きながら学習することができますが、コーディングに関しては説明が飛ばし飛ばしなので、こちらも最初のうちはざっと確認する程度でいいと思います。

https://thebookofshaders.com/?lan=jp

### [Three.js] Three.jsでシェーダー入門
GLSLの基本的な記述方法がまとめられている記事です。一読しましょう！

https://qiita.com/masato_makino/items/dcc2ba13d28a5a6abab1

<!-- ================================================ -->
# ドキュメントで組み込み変数・関数を確認する
<!-- ================================================ -->
### GLSL (OpenGL ES2.0)リファレンス
GLSLで使用できる**データ型**、**スウィズル演算子**、**組み込み変数・関数**を確認することができます。
関数の説明には使用例もあるので、かなり親切なドキュメントです。

https://gist.github.com/gyohk/abf13dbcb5be750b3b021752b280ccd3

### Three.js - WebGLProgram
Three.jsでGLSLを扱う場合、GLSL自体の組み込み変数にプラスして、**Three.js上でさらに使用できる組み込み変数**があります。

https://threejs.org/docs/#api/en/renderers/webgl/WebGLProgram

### Three.js - ShaderMaterial
MeshにShaderを割り当てる方法を確認することができます。

https://threejs.org/docs/index.html?q=shaderma#api/en/materials/ShaderMaterial

### Three.js - EffectComposer
こちらは、**Post-Processing**で画面全体にエフェクトをかける場合のドキュメントです。

https://threejs.org/docs/index.html?q=effect#examples/en/postprocessing/EffectComposer

<!-- ================================================ -->
# 環境を整える
<!-- ================================================ -->
Three.jsでGLSLを使用する場合、コーディングした内容は**String型**（文字列）として渡します。
でも、コーディングを文字列で行うのは大変です。**VSCodeを使用している場合**、以下の拡張機能を入れることで少しだけコーディングがしやすくなります。

### WebGL GLSL Editor
GLSLを記述するファイル名を`.glsl`（.vert .vs .frag .fs）にすることで、組み込み関数や定義した変数の**スニペット（予測変換）が表示される**ようになります。.glslなどのファイルを認識してくれるので、**コードが色付けされます。**
また、**コードフォーマット機能**もあります。

https://marketplace.visualstudio.com/items?itemName=raczzalan.webgl-glsl-editor

### glsl-canvas
GLSLを書いただけでその結果を確認することができる拡張機能です。
Three.jsの組み込み関数は認識されませんが、FragmentShaderを記述する際に使用すると便利です。
こちらにも**コードフォーマット機能**があり、個人的には`WebGL GLSL Editor`よりフォーマットルールが好きです。

https://marketplace.visualstudio.com/items?itemName=circledev.glsl-canvas

### TypeScript環境で.glslファイルを扱う
TypeScriptを使用している場合、そのままでは.glslファイルを文字列ファイルとして扱ってくれません。
型定義をして、文字列ファイルとして認識させる必要があります。

型定義ファイルの導入方法については、以下を参照してください。

https://stackoverflow.com/questions/48741570/how-can-i-import-glsl-as-string-in-typescript

<!-- ================================================ -->
# 便利なツール・サイトを使う
<!-- ================================================ -->
コーディング時に活用できそうなサイトです。

### graphtoy
GLSLで使用できる組み込み関数が揃った関数電卓のサイトです。ただし、GLSLでは使用できない関数もあります。
偉いのは、**時間変数（t）が使用できる**ところです。

https://graphtoy.com/

### The Book of Shaders Editor
FragmentShaderを記述して確認できるサイトです。`glsl-canvas`のWebサイト版みたいな感じです。

http://editor.thebookofshaders.com/

### vertexshaderart.com
多くのShader作品がアップロードされており、そのコードを編集することもできます。

https://www.vertexshaderart.com/

<!-- ================================================ -->
# 既存のMaterialを継承したShaderを実装する
<!-- ================================================ -->
MaterialにShaderを適用させる場合、通常`ShaderMaterial`を使用しますが、ライトや影、Fogの影響を自前で実装するのは面倒です。
既存のStandardMaterialなどのMaterialの特性は残しつつ、**Shaderでカスタマイズしたい特性だけを上書きする**こともできます。

:::note info
ただしShaderの上書きは、MaterialのパラメーターとShaderの理解が必要です。
:::

### 実装例
以下の質問の回答として、回答者が既存のMaterialを上書きするサンプルコードを提示しています。
[Custom GLSL bug, ‘vIndirectFront’ : undeclared identifier](https://discourse.threejs.org/t/custom-glsl-bug-vindirectfront-undeclared-identifier/19405)
[> コード](https://jsfiddle.net/e6p0hjoa/1/)

また、公式サンプルにも実装例があります。
[webgl materials modified](https://threejs.org/examples/#webgl_materials_modified)
[> コード](https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_modified.html)

:::note info
サンプルコードでは、<strong>[...].join('\n')</strong> で改行をしていますが、バッククォートを使用することで、改行コードなしで簡潔に記述できます。
:::
```.ts
shader.vertexShader = shader.vertexShader.replace(
	'#include <begin_vertex>',
	[
		`float theta = sin( time + position.y ) / ${ amount.toFixed( 1 ) };`,
		'float c = cos( theta );',
		'float s = sin( theta );',
		'mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );',
		'vec3 transformed = vec3( position ) * m;',
		'vNormal = vNormal * m;'
	].join( '\n' )
);
```
```.ts
shader.vertexShader = shader.vertexShader.replace(
	'#include <begin_vertex>',
	`float theta = sin( time + position.y ) / ${ amount.toFixed( 1 ) };
	float c = cos( theta );
	float s = sin( theta );
	mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );
	vec3 transformed = vec3( position ) * m;
	vNormal = vNormal * m;`
);
```

### 組み込みShader
Shader内で`#include <color_fragment>`のような形式で定義されている**color_fragment**は、Three.jsに組み込まれているShaderです。これらShaderのコードは以下で確認できます。
[Three.js - ShaderChunk](https://threejs.org/docs/#api/en/renderers/shaders/ShaderChunk)
[GitHub - ShaderChunk](https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk)

### 参考
[three.jsのマテリアルを継承してシェーダーを追記する](https://qiita.com/watabo_shi/items/bfffed0462c5e9768687)
[three.jsのデフォルトのマテリアルを拡張する](https://biwanoie.tokyo/o2o1j/)

<!-- ================================================ -->
# ShaderChunkを定義する
<!-- ================================================ -->
【既存のMaterialを継承したShader】では、Three.jsに組み込まれているShaderChunkを紹介しましたが、**自分で定義したShaderを組み込む**こともできます。

以下の例では、`customFunction`で定義した`random`関数を、Fragment Shader内で読み込んで利用している例です。
`customFunction`の名前はなんでも大丈夫です。（組み込みShaderと競合する名前はダメかもしれないです）

```.ts:MyShaderChunk.ts
import * as THREE from 'three';

THREE.ShaderChunk.customFunction = `
float random (vec2 st, float seed) {
	return fract(sin(dot(st.xy, vec2(12.9898, 78.233)) + seed) * 43758.5453123);
}
`
```
```.glsl:fragment.glsl
#include <customFunction>

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main() {
	vec2 st = gl_FragCoord.xy / u_resolution.xy;
	float rnd = random(st, u_time);
	gl_FragColor = vec4(vec3(rnd), 1.0);
}
```

<!-- ================================================ -->
# その他の参考サイト
<!-- ================================================ -->
* [GLSL で暖を取るための準備をしよう！ GLSL お役立ちマニュアル](https://qiita.com/doxas/items/5a7b6dedff4bc2ce1586)
* [2019年度GLSLお役立ちシート](https://qiita.com/7CIT/items/6cfac5849dce678f86d3)
* [GLSLを毎日書く](https://qiita.com/northprint/items/20d271013c9c6e4566ed)

<!-- ================================================ -->
# まとめ
<!-- ================================================ -->
Shaderは、トライ＆エラーで試しながら少しずつ表現の幅を広げていく技術だと実感しました。
焦らずじっくり理解していくことにします。