Home Index Search Links About Us
[LinuxFocus Image]
[Navegation Bar]
Новости Архивы Компании Подсказки

Программирование OpenGL: Рендеринг простых многоугольников

Автор Miguel Angel Sepulveda


Введение

Рисование точек

Рисование линий и многоугольников

Упражнения

В следующий раз...

Введение

Это первая статья в серии, посвященной OpenGL, который стал промышленным стандартом в 2D/3D графике (смотрите так же Что такое OpenGL?). Мы предполагаем, что читатель знаком со своей платформой для разработки программ на языке С, и немного знаком с библиотекой GLUT (в противном случае, ознакомьтесь с серией статей посвященных программированию GLUT в нашем журнале). Под Линуксом мы рекомендуем пользоваться библиотекой Mesa, которая является великолепной бесплатной реализацией стандарта OpenGL. В настоящее время есть даже аппаратная поддержка Mesa (смотрите графические карты 3Dfx).

Каждая новая команда OpenGL будет сопровождаться примером, поясняющим ее работу; по крайней мере, мы постараемся придерживаться этого правила! К концу нашей серии статей мы представим Вашему вниманию исходный код игры-симулятора, полностью написанный на OpenGL.

Прежде, чем я начну повествование, я хотел бы предупредить Вас, что я ученый, и почти весь мой опыт в общении с OpenGL заключался в создании моделей настоящих квантовых и классических систем. Поэтому мои примеры имеют легкий уклон ;-) Я надеюсь, читатели найдут эти примеры доступными для восприятия или, по крайней мере, забавными. Если Вы захотите взглянуть на другие примеры, просто дайте мне знать.

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

Рисование точек

OpenGL имеет всего лишь несколько геометрических примитивов: точки, линии и многоугольники. Все они описываются в терминах их соответствующих вершин. Вершина характеризуется двумя или тремя числами в формате с плавающей запятой, которые представляют собой Декартовы координаты этой вершины, (x,y) в двух- и (x, y, z) в трехмерном пространстве. Хотя Декартовы координаты являются наиболее общеупотребимыми, в компьютерной графике зачастую используется Гомогенная система координат, в которой каждая точка описывается четырьмя числами в формате с плавающей запятой, (x, y, z, w). Мы вернемся к системам координат после того, как опишем некоторые элементарные понятия трехмерного рендеринга.

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

void glVertex{234}{sifd}[v](TYPE coords); 

Разберитесь хорошенько с приведенной записью. (Фигурные скобки представляют набор возможных и обязательных значений, например {234} означает 2 или 3 или 4, в то время как значение в квадратных скобках опционально и может быть опущено - Прим. Пер.) Фигурные скобки показывают часть имени функции. Функции могут иметь 2, 3 или 4 параметра в форматах: короткое целое, длинное целое, число с плавающей запятой или число с двойной точностью. Эти параметры опционально могут быть представлены в векторной форме, и в этом случае мы будем пользоваться функциями v-типа. Вот несколько примеров:

void glVertex2s(1, 3);
void glVertex2i(23L, 43L);
void glVertex3f(1.0F, 1.0F, 5.87220972F);

float vector[3];
void glVertex3fv(vector);

Для простоты все приведенные функции имеют общее имя glVertex*.

Любая последовательность вершин интерпретируется OpenGL в соответствии с контекстом. Контекст объявляется парой функций glBegin(GLenum mode) и glEnd(), любая функция glVertex* между приведенными выше функциями контекста интерпретируется в соответствии со значением режима - mode, например:
glBegin(GL_POINTS);
glVertex2f(0.0, 0.0);
glVertex2f(1.0, 0.0);
glVertex2f(0.0, 1.0);
glVertex2f(1.0, 1.0);
glVertex2f(0.5, 0.5);
glEnd();

нарисует нам 5 точек в двухмерном пространстве с указанными координатами. Константа GL_POINTS наряду с другими объявлена в заголовочном файле <GL/gl.h>. Есть много других режимов, но мы будем рассматривать их по мере необходимости 

Каждая точка рисуется цветом, хранящимся в переменной состоянии OpenGL, ассоциированной с буфером цветов. Чтобы изменить текущий цвет, используйте набор функций glColor*; о выборе цвета и манипуляции с ним можно говорить долго (этому будет посвящена отдельная статья). Пока же мы будем использовать три числа в формате с плавающей точкой в диапазоне от 0,0 до 1,0 в кодировке RGB (Red-Green-Blue, Красный-Зеленый-Синий);
glColor3f(1.0, 1.0, 1.0); /* Белый */ 
glColor3f(1.0, 0.0, 0.0); /* Красный */ 
glColor3f(1.0, 1.0, 0.0); /* Сиреневый */ 
итд... 

