AccueilClientsExpertisesBlogOpen SourceJobsContact

21 juillet 2023

Animation 3D avec React Native

8 minutes de lecture

Animation 3D avec React Native

Vous avez surement dû croiser dans divers sites ou applications mobiles de belles animations qui fascinent et captivent le regard, n’est-ce pas ?

Et bien celles-ci sont pour une grande majorité, réalisées à partir de threeJS permettant des rendus d’objets 2D ou 3D.

Suite à la conférence inspirante, 3D and Canvas-Based Animations with Reanimated de Krzysztof Piaskowy présentée lors de l’event App.Js Conf 2023, nous avons donc souhaité explorer un peu plus en détails cet univers à travers un petit exemple d’objet 3D afin de mieux comprendre le fonctionnement et les mécanismes derrière ces animations.

Dans cet article, nous allons découvrir les bases de l’animation 3D en utilisant des bibliothèques JS compatibles avec React Native : Expo-GL, React-three-fiber et reanimated.

Quelques bases

Retour aux Mathématiques !

Ne vous inquiétez pas nous n’allons pas aborder des notions compliquées, le but n’étant pas de faire resurgir certains cauchemars du passé !

Lors de la création d’objets en 3D, nous allons souvent utiliser les coordonnées x, y et z.

En effet, la 3D est une dimension composée de 3 axes orthogonaux aussi connu sous le nom de x (abscisse/largeur), y (ordonnée/profondeur) et z (hauteur/longueur). Grâce à ces axes, les objets auront une forme, un volume et emplacement dans la dimension créée.

axes

La représentation de l’espace est une notion essentielle pour la création et la manipulation des objets 3D dans la scène. Vous serez souvent amené à utiliser également les fonctions de trigonométrie: sinus (SIN), cosinus (COS) et tangente (TAN) par la suite.

Petit rappel sur ces 3 fonctions légendaires: Utilisées dans les triangles rectangles, elles permettent de traiter les relations entre distances et angles. COS ⇒ relation entre le coté adjacent à l’angle et l’hypoténuse SIN ⇒ relation entre le côté opposé à l’angle et l’hypoténuse TAN ⇒ relation entre le COS et le SIN, côté adjacent et le côté opposé de l’angle

