Unick-soft
Статья будет интересна тем, кто ещё не начал или начал недавно изучать OpenGL. В статье сделана попытка указать правильный путь изучения OpenGL, на какие вещи стоит обратить внимание, а на какие не стоит, так как они уже устарели, какие знания необходимо иметь, чтобы не возникли трудности. Статья не содержит справок по функциям или информацию по настройке среды разработки, но в конце статьи вы можете найти полезные ссылки.
Что такое OpenGL
OpenGL — кросс-платформенная библиотека для 2D и 3D графики. В настоящее время используется для разработки приложений для Mac OS X, iOS, Android, Linux, конкурирует с DirectX для Windows. OpenGL используют не только для разработки игр, но и для приложений, работающих с 3D-графикой: СAD-системы, 3D редакторы, обучающие ПО, медицинские системы.
Свою популярность библиотека получила не только из-за кросс-платформенности, но и из-за возможности использования в проектах на различных языках программирования. И самое главное, OpenGL поддерживается производителями видеокарт, благодаря чему библиотека получает аппаратное ускорение видеокарт. Когда я только начинал работать с OpenGL, у меня возник вопрос: Как установить OpenGL? Где взять дистрибутив? Ответ прост: OpenGL устанавливается вместе с драйверами видеокарты, так как и список расширений и различные константы разнятся в зависимости от видеокарты (правда, Windows может иметь встроенный OpenGL старой версии без поддержки аппаратного обеспечения).
Для использования OpenGL, конечно, необходимо иметь библиотеку для линковки (gl.lib, glu.lib) и заголовочный файлы. Они входят в Visual Studio, но и их не трудно найти в Интернете.
Какие именно возможности вам предоставит OpenGL?
OpenGL предоставляет средства для рендера 3D сцены. OpenGL также позволяет гибко настраивать отрисовку. OpenGL поддерживает аппаратное ускорение, так как большинство команд выполняется на видеокарте.
Какими знаниями необходимо обладать?
Если вы захотели изучить OpenGL, то скорее всего, вы владеете языком программирования, если нет, то вам необходимо начать с этого.
Так как при работе с OpenGL, вы будете работать с графикой, вам необходимо понимать базовые понятия компьютерной графики, например: пиксель, хранение цвета в формате RGBA, принцип хранения изображений, альфа смешивание, графические примитивы и другие.
Позиционирование объектов — серьёзный вопрос, связанный с 3D сценами. Объекты позиционируются с помощью аффинных преобразований: перемещение, вращение и увеличение размера. С математической точки зрения эти преобразования записываются соответствующими матрицами. Если вы плохо разбираетесь в этом, то обязательно почитайте специализированную литературу, посвященную данному вопросу.
С чего начать?
Начать изучение я бы посоветовал с хорошей книги, при этом постоянно практикуясь. На русском языке не так много хороших книг, так как большинство из них уже устарели. Я бы посоветовал следующую книгу: «OpenGL. Руководство по программированию». При начальном обучении книга может показаться довольно сложной. Параллельно с ней можно изучать статьи с примерами http://nehe.gamedev.net (перевод статей на русский http://pmg.org.ru/nehe).
Для работы с окном можно использовать glut или взять инициализацию окна из готового примера. Небольшой совет, если вы будете брать код из готовых примеров, то изучите каждую функцию, так как простое копирование не поможет вам выучить OpenGL.
Начать программировать лучше с простых примеров, например, попробуйте текстурирование, модель освещения, виды смешивания. Это необходимо, чтобы полностью разобраться как работает та или иная техника. Только когда получите необходимый опыт в использовании базовых функций, вы можете начинать создавать сложные примеры.
Об устаревших и современных методах и расширениях
Стоит отметить, что OpenGL уже развивается на протяжении 10 лет. В течение этого времени многие функции успели устареть, а новые появлялись. Доступ к большинству современных функций производится через расширения, то есть вам необходимо запросить указатель на функции (wglGetProcAddress).
Когда вы начнёте читать книгу, то обнаружите, что в первых примерах объекты (треугольники или квадраты) вы будете строить по точкам с помощью функции glVertex*, первое освещение вы будете включать, используя glLight, задавать цвет материала функцией glMaterialf. Многие из них не используются в современных проектах. Например, вершины объектов не задаются по одной, а передаются все вместе с помощью Vertex Buffer Object (VBO). Это намного быстрее, так как вершины необходимо передать на видеокарту и намного быстрее передать все сразу, чем по одной. Стандартное освещение тоже не используется, все реализуется в шейдерах. Другими словами, объекты всегда отрисовываются с помощью шейдеров. В шейдеры передаются все необходимые параметры: освещение, материалы, матрицы. И когда вы освоите азы, вам необходимо переключаться на изучение шейдеров (GLSL).
Изучая каждую функцию OpenGL, стоит задуматься как вызов этой функции выполняется и сколько времени он может занять. Например, если вы создаёте и загружаете данные текстуры в видеопамять, это значит что пока все данные не будут пересланы программа дальше не будет выполняться. Многие операции могут выполняться параллельно, то есть вы даёте команду видеокарте и она её выполняет, и ваша программа исполняется дальше.
На будущее
Когда вы освоите OpenGL вас могут заинтересовать вопросы работы видеокарты, эффективная реализация тех или иных эффектов. Для этого могу вам порекомендовать:
Народ,с чего стоит начать практику после изучения OpenGL и GLSL?
С недавних пор изучаю изучаю OpenGL по книге Red Book,а затем планирую и glsl освоить.Что бы вы посоветовали написать в качестве начала практики? Пока познакомился с 2д,3д,освещением,работой с цветом,цветовым наложением и антиалиасингом.Что нужно ещё знать в будущем для создания рендер движка под игру? Ибо я вкурсах про то,что мне понадобится ещё изучить и кватернионы для вращения камеры персонажа.
#1
1:21, 11 июля 2021
вопрос и простой и сложный. ответ будет и просто и сложный.
изучать ничего не надо, ты не изучаешь ты просто читаешь документацию и делаешь. да самое полезное чему надо научиться искать в доках нужные функции и использовать именно их.
для вращения камеры надо привыкнуть представлять 3д пространсво и 3хмерную точку в нём. а кватерниону нужны если будешь делать скелетную анимацию для перемножения поворотов всех костей в их очерёдности.
а что нужно. придумать не большую игру без скелетной анимации и физики. и уже учится представлять какие структуры и сущьности будут в игре их представление для цпу и их 3д форма для гпу. игровой цикл обновление сущностей
такие штуки.
#2
1:42, 11 июля 2021
Первое что нужно решить — что ты хочешь — писать игру или очередной никому не нужный движок, который ты забросишь.
- std::variant
- Постоялец
#3
2:05, 11 июля 2021
EnderGames
> Что нужно ещё знать в будущем для создания рендер движка под игру?
На мой взгляд, если ты не написал ни одной 2D игрушки, самой примитивной,
то за создание рендер движка браться не стоит. 🙂
Напиши свою Змейку, Сапёр, Пятнашки, Линии, Тетрис и т.д. Какой-нибудь примитивный платформер.
Разберись со спрайтами и тайлами, а уж потом приступай к управлению персонажами.
Ну и не зная шаблонов проектирования, едва ли что-то вообще сможешь написать.
#4
3:37, 11 июля 2021
EnderGames
> Что нужно ещё знать в будущем для создания рендер движка под игру?
Нужно чувствовать вес каждой строчки кода по времени выполнения и уметь преждевременно оптимизировать код.
#5
3:38, 11 июля 2021
EnderGames
Возьми Unity и учись там писать шейдеры и изучать базовые принципы линейной алгебры, самопальный движок путь в никуда
#6
4:19, 11 июля 2021
EnderGames
> Что нужно ещё знать в будущем для создания рендер движка под игру?
Знать ничего не надо — все узнаешь и выучишь по ходу написания движка. Что гораздо важнее — надо иметь очень большое желание это делать и кайфовать от процесса, иначе перегоришь и забросишь, как это поисходит в 99.9% случаях (поэтому тебе и пишут что ничего не выйдет). Если не чувствуешь фанатизма, а хочешь быстрого результата — лучше сразу бери Юнити или Анрил.
Все сложнее на несколько порядков чем ты думаешь, узнавать придется гораздо больше чем ты думаешь и займет гораздо больше времени чем ты думаешь — лет пять минимум при удачном раскладе. Готов к такому?
- EnderGames
- Пользователь
#7
6:22, 11 июля 2021
У меня а арсенале только плюсы,а в готовые движки не полезу.Хочется самопальный движок в виде того самого API склепать,либо на самом опенгл,openal,bullet игру склепать.
#8
6:46, 11 июля 2021
если есть желание значит так тому и быть.
- EnderGames
- Пользователь
#9
10:20, 11 июля 2021
ИПавлов
Хочу после освоения всего нужного первым деом написать рендер простой сцены из blender с освещением и возможностью по ней перемещаться,ну и может добавлю тот самый туман для разнообразия ещё.
#10
10:49, 11 июля 2021
EnderGames
> Хочу после освоения всего нужного первым деом написать рендер простой сцены из
> blender с освещением и возможностью по ней перемещаться
Да, сделай когда-нибудь с сыном, когда сын будет.
#11
11:44, 11 июля 2021
ну давай для начала полностью сцену из руководства доделаешь. обычно вконце создаётся куб с текстурой и с управлением клавой и мышью ты летаешь по сцене. как сделаешь это уже там будет видно что сложно, а что нет.
#12
13:50, 11 июля 2021
EnderGames, ну возьми книжку, по OpenGL на русском. Там даже были те, что движки самодельные описывают. Как самому написать Квейк.
И вперёд, тренируйся.
А вообще, пройди уроки NeHe. И полезно и позновательно.
#13
15:00, 11 июля 2021
EnderGames
После OpenGL Bible ты уже должен смочь взять GPU Gems и уже кодить вещи оттуда.
- EnderGames
- Пользователь
#14
15:20, 11 июля 2021
Mirrel
OpenGL Red Book сейчас у меня.
Опыт изучения OpenGL — Часть 4 — Класс engine::ProgramBase
Я продолжаю рассказывать о написанном мною рисовательном API на базе OpenGL, размещенном в открытом репозитории на хостинге BitBucket. К сожалению, написание программы с помощью OpenGL требует существенной предварительной артподготовки в виде
- Загрузки функций OpenGL (инициализации библиотеки GLEW)
- Создания окна
- Создания контекста OpenGL
В сегодняшней заметке я освещу класс engine::ProgramBase, который собирает все перечисленные этапы воедино, и в следующих заметках перейду непосредственно к рисованию.
Задачи класса ProgramBase
Все программы, которые что-то рисуют при помощи OpenGL, как оказалось, имеют ряд общих черт. В объектно-ориентированном программировании, если у нескольких классов есть нечто общее, то это общее принято помещать в базовый класс. Таким образом класс ProgramBase в моем проекте служит базовым классом для всевозможных программ, которые что-то рисуют в окне. И вот ряд общих задач, которые всем этим программам приходится решать и которые вынесены в класс ProgramBase:
- Проинициализировать библиотеку GLEW
- Создать окно
- Создать контекст OpenGL
- Реализовать цикл обработки сообщений
- Реализовать измерение времени и отрисовку графики в окне
- Реализовать обработчики некоторых событий окна
Инициализация GLEW, создание окна и контекста OpenGL
class ProgramInitializer
{
protected :
ProgramInitializer ( )
{
opengl :: Initialize_GLEW_Library ( ) ;
std :: cout }
} ;
class ProgramBase : private ProgramInitializer
{
private :
win :: Window m_Window ;
opengl :: OpenGLWindow m_GLWindow ;
public :
ProgramBase ( ) :
m_Window ( «Test Window» , 1280 , 720 ) ,
m_GLWindow ( m_Window. getDeviceContext ( ) , 4 , 3 )
{ }
}
Тут нужно понимать последовательность действий программы при вызове конструктора класса, у которого есть базовый класс. Сначала вызывается конструктор базового класса. Затем инициализируются поля производного класса, причем в том порядке, в котором они объявлены в теле класса, а не в том порядке, в котором они перечислены в списке инициализации (чтобы избежать сомнений, рекомендуется делать тот и другой порядок одинаковыми). И наконец, выполняется тело конструктора производного класса. Вот почему я был вынужден создать базовый класс ProgramInitializer для класса ProgramBase — мне нужно было, чтобы инициализация GLEW выполнилась раньше, чем создание объекта OpenGLWindow m_GLWindow.
Цикл обработки сообщений
Цикл обработки сообщений вы уже видели в заметке про окно. Но нелишне будет привести его здесь еще раз. Цикл находится в методе ProgramBase::Main(), который вызывается из функции main нашей программы.
class ProgramBase
{
public :
void Main ( )
{
m_Window. Show ( ) ;
Тут появляются некоторые дополнительные детали. Во-первых перед тем, как запустить цикл обработки сообщений, мы отображаем окно на экране ( m_Window. Show ( ) ). Во-вторых в цикле мы не только обрабатываем сообщения. Функция PeekMessage возвращает ненулевое значение (TRUE), если в очереди есть сообщение, и нулевое (FALSE) — в противном случае. Так вот: если сообщений в очереди нет и обрабатывать нам нечего, то мы обновляем состояние программы и перерисовываем изображение на экране ( UpdateTimeAndRender ( ) ). Что значит «обновляем состояние программы»? В компьютерных играх обычно существует понятие «время»: даже если пользователь ничего не делает (и поэтому не генерирует никаких сообщений), время в игре все равно течет: NPC (non-player characters) занимаются своими делами, деревья и трава раскачиваются от ветра, вода течет и прочее. Тут и возникает проблема измерения времени.
Измерение времени
Об измерении времени я прочитал в книге [Jason Gregory — Game Engine Architecture, 7.5.3. Measuring Real Time with a High-Resolution Timer]. В центральном процессоре есть регистр, предназначенный для измерения времени, содержимое которого инкрементируется каждый период тактового сигнала. Операционная система предоставляет API для чтения этого регистра и определения времени по его содержимому. WinAPI предоставляет две простые функции: QueryPerformanceCounter возвращает число тактов с момента запуска ОС (т. е. значение того самого регистра), а QueryPerformanceFrequency возвращает число тактов в секунду. Разделив одно на другое, получим число секунд с момента запуска ОС. Чтобы сделать работу с этими функциями чуть более удобной, я сделал аналогичные две функции, которые поместил в пространство имен под названием win::HighResolutionTimer:
namespace win { namespace HighResolutionTimer
{
// Gets the number of ticks passed since the start of the operation system.
static uint64_t getTicks ( )
{
LARGE_INTEGER ticks ;
if ( QueryPerformanceCounter ( & ticks ) )
return ticks. QuadPart ;
else
throw make_winapi_error ( «HighResolutionTimer::getTicks() -> QueryPerformanceCounter()» ) ;
}
// Gets the number of ticks per second.
static uint64_t getTicksPerSecond ( )
{
static uint64_t s_TicksPerSecond { 0 } ;
if ( s_TicksPerSecond == 0 )
{
LARGE_INTEGER freq ;
if ( QueryPerformanceFrequency ( & freq ) )
s_TicksPerSecond = freq. QuadPart ;
else
throw make_winapi_error ( «HighResolutionTimer::getTicksPerSecond() -> QueryPerformanceFrequency()» ) ;
}
return s_TicksPerSecond ;
}
} } ;
Представленные функции используют WinAPI. Альтернативный вариант — воспользоваться для измерения времени стандартной библиотекой chrono.
Оказалось, что для того, чтобы обновлять состояние программы с течением времени, мне достаточно знать две величины: текущее значение времени (например, с момента старта программы) и время прошедшее с последнего обновления состояния программы. Вычисление этих величин происходит в функции ProgramBase::UpdateTimeAndRender, а непосредственное обновление состояния программы — в функции ProgramBase::UpdateTime, которая в классе ProgramBase является чисто виртуальной и должна быть переопределена в производном классе.
class ProgramBase
{
private :
void UpdateTimeAndRender ( )
{
static uint64_t timeStart = win :: HighResolutionTimer :: getTicks ( ) ;
static uint64_t lastFrameTime = 0 ;
uint64_t currentTime = win :: HighResolutionTimer :: getTicks ( ) — timeStart ;
UpdateTime (
/*currentTime*/ currentTime / static_cast < double >( win :: HighResolutionTimer :: getTicksPerSecond ( ) ) ,
/*timeDelta*/ ( currentTime — lastFrameTime ) / static_cast < double >( win :: HighResolutionTimer :: getTicksPerSecond ( ) ) ) ;
protected :
/*
Updates the state of the program with time.
Parameters:
currentTime — time in seconds passed since the start of the program.
timeDelta — time in seconds passed since the last call to UpdateTime().
*/
virtual void UpdateTime ( double currentTime, double timeDelta ) = 0 ;
}
Обработка событий окна
Некоторые события окна обрабатываются более или менее одинаковым образом во всех вариантах программы, поэтому можно обработчики этих событий реализовать в классе ProgramBase. Это события необходимости перерисовки (PaintEvent), нажатия-отпускания клавиш (KeyDownEvent, KeyUpEvent), движения мыши (MouseMoveEvent), изменения размера окна (SizeChangedEvent). Подписку на эти события я вынес в отдельный метод InitEventHandlers, который вызывается в конструкторе класса ProgramBase. Также есть событие OpenGLWindow::RenderEvent, на которое обязательно необходимо подписаться.
class ProgramBase
{
public :
ProgramBase ( ) :
.
{
.
InitEventHandlers ( ) ;
.
}
private :
void InitEventHandlers ( )
{
m_Window. PaintEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnPaint ) ;
m_Window. MouseMoveEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnMouseMove ) ;
m_Window. KeyDownEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnKeyDown ) ;
m_Window. KeyUpEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnKeyUp ) ;
m_Window. SizeChangedEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnSizeChanged ) ;
m_GLWindow. RenderEvent ( ) + = fastdelegate :: MakeDelegate ( this , & ProgramBase :: OnRender ) ;
}
void OnMouseMove ( std :: tuple < win :: Window * , int , int , WPARAM >eventArgs ) ;
void OnKeyDown ( std :: tuple < win :: Window * , WPARAM >eventArgs ) ;
void OnKeyUp ( std :: tuple < win :: Window * , WPARAM >eventArgs ) ;
void OnPaint ( std :: tuple < win :: Window * , HDC >eventArgs ) { UpdateTimeAndRender ( ) ; }
void OnSizeChanged ( std :: tuple < win :: Window * , int , int >eventArgs ) ;
virtual void OnRender ( opengl :: OpenGLWindow * sender ) = 0 ;
}
Обработчики событий клавиатуры и мыши занимаются тем, что изменяют позицию в пространстве и направление взгляда камеры. Понятие камеры конечно же имеет смысл только в трехмерном мире, мы обсудим что это такое в последующих заметках. Обработчик события SizeChangedEvent тоже имеет прямое отношение к камере — он устанавливает так называемый viewport, об этом тоже в следующих заметках. Обработчик события PaintEvent просто вызывает метод UpdateTimeAndRender. И наконец, обработчик события OpenGLWindow::RenderEvent является чисто виртуальной функцией и должен быть переопределен в производном классе, поскольку только производный класс «знает» что и как рисовать на экране.
В следующей заметке мы наконец займемся рисованием при помощи функций OpenGL и начнем с рисования цветного треугольника (с чего обычно начинаются все книжки по OpenGL).
Learn OpenGL. Урок 7.1 – Отладка
Графическое программирование — не только источник веселья, но еще и фрустрации, когда что-либо не отображается так, как задумывалось, или вообще на экране ничего нет. Видя, что большая часть того, что мы делаем, связана с манипулированием пикселями, может быть трудно выяснить причину ошибки, когда что-то работает не так, как полагается. Отладка такого вида ошибок сложнее, чем отладка ошибок на CPU. У нас нет консоли, в которую мы могли бы вывести текст, мы не можем поставить точку останова в шейдере и мы не можем просто взять и проверить состояние программы на GPU.
В этом уроке мы познакомимся с некоторыми методами и приемами отладки вашей OpenGL-программы. Отладка в OpenGL не так сложна, и изучение некоторых приемов обязательно окупится.
Содержание
Часть 1. Начало
Часть 2. Базовое освещение
- Цвета
- Основы освещения
- Материалы
- Текстурные карты
- Источники света
- Несколько источников освещения
Часть 3. Загрузка 3D-моделей
- Библиотека Assimp
- Класс полигональной сетки Mesh
- Класс 3D-модели
Часть 4. Продвинутые возможности OpenGL
- Тест глубины
- Тест трафарета
- Смешивание цветов
- Отсечение граней
- Кадровый буфер
- Кубические карты
- Продвинутая работа с данными
- Продвинутый GLSL
- Геометрический шейдер
- Инстансинг
- Сглаживание
Часть 5. Продвинутое освещение
- Продвинутое освещение. Модель Блинна-Фонга
- Гамма-коррекция
- Карты теней
- Всенаправленные карты теней
- Normal Mapping
- Parallax Mapping
- HDR
- Bloom
- Отложенный рендеринг
- SSAO
Часть 6. PBR
- Теория
- Аналитические источники света
- IBL. Диффузная облученность
- IBL. Зеркальная облученность
Часть 7. Практика
glGetError()
Когда вы некорректно используете OpenGL (к примеру, когда настраиваете буфер, забыв его связать (to bind)), OpenGL заметит и создаст один или несколько пользовательских флагов ошибок за кулисами. Мы можем эти ошибки отследить, вызывая функцию glGetError() , которая просто проверяет выставленные флаги ошибок и возвращает значение ошибки, если случились ошибки.
GLenum glGetError();
Эта функция возвращает флаг ошибки или вообще никакую ошибку. Список возвращаемых значений:
Флаг | Код | Описание |
---|---|---|
GL_NO_ERROR | 0 | Никакой ошибки не сгенерировано после последнего вызова glGetError |
GL_INVALID_ENUM | 1280 | Установлено, когда параметр перечисления недопустим |
GL_INVALID_VALUE | 1281 | Установлено, когда значение недопустимо |
GL_INVALID_OPERATION | 1282 | Установлено, когда команда с заданными параметрами недопустима |
GL_STACK_OVERFLOW | 1283 | Установлено, когда операция проталкивания данных в стек (push) вызывает переполнение стека. |
GL_STACK_UNDERFLOW | 1284 | Установлено, когда операция выталкивания данных из стека (pop) происходит с наименьшей точки стека. |
GL_OUT_OF_MEMORY | 1285 | Установлено, когда операция выделения памяти не может выделить достаточное количество памяти |
GL_INVALID_FRAMEBUFFER_OPERATION | 1286 | Установлено, когда происходит чтение/запись в/из буфер кадра (framebuffer), который не завершен |
Внутри документации к функциям OpenGL вы можете найти коды ошибок, которые генерируются функциями, некорректно используемыми. К примеру, если вы посмотрите на документацию к функции glBindTexture() , то вы сможете найти коды ошибок, генерируемые этой функцией, в разделе «Ошибки» (Errors).
Когда флаг ошибки установлен, никаких других флагов ошибки сгенерировано не будет. Более того, когда glGetError вызывается, функция стирает все флаги ошибок (или только один на распределенной системе, см. ниже). Это значит, что если вы вызываете glGetError один раз после каждого кадра и получаете ошибку, это не значит, что это — единственная ошибка и еще вы не знаете, где произошла эта ошибка.
Заметьте, что когда OpenGL работает распределенно, как это часто бывает на системах с X11, другие ошибки могут генерироваться, пока у них различные коды. Вызов glGetError тогда просто сбрасывает только один из флагов кодов ошибки вместо всех. Из-за этого и рекомендуют вызывать эту функцию в цикле.
glBindTexture(GL_TEXTURE_2D, tex); std::cout
Отличительной особенностью glGetError является то, что она позволяет относительно легко определить, где может быть любая ошибка, и проверить правильность использования OpenGL. Скажем, что у вас ничего не отрисовывается, и вы не знаете, в чем причина: неправильно установленный кадровый буфер? Забыл установить текстуру? Вызывая glGetError везде, вы сможете быстро понять, где возникает первая ошибка.
По умолчанию, glGetError сообщает только номер ошибки, который нелегко понять, пока вы не заучиваете номера кодов. Часто имеет смысл написать небольшую функцию, помогающую напечатать строку с ошибкой вместе с местом, откуда вызывается функция.
GLenum glCheckError_(const char *file, int line) < GLenum errorCode; while ((errorCode = glGetError()) != GL_NO_ERROR) < std::string error; switch (errorCode) < case GL_INVALID_ENUM: error = "INVALID_ENUM"; break; case GL_INVALID_VALUE: error = "INVALID_VALUE"; break; case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break; case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break; case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break; case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break; case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break; >std::cout return errorCode; > #define glCheckError() glCheckError_(__FILE__, __LINE__)
Если вы решите сделать больше вызовов glCheckError , будет полезно знать в каком месте произошла ошибка.
glBindBuffer(GL_VERTEX_ARRAY, vbo); glCheckError();
Осталась одна важная вещь: в GLEW есть давний баг: glewInit() всегда выставляет флаг GL_INVALID_ENUM . Чтобы это исправить, просто вызывайте glGetError после glewInit чтобы сбросить флаг:
glewInit(); glGetError();
glGetError не сильно помогает, поскольку возвращаемая информация относительно проста, но часто помогает отловить опечатки или отловить место возникновения ошибки. Это простой, но эффективный инструмент для отладки.
Отладочный вывод
Инструмент менее известный, но полезнее, чем glCheckError — расширение OpenGL "debug output" (Отладочный вывод), вошедшее в OpenGL 4.3 Core Profile. С этим расширением OpenGL отошлет сообщение об ошибке пользователю с деталями ошибки. Это расширение не только выдает больше информации, но и позволяет отловить ошибки там, где они возникают, используя отладчик.
Отладочный вывод входит в OpenGL начиная с версии 4.3, что означает, что вы найдете эту функциональность на любой машине, поддерживающей OpenGL 4.3 и выше. Если такая версия недоступна, то можно проверить расширения ARB_debug_output и AMD_debug_output . Также есть непроверенная информация о том, что отладочный вывод не поддерживается на OS X (автор оригинала и переводчик не тестировали, прошу сообщать автору оригинала или мне в личные сообщения через механизм исправления ошибок, если найдете подтверждение или опровержение данного факта; UPD: Jeka178RUS проверил этот факт: из коробки отладочный вывод не работает, через расширения он не проверял).
Чтобы начать использовать отладочный вывод, нам надо у OpenGL запросить отладочный контекст во время инициализационного процесса. Этот процесс отличается на разных оконных системах, но здесь мы обсудим только GLFW, но в конце статьи в разделе "Дополнительные материалы" вы можете найти информацию насчет других оконных систем.
Отладочный вывод в GLFW
Запросить отладочный контекст в GLFW на удивление просто: нужно всего лишь дать подсказку GLFW, что мы хотим контекст с поддержкой отладочного вывода. Нам надо сделать это до вызова glfwCreateWindow :
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
Как только мы проинициализировали GLFW, у нас должен появиться отладочный контекст, если мы используем OpenGL 4.3 или выше, иначе нам надо попытать удачу и надеяться на то, что система все еще может создать отладочный контекст. В случае неудачи нам надо запросить отладочный вывод через механизм расширений OpenGL.
Отладочный контекст OpenGL бывает медленнее, чем обычный, так что во время работ над оптимизациями или перед релизом следует убрать или закомментировать эту строчку.
Чтобы проверить результат инициализации отладочного контекста, достаточно выполнить следующий код:
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) < // успешно >else < // не получилось >
Как работает отладочный вывод? Мы передаем callback-функцию обработчик сообщений в OpenGL (похоже на callback'и в GLFW) и в этой функции мы можем обрабатывать данные OpenGL как нам угодно, в нашем случае — отправка полезных сообщений об ошибках на консоль. Прототип этой функции:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam);
Заметьте, что на некоторых операционных системах тип последнего параметра может быть const void* .
Учитывая большой набор данных, которыми мы располагаем, мы можем создать полезный инструмент печати ошибок, как показано ниже:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam) < // ignore non-significant error/warning codes if(id == 131169 || 131185 || 131218 || 131204) return; std::cout std::cout std::cout std::cout
Когда расширение определяет ошибку OpenGL, оно вызовет эту функцию и мы сможем печатать огромное количество информации об ошибке. Заметьте, мы проигнорировали некоторые ошибки, так как они бесполезны (к примеру, 131185 в драйверах NVidia говорит о том, что буфер успешно создан).
Теперь, когда у нас есть нужный callback, самое время инициализировать отладочный вывод:
if (flags & GL_CONTEXT_FLAG_DEBUG_BIT)
Так мы сообщаем OpenGL, что хотим включить отладочный вывод. Вызов glEnable(GL_DEBUG_SYNCRHONOUS) говорит OpenGL, что мы хотим сообщение об ошибке в тот момент, когда только она произошла.
Фильтрация отладочного вывода
С функцией glDebugMessageControl вы можете выбрать типы ошибок, которые хотите получать. В нашем случае мы получаем все виды ошибок. Если бы мы хотели только ошибки OpenGL API, типа Error и уровня значимости High, мы бы написали следующий код:
glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_HIGH, 0, nullptr, GL_TRUE);
С такой конфигурацией и отладочным контекстом каждая неверная команда OpenGL будет отправлять много полезной информации:
Находим источник ошибки через стек вызовов
Еще один трюк с отладочным выводом заключается в том, что вы можете относительно просто установить точное место возникновения ошибки в вашем коде. Устанавливая точку останова в функции DebugOutput на нужном типе ошибки (или в начале функции если вы хотите отловить все ошибки) отладчик отловит ошибку и вы сможете переместиться по стеку вызовов, чтобы узнать, где произошла ошибка:
Это требует некоторого ручного вмешательства, но если вы примерно знаете, что ищете, невероятно полезно быстро определить, какой вызов вызывает ошибку.
Свои ошибки
Наряду с чтением ошибок, мы можем их посылать в систему отладочного вывода с помощью glDebugMessageInsert :
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
Это очень полезно, если вы подключаетесь к другому приложению или к коду OpenGL, который использует отладочный контекст. Другие разработчики смогут быстро выяснить любую сообщенную ошибку, которая происходит в вашем пользовательском коде OpenGL.
В общем, отладочный вывод (если доступен) очень полезен для быстрого отлова ошибок и определенно стоит потраченных усилий на настройку, так как экономит значительное время разработки. Вы можете найти копию исходного кода здесь с использованием glGetError и отладочного вывода. Есть ошибки, попробуйте их исправить.
Отладочный вывод шейдера
Когда дело доходит до GLSL, у нас нет доступа к функции типа glGetError или возможности пройтись по коду по шагам в отладчике. Когда вы встречаетесь с черным экраном или совершенно неправильным отображением, бывает очень сложно понять, что происходит, если проблема в шейдере. Да, ошибки компиляции сообщают о синтаксических ошибках, но отлов семантических ошибок — та еще песня.
Один из часто используемых приемов для выяснения того, что не так с шейдером, состоит в том, чтобы отправить все соответствующие переменные в шейдерной программе непосредственно в выходной канал фрагментного шейдера. Выводя шейдерные переменные напрямую в выходной канал с цветом мы можем узнать интересную информацию проверяя картинку на выходе. К примеру, нам надо узнать, правильные ли нормали у модели. Мы можем отправить их (трансформированными или нет) из вершинного в фрагментный шейдер, где мы выведем нормали как-то так:
(прим. пер: почему нет подсветки синтаксиса GLSL?)
#version 330 core out vec4 FragColor; in vec3 Normal; [. ] void main()
Выводя нецветовую переменную в выходной канал с цветом как сейчас, мы можем быстро проверить значение переменной. Если, к примеру, результатом стал черный экран, то ясно, что нормали неправильно переданы в шейдеры, а когда они отображаются, сравнительно легко проверить их на правильность:
Из визуальных результатов мы можем видеть, что нормали верны, так как правая сторона костюма преимущественно красная (что говорит, что нормали примерно показывают в направлении полощительной оси x) и также передняя сторона костюма окрашена в направлении положительной оси z (в синий цвет).
Этот подход можно расширить на любую переменную, которую вы хотите протестировать. Каждый раз, когда вы застряли и предполагаете, что ошибка в шейдерах, попробуйте отрисовывать несколько переменных или промежуточных результатов и выяснить, в какой части алгоритма есть ошибка.
OpenGL GLSL reference compiler
В каждом видеодрайвере свои причуды. К примеру, драйвера NVIDIA немного смягчают требования спецификации, а драйвера AMD лучше соответствую спецификациям (что лучше, как мне кажется). Проблема в том, что шейдеры работающие на одной машине, могут не заработать на другой из-за отличий в драйверах.
За несколько лет опыта вы могли выучить все отличия между различными GPU, но если вы хотите быть уверены в том, что ваши шейдеры будут работать везде, то вы можете сверить ваш код с официальной спецификацией с помощью GLSL reference compiler. Вы можете скачать так называемый GLSL lang validator тут (исходник).
С этой программой вы можете проверить свои шейдеры, передавая их как 1-й аргумент к программе. Помните, что программа определяет тип шейдера по расширению:
- .vert : вершинный шейдер
- .frag : фрагментный шейдер
- .geom : геометрический шейдер
- .tesc : тесселяционный контролирующий шейдер
- .tese : тесселяционный вычислительный шейдер
- .comp : вычислительный шейдер
Запустить программу легко:
glslangValidator shader.vert
Заметьте, что если нет ошибок, то программа ничего не выведет. На сломанном вершинном шейдере вывод будет похож на:
Программа не покажет различий между компиляторами GLSL от AMD, NVidia или Intel, и даже не может сообщить обо всех багах в шейдере, но он хотя бы проверяет шейдеры на соответствие стандартам.
Вывод буфера кадра
Еще один метод для вашего инструментария — отобразить содержимое кадрового буфера в определенной части экрана. Скорее всего, вы часто используете кадровые буферы и, поскольку вся магия происходит за кадром, бывает трудно определить, что происходит. Вывод содержимого кадрового буфера — полезный прием, чтобы проверить правильность вещей.
Заметьте, что содержимое кадрового буфера, как тут объясняется, работает с текстурами, а не с объектами буферов отрисовки
Используя простой шейдер, который отрисовывает одну текстуру, мы можем написать небольшую функцию, быстро отрисовывающую любую текстуру в правом верхнем углу экрана:
// vertex shader #version 330 core layout (location = 0) in vec2 position; layout (location = 1) in vec2 texCoords; out vec2 TexCoords; void main()
//fragment shader #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D fboAttachment; void main()
//main.cpp void DisplayFramebufferTexture(GLuint textureID) < if(!notInitialized) < // initialize shader and vao w/ NDC vertex coordinates at top-right of the screen [. ] >glActiveTexture(GL_TEXTURE0); glUseProgram(shaderDisplayFBOOutput); glBindTexture(GL_TEXTURE_2D, textureID); glBindVertexArray(vaoDebugTexturedRect); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); glUseProgram(0); > int main() < [. ] while (!glfwWindowShouldClose(window)) < [. ] DisplayFramebufferTexture(fboAttachment0); glfwSwapBuffers(window); >>
Это даст вам небольшое окошко в углу экрана для отладочного вывода кадрового буфера. Полезно, к примеру, когда пытаешься проверить корректность нормалей:
Вы также можете расширить эту функцию так, чтобы она отрисовывала больше 1 текстуры. Это быстрый путь получить непрерывную отдачу от чего угодно в кадровых буферах.
Внешние программы-отладчики
Когда ничего не помогает, есть еще один прием: воспользоваться сторонними программами. Они встраиваются в драйвера OpenGL и могут перехватывать все вызовы OpenGL, чтобы дать вам очень много интересных данных о вашем приложении. Эти приложения могут профилировать использование функций OpenGL, искать узкие места, наблюдать за кадровыми буферами, текстурами и памятью. Во время работы над (большим) кодом, эти инструменты могут стать бесценными.
Я перечислил несколько популярных инструментов. Попробуйте каждый и выберите тот, который лучше всего вам подходит.
RenderDoc
RenderDoc — хороший (полностью опенсорсный) отдельный отладочный инструмент. Чтобы начать захват, выберите исполняемый файл и рабочую папку (working directory). Ваше приложение работает как обычно, и когда вы хотите понаблюдать за отдельным кадром, вы позволяете RenderDoc снять несколько кадров вашего приложения. Среди захваченных кадров вы можете просмотреть состояние конвейера, все команды OpenGL, хранилище буферов и используемые текстуры.
CodeXL
CodeXL — инструмент отладки GPU, работает как отдельное приложение и плагин к Visual Studio. CodeXL Дает много информации и отлично подходит для профилирования графических приложений. CodeXL также работает на видеокартах от NVidia и Intel, но без поддержки отладки OpenCL.
Я не так много использовал CodeXL, поскольку RenderDoc мне показался проще, но я включил CodeXL в этот список, потому что он выглядит довольно надежным инструментом и в основном разработан одним из крупных производителей графических процессоров.
NVIDIA Nsight
Nsight — популярный инструмент отладки GPU от NUIDIA. Является не только плагином к Visual Studio и Eclipse, но еще и отдельное приложение. Плагин Nsight — очень полезная вещь для графических разработчиков, поскольку собирает много статистик в реальном времени относительно использования GPU и покадрового состояния GPU.
В тот момент, когда вы запускаете свое приложение через Visual Studio или Eclipse с помощью команд отладки или профилирования Nsight, он запустится сам внутри приложения. Хорошая вещь в Nsight: рендер ГИП-системы (GUI, графический интерфейс пользователя) поверх запускаемого приложения, которую можно использовать для собирания информации всех видов о вашем приложении в реальном времени или покадровом анализе.
Nsight — очень полезный инструмент, который, по моему мнению, превосходит вышеперечисленные инструменты, но имеет один серьезный недостаток: работает только на видеокартах от NVIDIA. Если вы работаете на видеокартах от NVIDIA и используете Visual Studio — определенно стоит попробовать Nsight.
Я уверен, что есть еще инструменты для отладки графических приложений (к примеру, VOGL и APItrace), но я считаю, что этот список уже предоставил вам достаточно инструментов для экспериментов. Я не эксперт в вышеупомянутых инструментах, так что если есть ошибки, то пишите мне (переводчику) в личные сообщения и в комментарии к оригинальной статье (если конечно же, там еще осталась эта ошибка).
Дополнительные материалы
- Почему я вижу черный экран? — список возможных случаев появления черного экрана вместо нужной картинки от Reto Koradi.
- Отладочный вывод — обширный список методов настройки отладочного контекста в разных оконных менеджерах от Vallentin Source.
P.S.: У нас есть телеграм-конфа для координации переводов. Если есть серьезное желание помогать с переводом, то милости просим!