Урок 12. Использование списков отображения

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

Например. Скажем, вы создаете в игре астероиды. Каждый уровень начинается как минимум с двух астероидов. Итак, вы сидите с вашей графической бумагой ( :)) ) и рисуете, как можно создать 3D астероиды. После того, как вы все нарисовали, вы создаете астероид в OpenGL, используя полигоны или четырехугольники. Скажем астероид - это октагон (восемь сторон). Если вы умны, вы создаете цикл, и рисуете астероид один раз в цикле. Вы сделаете это, написав 18 или более небрежных строк кода, чтобы создать астероид. Создание астероида, перерисовка его на экран трудоемка для вашей системы. Когда вы возьмете более сложные объекты вы увидите, что я имел ввиду.

Ну и какое решение? Списки Отображения!!! Используя список отображения, вы создаете объект лишь один раз. Вы можете текстурировать его, раскрасить его, все что хотите. Вы даете списку название. Поскольку это астероид мы назовем список 'asteroid'. Теперь, всякий раз, когда я хочу нарисовать текстурированный/раскрашенный астероид на экране, все, что мне требуется сделать это вызов glCallList(asteroid). Предварительно созданный астероид немедленно появится на экране. Так как астероид уже создан в списке отображения, не нужно указывать OpenGL, как создать его. Он уже создан в памяти. Это снижает нагрузку на процессор и позволяет вашим программам выполняться быстрее.

Итак, вы готовы учиться? :). Мы назовем эту демонстрацию списков отображения Q- Bert. В этой демке мы создадим 15 кубиков на экране Q-Bert'а. Каждый кубик изготавливается из крышки (TOP) и коробки (BOX). Крышка будет отдельным списком отображения, так мы сможем закрасить ее в более темные цвета (как тень). Коробка - это куб без крышки :).

Этот код основан на уроке 6. Я перепишу большую часть программы - так будет легче видеть, где я внес изменения. Следующие строки - код, стандартно используемый во всех уроках.

#include <windows.h>      //Заголовочный файл для Windows
#include <stdio.h>        //Заголовочный файл стандартного ввода/вывода
#include <gl\gl.h>        //Заголовочный файл библиотеки OpenGL32
#include <gl\glu.h>       //Заголовочный файл библиотеки GLu32
#include <gl\glaux.h>     //Заголовочный файл библиотеки GLaux

HDC             hDC=NULL;       //Приватный контекст GDI устройства

HGLRC           hRC=NULL;       //Постоянный контекст отображения
HWND            hWnd=NULL;      //Содержит дискриптор нашего окна
HINSTANCE       hInstance;      //Содержит экземпляр приложения

bool    keys[256];              //Массив применяемый для подпрограммы клавиатуры
bool    active=TRUE;            //Флаг "Активное" окна устанавливается истинным (TRUE)
                                // по умолчанию.
bool    fullscreen=TRUE;        //Флаг "На полный экран" устанавливается в полноэкранный
                                // режим по умолчанию.

Теперь объявим наши переменные. Первым делом зарезервируем место для одной текстуры. Затем создадим две новых переменных для наших двух списков отображения. Эти переменные - указатели на списки отображения, хранящиеся в ОЗУ. Назовем их коробка (box) и крышка (top).

Затем заведем еще 2 переменные xloop и yloop, которые используем для задания позиций кубов на экране и 2 переменные xrot и yrot для вращения по осям x и y.

GLuint  texture[1];     // Память для одной текстуры
GLuint  box;            // Память для списка отображения box (коробка)
GLuint  top;            // Память для второго списка отображения top (крышка)
GLuint  xloop;  // Цикл для оси x
GLuint  yloop;  // Цикл для оси y

GLfloat xrot;   // Вращает куб на оси x
GLfloat yrot;   // Вращает куб на оси y

Далее создаем два цветовых массива. Первый, назовем его boxcol, хранит величины для следующих цветов: ярко-красного (Bright Red), оранжевого (Orange), желтого (Yellow), зеленого (Green) и голубого (Blue). Каждое значение внутри фигурных скобок представляет значения красного, зеленого и голубого цветов. Каждая группа внутри фигурных скобок - конкретный цвет.

