Анимация пролета камеры при прокрутке с помощью Theatre.js и React Three Fiber

Анимация пролета камеры при прокрутке с помощью Theatre.js и React Three Fiber

В этом уроке мы покажем вам, как отобразить 3D-сцену на вашей веб-странице и провести камеру по ней, пока пользователь прокручивает страницу, всего за 50 строк кода. Мы будем использовать Theatre.js, React Three Fiber, Drei (библиотека утилит React Three Fiber) и Vite в качестве нашего бандлера.

Требования:

Для начала создайте новый проект React с помощью Vite, выполнив команду

yarn create vite

и выбираем шаблон React.

Удалите все файлы из каталога /src, чтобы мы начали с чистого листа. Теперь мы можем приступить к реализации нашего приложения.

Давайте начнем с добавления всех зависимостей, которые мы будем использовать. Мы будем использовать 6 тесно связанных библиотек:

Three.js: библиотека JavaScript, используемая для создания и отображения анимированной трехмерной компьютерной графики в веб-браузере с помощью WebGL. Она включает функции для создания 3D-геометрии, управления камерой, освещения, отображения текстур, анимации и многого другого. Его можно использовать для создания интерактивных 3D-воздействий и игр, а также для создания 3D-визуализации данных.

React Three Fiber: рендерер React для Three.js, обеспечивающий интуитивно понятный декларативный подход к созданию 3D-сцен и компонентов. React Three Fiber упрощает работу с Three.js, позволяя разработчикам создавать трехмерный опыт, пользуясь при этом компонентно-ориентированной структурой и управлением состояниями React.

Drei: библиотека React Three Fiber, состоящая из полезных компонентов и крючков. Она включает компоненты для загрузки объектов и текстур Three.js, управления камерой, освещением, анимацией и многое другое. Она позволяет разработчикам быстро создавать трехмерные эффекты без необходимости вручную создавать и подключать компоненты Three.js.

Theatre.js: библиотека анимации с профессиональным набором инструментов для создания движений. Она поможет вам создать любую анимацию, от кинематографических сцен в THREE.js до восхитительных взаимодействий пользовательского интерфейса. @theatre/core - это основная библиотека анимации, @theatre/studio - это анимационная студия времени разработки, которую мы будем использовать для создания анимации Theatre.js, а @theatre/r3f - это расширение Theatre.js, обеспечивающее глубокую интеграцию с React Three Fiber.

# three.js, r3f, drei
yarn add three @react-three/fiber @react-three/drei

# theatre.js
yarn add @theatre/core @theatre/studio @theatre/r3f

Затем загрузите файл environment.glb и поместите его в папку /public. Этот файл содержит 3D-сцену, через которую мы будем пролетать с камерой. Конечно, вы можете использовать любой другой GLTF-файл для вашей сцены.

Соединяем все части

Установив все зависимости, создайте 3 файла в папке /src

main.jsx - Высокоуровневый код настройки для React и Theatre.js

App.jsx - код нашего приложения

main.css - Немного CSS для правильного позиционирования нашего холста.

main.jsx будет выглядеть очень похоже на то, что Vite создает по умолчанию:

import studio from "@theatre/studio";
import extension from "@theatre/r3f/dist/extension";
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./main.css";

studio.extend(extension);
studio.initialize();

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <Suspense fallback={null}>
      <App />
    </Suspense>
  </React.StrictMode>
);

Единственные 2 отличия

Мы настраиваем React Suspense в строке 13, чтобы мы могли загружать наши 3D-модели.

Мы настраиваем Theatre.js Studio в строках 8-9, сначала расширив его расширением r3f, а затем вызвав initialize().

Мы собираемся использовать main.css для заполнения экрана холстом, который мы создадим в следующем шаге с помощью React Three Fiber:

html,
body,
#root {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

body {
  display: flex;
  align-items: center;
  align-content: center;
}

Далее, давайте заполним App.jsx кодом нашего приложения:

import { Canvas, useFrame } from "@react-three/fiber";
import { Gltf, ScrollControls, useScroll } from "@react-three/drei";
import { getProject, val } from "@theatre/core";

import {
  SheetProvider,
  PerspectiveCamera,
  useCurrentSheet,
} from "@theatre/r3f";

