Вероятно, все знают про 23 проблемы Гильберта, определившие развитие математики XX века. Но мало кто знает, что в черновиках великого немца была 24-я проблема: она касалась критериев простоты доказательства и поиска наиболее прямых методов решения задач.

В разработке собственного движка MagnaVerse я столкнулся с классической проблемой GI (глобального освещения): расчет Radiosity на CPU просто по умолчанию обещал сожрать все ресурсы. Эта его особенность представлялась "красным флагом" еще на этапе проектирования системы освещения. Ну и по факту: профайлер горел красным, а код был усеян проверками вида
if (energy < 0) energy = 0; и if (isnan(val)) ...
и так далее.
Однако решение нашлось. Оно пришло не через оптимизацию ассемблера, а через философию. Я применил подход "Гильберт-24" (H24) - технику, которая гарантирует физическую корректность на уровне типов данных. Результат? Тяжелейшая задача по расчету отскоков света стала "легче перышка" для процессора.
В этой статье я расскажу, как обертка над float может ускорить ваш рендер и спасти от "ядерных взрывов" света.
Суть проблемы (float всё стерпит, ага, конечно)
Обычный код физики света в движках выглядит примерно так:
float incoming = GetIncomingLight(); // Может вернуть -0.001 из-за ошибки точность
float rho = material.albedo; // Может быть 1.5 из-за ошибки экспорта
float result = incoming * rho; // Энергия взялась из ниоткуда!
В "горячем цикле" (где миллионы итераций) мы вынуждены либо:
Забить на все и получить NaN, который заразит всю сцену черными квадратами.
Ставить проверки (std::max(0, val), min(1, rho)), убивая предсказатель переходов и векторизацию.
Что нам предлагает H24: идеология "Trust, but Verify (at the gate)"
Идея H24 в моем движке оказалась до нельзя проста в формулировке, а именно: физически некорректное состояние должно быть непредставимым в коде.
Итого: мы запрещаем "сырые" флоаты в формулах света. Вместо них мы вводим строгие типы.
1. Энергия (SpectralEnergy)
Энергия не может быть отрицательной. Никогда. Если только вы не физик-теоретик, который мечтает о гипердрайве.
namespace Hilbert24 {
struct SpectralEnergy {
Math::Vector3 v;
// "Таможня": проверяем данные ОДИН раз при создании
explicit SpectralEnergy(const Math::Vector3& raw) {
v.x = std::max(0.0f, raw.x);
v.y = std::max(0.0f, raw.y);
v.z = std::max(0.0f, raw.z);
}
// Zero overhead доступ для математики
const Math::Vector3& raw() const { return v; }
};
}
2. Материал (ValidatedMaterial)
Материал не может отразить больше света, чем получил (если только вы не хотите смоделировать реалистичное поведение куска урана). Альбедо > 1.0 — это вечный двигатель, который в рекурсивном Radiosity вызывает "взрыв" яркости (и, да, я увидел, как это бывает, когда переписывал движок на новую архитектуру. Белый экран нам ни к чему).
struct ValidatedMaterial {
Math::Vector3 rho;
explicit ValidatedMaterial(const Math::Vector3& raw) {
// Жесткий кламп до физического предела (0.95)
rho.x = std::min(raw.x, 0.95f);
rho.y = std::min(raw.y, 0.95f);
rho.z = std::min(raw.z, 0.95f);
}
};
3. Ядро (Core Logic)
Вся математика выносится в отдельные функции, которые принимают только валидные типы.
namespace Core {
[[nodiscard]] inline SpectralEnergy reflect(
const SpectralEnergy& incident,
const ValidatedMaterial& mat,
float formFactor)
{
// Здесь нет if (incident < 0) или if (mat > 1).
// Мы ГАРАНТИРОВАННО работаем с чистыми данными.
// Компилятор счастлив, SIMD работает на полную.
float ff = std::max(0.0f, formFactor);
Math::Vector3 r;
r.x = incident.raw().x * mat.rho.x * ff;
// ... y, z ...
return SpectralEnergy(r);
}
}
Реальное применение тут, конечно же - ускорение Radiosity
Посмотрим, как это изменило решатель глобального освещения.
Было: тяжелый и не слишком элегатный спагетти-код, смешивающий геометрию, проверки на NaN и бесконечные if.
Стало (H24 Style):
// Внутри цикла по патчам (Workset)
for (int t = 0; t < workset_pids.size(); ++t) {
auto& Pu = sc.patches[i];
// 1. Валидация на входе. Дальше - чистая математика.
Hilbert24::ValidatedMaterial mat(Pu.rho3);
Hilbert24::SpectralEnergy sumIncoming({ 0,0,0 });
for (auto j : neighbors) {
// ... расчет форм-фактора F ...
// H24: Безопасное сложение
// Мы просто складываем, зная, что B3 соседа валиден
Hilbert24::SpectralEnergy incoming(Pv.B3);
sumIncoming = Hilbert24::Core::add(sumIncoming, Hilbert24::Core::scale(incoming, F));
}
// 2. Отражение
// Гарантированно не превысит входящую энергию
Hilbert24::SpectralEnergy emission(Pu.E3);
Hilbert24::SpectralEnergy reflected = Hilbert24::Core::reflect(sumIncoming, mat, 1.0f);
// 3. Запись результата
Math::Vector3 B_target = Hilbert24::Core::add(emission, reflected).raw();
// Релаксация (SOR)
Pu.B3 = ...;
}
Почему это стало "легче перышка"?
Успешное предсказание ветвлений (Branch Prediction), ибо внутри цикла reflect и add их больше нет. Процессор "глотает" инструкции пачками.
Регистры. Компилятор видит, что ValidatedMaterial константен внутри цикла, и может загрузить его в регистры один раз.
Безопасность = скорость, ибо обычно проверки безопасности замедляют код. Здесь же у нас получилось наоборот. Мы убрали проверки из задачи с вычислительной сложностью
(что есть взаимодействие патчей) в задачу со сложностью
(т.е. в создание материалов).
Стабильность: исчезли "светлячки" (fireflies) и NaN-ы, которые раньше заставляли движок пересчитывать кадр или выдавать мусор.
Результаты и Перспективы
Внедрение Hilbert24 в MagnaVerse Engine дало парадоксальный эффект: код стал длиннее (из-за структур), но исполняется быстрее. Нагрузка на CPU при пересчете света в динамике упала радикально, освободив время для физики и AI.
Где это еще применимо?
Физика. Гарантия Mass > 0, Friction <= 1.0.
Звук. Семплы в диапазоне [-1, 1] без клиппинга на каждом шаге фильтра.
Искусственный интеллект виртуальных болванчиков. Вероятности, строго ограниченные [0, 1].
В перспективе, такой подход открывает дорогу к безопасной процедурной генерации. Если мы гарантируем физическую корректность «строительных блоков» на уровне типов, мы можем позволить алгоритмам генерировать контент без страха сломать симуляцию. Мы сможем ставить движку задачу на высоком уровне («создай объект, выдерживающий 70 кг»), и движок соберет его из валидных H24-компонентов, не порождая «глючных» артефактов.
Какие есть вероятные недостатки
Надо писать больше «оберточного» кода.
Вездесущие границы API. При передаче данных в GPU или в сеть приходится «разворачивать» структуры обратно в float.
Заключение
Задача Гильберта о «простоте доказательства» в программировании превращается в задачу о «простоте данных». Если ваши данные просты и гарантированно корректны, алгоритмы становятся тривиальными и быстрыми.
Не бойтесь создавать типы для физических величин. Пусть компилятор потеет за вас, а процессор занимается своим делом - перемалыванием чисел, а не проверкой ошибок.
ПостСкриптум. В следующей статье я расскажу, как сжимать геометрию в полиномы Чебышева (H16) и почему треугольники в 2030 году нам больше не понадобятся.