Второй массив цветов мы создаем для следующих цветов: темно-красного (Dark Red), темно-оранжевого (Dark Orange), темно-желтого (Dark Yellow), темно-зеленого (Dark Green) и темно-голубого (Dark Blue). Темные цвета будем использовать для окрашивания крышки коробок. Мы хотим, чтобы крышка была темнее коробки.

static GLfloat boxcol[5][3]=    //Массив для цветов коробки
{

//Яркие: Красный, Оранжевый, Желтый, Зеленый, Голубой
{1.0f,0.0f,0.0f},{1.0f,0.5f,0.0f},{1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f}
};

static GLfloat topcol[5][3]=    //Массив для цветов верха
{
//Темные: Красный, Оранжевый, Желтый, Зеленый, Голубой
{.5f,0.0f,0.0f},{0.5f,0.25f,0.0f},{0.5f,0.5f,0.0f},{0.0f,0.5f,0.0f},{0.0f,0.5f,0.5f}
};

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //Объявление для WndProc

Теперь мы создадим собственно список отображения. Если вы помните, весь код для создания коробки в первом списке, а для создания верха - в другом списке. Я попытаюсь разъяснить эту часть более детально.

GLvoid BuildLists()     //создаем список отображения
{

Мы начинаем, указывая OpenGL создать 2 списка. glGenLists(2) создает место для двух списков, и возвращает указатель на первый из них. 'box' будет хранить расположение первого списка. Всякий раз при вызове box будет нарисован первый список.

box=glGenLists(2);      //создаем два списка

Теперь мы начнем создавать первый список. Мы уже выделили память для двух списков, и мы знаем, что box указывает на место, где мы храним первый список. Все, что нам теперь надо сделать - это сказать OpenGL, где должен начаться список, какой тип списка сделать.

Мы используем для этого команду glNewList(). Укажите box в качестве первого параметра. Этим вы укажете OpenGL место хранения списка, на которое указывает переменная box. Второй параметр GL_COMPILE говорит OpenGL, что мы хотим предварительно создать список в памяти, таким образом, OpenGL не будет вычислять, как создать объект, всякий раз как мы хотим нарисовать его.

С флагом GL_COMPILE как в программирование. Если вы пишете программу и транслируете ее компилятором вы компилируете ее всякий раз, когда хотите запустить ее. Если же она уже скомпилирована в EXE файл, все, что вам нужно сделать - это запустить ее, кликнув на файле. Нет необходимости перекомпилировать ее заново. OpenGL компилирует список отображения только один раз, после этого он готов к применению, больше компилировать не надо. Поэтому мы получаем увеличение скорости при использовании списков отображения.

glNewList(box,GL_COMPILE);      // Новый откомпилированный список отображения box

В следующей части кода нарисуем коробку без верха. Она не окажется на экране. Она будет сохранена в списке отображения.

Вы можете написать любую команду, какую только захотите, между glNewList() и glEndList(). Вы можете задавать цвета, менять текстуры и т.д. Единственный тип кода, который вы не можете использовать - это код, который изменит список отображения "на лету". Однажды создав список отображения, вы не можете его изменить.

Если вы добавите строку glColor3ub(rand()%255,rand()%255,rand()%255) в нижеследующий код, вы можете подумать, что каждый раз, когда вы рисуете объект на экране, он будет другого цвета. Но так как список создается лишь однажды, цвет не будет изменяться всякий раз, как вы рисуете его на экране. Каким бы ни был цвет объекта, когда он создается первый раз, таким он и останется.

Если вы хотите изменить цвет списка отображения, вам необходимо изменить его ДО того как список отображения будет выведен на экран. Я больше расскажу об этом позже.

glBegin(GL_QUADS);      // Начинаем рисование четырехугольников (quads)
                        // Нижняя поверхность
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);        // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);        // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);        // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);        // Нижний правый угол текстуры и четырехугольник
                        // Передняя поверхность
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);        // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);        // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);        // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);        // Верхний левый угол текстуры и четырехугольник
                        // Задняя поверхность
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);        // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);        // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);        // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);        // Нижний левый угол текстуры и четырехугольник
                        // Правая поверхность
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);        // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);        // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);        // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);        // Нижний левый угол текстуры и четырехугольник
                        // Левая поверхность
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);        // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);        // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);        // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);        // Верхний левый угол текстуры и четырехугольник
glEnd();                        // Закончили рисование четырехугольников

