Русский
Русский
English
Статистика
Реклама

2d-графика в React с three.js

У каждого из вас может возникнуть потребность поработать с графикой при создании React-приложения. Или вам нужно будет отрендерить большое количество элементов, причем сделать это качественно и добиться высокой производительности при перерисовке элементов. Это может быть анимация либо какой-то интерактивный компонент. Естественно, первое, что приходит в голову это Canvas. Но тут возникает вопрос: Какой контекст использовать?. У нас есть выбор 2d-контекст либо WebGl. А как на счёт 2d-графики? Тут уже не всё так очевидно.

При работе над задачами с высокой производительностью мы попробовали оба решения, чтобы на практике определиться, какой из двух контекстов будет эффективнее. Как и ожидалось, WebGl победил 2d-контекст, поэтому кажется, что выбор прост.

Но тут возникает проблема. Вы сможете ощутить её, если начнете работать с документацией WebGl. С первых мгновений становится понятно, что она слишком низкоуровневая, в отличие от 2d context. Поэтому, чтобы не писать тонны кода, перед нами встаёт очевидное решение использование библиотеки. Для реализации этой задачи подходят библиотеки pixi.js и three.js с качественной документацией, большим количеством примеров и крупным комьюнити разработчиков.

Pixi.js или three.js

На первый взгляд, выбрать подходящий инструмент несложно: используем pixi.j для 2d-графиков, а three.js для 3d. Однако, чем 2d отличается от 3d? По сути дела, отсутствием 3d-перспективы и фиксированным значением по третьей координате. Для того чтобы не было перспективы, мы можем использовать ортографическую камеру.

Вероятно, вы спросите: Что за камера?. Camera это одно из ключевых понятий при реализации графики, наряду со scene и renderer. Для наглядности приведу аналогию. Представим, что вы стоите в комнате, держите в руках смартфон и снимаете видеоролик. Та комната, в которой вы снимаете видео это scene. В комнате могут быть различные предметы, например, стол и стулья это объекты на scene. В роли camera выступает камера смартфона, в роли renderer матрица смартфона, которая проецирует 3d-комнату на 2d-экран.

Ортографическая камера отличается от перспективной, которая и используется в реальной жизни, тем, что дальние объекты в ней имеют тот же размер, что и ближние. Другими словами, если вы будете отходить от камеры, то в перспективной камере вы будете становиться меньше, а в ортографической останетесь такими же. Можно сказать, что в этой камере нет координаты z, но это не совсем так. Она есть, но она управляет наложением одного объекта на другой.

Таким образом, three.js также подходит для 2d-графики. Так что же в итоге выбрать? Мы попробовали оба варианта и выявили на практике несколько преимуществ three.js.

  • Во-первых, нам нужно было выполнить интерактивное взаимодействие с элементами на сцене. Написать собственную реализацию достаточно трудозатратно, но в обеих библиотеках уже есть готовые решения: в pixi.js из коробки, в three.js библиотека three.interaction.

Казалось бы, в этом плане библиотеки равноценны, но это лишь первое впечатление. Особенность реализации интерактивности в pixi.js предполагает, что интерактивные элементы должны иметь заливку. Но как быть с линейными графиками? У них же нет заливки. Без собственного решения в этом случае не обойтись. Что же касается three.js, то тут этой проблемы нет, и линейные графики также интерактивны.

  • Еще одна задача это экспорт в SVG. Нам нужно было реализовать функциональность, которая позволит экспортировать в SVG то, что мы видим на сцене, чтобы потом это изображение можно было использовать в печати. В three.js для этого есть готовый пример, а вот в pixi.js нет.

  • Ну и будем честны с собой, в three.js больше примеров реализации тех или иных задач. К тому же, изучив эту библиотеку, при желании мы можем работать с 3d-графикой, а вот в случае pixi.js такого преимущества у нас нет.

Исходя из всего вышеописанного, наш выбор очевиден это three.js.

Three.js и React

После выбора библиотеки мы сталкиваемся с новой дилеммой использовать react-обертку или каноническую three.js.

Для react есть реализация обёртки это react-three-fiber. На первый взгляд, в ней довольно мало документации, что может показаться проблемой. Действительно, при переносе кода из примеров three.js в react-three-fiber возникает много вопросов по синтаксису.

