概要
Shaderの勉強を始めて、ある程度わかったことを以下の記事にまとめました。
本記事では、じゃあ実際にReact Three FiberでShaderをどのように使うのかについてまとめました。
ただ、すべての使用ケースをまとめると長くなるので、MaterialにShaderを適用するケースに絞ってまとめています。
Three.jsでShaderを使用するケース
Three.jsでは、以下のケースでShaderを使用します。(私が把握している限り)
※ Material Shadingは、勝手に呼んでいるだけで正式な名称ではないです。
-
**Material Shading**は、MeshのMaterialに対してShaderを適用します。ただ、Materialとは言っても、Vertex Shaderで頂点座標を変更することもできます。
-
Post-processingは、レンダリングされた後のシーン全体にShaderを適用します。
本記事では、Material Shadingの実装方法をまとめています。
さらに、Material Shadingでは、2パターンの実装方法があります。
- ShaderMaterial:ShaderMaterialを使用して、Materialの特性(色、影、光沢、粗さなど)をすべて自分で実装する方法
- Custom Material Shader:既存のMaterial(MeshStandardMaterialなど)を使用して、Materialの特性は残しつつ特定の属性(たとえば色)のみをShaderで上書きする方法
Post-processingについて
ShaderMaterialを使用した実装
ドキュメント
成果物
コード
import React, { useEffect, useRef, VFC } from 'react';
import * as THREE from 'three';
import { OrbitControls, Plane, Stats } from '@react-three/drei';
import { Canvas, useThree } from '@react-three/fiber';
import { ShaderObject } from './ShaderObject';
export const App: VFC = () => {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Canvas
camera={{
position: [-40, 40, 80],
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 */}
<Lights />
{/* objects */}
<ShaderObject />
<Plane args={[50, 50]} position={[0, -20, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<meshStandardMaterial color="#fff" />
</Plane>
{/* helper */}
<axesHelper />
</Canvas>
</div>
);
};
const Lights: VFC = () => {
const { scene } = useThree();
const lightRef = useRef<THREE.DirectionalLight>(null);
useEffect(() => {
const helper = new THREE.CameraHelper(lightRef.current!.shadow.camera);
scene.add(helper);
}, [scene]);
return (
<>
<ambientLight intensity={0.1} />
<directionalLight
ref={lightRef}
intensity={1}
position={[5, 30, 10]}
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
shadow-camera-near={0.1}
shadow-camera-far={70}
shadow-camera-top={20}
shadow-camera-bottom={-20}
shadow-camera-left={-20}
shadow-camera-right={20}
shadow-bias={-0.003}
/>
</>
);
};
import { useFrame } from '@react-three/fiber';
import React, { useRef, VFC } from 'react';
import * as THREE from 'three';
import { Box } from '@react-three/drei';
import { useDepthMaterial } from './useDepthMaterial';
export const ShaderObject: VFC = () => {
const boxRef = useRef<THREE.Mesh>(null);
const material = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0 },
u_radius: { value: 10 }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
// sync shadow
useDepthMaterial(boxRef);
useFrame(({ clock }) => {
material.uniforms.u_time.value = clock.getElapsedTime();
});
return (
<Box
ref={boxRef}
args={[10, 10, 10, 12, 12, 12]}
material={material}
castShadow
receiveShadow
/>
);
};
const vertexShader = `
uniform float u_time;
uniform float u_radius;
varying vec2 v_uv;
float getDelta(){
return (sin(u_time) + 1.0) / 2.0;
}
void main() {
v_uv = uv;
float delta = getDelta();
vec3 v = normalize(position) * u_radius;
vec3 pos = delta * position + (1.0 - delta) * v;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShader = `
#define PI 3.141592653589
uniform float u_time;
varying vec2 v_uv;
void main() {
float noise = 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) * noise;
gl_FragColor = vec4(color, 1.0);
}
`;
import { useEffect, useMemo } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
export const useDepthMaterial = (
meshRef: React.RefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>>
) => {
const depthMaterial = useMemo(() => {
const depth = THREE.ShaderLib['depth'];
return new THREE.ShaderMaterial({
vertexShader: depthVertexShader,
fragmentShader: depth.fragmentShader,
uniforms: Object.assign(depth.uniforms, {
u_time: { value: 0 },
u_radius: { value: 10 }
}),
defines: {
DEPTH_PACKING: THREE.RGBADepthPacking
}
});
// console.log(depth.vertexShader)
}, []);
useEffect(() => {
if (meshRef.current) {
meshRef.current.customDepthMaterial = depthMaterial;
}
}, [meshRef, depthMaterial]);
useFrame(({ clock }) => {
depthMaterial.uniforms.u_time.value = clock.getElapsedTime();
});
};
const depthVertexShader = `
#include <common>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 vHighPrecisionZW;
uniform float u_time;
uniform float u_radius;
float getDelta(){
return (sin(u_time) + 1.0) / 2.0;
}
void main() {
#include <uv_vertex>
#include <skinbase_vertex>
#ifdef USE_DISPLACEMENTMAP
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinnormal_vertex>
#endif
#include <begin_vertex>
float delta = getDelta();
transformed = delta * position + (1.0 - delta) * normalize(position) * u_radius;
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vHighPrecisionZW = gl_Position.zw;
}
`;
3つのコードブロックがあります。
-
App.tsx
は、Sceneを作成しています。(Shaderと関係ないので説明は割愛します) -
ShaderObject.tsx
は、Material ShadingしたMeshを作成しています。 -
useDepthMaterial.ts
は、床面に落ちる影を更新するための設定をしています。
ShaderObject.tsx
ShaderMaterialの使い方はシンプルで、uniforms
、vertexShader
、fragmentShader
をコンストラクタに渡すだけです。このうちvertexShader、fragmentShaderは文字列で指定します。
const material = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0 },
u_radius: { value: 10 }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
uniformsの動的な更新は、以下のようにmaterialから行います。
useFrame(({ clock }) => {
material.uniforms.u_time.value = clock.getElapsedTime();
});
あとは、他のmaterialのようにMeshに割り当てるだけです。
<Box
ref={boxRef}
args={[10, 10, 10, 12, 12, 12]}
material={material}
castShadow
receiveShadow
/>
ライトの影響や光沢、粗さなどの情報をFragment Shaderに実装していないので、Meshにはこれらの特性がないことがわかります。
ライティングしているのに、Meshには影がない。
useDepthMaterial.ts
Vertex Shaderで頂点座標を変更した場合、自身が受ける影(receiveShadow)は動的に更新されますが、他(床面)に落とす影(castShadow)は更新されません。
更新をするためには、Meshの**customDepthMaterial**を設定する必要があります。
customDepthMaterialには、depthMaterialを設定します。
-
Fragment Shader
は、組み込みShaderをそのまま使います。 -
Vertex Shader
は、MeshのVertex Shaderで適用した形状変更
を、組み込みShaderに挿入します。 -
uniforms
も、MeshのVertex Shaderの形状変更に必要な値
を追加します。 -
defines
では、DEPTH_PACKINGとしてRGBADepthPacking
を割り当てます。
const depthMaterial = useMemo(() => {
const depth = THREE.ShaderLib['depth'];
return new THREE.ShaderMaterial({
vertexShader: depthVertexShader,
fragmentShader: depth.fragmentShader,
uniforms: Object.assign(depth.uniforms, {
u_time: { value: 0 },
u_radius: { value: 10 }
}),
defines: {
DEPTH_PACKING: THREE.RGBADepthPacking
}
});
// console.log(depth.vertexShader)
}, []);
useDepthMaterialでは、Vertex Shaderだけさらに記述する必要があります。実装は、上記のとおり組み込みShaderに形状変更を挿入する形
になります。
ここで組み込みShaderがどうなっているかを知るために、コンソールに出力するのが良いです。
console.log(depth.vertexShader)
ソースコードを参照してもいいですが、使っているThree.jsのバージョンによって変わる可能性があるので、注意が必要です。(以下のリンクはr135のソースコード)
https://github.com/mrdoob/three.js/blob/r135/src/renderers/shaders/ShaderLib/depth.glsl.js
形状変更情報を追加する場合、<begin_vertex>
を置き換えるか、その下に挿入します。
const depthVertexShader = `
#include <common>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 vHighPrecisionZW;
// ↓ -----------------------------------
uniform float u_time;
uniform float u_radius;
float getDelta(){
return (sin(u_time) + 1.0) / 2.0;
}
// ↑ -----------------------------------
void main() {
#include <uv_vertex>
#include <skinbase_vertex>
#ifdef USE_DISPLACEMENTMAP
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinnormal_vertex>
#endif
#include <begin_vertex>
// ↓ -----------------------------------
float delta = getDelta();
transformed = delta * position + (1.0 - delta) * normalize(position) * u_radius;
// ↑ -----------------------------------
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vHighPrecisionZW = gl_Position.zw;
}
`;
#include
で始まるコードは、すべて組み込みShaderです。(自前のライブラリを使用していなければ)
組み込みShaderの一覧は、以下から参照できます。
https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk
参考にさせて頂いた記事
three.jsのcustomDepthMaterialを使ってみる
Custom Material Shaderを使用した実装
ドキュメント
成果物
コード
App.tsx
とuseDepthMaterial.ts
については、【ShaderMaterial】と同じなので割愛します。
import React, { VFC, useRef } from 'react';
import * as THREE from 'three';
import { Box } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useDepthMaterial } from './useDepthMaterial';
export const ShaderObject: VFC = () => {
const boxRef = useRef<THREE.Mesh>(null);
const material = new THREE.MeshStandardMaterial({
metalness: 0.5,
roughness: 0.8,
transparent: true,
side: THREE.DoubleSide,
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)
};
// sync shadow
useDepthMaterial(boxRef);
useFrame(({ clock }) => {
const shader = material.userData.shader;
if (shader) {
(shader as THREE.Shader).uniforms.u_time.value = clock.getElapsedTime();
}
});
return (
<Box
ref={boxRef}
args={[10, 10, 10, 12, 12, 12]}
material={material}
castShadow
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 noise = 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) * noise;
diffuseColor = vec4(color, 1.0);
`;
ShaderObject.tsx
既存のMaterial(例えばMeshStandardMaterial)のShaderを上書きする場合は、**onBeforeCompile**を使用します。
どこをどう上書きすればいいのかは、わたしもThree.jsやShaderの学習を始めて日が浅いのでほぼ理解できていません。
コードは、公式サンプルなどを見様見真似で書きました。
今回わかったことは、
- 頂点座標の変更に関しては、
<beginnormal_vertex>
・<begin_vertex>
を上書きする。 - 色の変更に関しては、
<color_fragment>
を上書きする。
ということです。ただ、ファイル名でおおよその予測はできます。
最終的にどの変数を上書きするのか知るためには、頑張って組み込みShaderを読むしかないです。
https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk
【ShaderMaterial】で実装したものとは違い、Mesh自体に影が落ちているのがわかります。
参考にさせて頂いた記事
three.jsのマテリアルを継承してシェーダーを追記する
three.jsのデフォルトのマテリアルを拡張する
まとめ
Shaderを使いこなせれば魔法のような表現ができますが、Three.jsの内部的な仕様まで把握しないといけないので敷居が高めです。
ただ、Three.js自体の勉強にもなるので個人的にはお得だと思います。