При помощи команды glEndList() мы сказали OpenGL, что мы создали список. Все, что находится между glNewList() и glEndList - часть списка отображения, все, что находится до glNewList() или после glEndList() не является частью списка отображения.

glEndList(); // Закончили создание списка box

Теперь создадим наш второй список отображения. Чтобы найти, где список отображения хранится в памяти, мы возьмем значение старого списка отображения (box) и добавим к нему еще единицу. Нижеследующий код присваивает переменной 'top' местоположение второго списка отображения.

top=box+1; // Значение top это значение box + 1

Теперь, когда мы знаем, где хранится второй список отображения, мы можем создать его. Мы сделаем это, так же как и первый список, но в этот раз мы укажем OpenGL хранить этот список под названием 'top' в отличие от предыдущего 'box'.

glNewList(top,GL_COMPILE);// Новый откомпилированный список отображения 'top'

Следующая часть кода рисует верх коробки. Это просто четырехугольник нарисованный на плоскости z.

glBegin(GL_QUADS);      // Начинаем рисование четырехугольника
                        // Верхняя поверхность
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);        // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);        // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);        // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);        // Верхний правый угол текстуры и четырехугольник
glEnd();                // Заканчиваем рисование четырехугольника

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

        glEndList();            // Закончили создание списка отображения 'top'
}

Код создания текстур тот же, который мы использовали в предыдущих уроках для загрузки и создания текстур. Мы хотим получить текстуру, которую сможем наложить на все шесть сторон каждого куба. Я, бесспорно, буду использовать мипмаппинг, чтобы сделать действительно сглаженные текстуры. Я ненавижу глядеть на пиксели :). Текстура для загрузки называется 'cube.bmp'. Она хранится в каталоге под названием 'data'. Найдите LoadBMP и измените эту строку таким образом, чтобы она выглядела как нижеследующая строка.

if (TextureImage[0]=LoadBMP("Data/Cube.bmp"))           // Загрузить картинку.

Код изменения размеров такой же, как в Уроке N6.

Только в коде инициализации есть несколько изменений. Я добавлю строку BuildList(). Это будет переход к части кода, в которой создаются списки отображения. Обратите внимание что BuildList() находится после LoagGLTextures(). Важно знать, что порядок должен быть именно таким. Сначала мы создаем текстуру, а затем создаем наши списки отображения, где уже созданные текстуру мы можем наложить на куб.

int InitGL(GLvoid)// Все настройки OpenGL начинаются здесь
{
        if (!LoadGLTextures())// Переход к процедуре загрузки текстуры
        {
                return FALSE;// Если текстура не загружена возращает FALSE
        }
        BuildLists();// Переход к коду, который создает наши списки отображения
       glEnable(GL_TEXTURE_2D);// Включение нанесения текстур
        glShadeModel(GL_SMOOTH);// Включение гладкой закраски (smooth shading)
        glClearColor(0.0f, 0.0f, 0.0f, 0.5f);// Черный фон
        glClearDepth(1.0f);// Установка буфера глубины
        glEnable(GL_DEPTH_TEST);// Включение проверки глубины
        glDepthFunc(GL_LEQUAL); // Тип выполняемой проверки глубины

Следующие три строки кода включают быстрое и простое освещение. Light0 предустановлен на большинстве видеоплат, и это спасет нас от трудностей установки источников освещения. После того как мы включили light0, мы включили освещение. Если light0 не работает на вашей видео плате (вы видите темноту), просто отключите освещение.

Еще одна строка GL_COLOR_MATERIAL позволяет нам добавить цвет к текстурам. Если мы, не включили закрашивание материала, текстуры всегда будут своего первоначального цвета. glColor3f(r,g,b) не будет действовать на расцвечивание. Поэтому важно включить это.

glEnable(GL_LIGHT0); // Быстрое простое освещение
                        // (устанавливает в качестве источника освещения Light0)
glEnable(GL_LIGHTING);  // Включает освещение
glEnable(GL_COLOR_MATERIAL);    // Включает раскрашивание материала

Наконец, мы устанавливаем коррекцию перспективы для улучшения внешнего вида и возвращаем TRUE, давая знать нашей программе, что инициализация прошла OK.

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);      // Изящная коррекция перспективы
return TRUE;                                            // Инициализация прошла OK
}

