{{notification.text}}

MirGames

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

Создание контекста рисования
Инициализация контекста

В этом разделе пойдем от обратного, сначала поймем, что нужно получить, затем, как это делать.

Мы знаем, что все рисующие команды GL выполняются применительно к текущему контексту воспроизведения (OpenGL rendering context). Значит, нам нужно создать этот самый контекст воспроизведения и сделать его текущим. Вообще говоря, в зависимости от нужд приложения, мы можем создать и несколько контекстов воспроизведения, и переключаться между ними при необходимости. Собственно тут проходит граница между епархией OpenGL и всем остальным приложением. Напомню, что само ядро OpenGL абстрагируется от целевой платформы, у него есть контекст, вот он в нем и  работает, и знать не знает о каких то там виндозах или линухах.

Создание же контекста является платформозависимой операцией. Рассмотрим создание контекста в среде Windows.

Выполняется это функцией wglCreateContext. В качестве входного параметра ей передается дескриптор устройства GDI (HDC). Функция возвращает дескриптор контекста отрисовки GL (HGLRC), если он равен 0, значит, произошла ошибка (чтобы получить дополнительную информацию об ошибке можно воспользоваться функцией GetLastError).

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

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

function SetWindowPixelFormat(DC: HDC; bpp, depth, stencil: integer): integer;
var
  pfnum: integer;
  pfd: PIXELFORMATDESCRIPTOR;
begin
  fillchar(pfd, sizeof(pfd), 0);
  
  pfd.nVersion := 1;
  pfd.nSize := sizeof(pfd);
  pfd.dwFlags := PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER;
  pfd.iPixelType := PFD_TYPE_RGBA;
  pfd.cColorBits := bpp;
  pfd.cStencilBits := stencil;
  pfd.cDepthBits := depth;

  {выбираем формат пикселей}
  pfnum := ChoosePixelFormat(DC, @pfd);

  {если вернули 0, значит ошибка}
  if pfnum = 0 then exit;

  {устанавливаем формат пикселей}
  SetPixelFormat(DC, pfnum, @pfd);

  Result := pfnum;
end;

ChoosePixelFormat принимает два параметра – HDC устройства, на котором будем рисовать и указатель на структуру PIXELFORMATDESCRIPTOR, в которой мы собственно, и указываем желаемые требования. PIXELFORMATDESCRIPTOR содержит кучу полей, назначение которых можно посмотреть в справке. Мы же рассмотрим только наиболее часто применяемые. Ну, с версией и размером все понятно без слов.

Хочу только отметить, что наличие поля размера в структурах довольно распространенная вещь в API Windows. По этому размеру система определяет версию структур, поэтому рекомендуется их не игнорировать.

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

Есть еще несколько флагов, но они редко используются (а некоторые устарели и ни на что не влияют). Все они описаны в справке (я еще не говорил, что лучшая справка это MSDN?).

В поле iPixelType указываем, что работать будем с полноцветными изображениями (как вариант с палитровыми, но сейчас это уже не актуально).

Вообще для оконного приложения остальные поля уже не нужны, но для полноэкранного необходимо детализировать наши запросы. Итак, в поле cColorBits указываем желаемую глубину цвета (bpp – bits per pixel, 8, 15,  16, 24, 32 … список поддерживаемых варьируется от видеоплаты к видеоплате и зависит от драйверов).

В поле cStencilBits мы можем задать разрядность буфера трафарета (Stencil buffer). Буфер трафарета довольно вкусная вещь и используется во многих продвинутых техниках рисования, например стенсильные тени и пр. Однако на современных видеоплатах его разрядность, как правило, ограничена 8 битами. К тому же в большинстве случаев аппаратно акселерируется буфер трафарета только в режиме 32bpp и, например, в режиме 16bpp любое обращение к буферу трафарета будет жутко тормозить.

В поле  cDepthBits указываем глубину Z-буфера. Обычные значения 16, 24, 32 бит. Немого подробнее про Z-буфер. Он применяется в 3D графике, и его разрядность влияет на точность вычислений операций с глубиной сцены. Недостаточная глубина Z-буфера будет приводить к визуальным артефактам, например, более дальний объект будет вылазить сквозь ближний объект. Глубина сцены задается как отношение задней плоскости отсечения к передней. К тому же точность вычислений в Z-буфере не одинаковая. Вблизи она выше, вдалеке меньше (график довольно сложный и зависит от производителя видеокарты (да еще прибавьте новомодные технологии у разных вендоров)). В общем, я говорю это к тому, что с глубиной сцены можно получить достаточно много проблем и надо внимательно относиться к этому вопросу. Когда нельзя наращивать аппаратные средства до бесконечности, приходиться применять различные махинации с объектами сцены, алгоритмами вывода и т.д.

Так же pfd содержит поля для буфера аккумулятора и вспомогательных буферов (auxiliary buffers). Но первые поддерживаются только на единицах профессиональных видеосистем, вторые вообще практически не поддерживаются. Поэтому не будем на них задерживаться. Есть еще куча полей, которые применяются редко, и как я уже отмечал, полное их описание можно найти в справке.

После того, как мы записали свои пожелания в pfd, вызываем ChoosePixelFormat. Эта функция возвращает номер одного из доступных в системе форматов пикселей (это зависит от видеокарты и дров). Тут нужно обратить внимание вот на какую штуку. Система вовсе не обязательно удовлетворит наше прошение. Она вернет номер наиболее подходящего (по ее мнению, разумеется) формата. Такое сплошь и рядом происходит для оконных приложений. Вы там не получите ничего, кроме того что установлено на десктопе. Если уж вернула 0, то вообще надо застрелиться (по крайней мере, приложение надо завершать и спамить на саппорт). В силу того, что наш запрос носит лишь рекомендательный характер, мы можем довольно вольготно запрашивать формат. Например, во многих книжках и примерах используются значения 64, мол, дайте нам лучшее из того, что есть. Но все-таки рекомендуется просить именно то, что нужно для наших конкретных целей.

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