Скачайте: ../../common/January1998/Makefile, ../../common/January1998/../../common/January1998/example1.c, ../../common/January1998/../../common/January1998/example2.c

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

yn+1 = yn + K sin(xn)
xn+1 = xn + yn+1

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

Перед вами код первого примера ../../common/January1998/../../common/January1998/example1.c:

    
#include <GL/glut.h>
#include <math.h>  

const  double pi2 = 6.28318530718;

void NonlinearMap(double *x, double *y){
    static double K = 1.04295;
    *y += K * sin(*x);
    *x += *y;
    *x = fmod(*x, pi2);
    if (*x < 0.0) *x += pi2;
};

void winInit(){
    /* Установка системных координат */
    gluOrtho2D(0.0, pi2, 0.0, pi2);
};

void display(void){
    const int    NumberSteps = 1000;
    const int    NumberOrbits = 100;
    const double Delta_x = pi2/(NumberOrbits-1);
    int step, orbit;

    glColor3f(0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1.0, 1.0, 1.0);

    for (orbit = 0; orbit < NumberOrbits; orbit++){
      double x, y;
      y = 3.1415;
      x = Delta_x * orbit;

      glBegin(GL_POINTS);
      for (step = 0; step < NumberSteps; step++){
        NonlinearMap(&x, &y);
        glVertex2f(x, y);
      };
    
      glEnd();
    };

    for (orbit = 0; orbit < NumberOrbits; orbit++){
      double x, y;
      x = 3.1415;
        y = Delta_x * orbit;

        glBegin(GL_POINTS);
        for (step = 0; step < NumberSteps; step++){
          NonlinearMap(&x, &y);
          glVertex2f(x, y);
        };
        glEnd();

     };
};

int main(int argc, char **argv) {
  glutInit(&argc, argv);  
  glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);  
  glutInitWindowPosition(5,5);  
  glutInitWindowSize(300,300);  
  glutCreateWindow("Standard Map");  
  
  winInit();
  glutDisplayFunc(display);  
  glutMainLoop();  
  
  return 0;  
}  
  
Прочтите статью Программирование GLUT для понимания функций glut* , код в основном взят из той статьи. Графическое окно открывается в режиме одиночного буфера и RGB-палитры. Вызываемая функция с именем display() рисует карту: вначале мы выбираем черный цвет для фона; glClear(GL_COLOR_BUFFER_BIT) сбрасывает буфер цветов, устанавливая текущий цвет (черный), затем после выбора белого цвета с помощью glColor, запускает NonlinearMap() (Нелинейная Карта) множество раз и рисует точки функциями glVertex* в режиме GL_POINTS. Очень просто. :-)

Заметьте, что функция инициализации окна winInit() содержит всего один вызов функции набора утилит OpenGL (GLUT), gluOrtho2D(). Эта функция устанавливает двухмерную прямоугольную систему координат. Параметрами функции являются "минимум x, максимум x, минимум y, максимум y".
Я выбрал режим одиночного окна и большое количество точек, чтобы у Вас был шанс посмотреть, как изображение прорисовывается. Обычно в режиме одиночного окна с большими изображениями, которые требуют много времени для построения, детали появляются на экране в том порядке, как вызываются функции OpenGL.

По окончании работы примера вы увидите такое изображение:

 

Давайте перейдем ко второй программе, ../../common/January1998/../../common/January1998/example2.c:

    
#include <GL/glut.h> 
#include <math.h>

const  double  pi2 = 6.28318530718; 
const  double  K_max = 3.5;
const  double  K_min = 0.1;
static double  Delta_K = 0.01;
static double  K = 0.1;          


void NonlinearMap(double *x, double *y){
    /* Стандартная карта */
    *y += K * sin(*x);
    *x += *y;

    /* Angle x is module 2Pi */
    *x = fmod(*x, pi2);
    if (*x < 0.0) *x += pi2;
};


/* Вызываемая функция: 
   Что будет происходить в отсутствие действий пользователя */
void  idle(void){
    /* Увеличиваем хаотическую составляющую */
    K += Delta_K;
    if(K > K_max) K = K_min;

    /* Перерисовываем экран */
    glutPostRedisplay();
};


/* Инициализация графического окна */
void winInit(void){
    gluOrtho2D(0.0, pi2, 0.0, pi2);
};

/* Вызываемая функция:
    Что будет происходить, когда экран нуждается в перерисовке */
