В литературе по OpenGL распространены два подхода к анимации. Рассмотрим их на примере программы, рисующей вращающийся треугольник.
#include <GL/glut.h>
// Глобальные переменные:
int startTime; // начальный момент времени, мс
static GLfloat rot = 0.0f;
static GLfloat rotRate = 45.0f; // скорость вращения, градус/секунда
void initGraphics()
{
glClearColor(0.0, 0.0, 0.0, 0.0);
// ###### другие команды инициализации графики ######
}
void display()
{
// Очищаем окно (буфер цвета)
glClear(GL_COLOR_BUFFER_BIT);
// Задаем перемещение и вращение фигуры
glLoadIdentity();
glTranslatef(0.0, 0.0, -3.0);
glRotatef(rot, 0.0, 0.0, 1.0);
// Рисуем треугольник
glBegin(GL_TRIANGLES);
glColor3f(1.0, 0.0, 0.0);
glVertex3f(-1.0, -1.0, 0.0);
glColor3f(0.0, 1.0, 0.0);
glVertex3f(1.0, -1.0, 0.0);
glColor3f(0.0, 0.0, 1.0);
glVertex3f(0.0, 1.0, 0.0);
glEnd();
glFlush();
glutSwapBuffers();
}
void reshape(int width, int height)
{
// Предотвращаем деление на ноль
if (height <= 0) height = 1;
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (double)width/(double)height, 1.0, 500.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case 27: // клавиша Escape
exit(0);
break;
default:
return;
}
// Принудительная перерисовка после нажатия клавиши
glutPostRedisplay();
}
void animate()
{
// Измеряем прошедшее время
int currTime = glutGet(GLUT_ELAPSED_TIME);
int elapsedTime = currTime - startTime;
// Вычисляем угол поворота треугольника
rot = (rotRate / 1000) * elapsedTime;
glutPostRedisplay();
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
// #1: Инициализация и создание окна GLUT
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
glutInitWindowSize(640, 480);
glutInitWindowPosition(0, 0);
glutCreateWindow("Animation");
initGraphics();
// #2: Регистрация функций-обработчиков событий
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutIdleFunc(animate);
// Получаем значение времени старта, мс
startTime = glutGet(GLUT_ELAPSED_TIME);
// #3: Запуск основного цикла GLUT
glutMainLoop();
}
По структуре эта программа мало чем отличается от рассмотренных ранее. Подробного рассмотрения заслуживают только функции display()
и animate()
.
В display()
мы рисуем не готовую фигуру, а нечто новое, хотя и очень простое:
(:source lang=C++
// Рисуем треугольник
glBegin(GL_TRIANGLES);
glColor3f(1.0, 0.0, 0.0);
glVertex3f(-1.0, -1.0, 0.0);
glColor3f(0.0, 1.0, 0.0);
glVertex3f(1.0, -1.0, 0.0);
glColor3f(0.0, 0.0, 1.0);
glVertex3f(0.0, 1.0, 0.0);
glEnd();
Перед тем как нарисовать, мы сдвигаем треугольник вглубь по оси `z` и поворачиваем вокруг оси `x` на величину, заданную переменной `rot`.
```C++
// Задаем перемещение и вращение фигуры
glLoadIdentity();
glTranslatef(0.0, 0.0, -3.0);
glRotatef(rot, 0.0, 0.0, 1.0);
Треугольник сдвигается затем, чтобы его можно было увидеть. Иначе бы он находился в плоскости z = 0
, и не был бы виден при заданных параметрах перспективной проекции (хотя можно изменить эти параметры, не перемещая треугольник).
Посмотрим, как эта программа будет работать.
- Создается графическое окно с заданными размерами, расположением и заголовком.
- Запускается функция
reshape()
, которая задает матрицы проекции и модели. - На экране отображается то, что указано в функции
display()
— треугольник, повернутый на уголrot
. - Поскольку никаких событий не происходит (точнее, нас они пока не интересуют), то запускается функция "времени бездействия" —
animate()
. Она пересчитывает угол поворота треугольникаrot
и принудительно обновляет содержимое окна, то есть опять вызываетdisplay()
. Так возникает цикл анимации из шагов 3 и 4, который будет продолжаться до тех пор, пока мы не закроем приложение.
Обратите внимание на функцию glLoadIdentity()
. Дело в том, что в OpenGL все изменения вносятся относительно текущего состояния. Например, если display()
"задвигает" треугольник на 3 единицы вдоль оси z
, то после первого вызова display()
он окажется в плоскости z = -3
, после второго — в z = -6
, после третьего — в z = -9
и т. д. Если закомментировать glLoadIdentity()
, то мы увидим удаляющийся от нас треугольник вращающийся все быстрей и быстрей. glLoadIdentity()
возвращает модель к исходному состоянию. Кстати, если мы уберем из display
очистку glClear(GL_COLOR_BUFFER_BIT)
, то будем наблюдать в окне не только нынешнее положение треугольника, но и всю их "историю".
В функции animate()
для вычисления угла поворота rot
используется время, прошедшее с запуска основного цикла GLUT — elapsedTime
. Функция glutGet()
с параметром GLUT_ELAPSED_TIME
возвращает время (в миллисекундах), прошедшее с момента запуска GLUT (вызова glutInit()
).
Отметим также реализацию выбора реакции на нажатие клавиш через switch..case
, что позволяет гибко настраивать ее, добавляя блоки case
:
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case 27: // клавиша Escape
exit(0);
break;
default:
return;
}
...
}
Основой рассмотренного выше подхода к анимации являлось использование функции "времени бездействия", заданной в glutIdleFunc()
. Существует и другой подход, при котором перерисовка изображения выполняется периодически, по сигналу таймера.
Следующая программа делает то же, что и предыдущая, но другим способом, поэтому повторяющиеся фрагменты кода не показаны.
#include <GL/glut.h>
// Глобальные переменные:
...
unsigned timerMSecs = (unsigned) 1000/60; // время между запусками таймера, мс
...
void animate(int value)
{
// Измеряем прошедшее время
int currTime = glutGet(GLUT_ELAPSED_TIME);
int elapsedTime = currTime - startTime;
// Вычисляем угол поворота треугольника
rot = (rotRate / 1000) * elapsedTime;
glutPostRedisplay();
// Перезапускаем таймер
glutTimerFunc(timerMSecs, animate, 0);
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
// #1: Инициализация и создание окна GLUT
...
// #2: Регистрация функций-обработчиков событий
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutTimerFunc(timerMSecs, animate, 0);
// Получаем значение времени старта, мс
startTime = glutGet(GLUT_ELAPSED_TIME);
// #3: Запуск основного цикла GLUT
glutMainLoop();
}
Вместо glutIdleFunc()
используется glutTimerFunc()
:
glutTimerFunc(unsigned int время_в_мс, функция_анимации, int значение)
время_в_мс
означает время (в миллисекундах), через которое будет запущена функция_анимации
; значение
нас сейчас не интересует.
Таким образом, через timerMSecs
после запуска основного цикла GLUT будет вызвана функция animate()
, которая вычислит угол поворота, заставит перерисовать изображение и, наконец, — задаст, через сколько времени будет вновь вызвана функция анимации.
glutTimerFunc()
запускает animate()
, которая снова вызывает glutTimerFunc()
... Не столкнулись ли мы с бесконечной рекурсией? Нет, в случае OpenGL это не так. Можно сказать, что glutTimerFunc()
добавляет еще одну операцию к череде операций, выполняемых OpenGL: посчитать, перерисовать, вызвать через N миллисекунд заданную функцию.
Выбор промежутка времени между запусками таймера диктуется возможностями видеоадаптера.
unsigned timerMSecs = (unsigned) 1000/60; // время между запусками таймера, мс
В данном случае предполагается, что частота обновления экрана составляет 60 Гц (1/60 c). Если задать обновления более редкими, то мы увидим, что треугольник будет двигаться "рывками".
Комментарии
comments powered by Disqus