profile picture of Olivier Larose

Olivier Larose

October 17, 2023

/

Intermediate

/

Medium

3D Float Effect

Build a 3D Float Effect using Three.js, Framer Motion, Next.js

A web animation tutorial that showcases how to create a 3D float effect using Three.js inside a Next.js application. The float is made with React Three Drei and Framer Motion.

Live DemoSource codeVideo Tutorial
background video

Initializing the project

Let's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client inside of a terminal.

Setting up the page.js

We can delete everything in the page.js , global.css and page.module.css and add our own HTML and CSS, to start with a nice blank application.

  • We will use Framer Motion for the animations, so we can run npm i framer-motion.
  • We will use Framer Motion 3D for the animations as well, so we can run npm i framer-motion-3d.
  • We will use React Three Fiber for the 3D, so we can run npm i @react-three/fiber.
  • We will use React Three Drei for 3D utilitary functions, so we can run npm i @react-three/drei.

page.js

page.mod...

1

import styles from './page.module.css'

2

import FloatingShape from '../components/floatingShape';

3

4

export default function Home() {

5

return (

6

<main className={styles.main}>

7

<FloatingShape />

8

</main>

9

)

10

}

Importing the Shapes

I made those shapes using Blender, but you can use any 3D modeling tool that can export a file in (.GLB) format.

  • You can use a tool from pmnd to visualize your .glb file.
Screenshot of the Blender scene

Extracting Models from a .GLB file

Once your scene is created inside a 3D software, all you we have to do is export it in (.GLB). After that, we can extract the nodes which come with all the necessary attributes to position them correctly.

First I initialze a Canvas from react-three-fiber and I also import a utility function called environment from react-three-drei.

  • The environment basically helps with the overall color and lighting of the scene

components/FloatingShapes/index.jsx

1

'use client';

2

import React from 'react'

3

import { Canvas } from '@react-three/fiber'

4

import Model from './Model';

5

import { Environment } from '@react-three/drei'

6

7

export default function Index() {

8

9

return (

10

<Canvas style={{background: "#e0e0e2"}} orthographic camera={{position: [0, 0, 200], zoom: 10}}>

11

<Model/>

12

<Environment preset="studio"/>

13

</Canvas>

14

)

15

}
  • Note here I'm using an orthographic camera on the Canvas to squash the perspective.

Here I basically extract all the nodes from the GLB file.

components/FloatingShapes/Model.jsx

1

import React from "react";

2

import { useGLTF } from "@react-three/drei";

3

import { motion } from 'framer-motion-3d';

4

5

export default function Model() {

6

7

const { nodes } = useGLTF("/medias/floating_shapes4.glb");

8

return (

9

<group>

10

<Mesh node={nodes.Sphere001}/>

11

<Mesh node={nodes.Sphere002}/>

12

<Mesh node={nodes.Cylinder002}/>

13

<Mesh node={nodes.Sphere003}/>

14

<Mesh node={nodes.Cylinder003}/>

15

<Mesh node={nodes.Cylinder005}/>

16

<Mesh node={nodes.Cube002}/>

17

<Mesh node={nodes.Cylinder006}/>

18

<Mesh node={nodes.Cylinder007}/>

19

<Mesh node={nodes.Cylinder009}/>

20

<Mesh node={nodes.Sphere}/>

21

</group>

22

);

23

}

24

25

useGLTF.preload("/medias/floating_shapes4.glb");

26

27

function Mesh({node}) {

28

const { geometry, material, position, scale, rotation } = node;

29

30

return (

31

<motion.mesh

32

castShadow={true}

33

receiveShadow={true}

34

geometry={geometry}

35

material={material}

36

position={position}

37

rotation={rotation}

38

scale={scale}

39

/>

40

)

41

}

We should have something like this:

Screenshot of the nodes inside the Three.js scene

Adding a Float Effect

To add a basic float effect, it's actually very easy with the help of a utilitary function called Float from react-three-drei.

components/FloatingShapes/Model.jsx

1

export default function Model() {

2

3

const { nodes } = useGLTF("/medias/floating_shapes4.glb");

4

return (

5

<Float>

6

<group>

7

<Mesh node={nodes.Sphere001}/>

8

<Mesh node={nodes.Sphere002}/>

9

...

10

</group>

11

</Float>

12

);

13

}

We should have something like this:

Storing the mouse event

