Skip to example

Three.js materials

An example of animating Three.js material colour, opacity, roughness and metalness with animate() and threeEffect in Motion.

JavaScript

Source code

<div class="stage">
  <canvas></canvas>
  <button class="toggle">Toggle material</button>
</div>

<script type="module">
  import { animate, frame } from "motion"
  import { threeEffect } from "motion/three"
  import * as THREE from "three"

  animate.addEffect(threeEffect)

  /**
   * Read colours from the page's CSS tokens. animate() accepts any CSS
   * colour string for Three.js Color properties.
   */
  const token = (name) =>
    getComputedStyle(document.body).getPropertyValue(name).trim()
  const mint = token("--hue-6")
  const pink = token("--hue-1")

  const canvas = document.querySelector("canvas")

  function runMaterialMotion(mesh, material) {
    let active = false
    document.querySelector(".toggle").addEventListener("click", () => {
      active = !active

      /**
       * Material properties can be animated through the mesh. Motion looks
       * on the object first, then on its material, so transforms and
       * material values can share the same animation.
       */
      animate(
        mesh,
        active
          ? { rotateY: 180, scale: 1.2, color: pink, opacity: 0.6 }
          : { rotateY: 0, scale: 1, color: mint, opacity: 1 },
        {
          type: "spring",
          visualDuration: 0.7,
          bounce: 0.3,
          color: { duration: 0.8, ease: "easeInOut" },
          opacity: { duration: 0.8, ease: "easeInOut" },
        }
      )

      /**
       * Or animate the material directly
       */
      animate(
        material,
        active
          ? { roughness: 0.15, metalness: 0.6 }
          : { roughness: 0.7, metalness: 0.1 },
        { duration: 0.8, ease: "easeInOut" }
      )
    })
  }

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: true,
  })
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
  renderer.setSize(canvas.clientWidth, canvas.clientHeight, false)

  const scene = new THREE.Scene()
  const camera = new THREE.PerspectiveCamera(
    35,
    canvas.clientWidth / canvas.clientHeight,
    0.1,
    100
  )
  camera.position.z = 5

  scene.add(new THREE.HemisphereLight(token("--white"), token("--layer"), 2))
  const keyLight = new THREE.DirectionalLight(token("--white"), 3)
  keyLight.position.set(3, 4, 5)
  scene.add(keyLight)

  const material = new THREE.MeshStandardMaterial({
    color: mint,
    roughness: 0.7,
    metalness: 0.1,
    transparent: true,
  })
  const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2, 1), material)
  scene.add(mesh)

  frame.render(() => renderer.render(scene, camera), true)
  runMaterialMotion(mesh, material)
</script>

<style>
  .stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    width: 480px;
    max-width: 100vw;
  }

  .stage canvas {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
  }

  .stage button {
    background-color: var(--hue-1);
    color: var(--white);
    font-size: 15px;
    padding: 10px 20px;
    border-radius: 0;
    border: none;
    cursor: pointer;
  }
</style>