Введение
Честно говоря, я долго не мог решиться написать и опубликовать эту статью. Зачем, думал я, возиться с не самой популярной технологией и изобретать велосипед — реализовывать функции, которые уже где‑то есть? На этот вопрос у меня нет универсального ответа — каждому своё.
Сначала мне казалось, что рассказывать о таких «подвигах» не слишком интересно. Все любят истории об успешном успехе. Потом я вспомнил: главное — не итог, а путь, опыт и знания, которые ты получаешь по дороге. Как только я начал смотреть на материал как на обучающий, делиться им стало намного проще.
Бывает так: с какой то технологией уже разобрался, а вот перейти к новой боязно. Учить новые движки непросто, да и текущий инструмент уже не справляется с задумкой… Сомнения часто мешают двигаться вперёд, но народная мудрость «глаза боятся, а руки делают» никогда не подводит.
В итоге я решился и попробовал FXGL для 3D‑рендеринга. Но не для того, чтобы сделать полноценную игру(хотя она и получилась), а чтобы соединить расчёты по системному моделированию с элементами геймификации. Уточню: я не призываю использовать FXGL во всех случаях. Для серьёзных 3D‑проектов есть отличные инструменты — Unigine, jMonkeyEngine, Godot, Unreal Engine. Я попытался собрать и упорядочить знания, которые получил в ходе своего небольшого эксперимента.
С чем имеем дело или из чего собрать Франкенштейна
У JavaFx в 3D есть некоторые плюсы и минусы, в этом разделе попытаюсь их про них рассказать.
Управление камерой
JavaFX 3D не просто даёт какую‑то камеру она под вашим полным контролем. Что можно делать:
-
настраивать поле зрения (FOV — Field of View), как в шутерах: хотите широкий обзор — увеличиваете, хотите «зумировать» — уменьшаете;
Можно управлять параметром налету, используя биндинги
camera3D.getPerspectiveCamera().fieldOfViewProperty().bind(cam_val);
-
регулировать ближнюю и дальнюю плоскости отсечения (near/far clipping planes) — это как фокус в фотоаппарате: отсекаем лишнее, чтобы не нагружать рендер ненужными объектами;
-
двигать и вращать камеру в 3D‑пространстве с помощью простых трансформаций (Translate, Rotate) — всё как в настоящем движке;
Вот, например, как привязать камеру к автомобилю
var distanceToCamera = -8; var pos = entity.getPosition3D().subtract(entity.getTransformComponent().getDirection3D().multiply(distanceToCamera)); transform.setPosition3D(pos); transform.lookAt(entity.getPosition3D()); transform.setY(entity.getY() + CAMERA_HEIGHT_OFFSET);
И если захочется добавить кинематографичности, то можно добавить «сглаживания» :
double smoothXY = 0.2; double smoothZ = 0.2; var distanceToCamera = -8; var targetPos = entity.getPosition3D().subtract( entity.getTransformComponent().getDirection3D().multiply(distanceToCamera) );// Интерполяция по осям с разными коэффициентами double newX = cameraPos.getX() + (targetPos.getX() - cameraPos.getX()) * smoothXY; double newY = cameraPos.getY() + (targetPos.getY() - cameraPos.getY()) * smoothXY; double newZ = cameraPos.getZ() + (targetPos.getZ() - cameraPos.getZ()) * smoothZ; cameraPos = new Point3D(newX, newY, newZ); transform.setPosition3D(cameraPos); transform.lookAt(entity.getPosition3D()); transform.setY(cameraPos.getY() + CAMERA_HEIGHT_OFFSET);
Или делать интерактивное управление: например, реализовать вид от первого лица или камеру, которая красиво вращается вокруг объекта. Типа такого.
Применение материалов типа PhongMaterial с поддержкой diffuseMap и normalMap
PhongMaterial — это билет в мир визуально приятных поверхностей. Разберём, что тут есть:
-
diffuseMap (диффузная карта) — задаёт основной цвет и текстуру. Проще говоря, это «обёртка» объекта: хотите дерево — клеите текстуру дерева, хотите камень — камень;
-
normalMap (карта нормалей) — магия, которая создаёт иллюзию мелких деталей без увеличения полигонов. Поверхность кажется рельефной, хотя на деле остаётся гладкой — экономия ресурсов налицо. И тут немного про вайб-кодинг. Решил попробовать сгенерировать скрипт с помощью Алисы для созданий кары нормалей для всех изначальных текстур, и результат вышел вполне рабочим. Да, правильнее, наверное, поработать с каждой текстурой в профессиональном редакторе, но это уже совсем другой пилотаж.
-
ещё есть specularMap — с ним можно контролировать, где и насколько сильно будут бликовать участки модели. Хотите, чтобы часть объекта блестела, как металл, а часть оставалась матовой? Легко;
-
коэффициент блеска (specularPower) — настраиваете, и вот уже ваша модель может быть как матовым пластиком, так и зеркальным хромированным шаром.
Импорт 3D‑моделей в форматах .obj, .3ds и других
Какие форматы поддерживаются и что потом с ними делать:
-
загружать готовые модели в популярных форматах — .obj (Wavefront), .3ds (3D Studio), а с дополнительными библиотеками и другие форматы подтянуть можно;
-
сохранять иерархию объектов и трансформации, которые вы задали в редакторе 3D‑моделирования — всё перенесётся как надо;
-
автоматически создавать JavaFX‑узлы (MeshView, Group) для импортированной модели. То есть вы загрузили объект — а он уже готов к использованию в сцене, без лишних телодвижений.
Интеграция пользовательских GLSL‑шейдеров через низкоуровневые API
-
Для тех, кому стандартных возможностей мало, есть доступ к «внутренностям» рендеринга:
-
пишите свои вершинные (vertex) и фрагментные (fragment) шейдеры на GLSL — и вуаля, у вас полный контроль над тем, как выглядит каждый пиксель;
-
можете заменить стандартное освещение Фонга на что‑то своё — например, сделать неоновое свечение, стилизованную графику в духе комиксов или эффект «под водой»;
-
работайте с текстурами и атрибутами вершин внутри шейдеров — создавайте процедурные эффекты прямо на лету;
-
подключаете через ShaderProgram и PhongMaterial — и стандартный пайплайн уже ваш, можно его подменять и творить что душе угодно.
Ключевые ограничения JavaFX 3D
Отсутствие реализации алгоритмов теневого отображения (shadow mapping)
Тут всё просто: теней нет. Совсем. Ни динамических, ни статических — оно и понятно, ведь это не игровой движок. Что это значит на практике:
-
объекты не отбрасывают тени друг на друга и на пол — выглядит это, мягко говоря, плоско и неестественно;
-
хотите простую тень под персонажем — придётся вручную рисовать полигон и приделывать её к модели. Да, вот так топорно;
-
динамические тени, которые так любят все современные игры, тут недоступны без танцев с бубном и низкоуровневых OpenGL‑вызовов. А это уже убивает весь кайф от использования высокоуровневого фреймворка.
Ограниченная и не интуитивная модель работы с источниками света
Освещение тут — это отдельная песня(смотри видео). Что не так:
-
выбор источников света скудный: точечный (PointLight), направленный (DirectionalLight), рассеянный (AmbientLight) — и всё. Прожектор (Spotlight)? Забудьте. Хотите подсветить сцену как в театре? Придётся изобретать велосипед;
-
настройки света какие‑то «деревянные»: интенсивность, радиус затухания, цвет — всё работает не так гибко, как хотелось бы;
-
освещение считается для вершин (per‑vertex lighting), а не для фрагментов (per‑fragment lighting). На низкополигональных моделях это даёт заметные артефакты и выглядит не очень реалистично по сравнению с современными движками.
Отсутствие встроенного физического движка
Физика? Какая физика? Её тут просто нет. Да и откуда ей взяться, только рендеринг только хардкор. Что это влечёт за собой:
-
никакой автоматической проверки столкновений (collision detection) — объекты будут проходить друг сквозь друга, как привидения;
-
гравитация, инерция, трение, отскоки — всё это нужно писать с нуля или искать сторонние библиотеки. Хотите, чтобы мяч отскакивал от пола? Придётся потрудиться;
Отсутствие WYSIWYG‑редактора сцен
Здесь всё по‑старинке: никакой визуальной магии, только код. Что это значит:
-
вся сцена — иерархия узлов (Group, SubScene, MeshView, Light, Camera) — создаётся и настраивается исключительно кодом на Java. Никаких drag‑and‑drop, никаких визуальных редакторов;
-
хотите поставить куб в угол комнаты? Пишите код. Повернуть лампу? Снова код. Настроить освещение? Вы уже поняли;
Однако, чтобы работалось слегка комфортнее, я добавил окна настройки для источников света и дерево объектов на сцене. Мелочи, но все же приятные. И да, на вайб кодить такие окна настроек тоже можно. Главное, потом проверить за ИИ помощником)
Собираем нашего монстра
Одна из основных идей этого хобби и обучающего проекта, создавать графические кроссплатформенные приложения на Java, где «под капотом» работают математические модели из платформы системного моделирования. К этой идее подтолкнули пользователи, которые хотели бы обмениваться моделями в виде закрытых приложений с графическим интерфейсом, при этом без браузерного интерфейса.
В платформе, итак, есть задокументированное API, а можно избавиться от накладных расходов подключив библиотеку к приложению.
Собственно, для этого нужно подключить в ваш проект библиотеку RepeatCore любым удобным способом, и в классе контроллера, предварительно скачав свой проект модели с сайта приложения.
RepeatCore.parseClassMap(); //Настраиваем библиотеку для работы в фоновом режиме RepeatCore.runtimeAsService = true; RepeatCore.isSingle = true; RepeatCoreService.runtimeAsService = true; RepeatCoreServiceDocument.verboseLevel = 0; RepeatCoreServiceDocument.startWithoutBinFile = true; var repeatCoreService=new RepeatCoreService(); var tempdoc = new RepeatCoreServiceDocument(); serviceDocument = tempdoc.openDocument(filename); // файл вашего проекта serviceDocument.setParentRepeatCoreService(repeatCoreService); serviceDocument.documentCalculate();
Далее, проинициализировать объекты для управления(входные параметры для модели).
speed_setpoint=(Constant) getObjbyIdFromList(serviceDocument.componentList,"1739110251130"); // это уникальный ID объекта, отображается на схеме
И затем, обновлять данные на графиках, диаграммах и других контролах любым способом, используя биндинги или отдельный поток.
Собственно, вот такое простенькое приложение можно получить, для управления электроприводом