void display(void){
    const int    NumberSteps = 1000;
    const int    NumberOrbits = 50;
    const double Delta_x = pi2/(NumberOrbits-1);
    int step, orbit;

    glColor3f(0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
    glColor3f(1.0, 1.0, 1.0);
                
    for (orbit = 0; orbit < NumberOrbits; orbit++){
        double x, y;
        y = 3.1415;
        x = Delta_x * orbit;

        glBegin(GL_POINTS);
        for (step = 0; step < NumberSteps; step++){
          NonlinearMap(&x, &y);
          glVertex2f(x, y);
        };
        glEnd();
     };

     for (orbit = 0; orbit < NumberOrbits; orbit++){
        double x, y;
        x = 3.1415;
        y = Delta_x * orbit;

        glBegin(GL_POINTS);
        for (step = 0; step < NumberSteps; step++){
          NonlinearMap(&x, &y);
          glVertex2f(x, y);
        };
        glEnd();
     };

     glutSwapBuffers();
};


int main(int argc, char **argv)  {  
  /* Инициализация GLUT */
  glutInit(&argc, argv);  
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);  
  glutInitWindowPosition(5,5);  
  glutInitWindowSize(300,300);
  
  /* Открываем окно */
  glutCreateWindow("Order to Chaos");  
  
  /* Инициализируем окно */
  winInit();

  /* Регистрируем вызываемые вункции */
  glutDisplayFunc(display);  
  glutIdleFunc(idle);

  /* Запускаем обработчик событий */
  glutMainLoop();  
  
  return 0;  
}  
  
    
    
Эта программа основана на первом примере, ../../common/January1998/../../common/January1998/example1.c, и главные отличия заключаются в том, что окно открывается в режиме двойного буфера, а параметр К меняется в течение работы программы. Есть так же новая вызываемая функция idle(), которая регистрируется в обработчике событий GLUT функцией glutIdleFunc(). У этой функции особое назначение - она вызывается каждый раз, как обработчик событий по завершению своего цикла не нашел никаких действий со стороны пользователя. Вызываемая функция idle() идеально подходит для создания анимации. Во втором примере, ../../common/January1998/example2, назначением функции является небольшое изменение параметра карты. В конце функции idle() присутствует вызов другой часто употребляемой функции, glutPostResDisplay(), которая перерисовывает окно, сохраняя предыдущую инициализацию. В общем, это более эффективно, чем простой повторный вызов функции display().

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

Перед Вами некоторые из кадров, отображаемых во время анимации:

ВАЖНОЕ ЗАМЕЧАНИЕ: вызываемая функция display() всегда вызывается как минимум один раз перед функцией idle(). Принимайте этот факт во внимание при написании собственных анимационных программ, для того, чтобы решить, какие команды поместить в функцию display() а какие - в idle().

Рисование линий и многоугольников

Скачайте:
../../common/January1998/../../common/January1998/example3.c

Как мы уже упоминали раньше, glBegin(GLenum mode) принимает в качестве параметра разные режимы (mode). Последовательность вершин v0, v1,v2,v3,v4,... vn-1 объявленных после этой функции интерпретируется соответственно выбранному режиму. Возможные значения для режимов и соответствующих действий:

  • GL_POINTS    Точки. Рисует точку в каждой из n вершин
  • GL_LINES    Линии. Рисует набор несоединенных друг с другом линий. Отрезки рисуются между парами вершин v0 и v1, v2 и v3,...итд. Если n - нечетное число, то vn-1 игнорируется
  • GL_POLYGON    Многоугольник. Рисует многоугольник, используя v0, v1,..,vn-1 как вершины. n должно быть равно минимум 3 чтобы что-либо было нарисовано, кроме того, многоугольник не может пересекаться сам с собой и должен быть выпуклым (из-за ограничений алгоритмов, реализованных аппаратно).
  • GL_TRIANGLES    Треугольники. Рисует серию треугольников, используя тройки вершин v0, v1 и v2, затем v3, v4 и v5 итд. Если n не кратно 3, оставшиеся точки игнорируются
  • GL_LINE_STRIP    Ломанная или полоса. Рисует линию из v0 в v1, затем из v1 в v2 итд. Наконец, из vn-2 в vn-1, всего n-1 отрезков ломанной. Какие-либо ограничения на расположения вершин ломанной не накладываются, линии могут произвольно пересекаться
  • GL_LINE_LOOP    Замкнутая ломанная. То же, что и ломанная GL_LINE_STRIP, за исключением того, что последний отрезок рисуется между vn-1 и v0, замыкая круг
  • GL_QUADS    Четырехугольники. Рисует серию четырехугольников, используя по четыре вершины v0, v1, v2, v3 затем v4, v5, v6, v7 итд.
  • GL_QUAD_STRIP    Полоса четырехугольников. Рисует серию четырехугольников, используя вершины v0, v1, v3, v2 затем v2, v3, v5, v4 итд.
  • GL_TRIANGLE_STRIP    Полоса треугольников. Рисует серию треугольников, используя вершины в следующем порядке: v0, v1, v2, затем v2, v1, v3, затем v2, v3, v4, итд. Порядок вершин должен гарантировать, что треугольники имеют правильную ориентацию, и полоса может использоваться для формирования части поверхности.
  • GL_TRIANGLE_FAN    Похоже на GL_TRIANGLE_STRIP, но треугольники используют вершины в таком порядке: v0, v1, v2, затем v0, v2, v3, затем v0, v3, v4, итд. Все треугольники имеют общую вершину v0
