Skip to content

Latest commit

Β 

History

History
275 lines (213 loc) Β· 8.91 KB

recipes.md

File metadata and controls

275 lines (213 loc) Β· 8.91 KB

Recipes

Table of Contents

Animating with react-spring

react-spring supports react-three-fiber out of the box:

import { Canvas } from 'react-three-fiber'
import { a, useSpring } from '@react-spring/three'

function Box(props) {
  const [active, setActive] = useState(0)

  // create a common spring that will be used later to interpolate other values
  const { spring } = useSpring({
    spring: active,
    config: { mass: 5, tension: 400, friction: 50, precision: 0.0001 },
  })

  // interpolate values from common spring
  const scale = spring.to([0, 1], [1, 5])
  const rotation = spring.to([0, 1], [0, Math.PI])
  const color = spring.to([0, 1], ['#6246ea', '#e45858'])

  return (
    // using a from react-spring will animate our component
    <a.mesh rotation-y={rotation} scale-x={scale} scale-z={scale} onClick={() => setActive(Number(!active))}>
      <boxBufferGeometry args={[1, 1, 1]} />
      <a.meshStandardMaterial roughness={0.5} color={color} />
    </a.mesh>
  )
}

CodeSandbox

Dealing with effects (hijacking main render-loop)

Managing effects can get quite complex normally. Drop the component below into a scene and you have a live effect. Remove it and everything is as it was without any re-configuration.

import { extend, Canvas, useFrame, useThree } from 'react-three-fiber'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass'
extend({ EffectComposer, RenderPass, GlitchPass })

