This post covers the required patches on the Create React App TypeScript template in order to get React Three Fiber working with shaders, plus how to integrate these shaders with animation and gesture libraries like React Spring and Use Gesture.
TLDR template repo @ GitHub / CodeSandbox. Each commit matches a section of the following post.
1. GLSL in a React App
Create the React app. yarn create react-app --template typescript
Eject. yarn eject
Add loader dependencies. yarn add raw-loader glslify-loader glslify
And loader configuration to config/webpack.config.js:
{
test: /\.(glsl|frag|vert)$/,
use: [
require.resolve("raw-loader"),
require.resolve("glslify-loader"),
],
}
Some TypeScript declarations in src/react-app-env.d.ts:
declare module "*.glsl" {
const src: string
export default src
}
declare module "*.frag" {
const src: string
export default src
}
declare module "*.vert" {
const src: string
export default src
}
declare module "glslify"
You can now import GLSL files as strings and you can use GLSL modules with glslify like so:
import rawFragmentShader from "./fragment.glsl"
import glsl from "glslify"
const fragmentShader = glsl(rawFragmentShader)
2. GLSL with Three.js via React Three Fiber
Add the dependencies.
yarn add @react-three/fiber three @types/three @react-three/drei
The standard Three.js vertex shader (as per documentation) can go in src/shaders/vertex.glsl:
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Here's a "hello, world" fragment shader
(src/shaders/fragment.glsl),
using the glsl-earth
module
(yarn add glsl-earth
):
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 vUv;
uniform vec2 u_offset;
uniform float u_time;
// clang-format off
#pragma glslify: planet = require('glsl-earth')
// clang-format on
void main() {
vec2 resolution = vec2(640, 640);
float size = 0.75;
vec2 rot = vec2(u_time * 0.03 + u_offset.x, u_time * 0.01 + u_offset.y);
vec3 color = planet(vUv, resolution, size, rot);
gl_FragColor.rgb = color;
gl_FragColor.a = 1.0;
}
Here's our material (src/materials/BasicShaderMaterial.tsx):
import { shaderMaterial } from "@react-three/drei"
import { extend } from "@react-three/fiber"
import * as THREE from "three"
import fragmentShader from "../shaders/fragment.glsl"
import vertexShader from "../shaders/vertex.glsl"
import glsl from "glslify"
export const BasicShaderMaterial = shaderMaterial(
{
u_time: 0,
u_offset: new THREE.Vector2(),
},
glsl(vertexShader),
glsl(fragmentShader)
)
export type BasicShaderMaterialImpl = {
u_time?: { value: number }
u_offset?: { value: THREE.Vector2 }
} & JSX.IntrinsicElements["shaderMaterial"]
extend({ BasicShaderMaterial })
declare global {
namespace JSX {
interface IntrinsicElements {
basicShaderMaterial: BasicShaderMaterialImpl
}
}
}
And finally our App (src/components/App.tsx):
import "../materials/BasicShaderMaterial"
const App = () => {
return (
<mesh>
<planeBufferGeometry args={[4, 4]} />
<basicShaderMaterial />
</mesh>
)
}
export default App
Hello, stationary Earth!
3. Bells & Whistles (Animation, Gestures)
Let's animate our Earth using @react-spring/three and by mutating a ref with R3F's useFrame, hooked up with @use-gesture/react's gestures, all at the same time!
Add the dependencies.
yarn add @react-spring/three @use-gesture/react
Add this to our material to make it spring:
import { animated } from "@react-spring/three"
// ...
export const AnimatedBasicShaderMaterial = animated(
forwardRef<BasicShaderMaterialImpl, any>((props, ref) => (
<basicShaderMaterial ref={ref} {...props} />
))
)
Now here's our App:
import { useSpring } from "@react-spring/three"
import { useFrame, useThree } from "@react-three/fiber"
import { useDrag } from "@use-gesture/react"
import { useRef } from "react"
import "../materials/BasicShaderMaterial"
import {
AnimatedBasicShaderMaterial,
BasicShaderMaterialImpl,
} from "../materials/BasicShaderMaterial"
const App = () => {
const ref = useRef<BasicShaderMaterialImpl>(null)
const [{ offset }, spring] = useSpring(() => ({
offset: [0, 0],
immediate: true,
}))
const factor = useThree(three => three.viewport.factor)
const bind = useDrag(
({ offset }) => {
spring.start({ offset })
},
{ transform: ([x, y]) => [x / factor, -y / factor] }
)
useFrame(({ clock }) => {
const material = ref.current
if (!material) return
if (material.uniforms?.u_time) {
material.uniforms.u_time.value = clock.getElapsedTime()
}
})
return (
<mesh {...(bind() as any)}>
<planeBufferGeometry args={[4, 4]} />
<AnimatedBasicShaderMaterial ref={ref} uniforms-u_offset-value={offset} />
</mesh>
)
}
export default App
Our Earth is now slowly spinning and you can drag it around to spin it manually.
More from this series
This post is part of the Dream Builder Series.
The previous post was Designing the Dream Builder.
The next post is Understanding Basic GLSL Shader Techniques.
Get in touch
If you have any questions or ideas, please email me at tom@bearjam.dev.