В нашем третьем примере мы используем GL_LINES и GL_POLYGON для создания новой анимации. Откомпилируйте программу, затем взгляните на исходный код, чтобы понять, как она работает. В основном она очень похожа на второй пример, но изображение в новом примере - это очень простой маятник. Анимация моделирует движение идеального маятника. Вот кадр из анимации:

Как и прежде, присутствует вызываемая функция idle() целью которой в данном случае является поддержание хода часов (изменением переменной time). Функция display() рисует два объекта: отвес (шнурок) и диск (белым и красным цветами соответственно). Движение маятника сокрыто в формулах для xcenter и ycenter: 

    

void display(void){
  static double radius = 0.05;
  const double delta_theta = pi2/20;
  double xcenter , ycenter;  
  double x, y;
  double theta = 0.0;

  double current_angle = cos(omega * time);

  glColor3f(0.0, 0.0, 0.0);
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3f(1.0, 1.0, 1.0);

  /* Рисуем отвес */  
  glColor3f(1.0, 1.0, 1.0);
  glBegin(GL_LINES);
  glVertex2f(0.0, 0.0);
  xcenter = -cord_length * sin(current_angle);
  ycenter = -cord_length * cos(current_angle);
  glVertex2f(xcenter, ycenter);
  glEnd();

  /* Рисуем диск */
  glColor3f(1.0, 0.0, 0.0);
  glBegin(GL_POLYGON);
  while (theta <= pi2) {
    x = xcenter + radius * sin(theta);
    y = ycenter + radius * cos(theta);
    glVertex2f(x, y);
    theta += delta_theta;
  };
  glEnd();
  
  glutSwapBuffers();
};

    
    

Упражнения

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

  • В первом примере, ../../common/January1998/../../common/January1998/example1.c попробуйте другие карты. Возьмите в библиотеке любую книгу по Фракталам, в которой Вы несомненно найдете множество примеров. Поэкспериментируйте, изменяя параметры, систему координат, применяя последовательно несколько карт перед отрисовкой точек. Получите удовольствие от игры с примером. :-)
  • Во втором примере, ../../common/January1998/../../common/January1998/example2.c , вы можете добавить цвет к каждой точке. Например, очень интересное цветовое кодирование может быть основано на присвоении каждой точке цвета в соответствии с локальной стабильностью орбиты. (Из обзора Physics Review Letters Vol 63, (1989) 1226), когда траектория проходит через хаотический регион, она становится более красной. Например, вблизи островков стабильности орбита может становиться более синей. Если Вы воплотите этот эффект, фрактальная природа карты из нашего примера станет очевидной. Это немного сложно для тех из Вас, кто не имел дела с дифференциальными уравнениями, но заслуживает изучения, если Вы хотите использовать карты и фракталы в своей компьютерной графике.
  • В третьем примере, ../../common/January1998/../../common/January1998/example3.c , попробуйте изменить тип линий, используемых для рисования диска. Используйте GL_LINES, GL_TRIANGLES, итд.. Посмотрите, что получится. Попробуйте оптимизировать рисование диска, нет необходимости каждый раз вычислять синусы и косинусы для рисования одного и того же диска в каждом кадре, Вы можете сохранить значения в массиве. Используйте многоугольники, чтобы нарисовать коробку, алмаз, или что еще придет в голову вместо диска на конце маятника. Нарисуйте два маятника в кадре, качающихся независимо или даже бьющихся друг об друга.

В следующий раз...

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

Перевод: Vladas Lapinskas


Дальнейшая информация:
© 1998 Miguel Angel Sepúlveda
This website is mantained by Miguel A Sepulveda.