Date Редакция Категория comp Теги OpenGL / Cpp

В литературе по 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, и не был бы виден при заданных параметрах перспективной проекции (хотя можно изменить эти параметры, не перемещая треугольник).

Посмотрим, как эта программа будет работать.

  1. Создается графическое окно с заданными размерами, расположением и заголовком.
  2. Запускается функция reshape(), которая задает матрицы проекции и модели.
  3. На экране отображается то, что указано в функции display() — треугольник, повернутый на угол rot.
  4. Поскольку никаких событий не происходит (точнее, нас они пока не интересуют), то запускается функция "времени бездействия" — 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