OpenGL в Delphi

Эту систему мы возьмем в качестве тестовой для сравнения методов создания анимации




Вся система вращается по двум осям, по оси Y вращение происходит с удвоенной скоростью:

glRotatef(2 * Angle, 0. 0, 1. 0, O. 0}; // поворот по ось Y
glRotatef(Angle, 0. 0, 0. 0, 1. 0}; // поворот по оси Z

Интервал таймера я задал равным 50 миллисекунд, т. e. экран должен обновляться двадцать раз в секунду. Попробуем выяснить, сколько кадров В секунду выводится в действительности.
Это делается в проекте из подкаталога Ex66. Введены переменные, счетчики кадров и количество кадров в секунду:

newCount, frameCount, lastCount: LongInt;
fpsRate: GLfloat;

При запуске приложения инициализируем значения:

lastCount: = GetTickCount;
frameCount: = 0; ;

Функция API GetTickCount возвращает количество миллисекунд, прошедших с начала сеанса работы операционной системы
При воспроизведении кадра определяем, прошла ли очередная секунда, и вычисляем количество кадров, выведенных за эту секунду:

newCount: = GetTickCount; // текущее условное время
Inc(frameCount); // увеличиваем счетчик кадров
If (newCount - lastCount) > 1000 then begin // прошла секунда
// определяем количество выведенных кадров
fpsRate: = frameCount * 1000 / (newCount - lastCounU;
// выводим в заголовке количество кадров
Caption: = 'FPS - ' + FloatToStr {fpsRate);
lastCount: = newCount; // запоминаем текущее время
frameCount: - 0; // обнуляем счетчик кадров
end;

Получающееся количество воспроизведенных кадров в секунду зависит от многих факторов, в первую очередь, конечно, от характеристик компьютера, и я не могу предсказать, какую цифру получите именно вы. Будет совсем неплохо, если эта цифра будет в районе двадцати.
Но если задаться целью увеличить скорость работы этого примера, то выяснится, что сделать это будет невозможно, независимо от характеристик компьютера. Сколь бы малым не задавать интервал таймера, выдаваемая частота воспроизведения не изменится, системный таймер в принципе не способен обрабатывать тики с интервалом менее пятидесяти миллисекунд Еще один недостаток такого подхода состоит в том, что если обработчик тика таймера не успевает отработать все действия за положенный интервал времени, то последующие вызовы этого обработчика становятся в очередь. Это приводит к тому, что приложение работает с разной скоростью на различных компьютерах.
Мы не будем опускать руки, а поищем другие способы анимации, благо их существует несколько. Рассмотрим еще один из этих способов (я его нахожу привлекательным), состоящий в использовании модуля MMSystem (Multimedia System). Мультимедийный таймер позволяет обрабатывать события с любой частотой, настолько часто, насколько это позволяют сделать ресурсы компьютера.
Посмотрим на соответствующий пример - проект из подкаталога Ex67 Здесь рисуются все те же пятьдесят параллелепипедов, но частота смены кадров существенно выше.
Список подключаемых модулей в секции implementation дополнился модулем MMSystem:

uses DGLUT, MMSystem;

Мультимедийный таймер также нуждается в идентификаторе, как и обычный системный, описываем его в разделе private класса формы:

TimerId: uint;

А вот процедура, обрабатывающая тик таймера, не может являться частью класса:



procedure TimeProc (uTimerID, uMessage: UINT; dwUser, dwl, dw2: DWORD; stdcall;
// значение угла изменяется каждый "тик"
With frmGL do begin
Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: - 0. 0;
InvalidateRect (HandJe, nil, False);
end;
end;

При создании окна таймер запускается специальной функцией API:

TimerID: =timeSetEvent (2, 0, @TimeProc, 0, TIME_PERTGDIC);

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

timeKillEvent (TimerID);

Самым важным в этой цепочке действий является, конечно, команда установки таймера, timeSetEvent. Первый аргумент команды - интервал таймера в миллисекундах. Второй аргумент - разрешение таймера, т. e. количество миллисекунд, ограничивающее время на отработку каждого тика таймера. Если это число задано нулем, как в нашем примере, то обработка таймера Должна происходить с максимальной точностью.

Замечание
В документации рекомендуется задавать ненулевое значение для уменьшения системных потерь

Следующий параметр - адрес функции, ответственной за обработку каждого тика. Четвертый параметр редко используется, им являются задаваемые пользователем данные возврата Последним параметром является символическая константа, при этом значение TIME_PERIODIC соответствует обычному поведению таймера.
Итак, в примере каждые две миллисекунды наращивается угол поворота системы и перерисовывается экран.
Конечно, за две миллисекунды экран не будет перерисован, однако те кадры, которые компьютер не успевает воспроизвести, не будут накапливаться. При использовании мультимедийного таймера сравнительно легко планировать поведение приложения во времени: на любом компьютере и в любой ситуации система будет вращаться с приблизительно одинаковой скоростью, просто на маломощных компьютерах повороты будут рывками В продолжение темы посмотрите проект из подкаталога Ex68, где все тот же мультфильм рисуется на поверхности рабочего стола, подобно экранным заставкам. Во второй главе мы уже рассматривали похожий пример, здесь добавлена анимация, а команда glviewPort удалена, чтобы не ограничивать область вывода размерами окна приложения.

Замечание
Напоминаю, что такой подход срабатывает не на каждой карте

Наиболее распространенным способом построения анимационных приложений является использование фоновой обработки, альтернативы таймерам. Разберем, как это делается.
В Delphi событие onidle объекта Application соответствует режиму ожидания приложением сообщений. Все, что мы поместим в обработчике этого события, будет выполняться приложением беспрерывно, пока оно находится в режиме ожидания.
Переходим к примеру, проекту из подкаталога Ex69. Сценарий приложения не меняем, чтобы можно было сравнить различные способы. Отличает пример то, что в нем отсутствуют какие-либо таймеры; код, связанный с анимацией, перешел в пользовательскую процедуру:

procedure TfrmGL. Idle (Sender: TObject; var Done: boolean);
begin
With frmGL do begin
Angle: = Angle + 0. 1;
If Angle >= З60. 0 then Angle: = 0. 0;
Done: = False; // обработка завершена
InvalidateRect{Handle, nil. False);
end;
end;

Второй параметр Done используется для того, чтобы сообщить системе, требуется ли дальнейшая обработка в состоянии простоя, или алгоритм завершен. Обычно дается False, чтобы не вызывать функцию WaitMessage.
При создании окна устанавливаем обработчик события onidle объекта Application:

Application. OnIdle: = Idle;

Вот и все необходимые действия, можете запустить приложение и сравнить этот метод с предыдущим.

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

При использовании последнего способа у нас нет никакого контроля над выполнением кода, есть только весьма приблизительное представление о том. сколько времени будет затрачено на его выполнение. Скорость работы приложения в этом случае полностью зависит от загруженности системы, другие приложения могут с легкостью отнимать ресурсы, и тогда работа нашего приложения замедлится, Запустите одновременно приложения, coответствующие мультимедийному таймеру и фоновой обработке Активное приложение будет генерировать большую частоту кадров, но приложение, построенное на таймере, менее болезненно реагирует на потерю фокуса,
В таких случаях можно повышать приоритет процесса, этот прием мы рассмотрим в главе 5.
Замечу, что при запуске приложения или изменении ею размеров требуется несколько секунд, чтобы работа вошла в нормальный режим, поэтому первые цифры, выдаваемые в качестве частоты воспроизведения, не являются особо надежными.
Теперь посмотрим, как реализовать фоновый режим в проектах, основанных только на функциях API. Это иллюстрирует проект из подкаталога Ex70.
Пользовательская функция idle содержит код, связанный с изменениями кадра.
Для отслеживания состояния активности окна заведен флаг AppActive, а оконная функция дополнилась обработчиком сообщения, связанного с активацией окна:

WM__ACTIVATEAPP:
If (wParam = WMACTIVE) or (wParam = WM__CLICKACTIVE)
then AppActive: = True
else AppActive: = False;

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

While True do begin
// проверяем очередь на наличие сообщения
If PeekMessage (Message, 0, 0, 0, pm_NoRemove) then begin
// в очереди присутствует какое-то сообщение
If not GetMessage(Message, 0, 0, 0)
then Break // сообщение WM_QUIT, прервать вечный цикл
else begin // обрабатываем сообщение
TranslateMessage(Message);
DispatchMessage(Message);
end;
end
else // очередь сообщений пуста
If AppActive
then Idle // приложение активно, рисуем очередной кадр
else WaitMessage; // приложение не активно, ничего не делаем
end;

Надеюсь, все понятно по комментариям, приведу только небольшие пояснения.
Функция PeekMessage с такими параметрами, как в этом примере, не удаляет сообщение из очереди, чтобы обработать его в дальнейшем традиционным способом.
Функция idle в этом примере вызывается при пустой очереди только в случае активности приложения.
Код можно немного сократить, если не акцентироваться на том, активно ли приложение; в данном же случае минимизированное приложение "засыпает". не внося изменений в кадр.
Рассмотрим еще один прием, использующийся для построения анимационных приложений и заключающийся в зацикливании программы. Для начала приведу самый простой способ зацикливания программы (проект из подкаталога Ex71).
Сценарий не изменился, рисуется все то же подобие шестерни. Никаких таймеров и прочих приемов, просто обработчик перерисовки окна заканчивается приращением управляющей переменной и командой перерисовки региона (окна):

Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: = 0. 0;
InvalidateRect(Handle, nil, False);

Все просто: воспроизведя очередной кадр, подаем команду на воспроизведение следующего.
В этом методе, как, впрочем, и в предыдущем, при уменьшении размеров окна частота кадров увеличивается, но и вращение происходит быстрее, здесь так же отсутствует контроль за поведением системы.
Приложение "замирает", будучи минимизированным
Если присмотреться внимательнее к поведению этого примера, то можно заметить некоторые необычные вещи, например, при наведении курсора на системные кнопки в заголовке окна подсказки не появляются. А при попытке активизации системного меню окна появляется явная накладка в работе приложения, область меню требуется перерисовать, двигая курсор в пределах его границ. Короче, зацикливание программы приводит к тому. что ожидающие сообщения могут и не обрабатываться. Может, это и не страшно, но ведь у нас нет гарантии, что мы обнаружили все странности работы приложения.
Решение проблемы состоит в использовании функции processMessages объекта Application, приостанавливающей работу приложения, чтобы система могла обрабатывать сообщения из очереди
Посмотрим на проект из подкаталога Ex72. Перед перерисовкой окна обрабатываем все сообщения очереди, вызвав функцию ProcessMessages, однако этого добавления не достаточно, иначе приложение невозможно будет закрыть. В примере введен вспомогательный флаг closed, принимающий истинное значение при попытке пользователя или системы закрыть приложение:

procedure TfrmGL. FormCloseQuery(3ender: TObject; var Car. Close: Boolean;
begin
Closed: = True end;

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

If not Closed then begin
Angle: = Angle + 0. 1;
If Angle >= 360. 0 then Angle: = 0, 0;
Application. ProcessMessages;
InvalidateRect{Handle, nil, False);
end;

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

Canvas. TextOut (0, 0, 'FPS - ' + FloatToStr (fpsRate));

При выводе на поверхность окна с помощью функций GDI не должно возникать проблем ни с одной картой, а вот если попытаться текст выводить на метку, то проблемы, скорее всего, возникнут со всеми картами: метка не будет видна.
Следующий пример, проект из подкаталога Ex73, является очередным моим переводом на Delphi классической программы, изначально написанной на С профессиональными программистами корпорации Silicon Graphics. Экран заполнен движущимися точками так, что у наблюдателя может появиться ощущение полета в космосе среди звезд (Рисунок 3. 39).



Содержание раздела