export default function App() {
  const sheet = getProject("Fly Through").sheet("Scene");

  return (
    <Canvas gl={{ preserveDrawingBuffer: true }}>
      <ScrollControls pages={5}>
        <SheetProvider sheet={sheet}>
          <Scene />
        </SheetProvider>
      </ScrollControls>
    </Canvas>
  );
}

function Scene() {
  const sheet = useCurrentSheet();
  const scroll = useScroll();

  // our callback will run on every animation frame
  useFrame(() => {
    // the length of our sequence
    const sequenceLength = val(sheet.sequence.pointer.length);
    // update the "position" of the playhead in the sequence, as a fraction of its whole length
    sheet.sequence.position = scroll.offset * sequenceLength;
  });

  const bgColor = "#84a4f4";

  return (
    <>
      <color attach="background" args={[bgColor]} />
      <fog attach="fog" color={bgColor} near={-4} far={10} />
      <ambientLight intensity={0.5} />
      <directionalLight position={[-5, 5, -5]} intensity={1.5} />
      <Gltf src="/environment.glb" castShadow receiveShadow />
      <PerspectiveCamera
        theatreKey="Camera"
        makeDefault
        position={[0, 0, 0]}
        fov={90}
        near={0.1}
        far={70}
      />
    </>
  );
}

Давайте разберемся, что здесь происходит.

В компоненте App (строки 11-23) мы устанавливаем все зависимости для нашего компонента Scene:

getProject(“Fly Through”).sheet(“Scene”) извлекает наш лист анимации. Листы - это контейнеры для анимируемых объектов. Мы собираемся сделать этот лист доступным для расширения Theatre.js r3f через SheetProvider, который будет автоматически использовать его, поэтому нам не нужно беспокоиться о его специфике.

Компонент Canvas из r3f создает элемент WebGL canvas, который растягивается до своего родительского элемента (элемента body, размер которого мы определили в предыдущем шаге, чтобы заполнить весь экран), и устанавливает цикл рендеринга. Позже мы подключимся к этому циклу рендеринга с помощью крючка useFrame.

Компонент ScrollControls из Drei устанавливает невидимый контейнер прокрутки, который мы будем использовать для привязки позиции прокрутки к воспроизведению анимации без фактической прокрутки видимого элемента HTML.

В компоненте Scene (строки 25-56):

Мы используем useCurrentSheet(), useScroll() и useFrame() для обновления позиции анимации с актуальной позицией прокрутки на каждом кадре.

Мы создаем сцену Three.js:

Мы создаем атмосферу, похожую на небо, используя объекты цвета и тумана (строки 41-42).

Мы создаем несколько огней (строки 43-44).

Мы отображаем нашу модель GLTF, которую мы ранее поместили в общую папку (строка 45).

Мы создаем нашу камеру с помощью PerspectiveCamera, которую мы будем анимировать с помощью Theatre.js. Этот компонент импортирован из библиотеки @theatre/r3f, благодаря чему Theatre.js Studio автоматически подхватывает его без каких-либо настроек. Хотя мы задаем здесь некоторые параметры по умолчанию, все они могут быть изменены или анимированы в пользовательском интерфейсе Studio.

Если все правильно, то после выполнения всех этих шагов вы должны увидеть следующее при запуске yarn dev:

Vite + React + TS 2023-02-13T16.53.53@2x.png

Не так много, чтобы смотреть на это, но мы скоро это изменим.

Создание анимации

Откройте редактор моментальных снимков, нажав на кнопку моментального снимка на панели инструментов:

Vite + React + TS 2023-02-13T16.54.12@2x.png

При открытом редакторе снимков выберите объект Камера и переместите его в начало сцены.

Выбрав камеру, щелкните правой кнопкой мыши на свойстве position на панели справа и выберите Sequence all.

Появится редактор последовательности:

Поместите первый ключевой кадр, нажав на значок ключевого кадра рядом со свойством position на панели деталей:

Затем прокрутите немного вниз. Обратите внимание, что индикатор головки воспроизведения переместился вперед в редакторе последовательности:

Обратите внимание, обычно вы можете перетаскивать головку воспроизведения по временной шкале, однако в данном случае, поскольку мы привязали положение головки воспроизведения к положению прокрутки, это невозможно. Вы можете временно восстановить эту функциональность, закомментировав хук useFrame в строках 30-33 в App.jsx.

