David Ekanem In love with technology and discussions around technology.

Implementing 3D animations in React Native

8 min read 2300

Implementing 3D animations in React Native

The scope of this article will cover the exploration of the Three.js library and the Animated API. You should have basic knowledge of JavaScript and React Native to follow along; to learn more about all the wonderful things that can be done in React Native, the React Native archives on the LogRocket blog is a great place to brush up.

We’ll split this article into two. In the first part, we explore the creation of advanced 3D animations in React Native. We’ll rely on the Three.js library, which is a cross-platform JavaScript 3D library for the creation of 3D content, such as graphics and animation, in web environments. Three.js incorporates WebGL enhancement behaviors to render 3D models on the web and TweenMax to enhance animation quality.

The second part of the article will explore the Animated API, which lets us make our animations fluid.

Jump ahead:

Installation and prerequisites

To get started, we need to create our React Native application. Install the Expo CLI to serve our project; it works hand-in-hand with the Expo GO library, which is a mobile client app that we’ll use to open our project in iOS or Android platforms.

Once the Expo CLI is installed, proceed to run the following command in the terminal. This application will use the TypeScript template.

expo init reactNative3D
cd reactNative3D
yarn start

Before proceeding, we need to install some core dependencies. Opening up the terminal window, run the following command.

yarn add three expo-three expo-gl
yarn add --dev @types/three

Let’s review these dependencies:

  • expo-gl: This provides a View that acts as an OpenGL ES render target, which is useful for rendering 2D and 3D graphics alike. Once mounted, an OpenGL context is created that accepts the onContextCreate prop, which has a WebGL RenderingContext interface
  • expo-three: Serves as a bridge between Three.js and ExpoGL; it also provides a WebGL interface for native OpenGL-ES in React Native, which helps to abstract the DOM away from Three.js
  • three: The 3D library for creating 3D content on a webpage

How rendering 3D models with Three.js works

When rendering 3D models with Three.js, we first create a scene to serve as the set for the model to render in. The image below illustrates the basic structure of a Three.js app, where it is required to create objects and connect them together.

Three.js rendering models
Source: Three.js

Let’s explore the diagram illustrated above.

  • The key part is the renderer, the main object of Three.js. Our created scene and camera are passed to the renderer, which renders (draws) the portion of the 3D scene
  • The scene is an object that defines the root of the scenegraph and contains some properties, like the background color
  • Mesh are objects that represent the drawing of a specific Geometry with a specific Material class
  • The vertex data of a piece of Geometry (sphere, cube) is represented by the Geometry object. Three.js provides inbuilt geometry primitives
  • The surface properties used to draw the geometry are represented by the Material object. It accepts values such as color and texture
  • Texture objects represent images loaded from image files

In the following sections, we’ll use each of these structures to create a 3D animation.

Create a scene with a 3D cube

In the App.tsx component at the root of our project directory, we’ll create a basic React Native component. Import the required packages into the App.tsx component.

code App.tsx
import React from 'react';
import { View } from 'react-native';
import Expo from 'expo';
import {Scene, Mesh, MeshBasicMaterial, PerspectiveCamera} from 'three';
import ExpoTHREE, {Renderer} from 'expo-three';
import { ExpoWebGLRenderingContext, GLView } from 'expo-gl';

Proceeding to create a scene, the GLView exported from expo-gl provides a view that acts as an OpenGL ES render target. This is very useful for rendering the 3D objects we’re creating.

In the App.tsx component, create a functional component.

const App = () => {
  const onContextCreate = async (gl: Object) => {}
  return (
    <View>
      <GLView
      onContextCreate={onContextCreate}
      />
    </View>
  )
}

export default App;

The basic skeleton of our application is complete. The onContextCreate prop is being passed into the GLView with a single argument, gl, which has a WebGL RenderingContext interface.

Shifting our focus, let’s create the onContextCreate function.

  const onContextCreate = async (gl: any) => {
    // three.js implementation.
    const scene = new Scene();
    const camera = new PerspectiveCamera(
      75,
      gl.drawingBufferWidth / gl.drawingBufferHeight,
      0.1,
      1000
    );
    gl.canvas = {
      width: gl.drawingBufferWidth,
      height: gl.drawingBufferHeight,
    };

    // set camera position away from cube
    camera.position.z = 2;

    const renderer = new Renderer({ gl });
    // set size of buffer to be equal to drawing buffer width
    renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);

    // create cube
    // define geometry
    const geometry = new BoxBufferGeometry(1, 1, 1);
    const material = new MeshBasicMaterial({
      color: "cyan",
    });

    const cube = new Mesh(geometry, material);

    // add cube to scene
    scene.add(cube);

    // create render function
    const render = () => {
      requestAnimationFrame(render);
      // create rotate functionality
      // rotate around x axis
      cube.rotation.x += 0.01;

      // rotate around y axis
      cube.rotation.y += 0.01;

      renderer.render(scene, camera);
      gl.endFrameEXP();
    };

    // call render
    render();
  };

With the completion of the onContextCreate function, our 3D cube is complete.



Your App.tsx file should look like this:

import React from "react";
import { View } from "react-native";
import Expo from "expo";
import {
  Scene,
  Mesh,
  MeshBasicMaterial,
  PerspectiveCamera,
  BoxBufferGeometry,
} from "three";
import ExpoTHREE, { Renderer } from "expo-three";
import { ExpoWebGLRenderingContext, GLView } from "expo-gl";
import { StatusBar } from "expo-status-bar";

const App = () => {

  const onContextCreate = async (gl: any) => {
    // three.js implementation.
    const scene = new Scene();
    const camera = new PerspectiveCamera(
      75,
      gl.drawingBufferWidth / gl.drawingBufferHeight,
      0.1,
      1000
    );
    gl.canvas = {
      width: gl.drawingBufferWidth,
      height: gl.drawingBufferHeight,
    };

    // set camera position away from cube
    camera.position.z = 2;

    const renderer = new Renderer({ gl });
    // set size of buffer to be equal to drawing buffer width
    renderer.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);

    // create cube
    // define geometry
    const geometry = new BoxBufferGeometry(1, 1, 1);
    const material = new MeshBasicMaterial({
      color: "cyan",
    });

    const cube = new Mesh(geometry, material);

    // add cube to scene
    scene.add(cube);

    // create render function
    const render = () => {
      requestAnimationFrame(render);
      // create rotate functionality
      // rotate around x axis
      cube.rotation.x += 0.01;

      // rotate around y axis
      cube.rotation.y += 0.01;

      renderer.render(scene, camera);
      gl.endFrameEXP();
    };

    // call render
    render();
  };

  return (
    <View>
      <GLView
        onContextCreate={onContextCreate}
        // set height and width of GLView
        style={{ width: 400, height: 400 }}
      />
    </View>
  );
};

export default App;

Stop the Metro server to ensure that all new files have been added and start it up again.

ctrl c
yarn start

Open up the application with the Expo app.

Our scene with a 3D cube

Exploring the Animated API

In this section, we’ll create a 3D carousel using a FlatList and the Animated API. Let’s first create the carousel without the 3D effect.

In the App.tsx, comment out the previous code and start the new implementation from scratch. We begin by installing the dependencies we’ll need in the project.

Install the react-native-uuid library and @expo/vector-icons.

yarn add react-native-uuid @expo/vector-icons

Now, import the libraries needed into the component.

import * as React from "react";
import {
  FlatList,
  Image,
  Text,
  View,
  Dimensions,
  TouchableOpacity,
  StyleSheet,
  Animated,
} from "react-native";
import { SafeAreaView } from "react-native";
import { AntDesign } from "@expo/vector-icons";
import uuid from "react-native-uuid";
import { StatusBar } from "expo-status-bar";
const { width, height } = Dimensions.get("screen");

When creating an image carousel, specifying the width and height properties of the images in the carousel enables a better view. The Spacing variable enables reusability across different styling needs.

const IMAGE_WIDTH = width * 0.65;
const IMAGE_HEIGHT = height * 0.7;
const SPACING = 20;

Using the Pexels Images API, we can generate an array of images to populate our application.

const images = [
  "https://images.pexels.com/photos/1799912/pexels-photo-1799912.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1769524/pexels-photo-1769524.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1758101/pexels-photo-1758101.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1738434/pexels-photo-1738434.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1698394/pexels-photo-1698394.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1684429/pexels-photo-1684429.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1690351/pexels-photo-1690351.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1668211/pexels-photo-1668211.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1647372/pexels-photo-1647372.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1616164/pexels-photo-1616164.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1799901/pexels-photo-1799901.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1789968/pexels-photo-1789968.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1774301/pexels-photo-1774301.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1734364/pexels-photo-1734364.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
  "https://images.pexels.com/photos/1724888/pexels-photo-1724888.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
];

We’ll use the react-native-uuid library to seed random data into the application.

const DATA = [...Array(images.length).keys()].map((_, i) => {
  return {
    key: uuid.v4(),
    image: images[i],
  };
});

Now it’s time to implement our carousel view.

export default () => {
  return (
    <View style={{ backgroundColor: "#A5F1FA", flex: 1 }}>
      <StatusBar hidden />
      <SafeAreaView style={{ marginTop: SPACING * 1 }}>
        <View style={{ height: IMAGE_HEIGHT * 2.1 }}>
          <FlatList
            data={DATA}
            keyExtractor={(item) => item.key}
            horizontal
            pagingEnabled
            bounces={false}
            style={{ flexGrow: 0, zIndex: 9999 }}
            contentContainerStyle={{
              height: IMAGE_HEIGHT + SPACING * 2,
              paddingHorizontal: SPACING * 4,
            }}
            showsHorizontalScrollIndicator={false}
            renderItem={({ item, index }) => {
              return (
                <View
                  style={{
                    width,
                    paddingVertical: SPACING,
                  }}
                >
                  <Image
                    source={{ uri: item.image }}
                    style={{
                      width: IMAGE_WIDTH,
                      height: IMAGE_HEIGHT,
                      resizeMode: "cover",
                    }}
                  />
                </View>
              );
            }}
          />
        </View>
      </SafeAreaView>
    </View>
  );
};

