Как упорядочить свой код Three.js более чистым способом

Простой подход к написанию понятного и поддерживаемого кода Three.js

Когда я только начинал работать с Three.js, я не знал, как организовать свои проекты таким образом, чтобы они были ясными, последовательными и простыми в обслуживании. Я не знал Javascript и вообще не имел опыта веб-разработки, что поначалу немного сбивало с толку.

После некоторой практики и размышлений над процессом, я думаю, что смог придумать достойный способ структурировать свои проекты. Это решение достаточно хорошо работает для приложений Three.js простой и средней сложности. В этом посте я поделюсь своим подходом.

Пожалуйста, если у вас есть лучшее решение или какие-то улучшения, которые вы хотите предложить: дайте мне знать. Это решение ни в коем случае не является окончательным, я постоянно его улучшаю и намерен продолжать это делать.

Для простоты в этой статье я сосредоточусь только на логической структуре проекта Three.js, я не буду использовать какие-либо инструменты или «расширенные» функции Javascript.

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

Обзор

Цель этого решения - максимально разделить каждый компонент приложения. Различные части приложения не должны много знать друг о друге и, по возможности, вообще ничего.

  • Веб-ориентированный код (относительно DOM, событий и т. Д.) Не должен ничего знать о мире Three.js, и, наоборот, код Three.js ничего не должен знать о DOM.
  • Сторона приложения Three.js должна быть модульной, она должна представлять собой набор независимых компонентов.

Компонент высокого уровня (или контейнер) используется в качестве точки входа в приложение Three.js, он отвечает за инициализацию и управление сценой, но не знать что-либо о фактическом содержании сцены. Единственное, что он знает, это то, что он содержит множество других компонентов, которые должны обновляться в каждом кадре.

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

Зависимости развиваются только в одном направлении, от самого внешнего компонента к самому внутреннему.

Пример

Мы хотим построить солнечную систему в нашем приложении Three.js.

Компонент более высокого уровня будет классом, отвечающим за инициализацию сцены Three.js (холст, средство визуализации и т. Д.), А также за инициализацию и удержание объектов сцены.

Солнечная система - это объект сцены. Он содержит множество планет. И, возможно, он движется в космосе. (Когда Солнечная система перемещается, все ее планеты также перемещаются)

Каждая планета представляет собой объект сцены.

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

Структура высокого уровня

Прежде чем мы сможем поговорить о Three.js, мне нужно объяснить основную структуру проекта.

Корневой уровень проекта содержит index и две папки.

Как и в большинстве веб-приложений, index - это точка входа на веб-страницу. Это две папки: css и js. Как видно из названий, первая папка содержит css приложения, вторая папка содержит код Javascript.

index довольно прост, он делает одно: он создает элемент ‹canvas›.

моем примере проекта index также импортирует Javascript ‹scripts›, но это не имеет отношения. Например, ‹script› тегов не было бы, если бы я использовал импорт js.)

Вы можете увидеть указатель здесь.

Папка Javascript

Поговорим о главном и SceneManager.

main - это точка входа в Javascript-часть приложения, она имеет доступ к DOM и содержит SceneManager.

SceneManager отвечает за обработку стороны Three.js приложения, которая полностью скрыта от main. Ему ничего не известно о DOM.
SceneManager - наш компонент более высокого уровня.

main выполняет три основных обязанности:

  1. создайте SceneManager, передав ему холст (чтобы SceneManager не вмешивался в DOM).
  2. присоединить слушателей к событиям DOM, которые нам важны (например, windowresize или mousemove).
  3. запустите цикл рендеринга, вызвав requestAnimationFrame().
// main.js
const canvas = document.getElementById('canvas');
const sceneManager = new SceneManager(canvas);
bindEventListeners();
render();

bindEventListeners () может быть пустой функцией (если все события DOM игнорируются).
render () не может, потому что он должен запустить цикл рендеринга .

function render() {
  requestAnimationFrame(render);
  sceneManager.update();
}

Three.js

На стороне Three.js приложения есть две простые концепции высокого уровня:

  1. SceneManager (компонент высокого уровня)
  2. SceneSubject (компоненты нижнего уровня)

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

  1. создать Scene, Renderer и Camera.
  2. инициализировать набор SceneSubjects.
  3. обновлять все в каждом кадре.

SceneSubject представляет одну сущность в сцене. SceneManager обычно содержит несколько SceneSubjects (подробнее об этом позже).

SceneManager

SceneManager имеет (как минимум) два общедоступных метода, которые вызываются основной функцией: update () и onWindowResize ().

(Если вам нужно прослушивать другие события DOM, SceneManager будет иметь больше общедоступных методов. Например, onClick(x, y), которые будут вызываться основным сервером при регистрации события onclick.)

Функция SceneManager.update ():

  1. вызывает функцию update () для каждого SceneSubject, содержащегося в SceneManager.
  2. вызывает метод render () Renderer Three.js.

Он вызывается main в каждом кадре.

this.update = function() {
  for(let i=0; i<sceneSubjects.length; i++)
    sceneSubjects[i].update();
  renderer.render(scene, camera);
}

Функция SceneManager.onWindowResize () обновляет соотношение сторон камеры и размер Renderer. Он вызывается main каждый раз при изменении размера окна.

this.onWindowResize = function() {
  const { width, height } = canvas;
  screenDimensions.width = width;
  screenDimensions.height = height;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
}

SceneManager имеет набор частных методов, которые вызываются сразу после создания объекта.

buildScene();
buildRender();
buildCamera();
createSceneSubjects();

Я не буду вдаваться в подробности, объясняя, что делает каждая из этих функций, это совершенно очевидно из названий.

Единственная нетривиальная функция - это createSceneSubjects (): она создает массив SceneSubjects. Чтобы добавить новый SceneSubject в вашу сцену, вам нужно только добавить его в этот массив.

function createSceneSubjects(scene) {
  const sceneSubjects = [ 
    new GeneralLights(scene),
    new SceneSubject(scene)
  ];
  return sceneSubjects;
}

SceneSubject

SceneManager отвечает исключительно за настройку и обновление сцены, он ничего не знает о ее содержании.

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

SceneSubject имеет базовый интерфейс:

  1. конструктор, который принимает объект Scene.
  2. открытый метод update ().

Вот основная структура SceneSubject:

function SceneSubject(scene) {
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
  this.update = function() { 
    // do something
  }
}

Здесь вы можете найти полный пример.

В сцене может быть много SceneSubject , и они могут быть настолько сложными, насколько это необходимо. Например, SceneSubject может быть композицией нескольких SceneSubject. например. SceneSubject Солнечной системы может содержать несколько Planet SceneSubject.

Связь между компонентами

SceneSubjects не знают друг друга и не знают своего контейнера.

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

Один из способов установления связи между разделенными компонентами может включать использование шины событий. Я написал отдельный пост, объясняющий, как построить и использовать простую шину событий, вы можете найти ее здесь.

Заключение

Я думаю, что это хороший способ разделить проблемы компонентов приложения, и он помог мне упростить создание приложений с помощью Three.js.

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

Полезные ссылки

Прочтите этот пост, если вы хотите использовать эту структуру с React.js.

Где ты меня найдешь?

Следуйте за мной в Twitter: https://twitter.com/psoffritti
Мой сайт / портфолио: pierfrancescosoffritti.com
Моя учетная запись GitHub: https://github.com/PierfrancescoSoffritti < br /> Моя учетная запись LinkedIn: linkedin.com/in/pierfrancescosoffritti/en