Этот же подход можно использовать и при соединении 3D рендеринга и расчетов, эдакий Франкенштейн получается. Главное не забыть синхронизировать обновление кадра с расчетом.
Франкенштейн оживает
Признаюсь: я просто не смог устоять. Наткнулся на классную статью про перенос карт из старых NFS — и загорелся идеей сделать что‑то похожее. А поскольку NFS 5 для меня — это не просто игра, а настоящая любовь с первого заезда, выбор карты был очевиден.
Взял ассеты из NFS 5 и решил перенести их в FXGL. Звучит просто, да? На деле же это обернулось миллиардами (ну, может, десятками) итераций:
-
разбирался, как вообще эти модели устроены;
-
мучился с масштабом — то машина размером с дом, то дорога в три метра шириной. Об этом автор статьи про перенос в UE тоже столкнулся;
-
настраивал освещение так, чтобы карта не напоминала тёмный подвал(Хотя может и получилось своеобразно).
Итак, по порядку.
Чтобы загрузить карту подгружаем библиотеку jimObjModelImporterJFX и загружаем модель, предварительно создав для нее Entity в фабричном классе.
@Spawns("city")public Entity newCity(SpawnData data) {Group modelRoot = new Group();var importer = new ObjModelImporter();importer.read("nfs5_industrial_grouped.obj"); // Загружаем модель в формате .obj, можно в .fbx,.3dsfor (MeshView view : importer.getImport()) {modelRoot.getChildren().addAll(view); // здесь можно поиграться с материалами мешей, загрузить карты нормалей и т.д.}importer.clear(); // убираем за собойimporter.close();// Спауним Entity картыvar e = entityBuilder(data) .view(modelRoot) .build();return e;}
Собственно, аналогично спауним и модель автомобиля, задав ей класс-компонент (он отвечает за анимации, физику и т.д.)
@Spawns("obj")public Entity newObj(SpawnData data) { Group modelRoot = new Group(); var importer = new ObjModelImporter(); importer.read("911_92.obj");var e = entityBuilder(data) .view(modelRoot) .with(new CarComponent()) .build();e.setScaleUniform(1);return e;}
Добавляем HUD. В main-классе игры добавляем свои элементы в метод initUI(). Я использовал пару собственных классов для графиков и gauge. Выглядит это примерно так
@Overrideprotected void initUI() { super.initUI(); CarComponent carComponent = car.getComponent(CarComponent.class); carSettingsWindow = new CarSettingsWindow(carComponent); torque_chart = new LineChart(new NumberAxis(), new NumberAxis()); CustomLineChart.setup(torque_chart,"Скорость, км/ч"); speedOmeter = new GaugeComponent("км/ч","Скорость",0,300,350,350); RPMmeter = new GaugeComponent("об/мин","",0,9000,350,350); gearratio = new GaugeComponent("","Передача",0,6,250,250); torque_gauge = new GaugeComponent("Н*м","Момент",0,1500,250,250); rpm_chart = new LineChart(new NumberAxis(), new NumberAxis()); xy_chart = new LineChart(new NumberAxis(), new NumberAxis()); gear_chart = new LineChart(new NumberAxis(), new NumberAxis()); CustomLineChart.setup(xy_chart,"Траектория"); CustomLineChart.setup(gear_chart,"Передача"); CustomLineChart.setupMultipleSeries(rpm_chart,"Об/мин"); CustomLineChart.setupMultipleSeries(rpm_chart,"Момент, Н*м"); // Создаем корневую панель BorderPane для разделения на зоны BorderPane root = new BorderPane(); root.setPrefHeight(FXGL.getAppHeight()); root.setPrefWidth(FXGL.getAppWidth()); // Верхняя часть (графики) HBox topPane = new HBox(); topPane.setSpacing(10); topPane.setPadding(new Insets(5, 5, 5, 5)); topPane.getChildren().addAll(rpm_chart, torque_chart, gear_chart, xy_chart); root.setTop(topPane); var hboxLeft = new HBox(); hboxLeft.getChildren().addAll(torque_gauge.gauge,speedOmeter.gauge); hboxLeft.setSpacing(10); hboxLeft.setPadding(new Insets(5, 5, 5, 5)); var hboxrRight = new HBox(); hboxrRight.getChildren().addAll(RPMmeter.gauge,gearratio.gauge); hboxrRight.setSpacing(10); hboxrRight.setPadding(new Insets(5, 5, 5, 5)); root.setLeft(hboxLeft); root.setRight(hboxrRight); getGameScene().addUINode(root);
Про пост эффекты, блюры и всякие блумы. Их можно применить к 2D узлу, но к 3D никакого эффекта не будет. А что если попробовать?
В общем, если наложить эффект на субсцену, то эффект может быть интересным. Ниже пример с легким motion blue эффектом.
Про нейронки, апскейлинг текстур, добавление карт нормалей – и картинка станет лучше. Что-то я попробовал – но возиться в Blendere и других инструментах – увольте, мне это не очень интересно)
А как же не упомянуть производительность и железо — вот на таком ноутбуке с AMD Ryzen 5 7430U with Radeon Graphics (2.30 GHz) и 16 ГБ памяти
я провожу эксперименты над своим Франкенштейном и 60 FPS стабильно выдерживаются.
Скрин с графиком и выкладками из встроенного профилировщика.
Скрытый текст
Для дополнительного профилирования вот строчка с опциями для JVM -Dprism.verbose=»true» -Dprism.vsync=»false» -Djavafx.animation.fullspeed=»true»
Заключение
Вот мы и добрались до заключения. Наблюдать за крупными и крутыми результатами команд с огромными бюджетами — это всегда восторг. Однако, мы забываем какого это реального труда стоит каждому из членов команды, поэтому, когда сам в одиночку пробуешь что-то сделать, да, ожидания размазывают тебе о твердыню реальности. Столько раз руки опускались, видя результаты своего творения, забывая о том, сколько было пройдено и реализовано.
Что я понял на практике:
-
визуализация расчётов — это быстро и доступно даже без продвинутых инструментов;
-
HUD‑интерфейс собирается из готовых библиотек буквально за полчаса, главное определить дизайн и концепцию;
Начав с «неподходящего» на первый взгляд инструмента (JavaFX в 3D), можно создать рабочий прототип фреймворка для аркадных гоночных симуляторов — с физикой, рендерингом и интерфейсом в одном флаконе.
А теперь — главный посыл: не бойтесь экспериментировать и не опускайте руки! В моделировании и компьютерной симуляции именно нестандартные подходы часто дают лучшие результаты. Возьмите привычный инструмент, попробуйте применить его в неожиданном контексте, соедините с вашими расчётами — и вы можете получить что‑то уникальное.
Дерзайте: каждый эксперимент приближает вас к собственному решению, которое однажды может перерасти в полноценный фреймворк или продукт. Даже если сначала кажется, что вы просто «изобретаете велосипед» — возможно, именно этот велосипед станет основой для чего‑то большего.
ссылка на оригинал статьи https://habr.com/ru/articles/1022594/