Внедрения JS в 3D-проекты

Поднимите руки те, кто знал, что с помощью JavaScript можно создавать полноценные 3D-приложения, причём с поддержкой AR и VR прямо в браузере?

Большинство новичков удивляются, когда узнают, насколько далеко шагнул веб в сторону 3D-графики и реального геймдева.

В этой статье вы узнаете:

  • Какие инструменты и фреймворки нужны, чтобы войти в мир Web-3D и AR-приложений;
  • Как связать интерфейс на Angular с 3D-сценой так, чтобы всё выглядело единым приложением;
  • С какими сложностями сталкивается разработчик при создании настоящего 3D-приложения;
  • Какие оптимизации позволяют увеличить производительность в разы?

Наш тестовый проект — Golf AR. Это небольшая игра, где задача пользователя проста — попасть в лунку мячом. Но для нас это было не столько про игру, сколько про исследование реальных задач, с которыми сталкивается разработчик при создании интерактивного 3D-приложения в браузере: работа с физикой, управление камерой, освещение, синхронизация событий и интеграция с UI.

Мы не будем подробно разбирать, как с нуля построить 3D-приложение в браузере — по этой теме уже есть десятки хороших статей и курсов. Здесь мы делимся практическим опытом разработки, рассказом о реальных сложностях и их решениях — на примере нашего проекта.

Поскольку проект затрагивает сразу несколько самостоятельных направлений, мы решили разделить материал на две части. В первой статье мы сосредоточимся исключительно на 3D-части проекта: архитектуре, производительности, физике и интеграции 3D-сцены с UI. А во второй подробно расскажем о связанных с AR ограничениях и практических решениях, к которым пришли в процессе разработки.

Идея и технологический стек

Почему мы вообще заинтересовались Web-3D

3D в браузере — сравнительно молодое направление, которое активно развивается благодаря WebGL и WebGPU. Сегодня веб уже позволяет создавать:

  • визуализации объектов и механизмов;
  • учебные и научные модели;
  • архитектурные сцены;
  • медицинские и научные модели;
  • AR- и VR-опыты;
  • интерактивные презентации и, конечно, игры.

И при этом пользователю не нужно устанавливать приложение — всё работает прямо в браузере.

Это и стало основной причиной интереса: проверить, насколько веб уже готов к созданию реалистичных интерактивных 3D-приложений.

Выбор инструментов

Когда речь заходит о 3D в браузере, чаще всего рассматривают два движка: Three.js и Babylon.js.

Оба активно развиваются и подходят для серьёзных проектов, но мы сделали выбор в пользу Babylon.js.

Причины:

  • полноценная поддержка TypeScript;
  • более дружелюбный и простой API на старте;
  • легко интегрируется с Angular.

Почему именно Angular

На момент разработки Angular был основным фреймворком, который использовался в компании, поэтому выбрали именно его.

И хотя может показаться, что Angular и 3D — не самая очевидная комбинация, на практике получилось удобно для разработки:

  • Angular взял на себя пользовательский интерфейс, состояние приложения и управление пользовательскими сценариями;
  • Babylon.js отвечал за 3D-сцену: рендеринг, камеры, физику, анимации и взаимодействие с объектами;
  • Связь между ними была выстроена через сервисы и реактивные потоки. Это позволило изолировать 3D-логику от UI и избежать жёсткой связки между слоями.

В результате получилась понятная и поддерживаемая архитектура.

Преимущества и ограничения 3D в вебе

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

Основные минусы

  • Производительность ниже, чем у нативных движков:

Браузер — это универсальная среда, которая изначально не проектировалась под высоконагруженные 3D-сценарии. Из-за этого:

  • код выполняется поверх браузерного движка и JavaScript-рантайма;
  • доступ к GPU и памяти ограничен;
  • производительность заметно ниже, чем у нативных приложений.
  • Ограничения доступа к оборудованию и системе:

В браузере сложнее полноценно работать:

  • с камерой;
  • с датчиками устройства (гироскоп, GPS);
  • с низкоуровневым доступом к GPU.

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

  • Сложные графические эффекты требуют оптимизации — такие как, пост-обработка и реалистичные тени.

Если сделать всё «как в AAA-движке», браузер просто не выдержит — приходится искать баланс.

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

Но главный плюс перекрывает всё: пользователю не нужно ничего устанавливать — достаточно перейти по ссылке или открыть сайт по QR‑коду, и уже через несколько секунд он получает полноценный 3D‑опыт.

Кратко о 3D

Чтобы дальше было проще читать статью, давайте быстро пройдёмся по базовым компонентам 3D-сцены.

  • Scene — это вся сцена, наш виртуальный мир, где находятся объекты, свет, камеры и физика.
  • Engine — движок, который отвечает за рендер сцены и обновление объектов.
  • Camera — точка зрения, через которую пользователь «смотрит» на сцену.
  • Light — источники света. Без освещения объекты выглядят плоскими и тёмными. Именно свет и тени позволяют увидеть форму объектов, их объём и создают ощущение реалистичной сцены.
  • Physics — физика объектов: силы, столкновения, трение, гравитация и всё, что делает объекты «живыми».
  • Mesh — сами 3D-объекты на сцене. Они могут быть любыми: от простых кубов до сложных моделей.
  • Material и Texture — внешняя оболочка объектов. Material определяет, как объект реагирует на свет, а Texture — его «кожуру»: цвет, картинки, видео.
  • Animation, Skeleton— отвечают за движения и скелетную анимацию моделей.

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

Проблемы производительности и их решения

В 3D и особенно в браузере — это одна из основных проблем. Даже на мощных устройствах браузер накладывает дополнительные ограничения, поэтому сцена, которая «летает» в нативном движке, в вебе может работать заметно хуже. На мобильных устройствах эта проблема проявляется ещё сильнее: ресурсы телефона (процессор, память, энергопотребление) заметно уступают возможностям десктопа.

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

Свет и тени

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

В небольших сценах это почти незаметно, но когда объектов становится больше, освещение начинает напрямую влиять на производительность и FPS.

Light baking

Один из самых эффективных способов оптимизации света и теней — light baking, который мы и использовали.

Суть подхода в том, что освещение рассчитывается заранее в 3D-редакторе (например, Blender или 3ds Max), а результат сохраняется прямо в текстуре объекта. Для движка это уже не динамическое освещение, а материал с заранее рассчитанным светом и тенями.

Чтобы использовать данный подход в Babylon.js, нужно отключили расчёт освещения у материалов и задать текстуру как самосветящуюся:

const material = mesh.material;
material.emissiveColor = Color3.White();
material.emissiveTexture = material.albedoTexture;
material.disableLighting = true;

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

Данный способ подойдет, если в вашем проекте:

  • нет динамического изменения освещения (например, смены дня и ночи),
  • не критичны реалистичные тени,
  • сцена в основном статична,

В зависимости от количества объектов, источников света и сложности сцены, прирост производительности может составлять от нескольких процентов до кратных значений.

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

Физика в мире 3D

Физика добавляет значительную вычислительную нагрузку и быстро становится узким местом при росте сложности сцены. Именно она отвечает за столкновения, движение объектов, трение и поведение сцены в целом.

На старте разработки Golf AR для работы с физикой был выбран движок Cannon. В начале он полностью нас устраивал: сцена была простой, объектов немного, и производительность оставалась стабильной.

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

Переход на Havok

В тот же период команда Babylon.js объявила об интеграции физического движка Havok. В демонстрационных примерах показывали серьёзный прирост производительности — от десятков до сотен раз в зависимости от сцены.

Мы изучили демо, посмотрели на API и приняли решение заменить Cannon на Havok.

Результат оказался заметен сразу:

  • FPS вырос в несколько раз,
  • исчезли подёргивания,
  • сцена стала вести себя стабильно даже при большом количестве объектов.

Оптимизация физических форм

Есть простой и очень эффективный способ снизить нагрузку на физику — использовать разные типы физических форм.

Если физическое тело не обязано в точности повторять форму объекта, можно использовать более простые представления, например PhysicsShapeType.CONVEX\_HULL — это упрощённая «оболочка» объекта, которая повторяет его общие очертания без мелких деталей и углублений.

Зачем это нужно?

Чем проще форма физического тела, тем меньше вычислений требуется для расчёта столкновений. В 3D все объекты состоят из полигонов — плоских граней треугольной формы. Чем меньше таких треугольников задействовано для физического расчёта, тем меньше нагрузка на процессор.

В проекте Golf AR мы используем два типа физических форм:

  • PhysicsShapeType.MESH — физическая форма полностью повторяет геометрию объекта. Мы используем её для поверхности поля, где важна точность.
  • PhysicsShapeType.CONVEX\_HULL — упрощённая форма, передающая только общие очертания объекта. Она применяется для деревьев, камней и других второстепенных объектов.

Использование PhysicsShapeType.MESH

Изображение 1 – Использование PhysicsShapeType.MESH

Исопльзовани PhysicsShapeType.CONVEX_HULL

Изображение 2 – Исопльзовани PhysicsShapeType.CONVEX_HULL

Пример кода:

const physicsAggregate = new PhysicsAggregate(
  mesh,
  isFieldSurface ? PhysicsShapeType.MESH : PhysicsShapeType.CONVEX_HULL,
  { mass: 0, restitution: 0.25 },
);

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

Оптимизация загрузки и моделей

В какой-то момент мы заметили, что сайт стал загружаться слишком долго.

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

Открыв DevTools → Network, мы обратили внимание на две основные проблемы:

  • бандлы Babylon.js имели большой размер,
  • 3D-модели занимали значительную часть трафика.

Уменьшение размера бандлов Babylon.js

Первым шагом стала оптимизация самих бандлов.

По умолчанию Babylon.js можно импортировать «целиком», и это удобно на старте, но приводит к лишнему коду в сборке.

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

Было так:

import { Engine, Scene, Vector3, HavokPlugin, IDisposable } from '@babylonjs/core';

Стало так:

import { Engine } from '@babylonjs/core/Engines/engine';
import { Scene, type IDisposable } from '@babylonjs/core/scene';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { HavokPlugin } from '@babylonjs/core/Physics/v2/Plugins/havokPlugin';

Этот шаг позволил заметно сократить размер итогового бандла примерно на 30 МБ — и стал составляет всего 5 МБ, при этом логика приложения осталась без изменений.

Оптимизация 3D-моделей

Следующей проблемой оказался вес самих моделей.

Мы пробовали разные способы оптимизации моделей и сначала обратились к 3D-дизайнеру. Его правки дали небольшой эффект — модели уже были достаточно хорошо оптимизированы. Постепенно мы поняли, что не всегда нужна высокая детализация. Например, модель мяча для гольфа мы заменили на простую сферу, созданную средствами Babylon.js. Для человеческого глаза разница оказалась практически незаметной, зато нам удалось сэкономить около 1.3 МБ, удалив исходную модель.

Управление загрузкой через пользовательский сценарий

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

  1. Загрузка основных бандлов, необходимых для работы сайта
  2. Страница welcome — в этот момент начинается фоновая загрузка моделей
  3. Туториал — пользователь изучает механику
  4. Игровая сцена — к этому моменту, обычно, все модели уже загружены

Такой подход позволил:

  • скрыть длительную загрузку,
  • улучшить первое впечатление,
  • снизить вероятность того, что пользователь просто закроет сайт.

Трудности с которыми мы столкнулись

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

Когда площадь контакта почти равна нулю

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