Однако, на практике все не так уж сложно. У этой библиотеки есть обёртка drei с неплохим storybook с готовой реализацией множества различных примеров. Впрочем, всё, что за находится пределами этой реализации, по-прежнему может причинять боль.

Еще одна проблема это жёсткая привязка к react. А если мы отлично реализуем view с графикой и захотим использовать где-то ещё? В таком случае снова придётся поработать.

Учитывая эти факторы, мы решили использовать каноническую three.js и написать свою собственную обертку на хуках. Если вы не хотите перебирать множество вариантов реализации, попробуйте использовать нативные ES6 классы это хорошее и производительное решение.

Вот пример нашей архитектуры. В центре сцены нарисован квадрат, который при нажатии на него меняет цвет с синего на серый и с серого на синий.

Создаём класс three.js для работы с библиотекой three.js. По сути, всё взаимодействие с ней будет проходить в объекте данного класса.

class Three {  constructor({    canvasContainer,    sceneSizes,    rectSizes,    color,    colorChangeHandler,  }) {    // Для использования внутри класса добавляем параметры к this    this.sceneSizes = sceneSizes;    this.colorChangeHandler = colorChangeHandler;     this.initRenderer(canvasContainer); // создание рендерера    this.initScene(); // создание сцены    this.initCamera(); // создание камеры    this.initInteraction(); // подключаем библиотеку для интерактивности    this.renderRect(rectSizes, color); // Добавляем квадрат на сцену    this.render(); // Запускаем рендеринг  }   initRenderer(canvasContainer) {    // Создаём редерер (по умолчанию будет использован WebGL2)    // antialias отвечает за сглаживание объектов    this.renderer = new THREE.WebGLRenderer({antialias: true});     //Задаём размеры рендерера    this.renderer.setSize(this.sceneSizes.width, this.sceneSizes.height);     //Добавляем рендерер в узел-контейнер, который мы прокинули извне    canvasContainer.appendChild(this.renderer.domElement);  }   initScene() {    // Создаём объект сцены    this.scene = new THREE.Scene();     // Задаём цвет фона    this.scene.background = new THREE.Color("white");  }   initCamera() {    // Создаём ортографическую камеру (Идеально подходит для 2d)    this.camera = new THREE.OrthographicCamera(      this.sceneSizes.width / -2, // Левая граница камеры      this.sceneSizes.width / 2, // Правая граница камеры      this.sceneSizes.height / 2, // Верхняя граница камеры      this.sceneSizes.height / -2, // Нижняя граница камеры      100, // Ближняя граница      -100 // Дальняя граница    );     // Позиционируем камеру в пространстве    this.camera.position.set(      this.sceneSizes.width / 2, // Позиция по x      this.sceneSizes.height / -2, // Позиция по y      1 // Позиция по z    );  }   initInteraction() {    // Добавляем интерактивность (можно будет навешивать обработчики событий)    new Interaction(this.renderer, this.scene, this.camera);  }   render() {    // Выполняем рендеринг сцены (нужно запускать для отображения изменений)    this.renderer.render(this.scene, this.camera);  }   renderRect({width, height}, color) {    // Создаём геометрию - квадрат с высотой "height" и шириной "width"    const geometry = new THREE.PlaneGeometry(width, height);     // Создаём материал с цветом "color"    const material = new THREE.MeshBasicMaterial({color});     // Создаём сетку - квадрат    this.rect = new THREE.Mesh(geometry, material);     //Позиционируем квадрат в пространстве    this.rect.position.x = this.sceneSizes.width / 2;    this.rect.position.y = -this.sceneSizes.height / 2;     // Благодаря подключению "three.interaction"    // мы можем навесить обработчик нажатия на квадрат    this.rect.on("click", () => {      // Меняем цвет квадрата      this.colorChangeHandler();    });     this.scene.add(this.rect);  }   // Служит для изменения цвета квадрат  rectColorChange(color) {    // Меняем цвет квадрата    this.rect.material.color.set(color);     // Запускаем рендеринг (отобразится квадрат с новым цветом)    this.render();  }}

А теперь создаём класс ThreeContauner, который будет React-обёрткой для нативного класса Three.

import {useRef, useEffect, useState} from "react"; import Three from "./Three"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeContainer = () => {  const threeRef = useRef(); // Используется для обращения к контейнеру для canvas  const three = useRef(); // Служит для определения, создан ли объект, чтобы не создавать повторный  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы изменить цвет  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на серый    colorChange((prevColor) => (prevColor === "grey" ? "blue" : "grey"));  };   // Создание объекта класса Three, предназначенного для работы с three.js  useEffect(() => {    // Если объект класса "Three" ещё не создан, то попадаем внутрь    if (!three.current) {      // Создание объекта класса "Three", который будет использован для работы с three.js      three.current = new Three({        color,        rectSizes,        sceneSizes,        colorChangeHandler,        canvasContainer: threeRef.current,      });    }  }, [color]);   // при смене цвета вызывается метод объекта класса Three  useEffect(() => {    if (three.current) {      // Запускаем метод, который изменяет в цвет квадрата      three.current.rectColorChange(color);    }  }, [color]);   // Данный узел будет контейнером для canvas (который создаст three.js)  return <div className="container" ref={threeRef} />;}; export default ThreeContainer;

А вот пример работы данного приложения.

При первом открытии мы получаем, как и было описано ранее, синий квадрат в центре сцены, которая имеет серый цвет.

После нажатия на квадрат он меняет цвет и становится белым.

Как мы видим, использование нативного three.js внутри React-приложения не вызывает каких-либо проблем, и этот подход достаточно удобен. Однако, на плечи разработчика в этом случае ложится нагрузка, связанная с добавлением/удалением узлов со сцены. Таким образом, теряется тот подход, который берёт на себя virtual dom внутри React-приложения. Если вы не готовы с этим мириться, обратите внимание на библиотеку react-three-fiber в связке с библиотекой drei этот способ позволяет мыслить в контексте React-приложения.

Рассмотрим реализованный выше пример с использованием этих библиотек:

import {useState} from "react";import {Canvas} from "@react-three/fiber";import {Plane, OrthographicCamera} from "@react-three/drei"; // Размеры сцены и квадратаconst sceneSizes = {width: 800, height: 500};const rectSizes = {width: 200, height: 200}; const ThreeDrei = () => {  const [color, colorChange] = useState("blue"); // Состояние отвечает за цвет квадрата   // Handler служит для того, чтобы  const colorChangeHandler = () => {    // Просто поочерёдно меняем цвет с серого на синий и с синего на белый    colorChange((prevColor) => (prevColor === "white" ? "blue" : "white"));  };   return (    <div className="container">      {/* Здесь задаются параметры, которые отвечают за стилизацию сцены */}      <Canvas className="container" style={{...sceneSizes, background: "grey"}}>        {/* Камера задаётся по аналогии с нативной three.js, но нужно задать параметр makeDefault,         чтобы применить именно её, а не камеру заданную по умолчанию */}        <OrthographicCamera makeDefault position={[0, 0, 1]} />        <Plane          // Обработка событий тут из коробки          onClick={colorChangeHandler}          // Аргументы те же и в том же порядке, как и в нативной three.js          args={[rectSizes.width, rectSizes.height]}        >          {/* Материал задаётся по аналогии с нативной three.js,               но нужно использовать attach для указания типа прикрепления узла*/}          <meshBasicMaterial attach="material" color={color} />        </Plane>      </Canvas>    </div>  );}; export default ThreeDrei;

Как видите, кода стало гораздо меньше и он стал более прозрачным, с точки зрения его чтения. Однако, как уже упоминалось выше, в этом случае разработчику доступноменьше примеров реализации различных компонентов. Кроме того, в процессе изучения некоторые особенности работы могут не быть очевидными, что заставляет подумать над их реализацией.

В этой статье мы с вами рассмотрели два подхода в использовании библиотеки three.js внутри React-приложения. Каждый из этих подходов имеет свои плюсы и минусы, поэтому выбор за вами.

Спасибо за внимание! Надеемся, что наш опыт был для вас полезен.

Источник: habr.com
К списку статей
Опубликовано: 06.06.2021 12:22:20
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании simbirsoft

Javascript

Программирование

Reactjs

React

2d

Three.js

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru