概要
Three.js(React Three Fiber)でShader使い始めるための基本的な情報をまとめました。
Shaderを使いこなせれば、もう一段階上の表現力を手に入れることができます。
コード
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);
`
私もまだ学習を始めたばかりなので、認識が正しくない情報があるかもしれませんがご了承ください。
GLSLをざっと知る
GLSLとはOpenGL Shading Language
の略で、WwbGLでShaderを扱うためのC言語ライクなプログラミング言語です。
この記事では、GLSLの基本的な記述方法は記載しませんが、以下のサイトを軽く読んでいただければ概ね理解できると思います。
wgld.org
まず概要を掴むために、以下のサイトを読むことをおすすめします。
ただ、ものすごく長く(情報量が多く)、Three.jsを対象としているわけでもないので、まずは頂点バッファの基礎まで読むといいと思います。
The Book of Shaders
多くの記事でGLSLの学習サイトとして取り上げられています。
Shaderを実際に書きながら学習することができますが、コーディングに関しては説明が飛ばし飛ばしなので、こちらも最初のうちはざっと確認する程度でいいと思います。
[Three.js] Three.jsでシェーダー入門
GLSLの基本的な記述方法がまとめられている記事です。一読しましょう!
ドキュメントで組み込み変数・関数を確認する
GLSL (OpenGL ES2.0)リファレンス
GLSLで使用できるデータ型、スウィズル演算子、組み込み変数・関数を確認することができます。
関数の説明には使用例もあるので、かなり親切なドキュメントです。
Three.js - WebGLProgram
Three.jsでGLSLを扱う場合、GLSL自体の組み込み変数にプラスして、Three.js上でさらに使用できる組み込み変数があります。
Three.js - ShaderMaterial
MeshにShaderを割り当てる方法を確認することができます。
Three.js - EffectComposer
こちらは、Post-Processingで画面全体にエフェクトをかける場合のドキュメントです。
環境を整える
Three.jsでGLSLを使用する場合、コーディングした内容はString型(文字列)として渡します。
でも、コーディングを文字列で行うのは大変です。VSCodeを使用している場合、以下の拡張機能を入れることで少しだけコーディングがしやすくなります。
WebGL GLSL Editor
GLSLを記述するファイル名を.glsl
(.vert .vs .frag .fs)にすることで、組み込み関数や定義した変数のスニペット(予測変換)が表示されるようになります。.glslなどのファイルを認識してくれるので、コードが色付けされます。
また、コードフォーマット機能もあります。
glsl-canvas
GLSLを書いただけでその結果を確認することができる拡張機能です。
Three.jsの組み込み関数は認識されませんが、FragmentShaderを記述する際に使用すると便利です。
こちらにもコードフォーマット機能があり、個人的にはWebGL GLSL Editor
よりフォーマットルールが好きです。
TypeScript環境で.glslファイルを扱う
TypeScriptを使用している場合、そのままでは.glslファイルを文字列ファイルとして扱ってくれません。
型定義をして、文字列ファイルとして認識させる必要があります。
型定義ファイルの導入方法については、以下を参照してください。
便利なツール・サイトを使う
コーディング時に活用できそうなサイトです。
graphtoy
GLSLで使用できる組み込み関数が揃った関数電卓のサイトです。ただし、GLSLでは使用できない関数もあります。
偉いのは、時間変数(t)が使用できるところです。
The Book of Shaders Editor
FragmentShaderを記述して確認できるサイトです。glsl-canvas
のWebサイト版みたいな感じです。
vertexshaderart.com
多くのShader作品がアップロードされており、そのコードを編集することもできます。
既存のMaterialを継承したShaderを実装する
MaterialにShaderを適用させる場合、通常ShaderMaterial
を使用しますが、ライトや影、Fogの影響を自前で実装するのは面倒です。
既存のStandardMaterialなどのMaterialの特性は残しつつ、Shaderでカスタマイズしたい特性だけを上書きすることもできます。
ただしShaderの上書きは、MaterialのパラメーターとShaderの理解が必要です。
実装例
以下の質問の回答として、回答者が既存のMaterialを上書きするサンプルコードを提示しています。
Custom GLSL bug, ‘vIndirectFront’ : undeclared identifier
> コード
また、公式サンプルにも実装例があります。
webgl materials modified
> コード
サンプルコードでは、[...].join('\n') で改行をしていますが、バッククォートを使用することで、改行コードなしで簡潔に記述できます。
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' )
);
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
GitHub - ShaderChunk
参考
three.jsのマテリアルを継承してシェーダーを追記する
three.jsのデフォルトのマテリアルを拡張する
ShaderChunkを定義する
【既存のMaterialを継承したShader】では、Three.jsに組み込まれているShaderChunkを紹介しましたが、自分で定義したShaderを組み込むこともできます。
以下の例では、customFunction
で定義したrandom
関数を、Fragment Shader内で読み込んで利用している例です。
customFunction
の名前はなんでも大丈夫です。(組み込みShaderと競合する名前はダメかもしれないです)
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);
}
`
#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);
}
その他の参考サイト
まとめ
Shaderは、トライ&エラーで試しながら少しずつ表現の幅を広げていく技術だと実感しました。
焦らずじっくり理解していくことにします。