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

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

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

Вот краткое изложение реализации:

Средство визуализации будет хранить вершины путей по мере создания изображения. Соединение любых двух последовательных вершин есть линия.

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

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

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

Расстояние между плоскостью изображения и фокусом коррелирует с полем зрения изображения и является важным свойством для вычисления координат в растровом пространстве. Чем короче расстояние, тем шире FOV и наоборот.

Теперь мы проецируем точки на изображение, соединяя линию с фокусом и экстраполируя эту линию на плоскость изображения.

Затем мы заполняем все пиксели между ними, используя алгоритм рисования линий Брэсингема, и бум! Мы закончили… верно? Не совсем. Сейчас мы нарисовали двухмерную линию над трехмерной средой. Мы хотим, чтобы линия казалась частью этого трехмерного пространства, поэтому нам нужно сделать еще один шаг вперед. Чтобы линия казалась убедительно трехмерной, она должна быть перекрыта геометрией окружающей среды. Для этого нам нужно будет сравнить в каждом пикселе расстояние до точки на линии, которую мы пытаемся нарисовать, и расстояние до поверхности, отображаемой в этом пикселе. Проблема здесь в том, что мы знаем только положение концов линии. Нам придется провести дополнительную математику, чтобы заполнить промежуточные точки.

Уточним некоторую терминологию. Я буду использовать слова «пиксель» и «указывать много». Пиксель всегда будет относиться к положению на плоскости изображения. Точка всегда будет относиться к положению на линии между двумя конечными точками. «Линия» — это соединение между двумя вершинами, представляющими часть пути. Z-буфер — это массив, в котором хранится расстояние до поверхности, визуализируемое в каждом пикселе. Мы будем сравнивать с этим, чтобы проверить видимость нашей линии.

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

Итак, мы берем идею перемещения опорной точки вдоль линии между двумя конечными точками, но меняем способ ее итерации. Мы больше не будем равномерно делить общее расстояние на пиксель.

Начнем с определения точки привязки. Якорь будет первой точкой на линии, которую мы рисуем, и будет иметь соответствующий якорный пиксель.

Мы используем точку привязки и пиксель привязки, чтобы получить некоторые важные данные. Во-первых, мы получаем отношение подобных треугольников, разделив p1_z и dist. С помощью этого соотношения мы можем найти сторону A, соответствующую расстоянию между пикселями.

p1_z — компонент z опорной точки, а dist — расстояние до плоскости изображения.

В конечном счете, нас интересует сторона C этого треугольника.

C представляет собой расстояние вдоль линии, коррелированное с расстоянием между пикселями на изображении. Это будет значение, по которому мы повторяем нашу контрольную точку. Обратите внимание, что мы не будем повторять в смысле +=. Мы всегда оцениваем контрольную точку по отношению к якорю, поэтому мы заменим положение контрольной точки на:

ссылка = якорь + C (режим)

Чтобы найти длину C, мы просто используем закон синусов.

Сторона C – желаемая длина. Сторона A — это длина, коррелирующая с дельтой пикселя. Отсюда нам потребуются только углы ∠a и ∠c. ∠a - угол между направлением линии и соединением фокуса с опорной точкой. ∠b — угол между направлением линии и горизонтальной осью.

Добавьте расстояние C в направлении линии к нашей опорной точке, и опорная точка будет правильно представлять позицию вдоль линии, которую мы хотим оценить.

Самая большая проблема, которую я обнаружил с этой стратегией, всегда связана с ошибкой округления. Мы используем числа с плавающей запятой почти во всех наших вычислениях ради скорости, но я знаю, что они могут быть довольно неточными. В частности, тригонометрические функции, такие как sin() или acos(), неудовлетворительны при работе с действительно точными углами. Наша опорная точка всегда будет в правильном положении, поскольку она начинается с точки p1, но по мере того, как наша точка отсчета приближается к точке p2 под очень острыми углами, точность с плавающей запятой становится очевидной в глючной окклюзии. Я обхожу это, переключая точку привязки между точками p1 и p2 в зависимости от того, какая из двух точек находится ближе к центру поля зрения. Таким образом, точки в центре экрана всегда будут отображаться четко, а точки с недостаточной точностью будут либо за пределами экрана, либо по краям.

И это все, что нужно сделать. Следите за новостями о 3D-графике и развитии Клайва. Спасибо за чтение!