Интуитивно это выглядело неправильно. В реальной жизни:

  • мяч в песке сложнее сдвинуть с места,
  • по высокой траве он катится медленнее,
  • по ровной поверхности — быстрее и дальше.

Чтобы добавить реалистичности, мы решили внедрить две механики:

  • Трение — для изменения скорости перекатывания мяча на разных участках поля.
  • Различную силу удара — в зависимости от поверхности, на которой находится мяч в момент удара

При попытке добавить трение мы столкнулись с неожиданной проблемой. Из-за очень маленькой площади соприкосновения мяча с поверхностью стандартный коэффициент трения не давал визуального эффекта — даже при завышенных значениях мяч продолжал катиться так же. Это заставило нас глубже изучить параметры физического тела.

Использование Inertia и Damping

Изучая API PhysicsBody, мы обратили внимание на два параметра: inertia и damping.

  • inertia отвечает за сопротивление вращению вокруг осей. Повышая этот параметр, мы уменьшали способность мяча долго вращаться, за счёт чего он быстрее терял скорость перекатывания.
  • damping отвечает за затухание движения, то есть за то, как быстро объект теряет энергию со временем. Важно, что damping влияет не только на движение мяча после удара, но и на саму силу удара.

Благодаря damping мы смогли естественным образом уменьшать силу удара на поверхностях с высокой “сопротивляемостью” — например, на песке или высокой траве — без введения дополнительной логики. Мяч просто получал меньше энергии и быстрее останавливался, что выглядело реалистично и предсказуемо.

В итоге:

  • inertia использовалась для контроля скорости вращения и перекатывания мяча.
  • damping — для имитации потери энергии и ослабления удара на разных типах поверхности.

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

Проблемы с обменом данных и отображением

Интеграция Angular и Babylon.js

В проекте интеграция Babylon.js с Angular реализована через сервисы и компонентный подход. Angular отвечает за UI, состояние и жизненный цикл приложения, а Babylon.js — за рендер и обновление 3D-сцены. Важно было выстроить их взаимодействие так, чтобы они не мешали друг другу.

Основная идея архитектуры была простой: Angular управляет приложением, Babylon — сценой.

Инициализация сцены

Angular-компонент не создаёт сцену напрямую. Его задача — подготовить <canvas> и передать ссылку на него в сервис. Вся инициализация происходит через SceneService, а не внутри компонента.

Компонент остаётся максимально «тонким»: он знает, когда сцену нужно создать или уничтожить, но не знает, как она устроена внутри.

@ViewChild('canvas')
public canvas: ElementRef<HTMLCanvasElement> | null = null;

private readonly sceneService = inject(SceneService);

ngAfterViewInit(): void {
  this.zone.runOutsideAngular(() => {
      this.sceneService.initialize(this.canvas.nativeElement);
  });
}
public ngOnDestroy(): void {
    this.sceneService.disposeScene();
}

Сервис сцены

Вся логика, связанная с Babylon.js, инкапсулирована в SceneService. Именно он:

  • создаёт экземпляр сцены;
  • хранит ссылку на текущую сцену;
  • управляет её состоянием;
  • предоставляет реактивные потоки состояния для Angular-приложения

Сервис выступает связующим звеном между UI и 3D-миром, позволяя не смешивать код Angular и Babylon.js.

public initialize(element: HTMLCanvasElement): void {
  const scene = new BaseScene(element);
  this.scene$.next(scene);
}

Реализация сцены

Отдельный класс сцены отвечает уже исключительно за 3D:

  • создание движка и сцены;
  • настройку камеры, света и объектов;
  • запуск render loop;
  • работу с физикой и анимациями;
  • и т.д.
export class BaseScene implements IDisposable {
  public constructor(private readonly canvas: HTMLCanvasElement) {
    this.engine = new Engine(this.canvas);
    this.scene = new Scene(this.engine);
        this.golfBall = new GolfBall(this.scene);

    // ...создание камеры, света, объектов поля и т.д.
    
        this.gameManager = new GameManager(
            this.golfBall,
            this.scene,
            ...
        );
    this.engine.runRenderLoop(() => this.scene.render());
  }
}