Теперь часть программы для рисования. Как обычно, я немного шизею с математики. Нет SIN и COS, но это все еще непривычно :). Мы начнем, как обычно, с очистки экрана и глубины буфера.

Затем мы привяжем текстуру к кубу. Я мог добавить эту строку в коде списка отображения, но, оставляя ее вне списка, я могу изменить текстуру, когда захочу. Если бы я добавил строку glBindTexture(GL_TEXTURE_2D, texture[0]) внутри кода списка отображения, список был бы создан с этой текстурой постоянно нанесенной на него.

int DrawGLScene(GLvoid)                 // Здесь мы выполняем все рисование
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);     // Очищаем экран и буфер глубины

glBindTexture(GL_TEXTURE_2D, texture[0]);               // Выбираем текстуру

Теперь немного позабавимся. Мы имеем цикл по yloop. Этот цикл используется для позиционирования кубов по оси Y (вверх и вниз). Мы хотим иметь 5 строк кубиков вверх и вниз, итак мы создадим цикл от 1 до 6 (то есть 5).

        for (yloop=1;yloop<6;yloop++)                           // Цикл по плоскости Y
        {

Создадим другой цикл по xloop. Он используется для позиционирования кубиков по оси x (слева направо). Количество кубов зависит от того, в какой они рисуются строке. В верхней строчке xloop будет изменяться только от 0 до 1 (рисование одного кубика). В следующей строке xloop изменяется от 0 до 2 (рисование 2-х кубиков) и так далее.

                for (xloop=0;xloop< yloop;xloop++)              // Цикл по плоскости X
                {

Мы очищаем наше окно с помощью glLoadIdentity().

                        glLoadIdentity();                       // Очистка вида

Следующая строка задает особую точку на экране. Это может сбить с толку, но не пугайтесь. На оси X происходит следующее:

Мы смещаемся вправо на 1.4 единицы так, чтобы центр пирамиды оказался в центре экрана. Умножим xloop на 2.8 и добавим к ней 1.4 (мы умножаем на 2.8, так как кубики стоят друг на друге, 2.8 это приближенная ширина куба, когда он повернут на 45 градусов, по диагонали куба). Наконец, вычитаем yloop*1.4. Это перемещение кубов влево в зависимости от того, в какой строке он находится. Если мы не сместим влево, пирамида начнется у левого края экрана (не будет выглядеть как пирамида).

На оси Y мы вычитаем yloop из 6, иначе пирамида будет нарисована сверху вниз. Результат умножим на 2.4. Иначе кубики будут находиться друг на друге. (2.4 примерная высота куба). Затем мы отнимем 7, чтобы пирамида начиналась от низа экрана и строилась вверх.

Наконец по оси Z мы переместимся на 20 единиц в глубь экрана. Так мы добьемся того, что бы пирамида хорошо вписалась в экран.

                        // Размещение кубиков на экране
glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),
        ((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);

Теперь мы повернемся вокруг оси X. Таким образом, мы наклоним кубики вперед на 45 градусов минус 2 умноженное на yloop. Режим перспективы наклоняет кубики автоматически, и я вычитаю, чтобы скомпенсировать наклон. Не лучший способ, но это работает.

Наконец добавим xrot. Это дает нам управлять углом с клавиатуры. (Получите удовольствие, играя с ним).

После этого повернем кубики на 45 градусов вокруг оси Y, и добавим yrot чтобы управлять с клавиатуры поворотом вокруг оси Y.

glRotatef(45.0f-(2.0f*yloop)+xrot,1.0f,0.0f,0.0f);      // Наклонять кубы вверх и вниз
glRotatef(45.0f+yrot,0.0f,1.0f,0.0f);                   // Вращать кубы вправо и влево

Далее мы выберем цвет коробки (яркий) перед тем как действительно нарисовать эту часть куба. Мы используем команду glColor3fv(), которая выполняет загрузку всех трех значений цвета (красный, зеленый, голубой). Аббревиатура 3fv означает 3 значения, числа с плавающей точкой, v указатель на массив. Цвет мы выбираем по переменной yloop-1, что дает нам разные цвета для каждого ряда кубов. Если мы используем xloop-1, мы получим различные цвета для каждого столбца.

glColor3fv(boxcol[yloop-1]);            // Выбор цвета коробки

Теперь, когда цвет установлен, все что надо сделать - нарисовать нашу коробку. В отличие от написания всего кода для рисования коробки, все, что нам нужно - вызвать наш список отображения. Мы сделаем это командой glCallList(box). box указывает OpenGL выбрать список отображения box. Список отображения box - это кубик без верха.

Коробка будет нарисована цветом, выбранным командой glColor3fv(), в месте, куда мы ее передвинули.

glCallList(box);                        // Рисуем коробку

Теперь выберем цвет крышки (темный) перед тем как нарисовать крышку коробки. Если вы действительно хотите сделать Q-Bert, вы измените этот цвет всякий раз, когда Q-Bert вскакивает на коробку. Цвет устанавливается в зависимости от ряда.

glColor3fv(topcol[yloop-1]);            // Выбор цвета верха

Наконец, осталось нарисовать список отображения top. Этим мы добавим более темного цвета крышку. Все. Очень легко!


                        glCallList(top);                        // Рисуем крышку
                }
        }
        return TRUE;                                            // Возвращаемся обратно.

}