Our image carousel

The image carousel has successfully been created.

Creating 3D effects with the Animated API

The next step is to use the Animated API to create the 3D effect. In order to make use of the Animated API, we’ll need to change our FlatList to an Animated.FlatList and add an onScroll event, in which we’ll pass in a NativeEvent.

A variable scrollX will be defined as the value for our x-axis. We’ll pass in a useRef() Hook to enable React to keep track of the animation. This persists the scrollX value even after re-renders.

export default () => {
  const scrollX = React.useRef(new Animated.Value(0)).current;
  return (
    <View style={{ backgroundColor: "#A5F1FA", flex: 1 }}>
      <StatusBar hidden />
      <SafeAreaView style={{ marginTop: SPACING * 1 }}>
        <View style={{ height: IMAGE_HEIGHT * 2.1 }}>
          <Animated.FlatList
            data={DATA}
            keyExtractor={(item) => item.key}
            horizontal
            pagingEnabled
            onScroll={Animated.event(
              [
                {
                  nativeEvent: { contentOffset: { x: scrollX } },
                },
              ],
              {
                useNativeDriver: true,
              }
            )}

Now we can interpolate values while relying on scrollX to create animations. In the renderItem of our FlatList, create an inputRange. We’ll use the input range figures for the interpolation. Then, create an opacity variable inside the renderItem.

           renderItem={({ item, index }) => {
              const inputRange = [
                (index - 1) * width, // next slide
                index * width, // current slide
                (index + 1) * width, // previous slide
              ];
              const opacity = scrollX.interpolate({
                inputRange,
                outputRange: [0, 1, 0],
              });
         const translateY = scrollX.interpolate({
                inputRange,
                outputRange: [50, 0, 20] // create a wave
              })

Moving on, we’ve converted the view in our project to an Animated.View, and the opacity variable we created earlier will be passed in as a style.

             return (
                <Animated.View
                  style={{
                    width,
                    paddingVertical: SPACING,
                    opacity,
                    transform: [{ translateY }]
                  }}
                >
                  <Image
                    source={{ uri: item.image }}
                    style={{
                      width: IMAGE_WIDTH,
                      height: IMAGE_HEIGHT,
                      resizeMode: "cover",
                    }}
                  />
                </Animated.View>
              );

Our image carousel with the opacity input range applied

Now when swiping, the opacity is applied based on the input range.

Adding backgrounds

Let’s add a white background to accentuate the 3D animation when we swipe the image.

Beneath the View, paste the below code block.

 <View
            style={{
              width: IMAGE_WIDTH + SPACING * 4,
              height: 450,
              position: "absolute",
              backgroundColor: "white",
              backfaceVisibility: true,
              zIndex: -1,
              top: SPACING * 1,
              left: SPACING * 1.7,
              bottom: 0,
              shadowColor: "#000",
              shadowOpacity: 0.2,
              shadowRadius: 24,
              shadowOffset: {
                width: 0,
                height: 0,
              },
            }}
          />
        </View>


Our image carousel with the white background

The next step is to animate the white background so that it will rotate in a 3D view. But before we do that, let’s figure out a way to view the inputRange between 0 and 1.

At the top of our Carousel component, create a progress variable using the methods divide() and modulo() from the Animated API, which let us modify and get the values between 0 and 1. The progress variable enables us to clamp our values to be between 0 and 1.

export default () => {
  const scrollX = React.useRef(new Animated.Value(0)).current;
  const progress = Animated.modulo(Animated.divide(scrollX, width), width);

We are now ready to start modifying the View component that holds our white background. As we did previously, convert the View component into an Animated.View.

A transform input is passed into the Animated.View component; the transform receives a perspective and a rotateY.

          <Animated.View
            style={{
              width: IMAGE_WIDTH + SPACING * 4,
              height: 450,
              position: "absolute",
              backgroundColor: "white",
              backfaceVisibility: true,
              zIndex: -1,
              top: SPACING * 1,
              left: SPACING * 1.7,
              bottom: 0,
              shadowColor: "#000",
              shadowOpacity: 0.2,
              shadowRadius: 24,
              shadowOffset: {
                width: 0,
                height: 0,
              },
              transform: [
                {
                  perspective: IMAGE_WIDTH * 4,
                },
                {
                  rotateY: progress.interpolate({
                    inputRange: [0, 0.5, 1],
                    outputRange: ["0deg", "90deg", "180deg"],
                  }),
                },

              ],
            }}
          />

Our final image carousel, with the white background and opacity input range

The repo for this project is available on GitHub.

Conclusion

In this article, we’ve explored using Three.js to create 3D content in React Native. Three.js enables the rendering of 3D models in React Native environments. When coupled with the Animated API, these tools can provide us with additional flexibility that enables us to build smoother and more enticing views for our users. This is just a taste of the amazing animations that can be carried out with the Animated API.

Hopefully, this article serves as an exploratory guide for future developers to create great user experiences.

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — .

David Ekanem In love with technology and discussions around technology.

Leave a Reply