Подобрав формата пикселей, мы назначаем его для устройства рисования функцией SetPixelFormat. По идее, как дополнительный шаг можно после этого еще и запросить обратно какой же именно формат пикселей мы установили, вызвав функции GetPixelFormat и DescribePixelFormat. Я бы даже рекомендовал это делать и, например, выводить эту информацию в лог – это может сильно пригодиться при отладке.

В результате у нас получилась функция создания GL контекста рисования:

function CreateContext(DC: HDC): boolean;
var
  rc: HGLRC;
begin 
  Result := false;

  //Проверим входные параметры
  if DC = 0 then exit;

  //создаем GL контекст
{настраиваем формат пикселей, параметры bpp, depth и stencil можно указать и
 произвольные - система подберет наиболее подходящие значения, однако
 рекомендуется указывать именно то, что вы хотите получить}

  if SetWindowPixelFormat(DC, 32, 0, 0) = 0 then exit;
  {теперь можно создать контекст GL}
  rc := wglCreateContext(DC);
  {если по каким либо причинам это не удалось (можно получить расширенную справку через
   GetLastError, то использовать OpenGL невозможно}

  if rc = 0 then exit;

  {назначаем GL контекст текущим}
  wglMakeCurrent(DC, rc);

  //инициализируем параметры OpenGL, которые не будут меняться на
  //протяжении работы нашего приложения
  {их можно настраивать и в каждом кадре отрисовки, но зачем? только FPS снижать...}
  OneTimeSceneInit;
  Result := true;
end;

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

Не отходя далеко от кассы, напишем функцию уничтожения контекста рисования GL.

function DeleteGLContext: boolean;
var
  rc: HGLRC;
begin
  //удаляем GL контекст, только сначала подчистим за собой
  FinalCleanup;

  //запрашиваем текущий GL контекст
  rc:=wglGetCurrentContext;

  //перед удалением контекста надо сбросить текущий контекст OpenGL подсистемы
  wglMakeCurrent(0, 0);

  //а теперь само удаление
  wglDeleteContext(rc);

  Result := true;
end; 

Функция FinalCleanup здесь для примера, она может освобождать ресурсы, которые мы создавали, например текстуры, фонты и прочее. Посмотрите, какую технику я здесь применил. Для освобождения контекста рисования GL нужно вызвать функцию wglDeleteContext (предварительно сделав его нетекущим).  Однако я не сохранил HGLRC при создании контекста. Тем не менее, можно в любой момент затребовать текущий контекст функцией wglGetCurrentContext. Также можно затребовать и текущий HDC функцией wglGetCurrentDC. В приложениях, управляющих несколькими контекстами рисования, их нужно сохранять в переменных и дополнительно переключаться между ними.

Будем продвигаться назад в будущее. Теперь мы видим, что для создание GL контекста нужен HDC. HDC мы должны получить от поверхности, на которой будем осуществлять рисование, например от окна нашего приложения функцией GetDC. Кстати говоря, в проектах с использованием VCL мы можем получить HDC например от графических контролов (Canvas.Handle), однако работа OGL на таких поверхностях отличается крайней неустойчивостью, а в большинстве случаев вообще ничего не работает. Поэтому лучше всего использовать наследники TWinControl (панели, формы, фреймы и т.п.). Причина этого кроется в любви VCL пересоздавать HDC для графических контролов в процессе работы, естественно старый теряется, а вместе с ним идет лесом и контекст отрисовки OpenGL.

Собственно в VCL мы дошли до начала инициализации контекста рисования – окно у нас уже есть (например форма). Идем в OnFormCreate, получаем HDC и далее по списку. Но мы сделаем еще один шаг в логической  цепочке инициализации приложения OpenGL, чтобы прочувствовать всю кухню этого процесса. Теперь для минимального приложения нам потребовалось создать окно. Создать окно достаточно просто, и подробно расписано во всяких справках и примерах, в том числе и в прилагающихся к этой статье. На всякий случай приведу последовательность действий:

  • регистрируем класс окна в системе
  • создаем окно данного класса
  • получаем HDC нашего окна
  • создаем контекст рисования GL
  • показываем окно
  • входим в цикл обработки сообщений
  • освобождаем занятые ресурсы
  • прибиваем контекст рисования GL
  • уничтожаем окно
  • отменяем регистрацию нашего класса окна

Хочу тут остановиться на двух моментах. Первое, окно необходимо подготовить к будущему использованию совместно с OpenGL. Стиль окна должен включать флаги WS_CLIPCHILDREN и WS_CLIPSIBLINGS. А также некоторые дополнительные настройки для оконного или полноэкранного режима. В оконном режиме у окна должны быть заголовок, бордюр, системное меню и прочие атрибуты. Для полноэкранного режима эти феньки соответственно не нужны (и даже наоборот мешают).

Второй момент связан с переключением между полноэкранным и оконным режимами. Поняв, что такое настройка формата пикселей, с большой долей вероятности можно предположить, что они не будут совпадать в оконном и полноэкранном режиме. Соответственно нам придется уничтожить старый контекст рисования GL и создать новый (и, разумеется, настроить новый формат пикселей). Т.е. по сути, при переключении видеорежимов нам надо будет заново пройти практически все этапы создания контекста рисования:

  • настроить стили окна
  • получить HDC
  • настроить формат пикселей
  • создать контекст рисования GL
  • инициализировать сцену

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

При изменении размеров окна, нам надо соответственно настроить область отображения (glViewport). Хорошее место для этого — обработка сообщения WM_SIZE (или OnFormResize).

Можно еще добавить, чтобы наше приложение было дружелюбным к пользователю и системе, надо позаботиться о таких вещах, как переключению на другие приложения (например, по [Alt]+[Tab]), ловить сообщения о смене видеорежима (WM_DISPLAYCHANGE), перезагрузки (WM_QUERYENDSESSION) и т.д.

Полноэкранный режим

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

Переключение осуществляется APIшной функцией ChangeDisplaySettings. Нужные нам параметры указываем в структуре DevMode. Вот пример кода:

function DoFullscreen: boolean;
var
  dm: DevMode;
  hr: HRESULT;
  DC: HDC;
begin
  //устанавливаем режим экрана  (800x600x32bpp 75Hz)
  fillchar(dm, sizeof(dm),0);

  dm.dmSize := sizeof(DevMode);
  dm.dmFields := DM_PELSWIDTH or DM_PELSHEIGHT or DM_BITSPERPEL or DM_DISPLAYFREQUENCY;
  dm.dmPelsWidth := 800;
  dm.dmPelsHeight := 600;
  dm.dmBitsPerPel := 32;
  dm.dmDisplayFrequency := 75;
  hr := AltChangeDisplaySettings(@dm, CDS_FULLSCREEN);
  { проверяем удалось ли переключить}
  Result := hr = DISP_CHANGE_SUCCESSFUL;
end;

Вернуть видеорежим к тому, что был установлен на десктопе (мы же не забудем этого сделать) можно так:

  AltChangeDisplaySettings(nil, 0);

Тут нужно некоторое пояснение. В Дельфи (в Windows.pas) функция ChangeDisplaySettings определена не совсем удачно. У нее параметр DevMode определен как var, что не дает нам возможности передать nil (как это прописано в MSDN). Поэтому я переопределяю свою функцию, с более удобным синтаксисом и ссылающуюся на тужу самую APIшную функцию:

function AltChangeDisplaySettings(lpDevMode: PDeviceMode; dwFlags: DWORD): Longint; stdcall;

В dmFields указываем, какие из параметров мы будем задавать. Остальные поля в объяснениях не нуждаются – все понятно из их названия. Нетрудно догадаться, что переключение видеорежимов необходимо делать ДО установки формата пикселей. Т.е. наша логическая цепочка инициализации удлинилась на один шаг:

  • переключаем видеорежим
  • регистрируем класс окна в системе
  • создаем окно данного класса
  • получаем HDC нашего окна
  • создаем контекст рисования GL
  • показываем окно
  • входим в цикл обработки сообщений
  • освобождаем занятые ресурсы
  • прибиваем контекст рисования GL
  • уничтожаем окно
  • отменяем регистрацию нашего класса окна
  • возвращаем исходный видеорежим

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

Двойная буферизация

Обещал – расскажу. Двойная буферизация позволяет избежать мелькания изображения, особенно при низких fps. Хотя нужно отметить, что существуют задачи и не требующие двойной буферизации. Принцип очень простой. Кадр отрисовывается во внеэкранный буфер. Затем этот буфер единым махом выводиться на экран (свопится или копируется – предоставим самим дровам выбирают оптимальный режим). Управляет этим процессом операционная система и к самому OpenGL имеет слабое отношение. Однако и от программиста требуется кое-какие действия. Первое – это нужно указать, что мы будем использовать двойную буферизацию. Делается это при настройке формата пикселей — добавляется флажок PFD_DOUBLEBUFFER.

Второе, после окончания вывода сцены, мы должны дать команду отобразить буфер. В WinAPI для этого есть функция SwapBuffers. Ей нужно передать дескриптор поверхности рисования HDC. Для большинства приложений нет нужды хранить HDC и мы можем сделать SwapBuffers(wglGetCurrentDC). Если приложение использует несколько контекстов, то можно их запоминать (или менеджировать GL контексты). Так же можно запоминать HDC, если мы хотим ускорить наше приложение на пару миллионных процента – все-таки считать число из переменной будет побыстрее вызова функции его нахождения.

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

Тайминг

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

Можно выделить два принципиально разных подхода для вызова функций отрисовки:

  • периодический
  • непрерывный

Из названий понятно, что в первом случае мы вызываем отрисовку через какой-то интервал времени, во втором случае мы начинаем вывод следующего кадра при первой же возможности. У обоих методов есть как достоинства, так и недостатки. Непрерывная отрисовка занимает 100% процессорного времени, имеет неровный fps и немного затрудняет синхронизацию анимации (просто для этого нужен отдельный механизм синхронизации), зато позволяет достичь максимальной производительности. При периодической отрисовке, мы освобождаем ресурсы процессора для прочих задач, имеем более ровный fps. Период отрисовки можно использовать так же для синхронизации анимации. Но имеем пенальти производительности (небольшое), да еще есть трудность выдерживания малых интервалов в системе Windows.

Необходимо еще отметить, что Windows никогда не была «real time OS». Ваше приложение в любой момент может быть приостановлено системой в угоду более важных дел. Понятно, что в таком случае, ни о каком постоянстве периода речи быть не может. Хотя на современных, мощных машинах это не так заметно. Как частный случай периодической отрисовки, можно рассматривать событийный метод. Это когда отрисовка запускается при наступлении какого-нибудь события. Например при WM_PAINT и WM_ERASEBKGND. Обычно такой метод используется для не слишком динамичных приложений.

Периодический

Итак, начнем с периодического метода. Его можно реализовать целой кучей способов. Самый простейший, завести в системе таймер и по его сигналу вызывать DoDraw.

Описывать применение TTimer из VCL я не буду в виду тривиальности.

Через API это делается функцией SetTimer. Ей передается дескриптор окна, которое будет ловить сообщения WM_TIMER от системы, идентификатор таймера (придет вместе с сообщением) – по нему мы можем определять какой же именно таймер сработал (ведь их можно завести и несколько) и интервал срабатывания. В оконной процедуре получаем WM_TIMER и запускаем отрисовку.

Чтобы остановить таймер вызывается функция KillTimer. Ей передается дескриптор окна и идентификатор таймера. Кстати, никогда не оставляйте неприбитых таймеров!

Все просто, не правда ли? К сожалению, разрешение такого таймера небольшое (в Win95/98 минимальное ~55мсек, что дает нам 20 fps), да еще и приоритет сообщения WM_TIMER низкое, так что оно может застрять в очереди сообщений (получим снижение и неровности fps). В Windows 2000/XP интервал может быть и меньше, но не гарантированно.

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

Лучше обстоят дела с мультимедийным таймером. Он может иметь разрешение вплоть до 1 мс (зависит от железа). В отличие от системного таймера, который шлет мессаги к нам в окно, этот работает через функцию обратного вызова (callback function). Т.е. по истечении заданного интервала вызывается функция из нашего приложения.

Обратите внимание – эта функция не может быть методом класса т.к. методы класса получают неявный параметр self, о чем не подозревает система. Так же при высокой частоте таймера сильно расходуются ресурсы процессора. Вот функции, нужные нам для работы с мультимедийным таймером:

  • timeGetDevCaps – позволяет узнать характеристики таймера
  • timeBeginPeriod – задает минимальное разрешение таймера
  • timeSetEvent – запускает таймер
  • timeKillEvent – останавливает таймер
  • timeEndPeriod – отменяет минимальное разрешение таймера

Так же полезна функция timeGetTime – возвращает кол-во миллисекунд, прошедших с момента запуска Windows ( c точностью ~1 мс). Однако и тут кроется подвох. Наша таймерная функция будет вызвана в контексте отдельного потока! В этом потоке наш контекст рисования GL неопределен. Соответственно, если мы просто из таймерной функции вызовем DoDraw, ничего не нарисуется.

Одним из решений является передача из таймерной функции сообщения в нашу оконную процедуру, по получении которого и будет осуществлена отрисовка. Например:

procedure TimerProc(uID, uMsg: UINT; dwUser, dw1, dw2: DWORD); stdcall;
begin
  //пора, что нибудь нарисовать
  if ReadyToDraw then PostMessage(hWindow, WM_PAINT, 0, 0);
end;  

Можно использовать и SendMessage, но не рекомендуется. Обработка сообщения (отрисовка), может быть слишком долгой, а из функций обратного вызова надо возвращаться как можно быстрее.

Несколько методов организации периодов может предложить нам работа с потоками. Вот парочка:

  • создается поток, который во время своей работы усыпляется функцией sleep
    на нужный интервал. После того, как он проснется, он посылает нашему окну сообщение о перерисовке.
  • создается поток, работающий в бесконечном цикле. На каждой итерации он получает текущее время (например,
    через timeGetTime или QueryPerformanceCounter), вычисляет прошедший интервал и при превышении заданного сообщает приложению о
    необходимости перерисовки.

Вообще в системе Windows можно довольно точно получить текущее время (показания счетчиков) и сложнее точно выдержать заданный интервал – прошу не путать эти два понятия.

Вот лишь несколько функций получения текущего показания часов/счетчиков: GetTickCount, GetLocalTime, GetSystemTime, timeGetTime, QueryPerformanceCounter, есть еще счетчик сверхвысокого разрешения – счетчик циклов процессоров Pentium.

Их можно применять в алгоритмах с организацией ожидающих функций.

Непрерывный

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

var
  AMsg: TMsg;
begin
  …
  fillchar(Amsg, sizeof(Amsg),0);
  //бесконечный цикл обработки сообщений
  while (Amsg.message <> WM_QUIT) do
  begin
    if PeekMessage(AMsg,0, 0, 0, PM_REMOVE) then
    begin
      TranslateMessage(AMsg);    
      DispatchMessage(AMsg);
    end;
    //вызываем функцию отрисовки
    if ReadyToDraw then DoDraw;
  end;
  …
end;

В приложениях с использованием VCL можно подвесить отрисовку на событие Application.OnIdle;

Другим способом является использование отдельного циклического потока, но тут все посложнее – необходимо будет дополнительно проводить синхронизацию потоков.

Еще нужно отметить такой момент как вертикальная синхронизация (VSYNC). Она включается для улучшения качества картинки.

C включенной синхронизацией, SwapBuffers не будет возвращать управления, пока не дождется обратного хода луча. Таким образом, мы не сможем получить fps превышающий частоту кадров монитора, вне зависимости от метода организации вызова рисующей функции.

Включить VSYNC можно через настройки драйвера либо используя расширение WGL_EXT_swap_control.

Подсчет FPS

Несколько слов о подсчете FPS. Конечно достижение высоких fps не является самоцелью, однако он является хорошим показателем качества ваших алгоритмов и их реализации. Комфортным для работы считается fps порядка 40-60. Как известно это количество «кадров в секунду». Вот мы и будем высчитывать сколько кадров мы отрисовали и за какое время. Для этого потребуется завести несколько переменных:

  • счетчик кадров
  • время отрисовки последнего кадра
  • текущий fps

Наращивать счетчик отрисованных кадров нужно, естественно, при вызове функции рисования. Засекать время будем с помощью таймера высокого разрешения QueryPerformanceCounter (только предварительно надо получить показания его частоты через QueryPerformanceFrequency). Раз в секунду (можно и почаще) будем вычислять новое значение fps и сбрасывать показания счетчиков.

interface
…
  // функции подсчета FPS
  procedure DoCalculateFPS;
  function GetFPS: single;

implementation
…
//-------------------------------------------------------------
//  Подсчет FPS

var
  LastTime     :int64 = 0;
  DrawedFrames :int64 = 0;
  FPS          :single;
  Freq         :int64;

//-------------------------------------------------------------
procedure DoCalculateFPS;
var
  t: int64;
  dt: single;
begin
  inc(DrawedFrames);

  QueryPerformanceCounter(t);
  dt := (t - LastTime);
  dt := dt / Freq;
  //1.0 интервал перерасчета fps - (раз в секунду) 
  //его лучше не делать меньше времени вывода одного кадра

  if dt>=1.0 then
  begin
    FPS := DrawedFrames / dt;
    DrawedFrames := 0;
    LastTime := t;
  end;
end;
//-------------------------------------------------------------

function GetFPS: single;
begin
  Result := FPS;
end;
…

initialization
  QueryPerformanceFrequency(Freq);
  if Freq = 0 then Freq := 1;

Ну и чтобы это работало, в функции DoDraw добавим вызов DoCalculateFPS.

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

Работа с расширениями

SGI поступило грамотно – разработали простое и понятное кросплатформенное ядро и включило поддержку бедующих расширений. Чем до сих пор и пользуются, куча народа кучу лет. К сожалению борландовцы подошли к OpenGL спустя рукава – включили хидер ядра 1.0 (это даже меньше чем идет вместе с VC) и успокоились на этом. Оно и понятно — GL не самый большой приоритет.

К счастью все не так уж и плохо – есть прекрасные хидеры третьих лиц (например http://www.delphi3d.net/dot). И мы даже научимся сами делать себе заголовочные файлы – мы же хотим понять, как все работает. Сначала немного истории и терминологии. Есть ядро OpenGL, которое должны поддерживать любые системы, работающие в GL, и есть расширения ядра, в которых обычно реализуются или новые фичи или специфичные для вендора или платформы.

Расширения поддерживаются выборочно, разными видюхами по разному. Ядро постепенно обновляется. Этим занимается комитет ARB, в который входят представители всех ведущих компаний на рынке железа и софта. Часть расширений, которые были признаны архинужными и архиполезными включаются в состав ядра. Текущая версия 1.5 и на подходе GL2.0. Самые большие вкусности находятся как раз в расширениях.

В наименованиях функций и констант есть закономерность (описанная в правилах комитета ARB). Например, специфичные для Windows функции начинаются с wgl (и не поддерживаются никакой другой платформой). Юниксоидам предусмотрен префикс glx и т.д. Есть еще glu, но это из дополнительной библиотеки и о ней поговорим попозже. Кроссплатформенные фичи начинаются просто с gl (например glVertex3 f). Вендоры застолбили за собой суффиксы. Так у Nvidia NV (например glLoadProgramNV), у ATI — ATI и ATIX (например glGetTexBumpParameterfvATI).

Вообще групп вендорских расширений много и ситуация с ними достаточно запутанная. Некоторые Nvidia или SGI (и других контор) расширения доступные на платах, например ATI и т.п. Есть еще расширения общего назначения EXT и ARB… Многие расширения вошли в состав новых версий ядра, некоторые устарели и ни одна система не поддерживает весь список (ну может быть за исключением софтверных эмуляций).

Сейчас насчитывается более 350 расширений, список и спецификации (хотя и не совсем полный) можно посмотреть тут http://oss.sgi.com/projects/ogl-sample/registry. Но выбор с какими расширениями работать и для чего, не входит в круг задач данной статьи, мы посмотрим, как организовать работу с ними.

Для Windows весь функционал GL реализован в драйверах видеоплаты и находится в кучке их dllшэк. Но мы в дебри дровописательства углубляться не будем, нам важно, что доступ к GL функциям идет через opengl32.dll (пусть и реальные функции вызываются из дровяных dllшек). OpenGL определяет множество констант и несколько типов данных.  Код доступен через функции. Если константы и типы прописываются в хидере и ничего делать дополнительно с ними не надо, то функции нужно уметь вызывать из библиотеки. Доступ к функциям ядра GL 1.0 и 1.1 можно получить статической линковкой (что делается в модуле opengl.pas из дистрибутива Дельфи для ядра 1.0) либо через GetProcAddress.

Адреса функций ядра выше 1.1 и всех расширений надо грузить динамически с помощью функции wglGetProcAddress. Объясняется это тем, что в библиотеке возможно наличие нескольких копий одной и той же функции, и выбор конкретного экземпляра зависит от параметров системы (например, тот же формат пикселей – есть функции оптимизированные под 32bpp, есть под 16bpp). Конечно, все это зависит от дров, но именно этим и занимается wglGetProcAddress.

Кстати, аналогичная функция есть и в unix системах – glxGetProcAddress. Отсюда вытекает еще один очень важный момент – инициализаций ядра >1.1 и всех расширений необходимо проводить ПОСЛЕ создания контекста рисования GL (HGLRC). В частности, даже glGetString (о ней еще поговорим) не будет корректно работать без настроенного контекста. Да, еще, функцию wglGetProcAddress, почему то прописали не вместе со всеми в windows.pas, а отдельно в opengl.pas. Или наоборот, остальные wgl функции прописали не там. Windows.pas мы переписывать не будем, а вот opengl.pas будет свой… ну не беда – пропишем ее сами (линкуется статически).

В статическое линковке нет ничего сложного – можно посмотреть файл opengl.pas для образования:

interface
type
  GLenum = cardinal;
  …
const
  GL_ACCUM = $0100;
  GL_LOAD = $0101;
  …
  function wglGetProcAddress(ProcName:PChar): Pointer; stdcall;
  …
  procedure glBegin(mode: GLenum); stdcall;
  …
implementation
  function wglGetProcAddress; external opengl32;
  …
  procedure glBegin; external opengl32;

Предпочтительнее использовать полностью динамическую загрузку (и ядра 1.0 и всех остальных). Тут мы получаем несколько преимуществ. Во первых, мы можем проверить доступность библиотеки и ее компонентов прямо в приложении, и принять соответствующие шаги. При статической линковке, если что-то не так, приложение закроется с ошибкой. Во вторых, Мы можем в рунтайме выбирать, что нам загружать, а что нет. Ну и в третьих (хотя это сомнительное преимущество), мы можем в пределах одного приложения или в разных потоках грузить разные библиотеки, например то, что идет с дровами, и отладочную — софтверную.

К минусам можно отнести мизерное увеличения времени загрузки, и некоторое раздувание кода по сравнению со статической линковкой.

Адреса библиотечных функций выдранных с помощью GetProcAddress/wglGetProcAddress нужно сохранить в процедурных переменных. Вызов функций через процедурные переменные ничем не отличается от вызова обычных функций. Если мы хотим использовать разные библиотеки для разных потоков, то вместо объявления обычных процедурных переменных (var), укажем threadvar – локальные для потока. Только обратите внимание – функции через threadvar вызываются чуток помедленнее. Вообще во всей этой бодяге нет ничего сложного, просто большой объем – тысячи констант, сотни функций, и все их надо прописать и сделать загрузку/выгрузку/проверку (и желательно без ошибок).

Итак, приступим. Предположим, что мы уже набили интерфейсную часть – константы, типы и прототипы функций (через процедурные переменные).  По сравнению со статической линковкой, различия только в определении функций. Bместо декларации функции мы указываем процедурную переменную такого же типа. Например:

threadvar
  glBegin: procedure(mode:  TGLenum); stdcall;
  …

А так же мы заведем несколько вспомогательных переменных, которые будем заполнять при инициализации библиотеки:

  GL_VERSION_1_0:   boolean;
  GL_VERSION_1_1:   boolean;
  GL_VERSION_1_2:   boolean;
  GL_VERSION_1_3:   boolean;
  GL_VERSION_1_4:   boolean;
  GL_VERSION_1_5:   boolean;
  GL_VENDOR_NAME:   string;
  GL_RENDERER_NAME: string;
  GL_EXT_STRING:    string;

   //флажки поддерживаемых расширений
  GL_ARB_depth_texture,
  GL_ARB_fragment_program,
  GL_ARB_fragment_program_shadow,
  GL_ARB_fragment_shader,
  …
  : boolean;

Перво-наперво, нам необходимо загрузить саму dllшку в адресное пространство нашего приложения. Как обычно, делается это функцией LoadLibrary. При успехе, возвращается дескриптор dllшки – нам его нужно сохранить для возможности получения адресов функций и последующей выгрузки. Затем будем грузить ядро 1.0 и 1.1. После загрузки ядра 1.1 нужно будет создать контекст отрисовки GL и вызвать последнюю функцию – загрузка ядра >1.1 и расширений. Двухступенчатая схема загрузки вытекает из различий в работе с ядром <=1.1 и остальной частью. Загрузка функций 1.0 и 1.1 не требует наличия контекста отрисовки (так сложилось исторически) и адреса функций достаются с помощью APIшной GetProcAdress. 1.2 и выше, требует наличие контекста и использования wglGetProcAddress.  Вот как будет выглядеть интерфейсная часть:

interface
  //константы, типы и процедурные переменные OpenGL и расширений
  {поскипано несколько сот килобайт текста}
  …
  {так как мы пишем свой хидер OpenGL, определяем функцию загрузки адресов функций}
  function wglGetProcAddress(ProcName:PChar): Pointer; stdcall;

  //определим интерфейсные функции загрузки библиотеки
  {загрузка библиотеки из указанного места}
  function LoadGLFromLibrary(LibName: string): boolean;

  {загрузка библиотеки по умолчанию ( вызывает LoadGLFromLibrary('opengl32.dll'))}
  function LoadGL: boolean;

  {второй этап – инициализация расширений}
  function InitGLExtensions: boolean;

  {незабудем почистить за собой}
  function CloseGL: boolean;

Вся черновая работа будет производиться функциями раздела имплементации. Но сначала я опишу механизм распознавания доступных расширений и их загрузки. Предположим,  у нас уже успешно загрузилось ядро 1.0 и 1.1 и создан контекст рисования GL. Определить поддерживаемую версию ядра можно посмотрев на строку, которую возвращает функция glGetString c параметром GL_VERSION (это функция ядра 1.0). Например, результат ‘ 1.5.1234’, говорит о том, что версия ядра 1.5. Кстати, по всем официальным алгоритмам определения версии, строка вида ‘ 1.4.9999’ должна интерпретироваться как 1.4. В этом случае, как хорошие мальчики, мы даже и не должны пытаться грузить функции ядра 1.5.

Определение поддерживаемых расширений работает похожим образом. Вызывается glGetString c параметром GL_EXTENSIONS. В результирующей строке перечислены все поддерживаемые расширения, разделенные пробелами. Если мы хотим определить, поддерживается ли, например ARB_vertex_program в системе, то нужно поискать соответствующее вхождение строки ‘GL_ARB_vertex_program’. Если его нет, то считаем, что расширение не поддерживается и грузить эти функции не будем. Хотя бывают ситуации, когда строка отсутствует, а функции все же есть. Или наоборот, строка присутствует, а функции воткнуть забыли. Однако такие оказии достаточно редки и мы можем действовать в соответствии со спецификацией – «нет строки – нет расширения, есть строка – есть расширение» И отписаться в read.me – мол, качайте свежие дрова и спамьте саппорт вендоров :).

Кстати, есть аналогичные glGetString функции, посвященные своим разделам OpenGL. Например, wglGetExtensionsStringARB и wglGetExtensionsStringEXT – возвращают строку расширений для wgl.gluGetString, glxQueryExtensionsString, glxGetClientString и т.п. Если версия ядра или расширение поддерживается, то мы для всех его функций должны получить адреса с помощью одного из вариантов xxxProcAddress (ядро 1.0 и 1.1 – APIшной GetProcAddress, остальное wglGetProcAddress).

По хорошему, для надежности, нужно еще и проверить – действительно ли загрузились функции. Ведь при неудаче, GetProcAddress возвращает nil, и последующая попытка вызова такой функции приведет к исключительной ситуации. По ходу дела можно выставить вспомогательный boolean, что, мол, такое расширение поддерживается (или наоборот), что бы потом, при работе программы, лишний раз не перепроверять – есть ли фича или нет (опять получить строку расширения, найти там подстроку и т.п.).

Ну и на последок, при завершении работы (или перезагрузки либы) можно сбросить все процедурные переменные в nil, все вспомогательные флаги в false и выгрузить dllшку из памяти приложения.

Определим парочку констант и переменных, необходимых нам для работы

implementation
const
  INVALID_MODULEHANDLE = 0;

  {имя библиотеки по умолчанию}
  SDefaultGLLibrary    =  'opengl32.dll';

Threadvar
  {дескриптор загруженной dll-шки - он нам еще понадобится}
  GLHandle: HINST;

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

procedure _split_ver(Buffer: string; var Major, Minor: integer);
var
  p: integer;
  Code: integer;
begin
  Minor := 0; Major := 0;
  try
    p := pos('.',Buffer);
    if (p > 1) and (p < Length(Buffer)) and (Buffer[p - 1] in ['0'..'9']) and (Buffer[p + 1] in ['0'..'9']) then begin
      Dec(p);
      while (p > 0) and (Buffer[p] in ['0'..'9']) do
        Dec(p);
      delete(Buffer, 1, p);
      p := pos('.', Buffer) + 1;
      while (p <= Length(Buffer)) and (Buffer[p] in ['0'..'9']) do
        Inc(p);
      delete(Buffer, p, 255);
      p := pos('.', Buffer);
      val(copy(Buffer, 1, p - 1), Major, Code);
      val(copy(Buffer, p + 1, 255), Minor, Code);
    end;
  except ;
  end;
end;

следующая функция определяет, содержится ли расширение в строке поддерживаемых расширений.


function _checkext(const Buffer: string; const Extension: string): boolean;
var
  ExtPos: integer;
begin
  // Сначала найдем позицию подстроки расширения в буфере.
  ExtPos := Pos(Extension, Buffer);
  Result := ExtPos > 0;
  //Теперь убедимся, что это не подстрока другого расширения 
  if result then
    Result := ((ExtPos + Length(Extension) - 1) = Length(Buffer))
              or (Buffer[ExtPos + Length(Extension)]=' ');
end;

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


procedure _read_version;
var
  Buffer: string;
  MajorVersion,
  MinorVersion: integer;
begin
  Buffer := glGetString(GL_VERSION);
  _split_ver(Buffer, Majorversion, MinorVersion);
  GL_VERSION_1_0 := true;
  GL_VERSION_1_1 := false;
  GL_VERSION_1_2 := false;
  GL_VERSION_1_3 := false;
  GL_VERSION_1_4 := false;
  GL_VERSION_1_5 := false;
  if MajorVersion > 0 then begin
    if MinorVersion > 0 then  begin
      GL_VERSION_1_1 := true;
      if MinorVersion > 1 then GL_VERSION_1_2 := true;
      if MinorVersion > 2 then GL_VERSION_1_3 := true;
      if MinorVersion > 3 then GL_VERSION_1_4 := true;
      if MinorVersion > 4 then GL_VERSION_1_5 := true;
    end;
  end; 
 
  //дополнительная информаци о ведоре, видеоплате и расширениях
  GL_VENDOR_NAME   := glGetString(GL_VENDOR);
  GL_RENDERER_NAME := glGetString(GL_RENDERER);
  GL_EXT_STRING    := glGetString(GL_EXTENSIONS);
end;

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


function _load_core_10: boolean;
begin
  Result:=false;
  if GLHandle <> INVALID_MODULEHANDLE then begin
    glAccum                           := GetProcAddress(GLHandle, 'glAccum');
    glAlphaFunc                       := GetProcAddress(GLHandle, 'glAlphaFunc');
    glBegin                           := GetProcAddress(GLHandle, 'glBegin');
    glBitmap                          := GetProcAddress(GLHandle, 'glBitmap');
    …
    //тут бы конечно проверку произвести
    //Result := assigned(glAccum);                if not Result then exit;
    //Result := Result and assigned(glAlphaFunc); if not Result then exit;
    //…
    Result := true;
    GL_VERSION_1_0                    := true;
  end; {if}
end;


Теперь очистка:


procedure _clear_core_10;
begin
  GL_VERSION_1_0                      := false;
  glAccum                             := nil;
  glAlphaFunc                         := nil;
  glBegin                             := nil;
  glBitmap                            := nil;
  …
end;

Небольшое замечания о кроссплатформенности. Можно применить условную компиляцию для загрузки функций. Вместо GetProcAddress(GLHandle, 'glAccum'); можно писать _get_proc_address(GLHandle, 'glAccum'); А сама эта вспомогательная функция будет выглядеть так:

function _get_proc_address(const Handle: Cardinal; const name: PChar): Pointer;
begin
{$ifdef WIN32}
  result := GetProcAddress(Handle, name);
{else}
  result := dlsym(Pointer(Handle), name);
{$endif WIN32}
end;

И для второго этапа


function _load_proc_address(const name: PChar): Pointer;
begin
{$ifdef WIN32}
  result := wglGetProcAddress(name);
{else}
  result := glxGetProcAddressARB(name);
{$endif WIN32}
end;

Точно тоже самое для загрузки ядра 1.1 И, как обычно, затираем адреса функций, невзирая ни на что (это наши процедурные переменные – мусор нам не нужен).

Все остальные версии ядра делаем совершенно аналогично, только вместо GetProcAddress используем wglGetProcAddress. Плюс еще у нас уже есть версия ядра после _read_version и мы вставляем дополнительную проверку в начале функции. Например


function _load_core_12: boolean;
begin
  Result := false;
  if not GL_VERSION_1_2 then exit;
  if GLHandle <> INVALID_MODULEHANDLE then begin
    glDrawRangeElements               := wglGetProcAddress('glDrawRangeElements');
    glTexImage3D                      := wglGetProcAddress('glTexImage3D');
    glTexSubImage3D                   := wglGetProcAddress('glTexSubImage3D');
    glCopyTexSubImage3D               := wglGetProcAddress('glCopyTexSubImage3D');
    Result := true;
  end;
end;

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


function _load_arb_point_parameters: boolean;
begin
  Result := false;
  //сразу выставим флажок – поддерживается ли расширение или нет
  //полный список расширений мы записали в переменную GL_EXT_STRING при вызове _read_version
  GL_ARB_point_parameters             := _checkext(GL_EXT_STRING, 'GL_ARB_point_parameters');
  //если поддерживается, то загружаем функции этого расшерения
  if GL_ARB_point_parameters then begin
    glPointParameterfARB              := wglGetProcAddress('glPointParameterfARB');
    glPointParameterfvARB             := wglGetProcAddress('glPointParameterfvARB');
    {опять ленимся сделать проверку}
    Result := true;
  end;
end;

Давайте теперь, наконец, напишем наши интерфейсные функции.

function LoadGL: boolean;
begin
  if (GLHandle = INVALID_MODULEHANDLE) then
    Result := LoadGLFromLibrary(SDefaultGLLibrary)
  else
    //библиотека уже загружена
    Result := true;
end;

Рабочая лошадка по загрузке библиотеки:


function LoadGLFromLibrary(LibName: string): boolean;
begin
  Result := false;
  {на всякий случай очищаем предыдущую реинкарнацию, чтобы не было утечек.}
  CloseGL;
 
  GLHandle  := LoadLibrary(PChar(LibName));
 
  if (GLHandle <> INVALID_MODULEHANDLE) then begin
    //загружаем ядро 1.0. при неудаче делать нам тут больше нечего будет.
    Result := _load_core_10;
    if not Result then begin
      //ошибка, подчистим за собой и выходим
      CloseGL;
      exit;
    end;
    //загружаем ядро 1.1.
    _load_core_11;
  end;
end;

Второй этап — загрузка ядра >1.1 и расширений. Тут же мы определяем версию и дополнительные параметры библиотеки.


function InitGLExtensions: boolean;
begin
  Result := false;
  try
    //определяем версию и дополнительные параметры библиотеки
    _read_version;
    //загружаем версии ядра
    _load_core_12;
    _load_core_13;
    _load_core_14;
    _load_core_15;
    //загружаем расширения
    _load_ext­­;
 
    Result := true;
  except;
  end;
end;

Напоследок, функция очистки OpenGL подсистемы.


function CloseGL: boolean;
begin
  //очищаем расширения
  _clear_ext;
  //очищаем ядро
  _clear_core_15;
  _clear_core_14;
  _clear_core_13;
  _clear_core_12;
  _clear_core_11;
  _clear_core_10;
 
  //выгружаем dllшку из памяти
  if GLHandle <> INVALID_MODULEHANDLE then begin
    FreeLibrary(GLHandle);
    GLHandle := INVALID_MODULEHANDLE;
  end;
end;

Как резюме этого раздела перепишем функции создания и удаления контекста рисования GL:

function CreateContext(DC: HDC): boolean;
var
  rc: HGLRC;
begin
  Result := false;
  //Проверим входные параметры
  if DC = 0 then exit;
 
  //загружаем ядро 1.0 и 1.1
  Result := LoadGL;
  if not Result then begin
    //не удалось инициализировать ядро 1.0
    //можно бибикнуть об ошибке, но делать нам тут больше нечего
    exit;
  end;
 
  // создаем GL контекст
  {настраиваем формат пикселей, параметры bpp,depth и stencil можно указать
  и произвольные - система подберет наиболее подходящие значения, однако
  рекомендуется указывать именно то, что вы хотите получить}
  if SetWindowPixelFormat(DC, 32, 0, 0) = 0 then exit;
 
  {теперь можно создать контекст GL}
  rc := wglCreateContext(DC);
 
  {если по каким либо причинам это не удалось (можно получить расширенную справку
  через GetLastError), то использовать OpenGL невозможно}
  if rc = 0 then exit;
 
  {назначаем GL контекст текущим}
  wglMakeCurrent(DC, rc);
 
  //второй этап инициализации OpenGL
  InitGLExtensions;
 
  //инициализируем параметры OpenGL, которые не будут меняться на
  //протяжении работы нашего приложения
  {их можно настраивать и в каждом кадре отрисовки, но зачем? только FPS снижать...}
  OneTimeSceneInit;
 
  Result := true;
end;
 
function DeleteGLContext: boolean;
var
  rc: HGLRC;
begin
  //удаляем GL контекст, только сначала подчистим за собой
  FinalCleanup;
 
  //запрашиваем текущий GL контекст
  rc := wglGetCurrentContext;
 
  //перед удалением контекста надо сбросить текущий контекст
  //OpenGL подсистемы
  wglMakeCurrent(0, 0);
 
  //а теперь само удаление
  wglDeleteContext(rc);
 
  //выгружаем OpenGL
  CloseGL;
 
  Result := true;
end; 

В заключение можно сказать, что информацию о том, какие константы и функции соответствуют какому ядру/расширению, можно почерпнуть в спецификациях на них (или подсмотреть в чужом коде). Также можно оставить оригинальный opengl.pas и свой хидер делать без ядра 1.0 – все зависит от ваших конкретных нужд. В качестве демонстрации я написал модуль glext.pas. Он является надстройкой на дельфийским opengl.pas и реализует доступ к возможностям ядра 1.1 – 1.5


Продолжение статьи
08.10.04 05:20