В предыдущих двух заметках я рассказал о том, как загрузить функции OpenGL (проинициализировать библиотеку GLEW) и как создать окно, в котором мы будем рисовать. Но для рисования нам потребуется так называемый OpenGL context — «an object that holds all of OpenGL» — он уже упоминался в первой заметке. Повторим вкратце основные факты о контексте OpenGL:
- Контекст обладает состоянием. Его состояние определяет, что будет нарисовано на экране. Изменить (повлиять на) это состояние могут только функции API OpenGL. Т. е. контекст можно представлять как объект, экземпляр некоего класса, а функции API OpenGL — как методы этого класса.
- Контекст OpenGL в операционной системе Windows связан с т. н. контекстом устройства (device context), т. е. по сути — с окном. У каждого окна может быть свой контекст OpenGL, но можно сделать и так, чтобы у двух окон был один и тот же контекст OpenGL. При этом с каждым с конкретным окном в каждый момент времени может быть связан только один контекст OpenGL (с одним окном не может одновременно быть связано два контекста OpenGL).
- Существует понятие «текущий» или «активный в данный момент» контекст OpenGL — это тот контекст который в данный момент используется для рисования на экране и состояние которого изменяют функции API OpenGL. Чтобы сделать некоторый контекст активным, надо вызвать функцию wglMakeCurrent. Понятие «активный контекст» принадлежит потоку управления, т. е. каждый поток управления может иметь свой активный контекст OpenGL. Заметим, что нежелательно, чтобы два разных потока управления имели одинаковый активный контекст OpenGL.
- Контекст можно создавать и уничтожать (тут прослеживается аналогия с конструктором и деструктором в ООП). Причем создавать можно контексты, отличающиеся по своим характеристикам: требования к характеристикам создаваемого контекста указываются при его создании в одном из параметров функции wglCreateContextAttribsARB. Что за характеристики? Об этом будет сказано ниже.
В первой заметке мы уже создавали контекст OpenGL, но это был «фейковый» контекст, при помощи которого мы не собирались ничего рисовать. Он был нужен только для того, чтобы можно было загрузить функции API OpenGL (без контекста это сделать невозможно). Теперь мы создадим нормальный контекст OpenGL, при помощи которого можно будет рисовать на экране.
Литература
- Статья OpenGL Context рассказывает о том, что такое контекст OpenGL.
- Статья Creating an OpenGL Context (WGL) рассказывает о том, как создать контекст OpenGL
Последовательность действий для создания контекста OpenGL
Создание контекста OpenGL и хранение ссылки на него (его дескриптора) я поручил отдельному классу opengl::OpenGLWindow — о нем расскажу ниже.
Создание окна и получение для него дескриптора контекста устройства
Для создания контекста OpenGL нужен контекст устройства окна (функция wglCreateContextAttribsARB принимает его дескриптор в качестве параметра). Если у вас есть окно, то получить его контекст устройства можно, вызвав функцию GetDC. В моей программе за создание окна отвечает класс win::Window, описанный в предыдущей заметке. У него есть метод getDeviceContext, который возвращает дескриптор контекста устройства для окна. Дескриптор контекста устройства окна передается в конструктор класса opengl::OpenGLWindow в качестве параметра. Экземпляр класса opengl::OpenGLWindow я создаю для каждого окна, в котором собираюсь что-то рисовать при помощи OpenGL.
Установка для полученного контекста устройства пиксельного формата (pixel format) с заданными характеристиками
Итак, у нас есть контекст устройства. Перед тем, как создать для него контекст OpenGL, надо задать для контекста устройства так называемый пиксельный формат (pixel format). Пиксельный формат — это нечто, описывающее параметры фреймбуфера (framebuffer). Фреймбуфер — это область видеопамяти, в которую записывается изображение, которое отрисовывается на экране. Фреймбуфер состоит из нескольких частей. В самом упрощенном виде фреймбуфер можно мыслить себе состоящим только из одной части — массива пикселей (этот массив называется color buffer). Пиксель имеет определенный формат. Как правило он представляет собой три целых 8-битных числа, которые определяют интенсивность красного (R), зеленого (G) и синего (B) цветовых каналов. Может сюда добавляться и еще одно 8-битное число — т. н. канал прозрачности (A — alpha), который используется для создания эффекта прозрачности (смешивания — blending).
Помимо массива пикселей (color buffer) во фреймбуфере может быть еще несколько буферов. Самый очевидный пример — это буфер глубины (depth buffer), который используется при рисовании трехмерных сцен, но об этом позже. Еще составными частями фреймбуфера могут быть stencil buffer и sample buffer. Каждому мельчайшему кусочку изображения соответствует один элемент из color buffer (пиксель), один элемент из depth buffer, один — из stencil buffer и т. д. Этот мельчайший кусочек изображения в терминологии компьютерной графики называется фрагментом.
Пиксельный формат определяет сколько различных буферов находятся во фреймбуфере и каков формат их элементов. Еще пиксельный формат содержит такие таинственные параметры как «наличие аппаратного ускорения», «поддержка OpenGL» и другие. Полный список параметров (атрибутов) пиксельного формата можно увидеть здесь.
Установить пиксельный формат можно только один раз за все время существования окна, поменять его потом не удастся. В моей программе пиксельный формат устанавливает функция OpenGLWindow::SetPixelFormat, которая вызывается в конструкторе класса OpenGLWindow:
{
// Pixel format attributes array.
int pixAttribs[] =
{
WGL_SUPPORT_OPENGL_ARB, GL_TRUE, // nonzero value means "support OpenGL"
WGL_DRAW_TO_WINDOW_ARB, GL_TRUE, // true if the pixel format can be used with a window
WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, // hardware acceleration through ICD driver
WGL_DOUBLE_BUFFER_ARB, GL_TRUE, // nonzero value means "double buffering"
WGL_SAMPLE_BUFFERS_ARB, GL_TRUE, // support multisampling
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, // color mode (either WGL_TYPE_RGBA_ARB or WGL_TYPE_COLORINDEX_ARB)
WGL_COLOR_BITS_ARB, 32, // bits number in the color buffer for R, G and B channels
WGL_DEPTH_BITS_ARB, 24, // bits number in the depth buffer
WGL_STENCIL_BITS_ARB, 8, // bits number in the stencil buffer
WGL_SAMPLES_ARB, 8, // multisampling factor
0 // "end of array" symbol
};
int numFormats = 0;
int pixelFormat = -1;
// Find the most relevant pixel format for the specified attributes.
wglChoosePixelFormatARB(
deviceContext, // device context
&pixAttribs[0], // list of integer attributes
NULL, // list of float attributes
1, // the maximum number of pixel formats to be obtained
&pixelFormat, // [out] pointer to the array of pixel formats
(UINT*)&numFormats); // the number of appropriate pixel formats found
// Throw an exception if we couldn't find an appropriate pixel format.
if (numFormats == 0)
throw GlException("OpenGLWindow::SetPixelFormat() -> wglChoosePixelFormatARB()");
// Set pixel format for the window device context.
PIXELFORMATDESCRIPTOR pfd;
::SetPixelFormat(
deviceContext,
pixelFormat,
&pfd);
}
Параметры пиксельного формата задаются при помощи массива атрибутов. Этот массив представляет собой набор пар целых чисел. В каждой паре первое число идентифицирует какой-либо параметр, второе число задает значение этого параметра. Массив атрибутов передается функции wglChoosePixelFormatARB, которая ищет среди поддерживаемых контекстом устройства пиксельных форматов те, которые удовлетворяют этому набору атрибутов. Функция возвращает массив целочисленных идентификаторов найденных пиксельных форматов. Затем один из этих идентификаторов надо передать функции ::SetPixelFormat.
Создание контекста OpenGL с заданными характеристиками
Теперь, когда у нас есть контекст устройства и для него задан пиксельный формат, можно наконец создать непосредственно контекст OpenGL — это делает функция wglCreateContextAttribsARB. У контекста OpenGL, как и у пиксельного формата, есть некие атрибуты, например, требуемая версия OpenGL, профиль OpenGL (core или compatibility), должен ли контекст быть отладочным и другие. В моей программе непосредственное создание контекста выполняет функция OpenGLWindow::CreateRenderingContext:
HDC deviceContext,
GLint majorVersion,
GLint minorVersion)
{
// Set the version that we want.
GLint attribs[] =
{
WGL_CONTEXT_MAJOR_VERSION_ARB, majorVersion,
WGL_CONTEXT_MINOR_VERSION_ARB, minorVersion,
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, // core profile
WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, // compatible with future versions of OpenGL
#ifdef _DEBUG
WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB, // enhanced error message testing
#endif
0
};
// Create OpenGL rendering context.
m_hGLRC = wglCreateContextAttribsARB(
deviceContext,
0,
attribs);
// Make the OpenGL rendering context current.
wglMakeCurrent(deviceContext, m_hGLRC);
}
Как и при выборе пиксельного формата, массив атрибутов контекста OpenGL представляет собой набор пар целочисленных значений. В каждой паре первое значение — идентификатор атрибута, второе — значение атрибута.
Какую версию OpenGL следует запрашивать при создании контекста? Надо запрашивать минимальную версию, которая требуется вашей программе. Чем ниже версия, тем больший класс оборудования будет способен работать с вашей программой. Минимальная версия зависит от тех функций OpenGL, которые вы вызываете в своей программе.
Класс opengl::OpenGLWindow
Помимо создания контекста и хранения ссылки на него класс opengl::OpenGLWindow выполняет еще одну задачу — он помогает программе отрисовывать изображение на экране. Это происходит в методе OpenGLWindow::Render. Рисуют изображение на экране различные функции OpenGL (такие например как glDrawArrays). Но до и после их вызова необходимо проводить некоторую стандартную работу. Так, перед тем, как начать рисовать, надо сделать контекст активным (или убедиться, что он уже является активным). Непосредственно перед рисованием надо очистить фреймбуфер (об этом позже), а непосредственно после рисования надо swap’нуть буферы (если используется двойная буферизация — об этом тоже поговорим позже). Вот эту стандартную работу и выполняет метод OpenGLWindow::Render. Но он ведь не знает как отрисовывать изображение, поэтому вместо вызова «рисовательных» функций OpenGL, он генерирует событие RenderEvent, а уж тот, кто на него подпишется, и должен будет непосредственно рисовать изображение. Ниже показан фрагмент кода класса OpenGLWindow, который относится к методу Render:
{
public:
using RENDER_FUNC_DELEGATE = util::Event<OpenGLWindow*>;
private:
RENDER_FUNC_DELEGATE m_RenderEvent;
public:
RENDER_FUNC_DELEGATE& RenderEvent() { return m_RenderEvent; }
void Render()
{
wglMakeCurrent(m_DeviceContext, m_hGLRC); // make OpenGL context current
opengl::ClearAllBuffers(); // this utility function clears the framebuffer
m_RenderEvent(this); // generate RenderEvent (where actual drawing takes place)
SwapBuffers(m_DeviceContext); // swap back and front buffers
}
}
Кто подписывается на событие RenderEvent? В моей программе это делает класс engine::ProgramBase, который и создает как объект win::Window, так и объект opengl::OpenGLWindow. Классу ProgramBase наследует класс Program, который создает все графические объекты и умеет их отрисовывать. Об этих классах мы и поговорим подробнее в следующей заметке.