Инициализация OpenGL

29.01.2000


Определения

Рендеринг
— в переводе с английского "исполнение". В графике рендеринг — это процесс вывода картинки на экран, или процесс подготовки фигур для вывода, или вообще все действия, связанные с выводом изображения.
Буфер
— область временного хранения данных.
Двойная буферизация
— способ рендеринга, при котором существует два буфера: содержимое одного показывается на экране, в то время как содержимое второго рисуется. Когда рисование второго буфера закончено, буфера меняются местами ("переключаются"), и все повторяется для каждого кадра.
Пиксель
— единица двухмерного изображения. Точка.

Общий подход

Независимость от драйвера

Будет хорошим стилем, если движок с самого начала не будет ориентирован на конкретный драйвер — OpenGL или Direct3D. Даже так: в будущем, возможно, придется дописать поддержку Direct3D.

Чтобы избежать переписывания всего движка из-за смены драйвера, вполне естественной кажется мысль выделить объект "Графика", в котором и делать всю работу по взаимодействию с драйвером. Этот объект не должен "привязывать" программу к конкретному драйверу — все его внутренности должны быть надежно спрятаны.

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

Цикл действий

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

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

Чтобы избежать всего этого, умные компьютерщики придумали трюк с двойной буферизацией.

Объект Graphics

Как и любой обычный объект, Graphics должен иметь конструктор. В конструктор будет передаваться идентификатор окна. Деструктор — дело обычное, в него передаваться не будет ничего. ;-)

Чтобы начать рисование, программа должна спрашивать у Graphics: "я начну?" И, когда закончит рисовать кадр, программа скажет: "готово". Этот диалог программы с Graphics нужен из следующих соображений. Во-первых, перед тем, как рисовать кадр, экран должен быть очищен. Во-вторых буфер, в который происходит рисование, может быть занят другой программой (так бывает у Direct3D). "Готово" программа говорит для того, чтобы Graphics переключил буфера.

class Graphics  
{
public:
    Graphics(HWND hwnd);
    bool virtual Begin();
    void virtual End();
    virtual ~Graphics();
};

Заметьте: все функции кроме конструктора — виртуальные. Это сделано с расчетом на возможность написания еще одного объекта, поддерживающего Direct3D; тогда нужно будет всего лишь выделить абстрактный класс, и перегрузить все его функции в обоих потомках. А так как объект Graphics — всего один, то его данные будут находиться в том же модуле, где находится и реализация.

Конструктор

Единственное, что я хочу передавать в конструктор Graphics при инициализации — это идентификатор окна. Окно создается безразлично как, и если я хочу выводить какую-то графику, она должна выводиться в окно.

Таким образом, объекту Graphics потребуется инициализировать OpenGL, пользуясь одним только идентификатором окна.

Для драйвера OpenGL необходимо сделать следующую последовательность действий:

  • Получить контекст устройства окна
  • Установить для него необходимый формат пикселя
  • Создать для контекста окна контекст OpenGL
  • Сделать контекст OpenGL текущим
#include "gl/gl.h"    // Подключить OpenGL

//////////////////////////////////////////////////////////////
// Немного данных!!!
// Эта информация используется в других функциях, поэтому
// должна сохраняться. Данные не содержатся в теле объекта, 
// как и включение gl.h по той причине, что нельзя привязывать 
// программу к одному из драйверов.
//////////////////////////////////////////////////////////////

HWND Ahwnd;
HDC Ahdc;
HGLRC Ahrc;

Graphics::Graphics(HWND hwnd)
{
    // Получить контекст устройства окна
    HDC hdc = GetDC(Ahwnd);

    PIXELFORMATDESCRIPTOR pfd;
    ZeroMemory(&pfd, sizeof(pfd));
    pfd.nSize = sizeof(pfd);
    pfd.nVersion = 1;
    
    // Желаем использовать окно, OpenGL, двойной буфер.
    pfd.dwFlags = PFD_DRAW_TO_WINDOW | 
        PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    
    // Использовать основанные не на палитре пиксели.
    pfd.iPixelType = PFD_TYPE_RGBA;
    // Для цвета хотелось бы использовать 24 бита. :-)
    pfd.cColorBits = 24;    
    
    // Подобрать самый подходящий формат пикселя.
    int iFormat = ChoosePixelFormat(Ahdc, &pfd);
    // Выбрать текущим формат пикселя для контекста.
    SetPixelFormat(Ahdc, iFormat, &pfd);

    // Создать контекст OpenGL.
    Ahrc = wglCreateContext(Ahdc);
    // Сделать его текущим.
    wglMakeCurrent(Ahdc, Ahrc);

    // Этим цветом буду очищать фон окна.
    glClearColor(0.3f, 0.1f, 0.3f, 1);
}

Деструктор

Здесь все просто — установить "никакой" текущий контекст OpenGL, и удалить контексты в порядке, обратном от порядка создания.

Graphics::~Graphics()
{
    wglMakeCurrent(0, 0);
    wglDeleteContext(Ahrc);
    ReleaseDC(Ahwnd, Ahdc);
}

Очистка и переключение буфера

В данном случае, для OpenGL, функция Begin всегда возвращает true. Это сделано для совместимости с Direct3D. Direct3D позволяет программе знать, если вывод графики чем-нибудь заблокирован. Таким образом программа может не рисовать, если ее все равно не видно.

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

Если не использовать glFlush, то в полноэкранном режиме можно увидеть, как части трехмерного мира продолжают прорисовываться... Я наблюдал такое GLQuake — у меня RivaTNT2. Когда я запускал игру, все работало отлично, но если я переключался на другие задачи, а потом снова в GLQuake, то начинал видеть, как оружие и полоса статуса рисуются прямо поверх уже нарисованного мира...

bool Graphics::Begin()
{
    // Очистить экран
    glClear(GL_COLOR_BUFFER_BIT);

    return true;
}

void Graphics::End()
{
    // Подождать!!! А то глюки полезут как в Кваке...
    glFinish();

    // Переключить буфера.
    SwapBuffers(Ahdc);
}

Следующая страница: Цикл программы


(c) 2000 Константин Михеев — проект "Elfish Engine"