Создание объекта OpenGL - PullRequest
       8

Создание объекта OpenGL

4 голосов
/ 29 сентября 2011

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

Моя проблема:

Все вызовы OpenGL должны выполняться потоком, которому принадлежит созданный контекст OpenGL (по крайней мере, в Windows, любой другой поток будетничего не делать и создать ошибку OpenGL).Итак, чтобы получить контекст OpenGL, я сначала создаю экземпляр класса окна (просто еще одну оболочку для вызовов Win API) и, наконец, создаю контекст OpenGL для этого окна.Это звучало довольно логично для меня.(Если в моем дизайне уже есть недостаток, который заставляет вас кричать, дайте мне знать ...)

Если я хочу создать текстуру или любой другой объект, для создания которого требуются вызовы OpenGL, я делаю это(вызываемый конструктор объекта OpenGL, пример):

opengl_object()
{
    //do necessary stuff for object initialisation
    //pass object to the OpenGL thread for final contruction
    //wait until object is constructed by the OpenGL thread 
}

Итак, на словах я создаю объект, как любой другой объект, используя

 opengl_object obj;

, который затем в своем конструкторе, помещает себя в очередь объектов OpenGL, создаваемых потоком контекста OpenGL.Затем поток контекста OpenGL вызывает виртуальную функцию, которая реализована во всех объектах OpenGL и содержит необходимые вызовы OpenGL для окончательного создания объекта.

Я действительно думал, что такой способ решения этой проблемы был бы хорош.Тем не менее, прямо сейчас, я думаю, что я ужасно неправ.

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

Допустим, у меня есть класс sprite, представляющий, очевидно, Sprite.Он имеет собственную функцию создания для потока OpenGL, в котором вершины и координаты текстуры загружаются в память графических карт и так далее.Это пока не проблема.Скажем так, я хочу иметь 2 способа рендеринга спрайтов.Один экземпляр и один через другой.Итак, я бы закончил с 2 классами, sprite_instanced и sprite_not_instanced.Оба являются производными от класса sprite, так как оба являются sprite, которые отображаются только по-разному.Однако sprite_instanced и sprite_not_instanced нуждаются в дополнительных вызовах OpenGL в своей функции create.

Мое решение до сих пор (и я чувствую себя по-настоящему ужасно!)

У меня есть какое-то понимание того, как работает генерация объектов в c ++ и как это влияет на виртуальные функции,Поэтому я решил использовать виртуальную функцию создания класса sprite только для загрузки данных вершин и т. Д. В графическую память.Затем виртуальный метод создания sprite_instanced выполнит подготовку к визуализации этого спрайта.Итак, если я хочу написать

sprite_instanced s;

Во-первых, вызывается конструктор спрайта, и после некоторой инициализации конструирующий поток передает объект в поток OpenGL.На этом этапе переданный объект является просто обычным спрайтом, поэтому будет вызван sprite :: create, а поток OpenGL создаст нормальный спрайт.После этого конструирующий поток вызовет конструктор sprite_instanced, снова выполнит некоторую инициализацию и передаст объект в поток OpenGL.На этот раз, однако, это sprite_instanced и, следовательно, будет вызываться sprite_instanced :: create.

Так что, если я прав с приведенным выше предположением, все происходит именно так, как должно, по крайней мере, в моем случае.Я провел последний час, читая о вызове виртуальных функций из конструкторов и о том, как строится v-таблица и т. Д. Я провел некоторый тест, чтобы проверить мои предположения, но это может зависеть от компилятора, поэтому я не полагаюсь на них на 100%,Кроме того, это просто ужасно и похоже на ужасный взлом.

Другое решение

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

Мой вопрос

Можно ли справиться с этим так, как я описал выше? Или мне лучше выбросить это и заняться чем-нибудь другим?

Ответы [ 6 ]

5 голосов
/ 29 сентября 2011

Вам уже дали хороший совет.Так что я просто добавлю немного:

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

