Конвейер рендеринга двигателей: создание шейдеров

10

Я пытаюсь сделать движок 2D-игр, используя OpenGL ES 2.0 (пока iOS). Я написал прикладной уровень в Objective C и отдельный автономный RendererGLES20 в C ++. За пределами рендерера не выполняется специальный вызов GL. Работает отлично.

Но у меня есть некоторые проблемы с дизайном при использовании шейдеров. Каждый шейдер имеет свои уникальные атрибуты и униформу, которые необходимо установить непосредственно перед основным вызовом отрисовки (в данном случае glDrawArrays). Например, чтобы нарисовать некоторую геометрию, я бы сделал:

void RendererGLES20::render(Model * model)
{
    // Set a bunch of uniforms
    glUniformMatrix4fv(.......);
    // Enable specific attributes, can be many
    glEnableVertexAttribArray(......);
    // Set a bunch of vertex attribute pointers:
    glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, m->pCoords);

    // Now actually Draw the geometry
    glDrawArrays(GL_TRIANGLES, 0, m->vertexCount);

    // After drawing, disable any vertex attributes:
    glDisableVertexAttribArray(.......);
}

Как видите, этот код очень жесткий. Если бы я использовал другой шейдер, скажем, эффект ряби, мне нужно было бы передать лишнюю униформу, атрибуты вершин и т. Д. Другими словами, мне пришлось бы изменить исходный код RendererGLES20, чтобы включить новый шейдер.

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

Следующее показывает актерский класс:

class Actor : public ISceneNode
{
    ModelController * model;
    AIController * AI;
};

Класс контроллера модели: класс ModelController {класс IShader * шейдер; int textureId; оттенок vec4; плавать альфа; struct Vertex * vertexArray; };

Класс Shader содержит только объект shader, компиляцию и компоновку подпрограмм и т. Д.

В классе Game Logic я фактически представляю объект:

void GameLogic::update(float dt)
{
    IRenderer * renderer = g_application->GetRenderer();

    Actor * a = GetActor(id);
    renderer->render(a->model);
}

Обратите внимание, что, хотя Actor расширяет ISceneNode, я еще не начал реализовывать SceneGraph. Я сделаю это, как только решу эту проблему.

Есть идеи как это улучшить? Связанные шаблоны дизайна и т.д.?

Спасибо, что прочитали вопрос.

fakhir
источник
Не уверен, что это точный дубликат, но я думаю, он доходит до сути того, что вы пытаетесь сделать.
Шон Миддледич

Ответы:

12

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

Во-первых, отказ от ответственности: система, управляемая данными, может упростить добавление новых шейдеров, но, с другой стороны, она сопряжена с затратами с точки зрения усложнения системы, что затрудняет ее обслуживание и отладку. Поэтому стоит подумать о том, насколько хорошо для вас будет работать с данными (для небольшого проекта ответ может быть «нет»), и не пытайтесь создать систему, которая слишком обобщена.

Хорошо, давайте сначала поговорим о форматах (атрибутах) вершин. Вы можете создать описание данных, создав структуру, содержащую данные для передачи glVertexAttribPointer- индекс, тип, размер и т. Д. Одного атрибута, - и массив этих структур, представляющих весь формат вершины. Учитывая эту информацию, вы можете программно настроить все состояния GL, связанные с атрибутами вершин.

Откуда берутся данные для заполнения этого описания? Концептуально, я думаю, что самый чистый способ - это принадлежать шейдеру. Когда вы строите данные вершины для меша, вы должны посмотреть, какой шейдер используется в меше, найти формат вершины, требуемый этим шейдером, и соответственно построить буфер вершин. Вам просто нужен какой-то способ для каждого шейдера указать ожидаемый формат вершины.

Есть множество способов сделать это; например, у вас может быть стандартный набор имен для атрибутов в шейдере («attrPosition», «attrNormal» и т. д.), а также некоторые жестко заданные правила, такие как «position is 3 float». Затем вы используете glGetAttribLocationили аналогично запрашиваете, какие атрибуты использует шейдер, и применяете правила для построения формата вершин. Другой способ - получить фрагмент XML, определяющий формат, встроенный в комментарий в исходный код шейдера и извлеченный вашими инструментами, или что-то в этом роде.

Для униформ, если вы можете использовать OpenGL 3.1 или более позднюю версию, рекомендуется использовать объекты единого буфера (эквивалент OpenGL постоянных буферов D3D). Увы, GL ES 2.0 не имеет таковых, поэтому с униформой нужно обращаться индивидуально. Один из способов сделать это - создать структуру, содержащую единое местоположение для каждого параметра, который вы хотите установить - матрицу камеры, зеркальную мощность, мировую матрицу и т. Д. Здесь также могут быть местоположения сэмплера. Этот подход зависит от наличия стандартного набора параметров, общих для всех шейдеров. Не каждый шейдер должен использовать каждый параметр, но все параметры должны быть в этой структуре.

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

Натан Рид
источник