ФПМИ МФТИ весна 2025
Булдаков Арсений Б05-416
Проект представляет собой систему генерации и рендеринга 3D-сцен по текстовому описанию с использованием технологии RayMarching.
Демонстрация работы:
- Что сделано и что осталось сделать
- Быстрый старт
- Библиотеки
- Архитектура
- Линтинг
- Тесты
- Как пользоваться движком?
- Как устроен движок?
- Вопросы и предложения
- 3D движок
- Простые фигуры
- Простое освещение
- Простые материалы
- Динамическое освещение с помощью трассировки лучей
- Гамма-коррекция
- Антиалиасинг
- Рендеринг фракталов
- Обработка текстового описания
- Не успел сделать (
- Фронтенд
- Веб-интерфейс
- Развёртывание
- Docker
- CI/CD
Минимальные требования:
- GPU с поддержкой CUDA (например, NVIDIA GeForce. Я тестировал на GeForce RTX 3060 и GeForce RTX 3050). Можно ли запустить без GPU?
- CUDA Toolkit 12.0 или выше (желательно поставить 12.9). Как проверить версию?
- docker
Шаг 1. Клонируем репозиторий
git clone https://gitlab.com/g8150507/raymarching-with-llm-support.git --depth 1Шаг 2. Настройка переменных окружения
В репозитории есть файл .env.template, в котором указаны переменные окружения. Вам нужно создать файл .env и заполнить его своими значениями. Например:
POSTGRES_USER=root
POSTGRES_PASSWORD=you-better-change-this
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=root
SECRET_KEY=you-better-change-this
RABBITMQ_HOST=rabbitmq
RABBITMQ_DEFAULT_USER=root
RABBITMQ_DEFAULT_PASS=you-better-change-this
Шаг 3. Запуск приложения
docker compose upШаг 4. Перейдите по адресу http://localhost:8000 и посмотрите на приложение)
| Название | Описание |
|---|---|
| numba-cuda | Just in time compilator. Используется для ускорения вычислений на CUDA ядрах |
| numpy | Библиотека для работы с многомерными массивами и матрицами. Используется для хранения вершин, нормалей и других данных |
| PIL | Библиотека для работы с изображениями. Используется для сохранения и отображения изображений |
| NiceGUI | Библиотека для создания графического интерфейса. Используется для создания интерфейса приложения |
| FastAPI | Веб-фреймворк для создания API. Используется для создания API приложения |
Я решил использовать в данном проекте uv. Вместо requirements.txt используется pyproject.toml и uv.lock. Для ручной установки зависимостей нужно выполнить команду
uv syncflowchart TD
User[<img src='https://www.svgrepo.com/show/525577/user-circle.svg' width='40' height='40' />]
3DEngine[<img src='https://www.svgrepo.com/show/163326/coordinates.svg' width='40' height='40' />]
UI[<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/Logo_nicegui_black.svg/500px-Logo_nicegui_black.svg.png?20231119060036' width='40' height='40' />]
Postgre[<img src='https://www.svgrepo.com/show/354200/postgresql.svg' width='40' height='40' />]
RabbitMQ[<img src='https://www.svgrepo.com/show/354250/rabbitmq-icon.svg' width='40' height='40' />]
User --> UI
UI --> RabbitMQ
UI --> Postgre
RabbitMQ --> 3DEngine
Для линтинга используется ruff
Для запуска линтинга сервис engine нужно выполнить команду
cd engine
uvx ruff checkДля запуска линтинга сервиса user_interface нужно выполнить команду
cd user_interface
uvx ruff checkУвы, их нет (
Движок принимает на вход JSON сцены следующего вида
{
"width": 1080, // Ширина выходного изображения в пикселях
"height": 720, // Высота выходного изображения в пикселях
"filename": "cornell1", // Базовое имя для сохраняемого файла (без расширения)
"max_ray_march_steps": 1024, // Максимальное количество шагов ray marching'а до срабатывания ограничения
"max_ray_cast_depth": 1, // Глубина рекурсии для лучей (отражения/преломления)
"paths_per_pixel": 4, // Количество сэмплов на пиксель для патч-трейсинга
"max_dist": 1000000.0, // Максимальная дистанция видимости (дальность рендеринга)
"epsilon": 0.0005, // Точность определения пересечения (шаг точности)
"sky": "empty", // Тип фона ["empty", "mountains", "space"]
"gamma_correction": 1, // Коэффициент гамма-коррекции (1.0 = отключено)
"threadsperblock": 16, // Размерность блоков потоков CUDA (оптимизация для GPU)
"SSAA": 1, // Коэффициент суперсэмплинга (антиалиасинг)
"cam": {
"position": [1.81, 0, 0], // Позиция камеры в мировых координатах (x,y,z)
"direction": [1, 0, 0], // Вектор направления взгляда (нормализованный)
"up": [0, 0, 1], // Вектор "вверх" для камеры (обычно [0,0,1] или [0,1,0])
"fov": 90 // Угол обзора в градусах (перспективная проекция)
},
"world": [
{
"type": "cube", // Тип объекта ["cube", "sphere", "torus", "mandelbulb"]
"center": [3, 0, -1.5], // Центр объекта в мировых координатах
"scale": 1, // Масштаб объекта (единичный размер)
"material": {
"type": "diffuse", // Тип материала ["diffuse", "metal", "glass", "emissive"]
"color": [0.7, 0.7, 0.7] // RGB цвет [0..1] для диффузных материалов
},
"rotation": [0, 0, 0] // Углы Эйлера вращения (в градусах)
}
]
}В основе движка лежит алгоритм RayMarching. Каждый элемент сцены описывается функцией расстояния (SDF), вычисляющей минимальную дистанцию от произвольной точки до поверхности объекта.
Возьмем, к примеру, сферу. Её можно задать следующей SDF-функцией:
Вот результат её визуализации:
Перед нами простая белая сфера — ничего лишнего.
Добавим базовое освещение: сделаем пиксели ярче, если их нормаль направлена ближе к источнику света.
Может показаться, что освещение работает некорректно, так как подсвечивается лишь одна полусфера. Но на самом деле это физически точный результат.
В привычной нам среде объекты часто освещены рассеянным светом от множества источников — солнца, неба, искусственного освещения. Но в космическом вакууме, где свет исходит только от одной звезды, мы увидим именно такой эффект: резкую границу между освещённой и тёмной стороной.
Это хорошо заметно на фотографиях планет:
Как видно, у них тоже ярко выражена "дневная" и "ночная" сторона — ровно как в нашем рендере. Так что всё работает как задумано!
Сейчас сфера имеет цвет [1.0, 1.0, 1.0] — это классический белый в RGB-формате (где значения ограничены диапазоном от 0 до 1). Однако если поэкспериментировать и выйти за эти границы, можно получить неожиданные визуальные эффекты:
Выглядит стильно, хотя и не имеет практического применения.
Пока изображение выглядит «рваным», с чёткими ступенчатыми переходами. Но это временно — дальше мы разберём, как добавить сглаживание для более плавного рендера.
Сейчас мы можем добавить базовые тени. Алгоритм прост:
- Находим точку пересечения луча с объектом
- Вычисляем нормаль к поверхности
- Пускаем дополнительный луч к источнику света
- Если луч перекрыт другим объектом — точка в тени
Результат:
Недостаток: такие тени получаются резкими и неестественными. Поэтому переходим к более продвинутой системе — трассировка лучей.
Добавляем два типа материалов:
-
Диффузный — отражает свет равномерно во всех направлениях (как матовая поверхность)
-
Светящийся — сам является источником света (цвет добавляется прямо в пиксель)
Принцип рендеринга:
- Если луч попадает на светящийся материал — сразу возвращаем его цвет
- Если на диффузный — случайно отражаем луч и продолжаем трассировку
- Если после нескольких отскоков луч не встретил источник света — пиксель остаётся чёрным
Иллюстрация:
Первая попытка:
Проблема: изображение получается «шумным», так как для каждого пикселя используется только один случайный луч.
Чтобы исправить это, необходимо использовать метод Монте-Карло, когда мы выбираем несколько направлений и усредняем результат.
Добавим большой источник света и посмотрим на результат:
1 луч на пиксель
10 лучей на пиксель
100 лучей на пиксель
1000 лучей на пиксель
Чем больше лучей — тем плавнее и реалистичнее результат.
Добавим новый материал — металл. Вместо того, чтобы выбирать случайное направление, мы будем отражать луч относительно нормали к поверхности.
Особенности:
- Луч отражается зеркально (угол падения = углу отражения)
- Цвет металла влияет на оттенок отражения
Результат:
Сейчас изображение выглядит блёклым и неестественным. Это происходит потому, что:
- Человеческое зрение воспринимает яркость нелинейно
- Стандартные дисплеи работают в sRGB-пространстве
Решение:
- Гамма-коррекция (степень 2.2) - компенсирует нелинейность восприятия
- Tone mapping - адаптирует HDR-цвета к диапазону дисплея
Результат после применения:
Разница очевидна - цвета стали насыщенными и естественными.
Сейчас, если луч никуда не попал, мы красим его в чёрный. Но мы можем добавить скайбокс — текстуру, которая будет отображаться в фоновом режиме.
Добавим два скайбокса:
- Горы
- Космос
Картинку гор я подрезал у автора Capricorn4049 с этого сайта В описании сказано, что при иcпользовании нужно указать автора. Поэтому вот указываю.
Посмотрим на результат:
Пришло время бороться с ступенчатыми переходами.
Самый простой способ - это сделать суперсэмплинг (SSAA) - рендерить изображение в 4 раза больше, а потом уменьшить его до нужного размера. Это позволяет сгладить резкие переходы. Остаётся только выбрать, как именно уменьшать изображение. Я выбрал метод Lanczos, так как он даёт наилучший результат.
Вот разные методы уменьшения изображения:
BICUBIC
BILINEAR
BOX
HAMMING
LANCZOS
NEAREST
Ещё один способ борьбы с ступенчатыми переходами - это использовать антиалиасинг. Он работает по тому же принципу, что и суперсэмплинг, но вместо того, чтобы рендерить изображение в 4 раза больше, он просто добавляет шум к лучам, которые мы пускаем
Нужно только выбрать на сколько процентов добавлять шум. Я выбрал 0.15% от длины луча. Это позволяет получить хороший результат без сильного ухудшения качества.
Вот разные проценты добавления шума:
1%
0.5%
0.15%
0.025%
Добавим ещё один простой объект - куб. Он описывается SDF-функцией, которая вычисляет расстояние от точки до поверхности куба.
Пришло время добавить более интересные объекты.
Как я уже говорил, каждый объект описывается знаковой формулой расстояния, поэтому мы можем спокойно отрисовывать объекты с бесконечной кривизной. Например, фракталы.
Самым красивым, на мой взгляд, и простым фракталом является вот такой трёхмерный аналог множества Мандельброта. Описывается он вот такой итеративной формулой.
Результат:
Это выглядит просто невероятно, особенно на фоне космоса. По правде говоря, это мой любимый рендер из всех сделанных в проекте.
На последок, попробуем повторить эксперимент Корнелла. Известный как Корнеловская коробка. Это классический тест для рендеринга, который позволяет проверить качество освещения и материалов.
Кстати, если поставить глубину отражения лучей равной единице, то можно получить вот такой результат.
Довольно интересно, не правда ли?
Далее идут ещё больше рендеров, которые я успел сделать.
Можно заметить, что металлическое множество мандельброта выглядит хуже, чем диффузный. Сложно сказать с чем это связано. Наверное, мы просто не привыкли видеть металлические фракталы в обычной жизни)
А вот тут можно увидеть, как красиво сочетаются тени и полутени. Без трассировки лучей это было бы невозможно.
Нет. Движок использует CUDA для ускорения вычислений. Если у вас нет GPU от Nvidia, вы можете попробовать запустить эмуляцию CUDA на CPU. Я пытался - у меня не получилось. Поэтому проект нативно не поддерживает рендеринг на CPU. Даже если вы сможете запустить, производительность будет очень низкой. Если у вас есть GPU от AMD, вы можете попробовать использовать ROCm, но я не тестировал. Скорее всего работать не будет.
Запустите команду
nvidia-smiВы должны увидеть что-то такое
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 576.28 Driver Version: 576.28 CUDA Version: 12.9 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Driver-Model | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 NVIDIA GeForce RTX 3060 WDDM | 00000000:01:00.0 On | N/A |
| 0% 43C P8 17W / 170W | 1796MiB / 12288MiB | 16% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
Если версия CUDA меньше 12.0 или ваша команда выдает ошибку, то вам нужно установить CUDA Toolkit. Вы можете скачать его здесь




