Однако вполне нормально создавать их во время обычноговыполнение программы.В моем 3D-движке я использую свободное время ЦП во время двойной замены буфера для асинхронных загрузок в объекты буфера (for(b in buffers){glMapBuffer(b.target, GL_WRITE_ONLY);} start_buffer_filling_thread(); SwapBuffers(); wait_for_buffer_filling_thread(); for(b in buffers){glUnmapBuffer(b.target);}).

Также важно понимать, что для простых вещей, таких как спрайты, не следует давать свой собственный VBOза каждый спрайт.Обычно группируются большие группы спрайтов в одном VBO.Вам не нужно рисовать их все вместе, так как вы можете сместить в VBO и сделать частичные вызовы рисования.Но этот общий паттерн OpenGL (геометрические объекты, совместно использующие буферный объект) полностью противоречит принципу ваших классов.Таким образом, вам нужен некоторый менеджер объектов буфера, который раздает кусочки адресного пространства потребителям.

Использование иерархии классов с OpenGL само по себе неплохая идея, но тогда оно должно быть на несколько уровней выше, чем OpenGL.Если вы просто сопоставите OpenGL 1: 1 с классами, вы получите только сложность и раздувание.Если я вызову функции OpenGL напрямую или по классу, мне все равно придется выполнять всю основную работу.Таким образом, класс текстуры должен не просто отображать концепцию объекта текстуры, но также должен заботиться о взаимодействии с объектами Pixel Buffer (если используется).

Если вы действительно хотите обернуть OpenGL в классы, я настоятельно рекомендуюне используя виртуальные функции, а статические (т.е. на уровне модуля компиляции) встроенные классы, так что они становятся синтаксическим сахаром, и компилятор не будет слишком много.

2 голосов
/ 29 сентября 2011

Вопрос упрощается тем фактом, что один контекст считается текущим в одном потоке;на самом деле может быть несколько контекстов OpenGL, также в разных потоках (и пока мы находимся, мы рассматриваем совместное использование пространств имен контекста).


Прежде всего, я думаю, что вы должны отделить вызовы OpenGL отконструктор объекта.Это позволит вам настроить объект без использования контекстной валюты OpenGL;последовательно объект можно поставить в очередь для создания в главном потоке рендеринга.

Пример.Предположим, у нас есть 2 очереди: одна, которая содержит Texture объекты для загрузки данных текстуры из файловой системы, другая, которая содержит Texture объекты для загрузки данных текстуры в память GPU (после загрузки данных, конечно,).

Поток 1: загрузчик текстур

{
    for (;;) {
        while (textureLoadQueue.Size() > 0) {
            Texture obj = textureLoadQueue.Dequeue();

            obj.Load();
            textureUploadQueue.Enqueue(obj);
        }
    }
}

Поток 2: раздел кода загрузчика текстур, по сути основной поток рендеринга

{
    while (textureUploadQueue.Size() > 0) {
        Texture obj = textureUploadQueue.Dequeue();

        obj.Upload(ctx);
    }
}

Текстура конструктор объекта должна выглядеть так:

Texture::Texture(const char *path)
{
    mImagePath = path;
    textureLoadQueue.Enqueue(this);
}

Это только пример.Конечно, к каждому объекту предъявляются разные требования, но это решение является наиболее масштабируемым.


Мое решение по существу описывается интерфейсом IRenderObject (документация сильно отличается от текущей реализации, так как я много рефакторинг в данный момент, и развитие находится на очень альфа-уровне).Это решение применяется к языкам C #, которые вносят дополнительную сложность из-за управления сборкой мусора, но концепция прекрасно адаптируется к языку C ++.

По сути, интерфейс IRenderObject определяет базовый объект OpenGL:

  • У него есть имя (возвращаемое Gen подпрограммами)
  • Может быть создано с использованием текущего контекста OpenGL
  • Можетбыть удаленным с использованием текущего контекста OpenGL
  • Может быть выпущено асинхронно с использованием "сборщика мусора OpenGL"