Angular напрямую с этим классом не взаимодействует — всё общение идёт через сервис.

Взаимодействие с UI

Для связи UI и 3D мы использовали реактивный подход. Сервис сцены предоставляет потоки состояния, на которые могут подписываться Angular-компоненты. Это позволяет, например, отображать загрузку сцены или блокировать интерфейс до её полной инициализации.

public readonly isLoading$ = this.scene$.pipe(
  filterNull(),
  switchMap(scene => scene.gameManager.isLoading$),
);

Вывод

Такой подход позволил нам избежать хаоса в коде и чётко разделить зоны ответственности:

Angular управляет приложением и UI, Babylon.js — 3D-сценой, а сервис связывает их между собой. Это упростило поддержку, отладку и дальнейшее развитие проекта.

Вторая камера

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

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

Изначальная концепция

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

  1. сначала в нём воспроизводится короткое видео, где гольфист бьёт по мячу;
  2. затем показывается сам полёт мяча — уже с помощью второй камеры;
  3. после остановки мяча блок исчезает.

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

Проблемы с производительностью

Изображение с камеры можно было отрисовывать только на стороне Babylon.js, поэтому первым решением стало использование VideoTexture. Видео и изображение с камеры воспроизводились внутри одного GUI.Rectangle, что визуально решало задачу.

Однако на мобильных устройствах почти сразу возникли проблемы с производительностью. FPS заметно падал, появлялись подёргивания, которые сильно били по ощущению от игры. Причина оказалась в том, что рендеринг видео в 3D-сцене создавал слишком большую нагрузку для телефонов.

Разделение реализации

Следующим шагом мы попробовали разделить задачи:

  • видео воспроизводилось на стороне Angular;
  • изображение со второй камеры — на стороне Babylon.js.

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

Итоговое решение

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

Пример анимации удара по мячу

Изображение 3 – Пример анимации удара по мячу

Это решение оказалось:

  • проще с точки зрения реализации;
  • стабильнее на мобильных устройствах;
  • и визуально вполне достаточным для нужного уровня погружения.

Иногда отказ от более «эффектного» решения в пользу стабильности и производительности — лучший выбор, особенно в вебе и AR.

Итоги

Проект Golf AR всё ещё находится в стадии разработки. У нас имеется много идей: что можно улучшить в архитектуре, где провести оптимизации и какую функциональность добавить дальше. Это был наш первый опыт работы с 3D на JavaScript, и, как это часто бывает, на старте мы не учли многих нюансов.

Часть проблем удалось исправить по ходу разработки, часть стала заметна лишь спустя время. Проект развивался, росла его сложность, и код требовал рефакторинга. Этот опыт позволил нам лучше понять, как выстраивать архитектуру и оптимизировать 3D-приложения в вебе.

За время работы над проектом было много сложностей, ошибок и потрачено немало сил и времени. Но всё это оказалось не напрасно. Мы получили ценный практический опыт, продумали взаимодействие Angular и Babylon.js внутри одного приложения, научились работать с 3D-моделями, физикой в браузере.

Эта статья была написана, чтобы познакомить вас с относительно молодой, но быстро развивающейся технологией Web-3D на примере проекта Golf AR. Да, в основном мы говорили о трудностях, с которыми столкнулись, но именно через них хотели показать, насколько много уже сейчас можно сделать в вебе и какой большой путь этой технологии ещё предстоит пройти.

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

В следующей статье мы углубимся в AR, расскажем, какие подводные камни там поджидают, и поделимся практическими решениями, которые позволили создать живое и интерактивное 3D-AR пространство. Вы узнаете, как мы справлялись с ограничениями браузеров и мобильных устройств, и какие подходы помогли нам сделать опыт плавным и реалистичным.

Читайте также

Наверх