Рендеринг гор и воздушного шара на чистом C++ с трассировкой лучей и тенями

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

Зачем все это
Задача звучала просто: построить правдоподобную сцену горного массива с воздушным шаром, не опираясь на готовые графические движки. Только “голый” C++ и QT как способ вывести пиксели на экран. Хотелось пройти путь от геометрии и уравнений до конечной картинки и понять, где именно рождается изображение и сколько за него платит процессор.

От объектов к пикселям: перспектива на пальцах
В реальном мире свет от поверхностей попадает в глаз, формируя перспективное изображение. В программной модели мы знаем координаты точки наблюдения (глаза), точки на объекте и положение экрана. Если рассмотреть два подобных прямоугольных треугольника, построенных через глаз-экран-точку, становится ясно: координаты на экране находятся через коэффициент подобия. Проще говоря, мы вычисляем, где пересечется луч, идущий от глаза через точку, с плоскостью экрана. Это и есть номер пикселя, который следует засветить. Но такая схема опасна: если объект расположен между глазом и экраном, он все равно будет “проецироваться” — придется отсекать лишнее.

Пирамида видимости и отсечение
Чтобы не рисовать то, чего не видно, вводится усеченная пирамида видимости: ближняя плоскость (Near), дальняя (Far) и боковые грани. Все, что снаружи — игнорируем. Все, что внутри — оставляем как есть. А объекты, пересекающие границы, обрезаем. На практике сцены раскладывают на треугольники. Обрезать ребро треугольника по границе можно разными алгоритмами, один из классических — Сазерленда—Коэна. После отсечения получаем новый многоугольник, который и закрашиваем.

Почему я все же ушел в трассировку на CPU
Растеризация быстра и логична, но приходится решать немало подзадач: скрытые поверхности, порядок рисования, тени, корректная интерполяция нормалей и текстурных координат. Вместо этого я выбрал обратную трассировку лучей: для каждого пикселя выпускаем луч из глаза и ищем ближайшее пересечение с геометрией. Это сразу решает проблему видимости, значительно упрощает расчет освещения и теней, а также делает добавление эффекта “мягких” материалов и сферической геометрии прямолинейным. Цена — вычислительная тяжесть: на CPU такое решение легко загружает ядра “под завязку”.

Цвет пикселя и освещение: от Ламберта до простого ambient
Если окрасить все грани объекта одним цветом, куб перестанет быть кубом. Вводим плоскую закраску: для каждой грани вычисляем ее нормаль и освещенность, а затем этим цветом заполняем все пиксели грани. Базовая модель — диффузная компонента по Ламберту: яркость зависит от косинуса угла между нормалью N и направлением на источник L. Величина I ∼ max(0, dot(N, L)) масштабирует исходный цвет материала. Чтобы не погружать невидимые участки в абсолютную тьму, добавляем постоянную фоновой подсветки (ambient). Простое приближение: I = I_ambient + I_diffuse, где, скажем, I_ambient = 0.2. Значение 0.2 выглядит правдоподобно для стартовой сцены, хотя в физически корректных моделях оно определяется окружением и глобальным освещением.

Тени: луч до источника
Тень в трассировке получается натурально. Нашли точку пересечения пиксельного луча с поверхностью — выпустили из этой точки луч к источнику света. Если по пути встретили препятствие, пиксель затемняем. Полное обнуление дает “черную дыру”, поэтому оставляем ambient-компоненту, чтобы сцена не выглядела картонной. Так получаем контраст и читаемость формы даже без сложных моделей отражений.

Краткая математика пересечения
Основа трассировки — надежные тесты пересечения. Для треугольников часто применяют Möller–Trumbore: вычисляем t для луча R(t) = O + tD и барицентрические координаты внутри треугольника, проверяем 0 ≤ u, v, u+v ≤ 1 и t > 0. Для сфер решение еще проще: подставляем R(t) в уравнение сферы и решаем квадратное уравнение. Для плоскостей — аналитическое пересечение и проверка попадания в полигон. Стабильность и численная устойчивость здесь критичны: от них зависят артефакты на стыках и дрожание пикселей.

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

Как занять процессор по полной
CPU-рендерер сразу упирается в производительность. Лучи независимы — значит легко распараллелить по строкам или тайлам картинки. Включите многопоточность, распределяйте работу пакетами, чтобы избежать простоя. SIMD-инструкции ускоряют вычисление скалярных произведений и нормализаций. Кэш-линия важна: храните геометрию компактно, уменьшайте случайные обращения в память.

Генерация гор: шум вместо ручной моделировки
Горный ландшафт удобнее не моделировать вручную, а синтезировать. Классика — фрактальные шумы (Перлина, Simplex), midpoint displacement или diamond-square. Из 2D-шума получаем карту высот, превращаем ее в сетку треугольников или прямые аналитические поверхности. Нормали берутся из градиента высотной карты или вычисляются по треугольникам. От распределения частот шума зависят характер “кручения” рельефа, детальность склонов и правдоподобие гребней.

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

Текстуры: путь к “легкой” стилистике
Даже простая диффузная текстура сильно оживляет картинку. Для ландшафта это смешение нескольких материалов по высоте и уклону: трава внизу, камень на обрывах, снег на верхах. В отсутствие физического PBR можно использовать “дешевую” стилизацию: контрастные карты альбедо, несложный нормалмап, минимальные затраты на вычисления. Такой подход напоминает графику старых игр, но при аккуратном подборе карт дает чистый и читаемый результат.

