Найти тему
ZDG

Олдскульная растеризация треугольников

Одна из базовых задач компьютерной графики – нарисовать на экране произвольный закрашенный треугольник:

Сам треугольник задан в векторном виде, то есть у него есть три вершины с координатами: (x0, y0), (x1, y1) и (x2, y2). Между каждой парой координат можно провести линию:

(x0, y0) – (x1, y1)
(x1, y1) – (x2, y2)
(x2, y2) – (x0, y0)

Эти линии задают геометрический контур треугольника. Растеризация – это вывод треугольника на экран в растровом виде, то есть в виде множества закрашенных пикселов.

Где это нужно?

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

Любые другие многоугольники можно растеризовать, разбив их на треугольники, и растеризовав каждый треугольник отдельно.

Почему олдскульная?

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

Мы вскоре доберемся до видеоуcкорителей и шейдеров. А пока что рассмотрим чисто программный алгоритм растеризации, чтобы:

  • понять, как это работает
  • работало на любых компьютерах, даже ZX Spectrum и БК-0010
  • работало даже на текстовом экране
  • получить представление о скорости работы

Я также планирую использовать эту реализацию в генетическом алгоритме, воспроизводящем картинку с помощью треугольников.

Реализация

Писать будем на языке C, так как нужна максимальная скорость. Всё, что необходимо для компиляции программ на C, перечислено в материале Пишем Сапёра на C.

Чтобы растеризовать треугольник, мы в цикле запускаем воображаемую горизонтальную линию, которая перемещается от верха до низа с шагом один пиксел:

-2

На каждом шаге эта линия всегда пересекается с двумя из трёх сторон треугольника (и вот поэтому треугольники удобны!)

-3

Зная две точки пересечения, мы рисуем линию между ними, и вот мы закрасили одну линию в треугольнике! Последовательно смещаясь вниз, вычисляя точки пересечения и рисуя между ними линии, мы закрашиваем весь треугольник.

Общее описание алгоритма:

  • Двигать горизонтальную линию сверху вниз
  • Для каждой стороны треугольника находить пересечение с этой линией
  • Пересекаться будут только какие-то две стороны из трёх, нужно отобрать их
  • Между точками пересечения нарисовать линию

Нахождение пересечения произвольной линии с горизонтальной линией описано в материале Пересечение линии с препятствием.

У этого общего описания есть скрытые проблемы.

  • Нужно постоянно проверять, какая из сторон треугольника пересекается с линией, а какая нет.
  • Из-за целочисленных округлений координат пикселов в некоторых местах происходит неверное определение пересечения и появляются артефакты.

Поэтому сразу оптимизируем процесс. Это будет нудновато и громоздко, но разбивается на элементарные, легко решаемые задачи. Для начала треугольник нужно нормализовать так, чтобы мы знали, какая вершина самая верхняя, а какая самая нижняя. При этом третья вершина автоматически становится средней (спасибо треугольнику!)

-4

Сперва находим самую верхнюю вершину c помощью двух сравнений – её y-координата должна быть меньше двух остальных. Если она оказалась больше, то меняем вершины местами, вместе с координатой x:

-5

Затем из двух оставшихся вершин выбираем среднюю и нижнюю:

-6

И вот у нас нормализованный треугольник. Как я и говорил, это громоздко и нудно, но по факту тут не происходит ничего серьёзного.

Теперь, зная, что y0 это точно верхняя вершина, а y1 это точно средняя вершина, мы точно знаем, какие две стороны треугольника будут пересекаться с горизонтальной линией вплоть до координаты y1. Это стороны (y0 - y1) и (y0 - y2).

-7

Двигая горизонтальную линию (переменная top_y) от y0 до y1, вычисляем пересечения и рисуем первую половину треугольника:

-8

Затем двигаем линию от y1 до y2, и на этот раз в пересечениях участвует третья сторона треугольника (y1 - y2), в то время как первая (y0 - y2) продолжает использоваться дальше (это гарантируется треугольником):

-9

Рисуем вторую часть, и треугольник готов:

-10

Я сделал вывод случайных треугольников на экран, и вот как это выглядит:

-11

Теперь попробуем оценить скорость рисования.

На моём ноутбуке (он еле-еле тянет игру Cyberpunk 2077 на самых низких настройках) 100 000 (сто тысяч) треугольников рисуется за примерно 42 секунды. Это 2380 треугольников в секунду. Если игра, допустим, работает на частоте 60 кадров в секунду, то в одном кадре можно отрисовать только около 40 треугольников. Это чудовищно мало.

Но если посмотреть, что отбирает больше всего времени, то это будет вызов функции SDL_UpdateWindowSurface(). Данная функция отображает на экране то, что было нарисовано в буфере видеопамяти.

Сейчас она вызывается после рисования каждого треугольника. Если её не вызывать, то треугольники будут по-прежнему рисоваться, только мы их не увидим. Без вызова этой функции время работы программы сокращается до 2.5 секунды. Получаем 40 000 треугольников в секунду, или 667 треугольников в кадр. Это более реалистичный вариант, так как в каждом кадре игра сначала рисует все необходимые треугольники и затем только один раз вызывает SDL_UpdateWindowSurface().

Теперь посмотрим, сколько времени занимает собственно рисование треугольника. Оставим все вычисления, но не будем рисовать, то есть не будем вызывать метод SDL_FillRect(). Время сокращается до 0.3 секунды. Это 333 333 треугольников в секунду, или 5 555 треугольников в кадр.

Как видим, основной проблемой являются не вычисления, а вызовы SDL_UpdateWindowSurface() и SDL_FillRect(). Надо подумать над тем, как исправить ситуацию.

Но как бы то ни было, 5 555 треугольников всё равно довольно мало. Игры оперируют сотнями тысяч треугольников на ускорителях. Поэтому в следующий раз сделаю OpenGL-вариант и сравним.

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

Скриншот, который может претендовать на современное искусство:

-12

Полный код программы на GitHub

Читайте дальше: Альфа-канал вручную

Наука
7 млн интересуются