Теперь немного подвигайте камеру в редакторе моментальных снимков. Обратите внимание, что для вас был создан новый набор ключевых кадров. Если теперь вы попытаетесь прокрутить кадр вверх и вниз, камера будет двигаться вместе с ним.

Повторив эти действия, попробуйте переместить камеру в другой конец сцены. Аналогичным образом можно создать ключевые кадры для вращения камеры, чтобы заставить ее оглядеться вокруг.

Когда вы закончите, вы можете заметить, что движение камеры немного дрожит. Это происходит потому, что стандартное смягчение по умолчанию смягчает движение между каждым ключевым кадром. Чтобы получить плавное движение по всему пути камеры, нужно установить линейную интерполяцию. Для этого выделите все ключевые кадры положения, удерживая Shift, и перетащите поле выделения на ключевые кадры. Когда все они будут выделены, щелкните на любой из соединительных линий и выберите линейный вариант.

Чтобы проверить, как выглядит наша страница без Студии, вы можете нажать Alt/Option + , чтобы скрыть ее. Также вы можете закомментировать studio.initialize().

После завершения анимации ваша готовая сцена может выглядеть примерно так, когда вы начнете прокрутку:

Готовимся к производству

Пока что все созданные вами ключевые кадры сохраняются в localStorage браузера, так что ваша анимация будет запоминаться между обновлениями страницы.

Чтобы распространить анимацию как часть вашего сайта, экспортируйте проект Theatre.js, нажав на “Fly Through” в контурном меню в верхней левой части пользовательского интерфейса, а затем нажмите кнопку “Export Fly Through to JSON” справа.

В результате будет загружен файл JSON. Мы можем переместить этот файл в наш каталог src и импортировать его.

import flyThrougState from "./state.json"

Чтобы использовать его, достаточно заменить следующую строку (строка 12):

const sheet = getProject("Fly Through").sheet("Scene");

Со следующими:

const sheet = getProject("Fly Through", {state: flyThroughState}).sheet("Scene");

Теперь мы передаем сохраненное состояние анимации в getProject. Благодаря этому проект Theatre.js будет инициализирован с сохраненной анимацией из state.json, а не с анимацией, сохраненной в localStorage. Не волнуйтесь, все изменения, внесенные в анимацию в Studio, будут сохранены в localStorage после того, как вы это сделаете (ваши правки сохранятся после обновления страницы).

Развертывание в производство

Когда мы закончили и готовы развернуть нашу веб-страницу на производстве, нам нужно сделать только две вещи.

Убедитесь, что у нас есть последнее состояние проекта, экспортированное в JSON-файл и переданное в getProject.

Удалить studio.initialize и studio.extend (строки 8-9 в main.jsx).

Советы для дальнейшего изучения

Утилита редактирования

Импортируйте экспорт editable из @theatre/r3f.

import { editable as e } from "@theatre/r3f"

После этого, если вы хотите сделать другие объекты threejs редактируемыми в редакторе снапшотов, например, свет или туман, просто добавьте к ним префикс e., и добавьте реквизит theatreKey=”ваше имя здесь”:

<color attach="background" args={[bgColor]} />
<e.fog theatreKey="Fog" attach="fog" color={bgColor} near={-4} far={10} />
<ambientLight intensity={0.5} />
<e.directionalLight theatreKey="Sun" position={[-5, 5, -5]} intensity={1.5} />
<Gltf src="/environment.glb" castShadow receiveShadow />
<PerspectiveCamera
  theatreKey="Camera"
  makeDefault
  position={[0, 0, 0]}
  fov={90}
  near={0.1}
  far={70}
/>

После этого вы можете свободно регулировать или даже анимировать их свойства в редакторе, как мы это делали с камерой.

Пользовательские камеры Three.js от Theatre

PerspectiveCamera и OrthogramphicCamera от @theatre/r3f имеют идентичный API, что и камеры, экспортируемые @react-three/drei, с одной дополнительной фишкой: вы можете передать Vector3 или любой объект Three.js ref в свойство lookAt, чтобы камера сфокусировалась на нем. Это можно использовать для упрощения работы с камерой, например, так:

<PerspectiveCamera
  lookAt={cameraTargetRef}
  theatreKey="Camera"
  makeDefault
/>
<e.mesh theatreKey="Camera Target" visible="editor" ref={cameraTargetRef}>
  <octahedronBufferGeometry args={[0.1, 0]} />
  <meshPhongMaterial color="yellow" />
</e.mesh>