Все необходимые изменения будут выполнены в WinMain(). Код будет добавлен сразу после строки SwapBuffers(hDC). Этим кодом мы проверим нажатие кнопок "стрелка влево", "вправо", "вверх", "вниз" и соответственно повернем кубы.

                SwapBuffers(hDC);       // Поменяем буферы (Двойная буферизация)
                if (keys[VK_LEFT])      // Была нажата стрелка влево?
                {
                        yrot-=0.2f;     // Если так, то повернем кубы влево
                }
                if (keys[VK_RIGHT])     // Была нажата стрелка вправо?
                {
                        yrot+=0.2f;     // Если так, то повернем кубы вправо
                }
                if (keys[VK_UP])        // Была нажата стрелка вверх?
                {
                        xrot-=0.2f;     // Если так, то наклоним кубы вверх
                }
                if (keys[VK_DOWN])      // Была нажата стрелка вниз?
                {
                        xrot+=0.2f;     // Если так, то наклоним кубы вниз
                }

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

                if (keys[VK_F1])                // Была нажата кнопка F1?
                {
                        keys[VK_F1]=FALSE;// Если так - установим значение FALSE
                        KillGLWindow(); // Закроем текущее окно OpenGL
                        // Переключим режим "Полный экран"/"Оконный"
                        fullscreen=!fullscreen;
                        // Заново создадим наше окно OpenGL
                        if (!CreateGLWindow("NeHe's Display List Tutorial",
                                640,480,16,fullscreen))
                        {
                                return 0;// Выйти, если окно не было создано
                        }
                }
        }
}

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

Я надеюсь, вам понравился этот урок. Если у вас есть вопросы или вы чувствуете, что что-нибудь не ясно, пожалуйста, напишите мне и дайте мне знать.

© Jeff Molofee (NeHe)

 25 сентября 2001 (c)  Alexey Okulov