function Effects() {
  const { gl, scene, camera, size } = useThree()
  const composer = useRef()
  useEffect(() => void composer.current.setSize(size.width, size.height), [size])
  useFrame(() => composer.current.render(), 1)
  return (
    <effectComposer ref={composer} args={[gl]}>
      <renderPass attachArray="passes" args={[scene, camera]} />
      <glitchPass attachArray="passes" renderToScreen />

Using your own camera rig

function Camera(props) {
  const ref = useRef()
  const set = useThree(state => state.set)
  // Make the camera known to the system
  useEffect(() => void set({ camera: ref.current }), [])
  // Update it every frame
  useFrame(() => ref.current.updateMatrixWorld())
  return <perspectiveCamera ref={ref} {...props} />
}

<Canvas>
  <Camera position={[0, 0, 10]} />

Heads-up display (rendering multiple scenes)

useFrame allows components to hook into the render-loop, or even to take it over entirely. That makes it possible for one component to render over the content of another. The order of these operations is established by the priority you give it, higher priority means it renders first.

function Main() {
  const scene = useRef()
  const { camera } = useThree()
  useFrame(({ gl }) => void ((gl.autoClear = true), gl.render(scene.current, camera)), 100)
  return <scene ref={scene}>{/* ... */}</scene>
}

function HeadsUpDisplay() {
  const scene = useRef()
  const { camera } = useThree()
  useFrame(({ gl }) => void ((gl.autoClear = false), gl.clearDepth(), gl.render(scene.current, camera)), 10)
  return <scene ref={scene}>{/* ... */}</scene>
}

function App() {
  const camera = useRef()
  const { size, setDefaultCamera } = useThree()
  useEffect(() => void setDefaultCamera(camera.current), [])
  useFrame(() => camera.current.updateMatrixWorld())
  return (
    <>
      <perspectiveCamera
        ref={camera}
        aspect={size.width / size.height}
        radius={(size.width + size.height) / 4}
        onUpdate={self => self.updateProjectionMatrix()}
      />
      <Main />
      <HeadsUpDisplay />

Managing imperative code

Stick imperative stuff into useMemo and write out everything else declaratively. This is how you can quickly form reactive, re-usable components that can be bound to a store, graphql, etc.

function Extrusion({ start = [0, 0], paths, ...props }) {
  const shape = useMemo(() => {
    const shape = new THREE.Shape()
    shape.moveTo(...start)
    paths.forEach((path) => shape.bezierCurveTo(...path))
    return shape
  }, [start, paths])
  return (
    <mesh>
      <extrudeGeometry args={[shape, props]} />
      <meshPhongMaterial />
    </mesh>
  )
}

;<Extrusion
  start={[25, 25]}
  paths={[
    [25, 25, 20, 0, 0, 0],
    [30, 0, 30, 35, 30, 35],
    [30, 55, 10, 77, 25, 95],
  ]}
  bevelEnabled
  amount={8}
/>

ShaderMaterials

function CrossFade({ url1, url2, disp }) {
  const [texture1, texture2, dispTexture] = useLoader(THREE.TextureLoader, [url1, url2, disp])
  return (
    <mesh>
      <planeBufferGeometry  args={[1, 1]} />
      <shaderMaterial
        args={[CrossFadeShader]}
        uniforms-texture-value={texture1}
        uniforms-texture2-value={texture2}
        uniforms-disp-value={dispTexture}
        uniforms-dispFactor-value={0.5} />
    </mesh>
  )

Re-parenting

We support portals. You can use them to teleport a piece of the view into another container. Click here for a small demo.

import { createPortal } from 'react-three-fiber'

function Component() {
  // "target" can be a three object, like a group, etc
  return createPortal(<mesh />, target)

Rendering only when needed

By default it renders like a game loop 60fps. Set frameloop="demand" to activate loop invalidation. Now it will render on demand when it detects prop changes.

<Canvas frameloop="demand" ... />

Sometimes you want to render single frames manually, for instance when you're dealing with async stuff:

const invalidate = useThree((state) => state.invalidate)
// request a frame for *this* root
const texture = useMemo(() => loader.load(url, invalidate), [url])

For cases where you want to want to invalidate all roots:

import { invalidate } from '@react-three/fiber'
// request a frame for all roots
invalidate()

For camera controls here's an example sandbox which uses:

const Controls = () => {
  const { camera, gl, invalidate } = useThree()
  const ref = useRef()
  useFrame(() => ref.current.update())
  useEffect(() => void ref.current.addEventListener('change', invalidate), [])
  return <orbitControls ref={ref} args={[camera, gl.domElement]} />
}

Enabling VR

Supplying the vr flag enables Three's VR mode and switches the render-loop to gl.setAnimationLoop as described in Three's docs.

import * as VR from '!exports-loader?WEBVR!three/examples/js/vr/WebVR'
import { Canvas } from 'react-three-fiber'
;<Canvas vr onCreated={({ gl }) => document.body.appendChild(VR.createButton(gl))} />

Reducing bundle-size

Threejs is quite heavy and tree-shaking doesn't yet yield the results you would hope for atm. But you can always create your own export-file and alias "three" towards it. This way you can reduce it to 80-50kb, or perhaps less, depending on what you need. Gist: https://gist.github.com/drcmda/974f84240a329fa8a9ce04bbdaffc04d

Usage with React Native

You can use react-three-fiber to build universal (native and web) apps via Expo's WebGL package (expo-gl).

πŸ’‘ Bootstrap: npx create-react-native-app -t with-react-three-fiber

Be sure to use a physical iOS or Android device for testing because the simulator can have issues running graphics heavy apps.

Manual setup

# Install the Expo CLI

npm i -g expo-cli

# Create a new project

expo init myapp
cd myapp

# Install packages

yarn add expo-gl expo-three three@latest react-three-fiber@beta

# Start the project

yarn start

Safari support

Safari 12 does not support ResizeObserver out of the box, which causes errors in the react-use-measure dependency if you don't polyfill it. @juggle/resize-observer is the recommended ResizeObserver polyfill. It can be configured through the resize property on the <Canvas>:

import { ResizeObserver } from '@juggle/resize-observer'
;<Canvas resize={{ polyfill: ResizeObserver }} />