AirBalloon: завершающий штрих
Воздушный шар в сцене — не только декоративная деталь, но и контрольный объект. Сфера купола, цилиндрическая корзина, тросы в виде тонких цилиндров — на них удобно проверять мягкие тени, устойчивость нормалей и правильность бликов. Яркая раскраска купола помогает выявлять ошибки интерполяции и гаммы.

Дополнительные улучшения и практические приемы

- Антиалиасинг без боли. Простое сверхсэмплирование: по 4–8 подлучей на пиксель с джиттером. Это сгладит “лесенки” на профиле гор и тросах шара. Чтобы не убить производительность, комбинируйте: SSAA по краям объектов, один сэмпл на ровных участках.

- Туман и атмосферное затухание. Горы вдалеке должны терять контраст. Введите экспоненциальный или линейный туман: смешивайте цвет пикселя с цветом неба в зависимости от расстояния t пересечения. Это мгновенно добавляет глубины и реализма, даже при простом освещении.

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

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

- Ускоряющие структуры. Даже на CPU крайне желательно ввести иерархии: BVH или хотя бы равномерную сетку. Горы как сетка треугольников отлично бьются на тайлы; луч сначала пересекает клетки, и только затем — треугольники внутри. Это уменьшает число тестов в десятки раз.

- Баланс точности и скорости. Включайте epsilon-смещение для вторичных лучей (теневых), чтобы избежать самопересечений и “черных пятен” на наклонных плоскостях. Следите за порядком чисел: нормализуйте направления, ограничивайте дальность t, избегайте лишних корней и делений.

- Гамма и тонмаппинг. Складывать свет в линейном пространстве, выводить — в гамме дисплея. Простой тонмап (Reinhard) не даст ярким вершинам выбиваться в клип, а теням — проваливаться в черный. Это особенно полезно при ярком солнце и темных расщелинах.

- Псевдо-ambient occlusion. Быстрый прием: из точки нормали выпускаем несколько коротких пробных лучей в полусферу. Чем больше пересечений поблизости, тем темнее точка. Даже 8–16 сэмплов дают приятную “впуклость” трещин и кромок, подчеркивая рельеф.

- Многопоточность и тайловый вывод. Разбейте кадр на квадраты 16×16 или 32×32. Планировщик распределяет тайлы по потокам, а запись в буфер минимизирует гонки. Такой подход легко масштабируется от двух до шестнадцати ядер и больше.

- Профилирование критических мест. Самые дорогие операции — пересечения и нормализация. Кэшируйте нормали треугольников, предвычисляйте обратные длины, используйте предикаты “сверху вниз”: быстрые отсекающие проверки до точной.

О практической генерации гор
Смешивайте несколько уровней шума с разными частотами и амплитудами — так образуются крупные гребни и мелкие трещины. Введите эрозию как постобработку высотной карты: вода “смывает” пики, заполняет котловины, формирует русла. Даже простой итеративный фильтр с направленным сглаживанием дает узнаваемую “геологию”. Наклон склонов можно использовать для распределения материалов: круто — камень, полого — трава, высоко — снег. Это легко вычисляется локальными градиентами, не требует UV-развертки и хорошо работает с процедурными текстурами.

Материалы без PBR, но с характером
Даже без физкорректной модели можно добавить спекулярную составляющую (Фонг/Блинн-Фонг) с маленьким коэффициентом для камня и большим для ткани купола. Блик на воздушном шаре помогает читать объем; на влажных камнях — подчеркивает микрорельеф. Важно удерживать баланс: чуть-чуть глянца и аккуратный размер блика.

Что делать с “ступеньками” на горизонте
Помимо антиалиасинга помогает микрошум нормалей: добавьте слабую высокочастотную составляющую в нормали склонов. Край горизонта станет менее “синтетическим”, особенно в контровом освещении. Другой прием — jitter в позициях первичных лучей внутри пикселя с последующим усреднением.

Управление качеством кадра
Сделайте настраиваемый пресет рендера: количество сэмплов, наличие AO, размер тайла, глубина теней. Для статичной сцены можно считать один “тяжелый” кадр с высоким качеством, а далее фиксировать настройки. Если планируется анимация полета шара, уместны временные фильтры: накапливайте несколько кадров с низким шумом, применяйте экспоненциальное сглаживание, следите за дрожанием.

Итоги
- Растеризация проще для быстрого вывода, но требует сложной инфраструктуры: отсечение, порядок рисования, буферы глубины, тени.
- Трассировка на CPU радикально упрощает видимость, тени и освещение, но съедает процессор и требует ускоряющих структур.
- Горный ландшафт удобно строить процедурно: шумы, эрозия, смешение материалов по высоте и уклону.
- Плоская закраска дает форму, Фонг сглаживает тон. Ambient-компонента спасает от провалов в черный, тени из вторичных лучей дают читаемость сцены.
- Воздушный шар — полезный тестовый объект для проверки геометрии, нормалей, бликов, цветопередачи и теней.

Когда вся цепочка — от луча в пиксель до тени на склоне — заработала, на сцене родился AirBalloon: яркий купол над седыми горами, мягкий туман в долинах и твердые тени под солнечным диском. Рендеринг на чистом C++ перестал быть упражнением “на бумаге” и стал инструментом, который позволяет понимать, как каждый математический шаг превращается в картинку.

Прокрутить вверх