Создание / удалениеОперации очень интуитивно понятны.Взятие RenderContext , абстрагирующее текущий контекст;Используя этот объект, можно выполнить проверки, которые могут быть полезны для поиска ошибки в создании / удалении объекта:

  • Метод Create проверяет, является ли контекст текущим, если контекст может создать объектэтот тип и т. д. ...
  • Метод Delete проверяет, является ли контекст текущим, и, что более важно, проверяет, использует ли контекст, переданный в качестве параметра, то же пространство имен объекта контекста, который создаллежащий в основе IRenderObject

Вот пример метода Delete.Здесь код работает, но он не работает должным образом:

RenderContext ctx1 = new RenderContext(), ctx2 = new RenderContext();
Texture tex1, tex2;

ctx1.MakeCurrent(true);
tex1 = new Texture2D();
tex1.Load("example.bmp");
tex1.Create(ctx1);            // In this case, we have texture object name = 1

ctx2.MakeCurrent(true);
tex2 = new Texture2D();
tex2.Load("example.bmp");
tex2.Create(ctx2);            // In this case, we have texture object name = 1, the same has before since the two contexts are not sharing the object name space

// Somewhere in the code
ctx1.MakeCurrent(true);

tex2.Delete(ctx1);            // Works, but it actually delete the texture represented by tex1!!!

Асинхронная операция освобождения направлена ​​на удаление объекта, но не имеет текущего контекста (в действительности метод не принимает параметр RenderContext).Может случиться, что объект расположен в отдельном потоке, который не имеет текущего контекста;но также я не могу полагаться на сборщик мусора (в C ++ его нет), так как он выполняется в потоке, и у меня нет контроля.Кроме того, желательно реализовать интерфейс IDisposable, чтобы код приложения мог управлять временем жизни объекта OpenGL.

OpenGL GarbageCollector выполняется в потоке с правильным текущим контекстом.

2 голосов
/ 29 сентября 2011
  1. Вызывать любую виртуальную функцию в конструкторе всегда плохо.Виртуальный вызов не будет завершен как обычно.

  2. Ваши структуры данных очень запутаны.Вам следует изучить концепцию объектов фабрики.Это объекты, которые вы используете для создания других объектов.У вас должен быть SpriteFactory, который помещается в какую-то очередь или что-то в этом роде.Именно SpriteFactory должен быть тем, что создает сам объект Sprite.Таким образом, у вас нет понятия частично сконструированного объекта, когда его создание помещается в очередь и т. Д.

    Действительно, каждый раз, когда вы начинаете писать «Objectname :: Create», остановитесьи подумайте: «Я действительно должен использовать объект Factory».

1 голос
/ 29 сентября 2011

Я думаю, что проблема здесь не в RAII, или в том, что OpenGL - это интерфейс в стиле c. Дело в том, что вы предполагаете, что sprite и sprite_instanced должны происходить из общей базы. Эти проблемы возникают постоянно с иерархиями классов, и один из первых уроков, которые я узнал об объектной ориентации, в основном из-за множества ошибок, заключается в том, что почти всегда лучше инкапсулировать, чем выводить. ИСКЛЮЧИТЬ, что если вы собираетесь извлечь, сделайте это через абстрактный интерфейс.

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

1 голос
/ 29 сентября 2011

Я бы не стал вставлять ваши объекты в очередь потока GL при создании.Это должен быть явный шаг, например

gfxObj_t thing(arg) // read a file or something in constructor
mWindow.addGfxObj(thing) // put the thing in mWindow's queue

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

1 голос
/ 29 сентября 2011

OpenGL был разработан для C, а не C ++. То, что я узнал, работает лучше всего - это писать функции, а не классы, чтобы обернуть их вокруг функций OpenGL, поскольку OpenGL управляет своими собственными объектами внутри. Используйте классы для загрузки ваших данных, а затем передайте их функциям в стиле C, которые работают с OpenGL. Вы должны быть очень осторожны, генерируя / освобождая буферы OpenGL в конструкторах / деструкторах!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...