Как написать быстрый и красивый графический движок?
Давно уже думаю о том, как можно написать очень быстрый и в то же время графически продвинутый движок. То есть сделать максимально возможные оптимизации в нем. Но увы, времени на написание движка у меня сейчас нет. Поэтому изложу некоторые свои соображения здесь, а вы можете дополнить или поспорить.
В качестве формата карты проще всего взять Half-Life BSP со вшитыми в нее текстурами, дабы не возиться с загрузкой вадов и прочей хрени.
Итак, что мы ходим увидеть в нашем движке?
- Высокоскоростной рендеринг геометрии
- Максимальное использование возможностей драйвера
- Вершинные трансформации на GPU вместо CPU (это быстро!)
- Попиксельное освещение по модели Блинна
- Динамические и статические источники света, причем желательно, чтобы можно было рисовать сразу много!
- Стенсильные тени
- Шейдерная вода с кубемапным отражением (это быстро!) и преломлением
Это "джентльменский набор" примитивного движка 2006 года Если решать задачу "в лоб", мы получим очень низкую производительность. К тому же, придется писать разный код для разных видеокарт. Поэтому остановимся на Shader Model 2.0 и GPU класса Radeon 9800 (или аналогичном GeForce - NV4x, на NV3x шейдеры 2.0 дают катастрофически низкую производительность).
Итак, Radeon 9800/GeForce6... Исходим из расширений этих видеокарт, и игнорируем более старые (т.е. попросту не запускаемся, либо запускаемся, но предупреждаем, что не несем ответственности за то, что будет рисоваться на экране ).
Теперь по пунктам.
1) Высокоскоростной рендеринг геометрии
Выбираем статический VBO, однозначно. Для брашевых моделей мы сможем задавать нужные матрицы и применять их в вершинном шейдере. А мир вообще статичен по своей природе. Структура вершины - примерно такая:
vec3 vertex;
vec3 normal;
vec3 tangent;
vec2 texcoords;
бинормаль мы вычислим в вершинном шейдере, чтобы не пересылать лишние данные. Нормаль и тангент - это интерполированные значения для вершины! Иначе освещение будет с резкими переходами.
С вершиной все понятно. Координаты текстуры мы тоже прерасчитаем. Будем использовать текстурные атласы 512х512 или 1024х1024 (какие - нужно проверить экспериментально, но думаю, лучше второй вариант - больше влезет).
Да, все наши полигоны мы триангулируем. Это даст лишние вершины, но драйвер умеет сам их убирать, так что можно не беспокоиться об этом. Обратите внимание - триангулировать мы будем в индексных массивах (см. далее), а не в общем вершинном. Триангулировали, построили атласы, создали массивы. Что дальше?
А дальше мы работаем с визлифами. Каждый визлиф содержит в себе часть поверхностей мира. Мы сортируем эти поверхности по индексу атласа (т.к. все текстуры в один атлас вряд ли влезут) и строим буфер индексов. Рисовать мы будем каждый визлиф отдельным вызовом. Но согласитесь, гораздо быстрее, чем по одному сурфасу?
Это все мы рассчитали при загрузке карты. А каждый кадр мы еще будем устанавливать видимость для каждого визлифа, причем двумя способами:
- отсечение по алгоритму BSP (стандартный)
- быстрая сортировка видимых визлифов по расстоянию от игрока и рисование их в буфер глубины с одновременным occlusion test.
Оба этих алгоритма в сумме нам дадут ТОЛЬКО видимые визлифы! Вот их-то мы и нарисуем...
Прочитал, подумал, посчитал... Собственно, мне кажется, что при том количестве полигонов в кадре, под которое заточен формат карт ку/хл, подобные оптимизации будут малоэффективны.
VBO для веполей ещё в принципе дает небольшой бонус, но и то на GF2. Чтобы заметить разницу на чем-то помощнее, типа GF FX, как ты помнишь, надо было запустить карту с 10000 wpoly в кадре. А ты говоришь, что ориентируешься на R9800/GF6..
Конечно, ты не хочешь рендерить по одному wpoly за вызов. Но таким способом, как ты предложил (минимум по вызову на leaf), будешь рисовать всего по два-три wpoly за раз. Если подбить статистику на примере c1a0, то выйдет, что один leaf в среднем содержит лишь четыре полигона. К тому-же, на один и тот-же полигон могут ссылаться несколько листьев (хотя чаще всего один). Учитывая сортировку по текстурам и возможную разницу в способе рендеринга полигона (могут понадобиться еще проходы для отдельно взятых полигонов, разные шейдеры и т.п.), а также, что нарушается строгий порядок рендеринга от ближнего к дальнему, получается, что пользы будет мало (если даже не пойдет во вред).
Да и стоит ли пытаться экономить на вызовах DrawArrays/DrawElements? Веполей в кадре, как правило, не больше тысячи, и если тратить на каждый по вызову-два (для многопроходности), то для гл это в принципе нормально - мы ведь не в директ3д с его дорогим DIP-cost'ом.
В NV SDK есть демка псевдо-инстансинга. Там DrawElements вызывается 32 тысячи раз за кадр, и каждый раз рисуется по два полигона. Меняются только текстурные координаты (их вершинный шейдер юзает как матрицу). И у меня это выдает 40 fps (притом, как мне кажется, всё упирается лишь в вершинный шейдер).