Now of course we need to add a little spice to that animation. Right now it's a vanilla float. An effect that everyone can have by simply using a utility function. To enhance the effect and make it a big more unique, I'll use Framer-Motion to rotate and translate the shapes on mouse move.

  • I store the relative position of the mouse (number between 0 and 1) inside a Motion Value.
  • The mouse position is smoothed out using the useSpring hook.
  • The mouse position is given to the Model Component.

components/FloatingShapes/index.jsx

1

...

2

import React, { useEffect } from 'react'

3

import { useMotionValue, useSpring } from "framer-motion"

4

5

export default function Index() {

6

7

const mouse = {

8

x: useMotionValue(0),

9

y: useMotionValue(0)

10

}

11

12

const smoothMouse = {

13

x: useSpring(mouse.x, {stiffness: 75, damping: 100, mass: 3}),

14

y: useSpring(mouse.y, {stiffness: 75, damping: 100, mass: 3})

15

}

16

17

const manageMouse = e => {

18

const { innerWidth, innerHeight } = window;

19

const { clientX, clientY } = e;

20

const x = clientX / innerWidth

21

const y = clientY / innerHeight

22

mouse.x.set(x);

23

mouse.y.set(y);

24

}

25

26

useEffect( () => {

27

window.addEventListener("mousemove", manageMouse)

28

return () => window.removeEventListener("mousemove", manageMouse)

29

}, [])

30

31

return (

32

<Canvas style={{background: "#e0e0e2"}} orthographic camera={{position: [0, 0, 200], zoom: 10}}>

33

<Model mouse={smoothMouse}/>

34

<Environment preset="studio"/>

35

</Canvas>

36

)

37

}

Enhancing the Float Effect

To enhance the float effect, I'll use Framer-Motion to transform the mouse position into new values. I'll use the useTransform hook for that.

I will basically modify 4 attributes on mouse move:

  • Rotation-x
  • Rotation-y
  • Position-x
  • Position-y

components/FloatingShapes/Model.jsx

1

import { useTransform } from 'framer-motion';

2

import { motion } from 'framer-motion-3d';

3

4

export default function Model({mouse}) {

5

6

const { nodes } = useGLTF("/medias/floating_shapes4.glb");

7

return (

8

<Float>

9

<group>

10

<Mesh node={nodes.Sphere001} multiplier={2.4} mouse={mouse}/>

11

<Mesh node={nodes.Sphere002} multiplier={2.4} mouse={mouse}/>

12

<Mesh node={nodes.Cylinder002} multiplier={1.2} mouse={mouse}/>

13

...

14

</group>

15

</Float>

16

);

17

}

18

19

useGLTF.preload("/medias/floating_shapes4.glb");

20

21

function Mesh({node, multiplier, mouse}) {

22

const { geometry, material, position, scale, rotation } = node;

23

const a = multiplier / 2;

24

const rotationX = useTransform(mouse.x, [0,1], [rotation.x - a, rotation.x + a]);

25

const rotationY = useTransform(mouse.y, [0,1], [rotation.y - a, rotation.y + a]);

26

const positionX = useTransform(mouse.x, [0,1], [position.x - multiplier * 2, position.x + multiplier * 2]);

27

const positionY = useTransform(mouse.y, [0,1], [position.y + multiplier * 2, position.y - multiplier * 2])

28

return (

29

<motion.mesh

30

...

31

rotation-y={rotationX}

32

rotation-x={rotationY}

33

position-x={positionX}

34

position-y={positionY}

35

/>

36

)

37

}

38

Couple notes about the code above:

  • I use the useTransform hook to transform the position of the mouse (number between 0 and 1) into a new value based on the current position / rotation of the mesh.
  • I use a mulitplier value to create variations between the different mesh.

We should have something like this:

Wrapping up

That's it for this animation!

I was super glad to discover how easy it is to create a float effect. And by adding a little bit of asymetry with Framer-Motion I'm satisfied with the result. Hope you learned something!

-Oli

Related Animations

image

April 21, 2024

Ripple Shader

A website animation tutorial featuring a ripple shader effect using React Three Fiber, Next.js and React. Inspired by https://homunculus.jp/ and Yuri Artiukh.

image

April 21, 2024

Bulge Effect

A website tutorial featuring a bulge distortion animation, made with a shader in GLSL, using React Three Fiber, Next.js and React.

image

April 18, 2024

3D Wave on Scroll

A website animation tutorial featuring a vertex shader with a wave animation applied on a plane. Made with React-three-fiber, Framer Motion and Next.js