Schéma récapitulatif (source: https://lespritsorcier.org/blogs-membres/introduction-trigonometrie)

trigonometrie

Voilà, le petit tour récapitulatif est terminé.

Présentation des Librairies

Expo-gl, ce package fournit une vue sur laquelle se baseront toutes les actions et traitements à OpenGL (un ensemble de fonctions de calcul d'images 2D ou 3D lancé par Silicon Graphics en 1992). Grâce à cela il est possible de créer des composants graphiques 2D ou 3D et de les afficher dans la vue. Mettons en place un exemple de rendu.

Pour importer la librairie, conférer à la documentation officielle. L’importer à la racine du projet:

npx expo install expo-gl

Ensuite dans le fichier App.tsx, nous allons instancier la vue avec une caméra et une scène de départ:

const App = () => {
  const _onContextCreate = async (gl: ExpoWebGLRenderingContext) => {
    // step 1:  Création de la scène à afficher
    const scene = new THREE.Scene()

    // step 2: Création de la caméra dans la dimension 3D
    const camera = new THREE.PerspectiveCamera(
      75, // Angle de vue
      gl.drawingBufferWidth / gl.drawingBufferHeight, // Ratio
      0.1, // Distance de début de rendu
      1000 // Distance de fin de rendu
    )
    camera.position.z = 5 // initialisation de la position sur l'axe z

    // Création d'un "Renderer" WEBGL sans le DOM habituel avec THREE
    let renderer = new ExpoTHREE.Renderer({
      gl,
      height: gl.drawingBufferHeight,
      width: gl.drawingBufferWidth,
    })

    // Rendu de la scène
    renderer.render(scene, camera)
    // informe le contexte que la trame actuelle est prête à être présentée.
    gl.endFrameEXP()
  }

  return <GLView style={{ flex: 1 }} onContextCreate={_onContextCreate} />
}
export default App

Et voilà notre première base 3D est prête ! Mais semble un peu vide..

Ajoutons un élément graphique pour combler cet espace:

const App = () => {
	const drawLine() {
		const curve = new THREE.EllipseCurve(
	    0, // ax
	    0, // aY
	    1.5, // xRadius
	    0.5, // yRadius
	    0, // aStartAngle
	    2 * Math.PI, // aEndAngle
	    false, // aClockwise
	    0 // aRotation
	  );

	  const points = curve.getPoints(100);
	  const geometryLine = new THREE.BufferGeometry().setFromPoints(points);
	  const materialLine = new THREE.LineBasicMaterial({ color });
	  materialLine.linewidth = 10;

	  const ellipse = new THREE.Line(geometryLine, materialLine);
	  const rotationEuler = new THREE.Euler(posX, posY, posZ, "XYZ");
	  const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(
	    rotationEuler
	  );
	  // ou ellipse3.rotation.z = -1;  / ellipse3.rotation.x = 0; / ellipse3.rotation.y = 0;
	  ellipse.applyMatrix4(rotationMatrix);

	  return ellipse;
	}

	const drawSphere() => {
	  const sphere = new THREE.Mesh(
	    new THREE.SphereGeometry(0.15),
	    new THREE.MeshBasicMaterial({ color, toneMapped: false })
	  );
	  sphere.position.set(posX, posY, posZ);
	  sphere.rotation.set(rotateX, rotateY, rotateZ);

	  return sphere;
	}

	const _onContextCreate = async (gl: ExpoWebGLRenderingContext) => {
		[...]

		const ellipse1 = drawLine(0x0000ff, 0, 0, 0);
    scene.add(ellipse1);

    const ellipse2 = drawLine(0x0000ff, 0, 0, 1);
    scene.add(ellipse2);

    const ellipse3 = drawLine(0x0000ff, 0, 0, -1);
    scene.add(ellipse3);

    const sphere = drawElectron(0, 0, 0.5, 0, 0, 0, 0x0000ff);
    scene.add(sphere);

		[...]
	}

	return <GLView style={{ flex: 1 }} onContextCreate={_onContextCreate} />
}
export default App;

Résultat :

screen-logo-react

Incroyable un atôme est apparu ! Mais vous me direz que nous sommes loin de l’écosystème React avec ses composants réutilisables gagne-temps et autonomes. Heureusement, il existe react-three-fiber, une librairie simplifiant l'utilisation de ThreeJS avec React, compatible avec Expo.

Faisons une petite comparaison de code entre expo-gl et react-three-fiber :

- import { ExpoWebGLRenderingContext, GLView } from "expo-gl";
- import * as React from "react";
- import * as THREE from "three";
+ import { Canvas } from "@react-three/fiber";

const App = () => {
- const _onContextCreate = async (gl: ExpoWebGLRenderingContext) => {
-   const scene = new THREE.Scene();
-   const sphere = new THREE.Mesh(
-     new THREE.SphereGeometry(0.15),
-     new THREE.MeshBasicMaterial({ color: "tomato", toneMapped: false })
-   );
-   sphere.position.set(0, 0, 0);
-   sphere.rotation.set(2, 0, 0);

-   const group = new THREE.Group();
-   group.add(sphere);

-   scene.add(group);
- };

- return <GLView style={{ flex: 1 }} onContextCreate={_onContextCreate} />;

  return (
+   // Scène rendue depuis le composant Canvas
+   <Canvas>
+     <group>
+       <mesh>
+         <sphereGeometry args={[0.15]} />
+         <meshStandardMaterial color="tomato" toneMapped={false} />
+       </mesh>
+     </group>
+   </Canvas>
  );
};

export default App;

Une nette différence s’impose.

Démo avec react-three-fiber

Ajoutons à notre projet la librairie

npx expo install three @react-three/fiber expo-three

Adaptons notre logo React façon composants et en 3D :

const App = () => {
  return (
    <Canvas>
      <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
      <ambientLight />
      // Création de l'Atom
      <group>
        <Ring color={0x0053ff} />
        <Ring color={0x0053ff} rotation={[0, 0, Math.PI / 3]} />
        <Ring color={0x0053ff} rotation={[0, 0, Math.PI / -3]} />

        <Electron rotation={[0, 0, 0]} color="lightblue" />
        <Electron
          position={[0, 0, 0]}
          rotation={[0, 0, Math.PI / 3]}
          speed={2.5}
          color="lightblue"
        />
        <Electron
          position={[0, 0, 0]}
          rotation={[0, 0, -Math.PI / 3]}
          speed={3}
          color="lightblue"
        />
      </group>
    </Canvas>
  )
}

Plus pratique ne trouvez-vous pas ?

Prochaine étape, l’animation 💃

Animation de l’objet

Comment cela se présente :

useFrame((state) => {
  const time = state.clock.getElapsedTime() * speed
  ref.current.position.set(
    Math.sin(time) * radius, // x
    (Math.cos(time) * radius * Math.atan(time)) / Math.PI / 1.25, // y
    0
  ) // z
})

return (
  <group>
    <mesh ref={ref}>
      <sphereGeometry args={[0.15]} />
      <meshBasicMaterial color={[0, 1, 5]} toneMapped={false} />
    </mesh>
  </group>
)

Le hook useFrame permet d'exécuter du code sur chaque image rendue comme un changement de position, de rotation ou d’autres paramètres facilement.

Néanmoins, l’animation des objets 3D peut vite devenir coûteuse et créer des soucis de performances et d’affichages suivant les appareils. L’objectif est d’obtenir un effet fluide et naturel sans charger les thread JS.

Il existe plusieurs solutions pour anticiper ou pallier ce soucis.

La fonction de callback React associé à React-three-fiber :

  • Suspense : qui s’occupe des états de chargement progressifs
import { Suspense } from "react";
<Canvas>
  <Suspense fallback={null}>
		{*/ vos composants */}
	</Suspense>
</Canvas>

React-three-fiber met également à disposition des composants et des méthodes pour aider:

  • frameloop : autoriser le rendu sur demande et réduire les rendus avec un système d’immobilisation
<Canvas frameloop="demand">
  • invalidate : déclencher manuellement une nouvelle demande de rendu d’image
invalidate()
  • useLoader : charger et mettre en cache la ressource
const { nodes, materials } = useLoader(GLTFLoader, '/shoe.glb')

Une autre alternative serait l’utilisation d’une librairie: Reanimated.

Celle-ci est déjà intégrée avec Expo GL et permet une mise à jour du Context GL directement depuis le front sans latence. Une utilisation basée sur le thread UI afin d'alléger le thread JS et ainsi réduire le temps d'execution du JS.

Adaptons cette solution à notre projet.

Reanimated

Import de la librairie

npx expo install react-native-reanimated

Ajouter le plugin dans le return du fichier babel.config.js

plugins: ['react-native-reanimated/plugin']

Mis en place d’une animation interactive à l’aide des capteurs de l’appareil avec le hook useAnimatedSensor. Il permet d’utiliser les données du gyroscope par exemple.

import { SensorType, useAnimatedSensor } from "react-native-reanimated";
import { useFrame } from "@react-three/fiber";

const ref = useRef<Mesh>(null!);

// SensorType: ACCELEROMETER, GYROSCOPE,GRAVITY, MAGNETIC_FIELD, ROTATION
const animatedSensor = useAnimatedSensor(SensorType.GYROSCOPE, {
  interval: 100,
});

useFrame(() => {
  if (!ref.current) return;
	// update des rotations x,y et z en fonction du mouvement donné à l'appareil
  let { x, y, z } = animatedSensor.sensor.value;
  x = (x * 100) / 5000;
  y = (y * 100) / 5000;
  ref.current.rotation.x += x;
  ref.current.rotation.y += y;
  ref.current.rotation.z += z;
});

<mesh ref={ref}>
	<CircleLine color={0x0053ff} />
	<CircleLine color={0x0053ff} rotation={[0, 0, Math.PI / 3]} />
	<CircleLine color={0x0053ff} rotation={[0, 0, Math.PI / -3]} />
</mesh>

1, 2, 3. . . 🪄 !

Conclusion

L’animation ainsi que les objets 2D-3D apportent du charme, du dynamisme et un amusement visuel entraînant une certaine modernité dans l’usage d'un site ou d’une application.

Ces éléments sont généralement peu présents dans des applications métiers traditionnelles, mais peuvent facilement être mis en place avec les technologies vues dans cet article. Même si cela n'est pas notre expertise, nous n'hésiterons pas à proposer ce type de solutions à nos clients.

N'hésitez pas à expérimenter avec ces nouvelles connaissances et librairies vous aussi ! Bonne création !

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
Twitter
Flux RSS

Naviguez à vue