Урок 9. Передвижение изображений в 3D

Добро пожаловать на 9-й урок. На данный момент вы должны уже хорошо понимать суть OpenGL. Вы уже научились всему, начиная от создания окна и установки контекста OpenGL, до текстурирования вращающихся объектов с использованием освещения и смешивания (blending). Этот урок будет первым из серии "продвинутых" уроков. Вы научитесь следующему: перемещать изображение (bitmap) по экрану в 3D, удаляя, черные пикселы (pixels) из изображения (используя смешивание), дополнять цветность в черно-белые текстуры и, наконец, узнаете, как создавать красивые цвета и простую анимацию путём смешивания различных цветных текстур вместе.

Мы изменим код из первого урока. И начнем с добавления некоторых новых переменных в начале программы. Я переписал начальную секцию программы, для того чтобы было легче определять, где произведены изменения.

#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;// Флаг полноэкранного режима (по умолчанию: полноэкранное)

Следующие строчки новые. twinkle и tp логические переменные, которые могут быть TRUE (истина) или FALSE (ложь). twinkle будет говорить о включении/выключении эффекта twinkle. tp используется для определения состояния клавиши 'T' (была ли нажата или нет). Если нажата, то tp=TRUE, иначе tp=FALSE.


BOOL    twinkle;                        // Twinkling Stars (Вращающиеся звезды)
BOOL    tp;                             // 'T' клавиша нажата?

num переменная, хранит информацию о количестве звезд, которые мы рисуем на экране. Она определена как константа. Это значит, в коде программы мы не может поменять её значение. Причина, по которой мы определяем её как константу, в том, что мы не можем переопределить (увеличить/уменьшить) массив. Так или иначе, если мы задаём массив только на 50 звезд и хотим увеличить num до 51 звезды где-нибудь в программе, то массив не сможет увеличиться, и выдаст ошибку. Вы можете изменить num только в этой строчке программы. И не пытайтесь изменить значение num где-то в другом месте, если вы не хотите, чтобы случилось страшное :).

const   num=50;                         // Количество рисуемых звезд

Сейчас мы создадим структуру (structure). Слово структура звучит глобально, но это не так на самом деле. Структура это совокупность простых данных (переменных, и т.д.) сгрупированых по какому-либо признаку в одну группу. Мы знаем, что мы будем хранить цепочку звезд. Вы увидите, что 7-ая строчка ниже это stars;. Мы знаем, что каждая звезда имеет 3 значения для цвета, и все эти значения целые числа: 3-я строчка: int r,g,b задаёт эти значения. Одно для красного (red) (r), одно для зелёного (green) (g), и одно для голубого (blue) (b). Мы знаем, что каждая звезда будет иметь разное расстояние от центра экрана, и расположена на одном из 360 углов от центра. Если вы посмотрите на 4-ую строчку ниже, вы увидите это. Мы создаём переменную типа число с плавающей точкой (floating point value) называется dist. Она означает расстояние. 5-ая строчка создаёт переменную того же типа с именем angle. Она будет отвечать за угол звезды.

И так мы имеем группу данных, которая содержит цвет, расстояние и угол звезды на экране. К сожалению, у нас больше чем одна звезда, и вместо того чтобы создавать 50 переменных для красного цвета, 50 переменных для зеленого цвета, 50 переменных для синего цвета, 50 переменных для расстояния, 50 переменных для угла мы просто создадим массив и назовем его star. Под каждым номером в массиве star содержится информация о нашей структуре stars. Это мы делаем в 8-ой строке ниже: stars star[num]. Тип элемента массива будет stars. stars это структура. И массив содержит всю информацию в структурах. Массив называется star. Количество элементов - [num]. И так как num=50, мы имеем массив с именем star. Наш массив содержит элементы типа структура stars. Намного проще, чем хранить каждую звезду в отдельных переменных. Что было бы большой глупостью и не позволило добавлять или уменьшать количество звезд с помощью переменной num.

(Прим. перев. - Такое ощущение, что объясняешь слону, как ходить :). Можно было одной строчкой это объяснить.)

typedef struct                          // Создаём структуру для звезд
{
        int r, g, b;                    // Цвет звезды
        GLfloat dist;                   // Расстояние от центра
        GLfloat angle;                  // Текущий угол звезды
}
stars;                                  // Имя структуры - Stars
stars star[num];                        // Делаем массив 'star' длинной 'num',
                                        // где элементом является структура 'stars'

Далее мы задаём переменную для хранения расстояния от наблюдателя до звезд (zoom), и какой будет начальный угол(tilt). Также мы делаем переменную spin, которая будет вращать звезды по оси z, и это будет выглядеть как вращение вокруг их текущей позиции.

loop это переменная, которую мы используем в программе для отрисовки всех 50-ти звезд, и texture[1] будет использоваться для хранения одной черно-белой текстуры, которую мы загружаем. Если вы хотите больше текстур, вы должны увеличить длину массива, с одного до нужной длины.

GLfloat zoom=-15.0f;                    // Расстояние от наблюдателя до звезд
GLfloat tilt=90.0f;                     // Начальный угол
GLfloat spin;                           // Для вращения звезд

GLuint  loop;                           // Используется для циклов
GLuint  texture[1];                     // Массив для одной текстуры

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

Сразу же за переменными мы добавляем код для загрузки текстур. Я не буду объяснять этот код в деталях, это тот же код что был в 6, 7 и 8 уроке. Изображение, которое мы загружает называется star.bmp. Мы генерируем только одну текстуру используя glGenTextures(1, &texture[0]). Текстура будет иметь линейную фильтрацию (linear filtering).

AUX_RGBImageRec *LoadBMP(char *Filename)// Функция для загрузки bmp файлов
{
        FILE *File=NULL;                // Переменная для файла

        if (!Filename)                  // Нужно убедиться в правильности переданого имени
        {
                return NULL;            // Если неправильное имя, то возвращаем NULL
        }

        File=fopen(Filename,"r");       // Открываем и проверяем на наличие файла

        if (File)                       // Файл существует?
        {
                fclose(File);           // Если да, то закрываем файл
        // И загружаем его с помощью библиотеки AUX, возращая ссылку на изображение
                return auxDIBImageLoad(Filename);
        }
        // Если загрузить не удалось или файл не найден, то возращаем NULL
        return NULL;
}

Эта секция кода загружает изображение (описанным выше кодом) и конвертирует в текстуру. Status хранит информацию об успехе операции.

int LoadGLTextures()    // Функция загрузки изображения и конвертирования в текстуру
{
        int Status=FALSE;               // Индикатор статуса

        AUX_RGBImageRec *TextureImage[1];// Создаём место для хранения текстуры

        memset(TextureImage,0,sizeof(void *)*1);// устанавливаем ссылку на NULL

        // Загружаем изображение, Проверяем на ошибки, Если файл не найден то выходим
        if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
        {
                Status=TRUE;            // Ставим статус в TRUE

                glGenTextures(1, &texture[0]);  // Генерируем один индификатор текстуры

                // Создаём текстуру с линейной фильтрацией (Linear Filtered)
                glBindTexture(GL_TEXTURE_2D, texture[0]);
                glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
                glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX,
        TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
        }

        if (TextureImage[0])            // Если текстура существует
        {
                if (TextureImage[0]->data)   // Если изображение существует
                {
                        // Освобождаем место выделенное под изображение
                        free(TextureImage[0]->data);
                }

                free(TextureImage[0]);  // Освобождаем структуры изображения
        }

        return Status;                  // Возвращаем статус
}

Теперь мы настраиваем OpenGL для отрисовки того, что будет нужно. Нам не нужен Z-буфер (тест глубины) для нашего проекта, убедитесь что удалены строчки из первого урока: glDepthFunc(GL_LEQUAL); и glEnable(GL_DEPTH_TEST); иначе вы не получите нужного результата Мы используем текстурирование в этом коде, значит надо добавить все необходимые строки, которых нет в первом уроке. Включаем текстурирование и смешивание.

int InitGL(GLvoid)                      // Всё установки OpenGL будут здесь
{
        if (!LoadGLTextures())          // Загружаем текстуру
        {
                return FALSE;           // Если не загрузилась, то возвращаем FALSE
        }

        glEnable(GL_TEXTURE_2D);        // Включаем текстурирование
        // Включаем плавную ракраску (интерполирование по вершинам)
        glShadeModel(GL_SMOOTH);
        glClearColor(0.0f, 0.0f, 0.0f, 0.5f);   // Фоном будет черный цвет
        glClearDepth(1.0f);                     // Установки буфера глубины (Depth Buffer)
        // Максимальное качество перспективной коррекции
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
        // Устанавливаем функцию смешивания
        glBlendFunc(GL_SRC_ALPHA,GL_ONE);
        glEnable(GL_BLEND);                     // Включаем смешивание

Следующий фрагмент кода новый. Он устанавливает начальные углы, расстояние и цвет для каждой звезды. Заметьте, как просто менять информацию в структуре. Цикл по всем 50 звездам. Чтобы изменить угол star[1] все, что мы должны написать - это star[1].angle={некоторое значение}. Это просто!

        for (loop=0; loop<num; loop++)       // Делаем цикл и бежим по всем звездам
        {
                star[loop].angle=0.0f;  // Устанавливаем всё углы в 0

Я рассчитываю дистанцию взяв текущий номер звезды (это значение loop) и разделив на максимальное значение звезд. Потом я умножаю результат на 5.0f. Коротко, что это даёт - это отодвигает каждую звезду немного дальше, чем предыдущую. Когда loop равен 50 (последняя звезда), loop разделенный на num будет равен 1.0f. Я умножаю на 5 потому, что 1.0f*5.0f будет 5.0f. 5.0f это почти на границе экрана. Я не хочу, что бы звезды уходили за пределы экрана, так что 5.0f это идеально. Если вы установите zoom подальше в экран, вы должны использовать большее число, чем 5.0f, но ваши звезды должны быть немного меньше (из-за перспективы).

Заметьте, что цвета для каждой звезды задаются случайным образом от 0 до 255. Вы можете спросить, как мы может использовать эти числа, когда нормальное значение цвета от 0.0f до 1.0f. Отвечаю. Когда мы устанавливаем цвет, мы используем glColor4ub вместо glColor4f. ub значит Unsigned Byte (беззнаковый байт). И байт может иметь значения от 0 до 255. В этой программе легче использовать байты, чем работать с со случайными числами с плавающей запятой.

                // Вычисляем растояние до центра
                star[loop].dist=(float(loop)/num)*5.0f;
                // Присваиваем star[loop] случайное значение (красный).
                star[loop].r=rand()%256;
                // Присваиваем star[loop] случайное значение (зеленый)
                star[loop].g=rand()%256;
                // Присваиваем star[loop] случайное значение (голубой)
                star[loop].b=rand()%256;
        }
        return TRUE;                    // Инициализация прошла нормально.

}

Код функции Resize тот же самый, так что рассмотрим код отрисовки сцены. Если вы используете код из первого урока, то удалите весь код из DrawGLScene и просто скопируйте, то, что написано ниже. Здесь только две строки из первого урока, вообщем удалять немного придется.

int DrawGLScene(GLvoid)                 // Здесь мы всё рисуем
{
        // Очищаем буфер цвета и глубины
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // Выбираем нашу текстуру
        glBindTexture(GL_TEXTURE_2D, texture[0]);

        for (loop=0; loop<num; loop++)               // Цикл по всем звездам
        {
                // Обнуляем видовую матрицу (Model Matrix) перед каждой звездой
                glLoadIdentity();
                // Переносим по оси z на 'zoom'
                glTranslatef(0.0f,0.0f,zoom);
                // Вращаем вокруг оси x на угол 'tilt'
                glRotatef(tilt,1.0f,0.0f,0.0f);

Теперь мы двигаем звезды! :) Звезда появляется в середине экрана. Первым делом мы вращаем сцену вокруг оси y. Если угол 90 градусов, то ось x будет лежать не слева направо, а наоборот и выходить за пределы экрана. В качестве примера: представьте, что вы стоите в центре комнаты. Теперь представьте, что слева на стене написано -x, впереди на стене написано -z, справа написано +x, в сзади написано +z. Если повернуться налево на 90 градусов, но не двигаться с места, то на стене впереди будет не -z, а -x. Все стены поменяются. -z будет справа, +z будет слева, -x впереди, и +x сзади. Проясняется? Вращая сцену, мы изменяем направления осей x и z.

Во второй строчке мы сдвигаем позицию по плоскости x. Обычно положительное значение по x двигает нас вправую сторону экрана (где обычно +x), но так как мы повернулись вокруг оси y, +x может быть хоть где. Если мы повернём на 180 градусов, +x будет с левой стороны экрана вместо правой. И так, когда мы двигаемся вперед по оси x, мы можем подвинуться влево, вправо, вперед и даже назад.

                // Поворачиваем на угол звезды вокруг оси y
                glRotatef(star[loop].angle,0.0f,1.0f,0.0f);
                // Двигаемся вперед по оси x
                glTranslatef(star[loop].dist,0.0f,0.0f);

Теперь немного хитрого кода. Звезда это всего лишь плоская текстура. И если мы нарисовали плоский квадрат в середине экрана с наложенной текстурой - это будет выглядеть отлично. Он будет, повернут к вам, как и должен быть. Но если повернёте его вокруг оси y на 90 градусов, текстура будет направлена вправо или влево экрана. Всё что вы увидите это тонкую линию. Нам не нужно чтобы это случилось. Мы хотим, чтобы звезды были направлены на наблюдателя всё время, не важно как они вращаются и двигаются по экрану.

Мы добиваемся этого путем отмены вращения, которое мы делаем, перед тем как нарисовать звезду. Мы отменяем вращение в обратном порядке. Выше мы повернули экран, когда сделали вращение на угол звезды. В обратном порядке, мы должны повернуть обратно звезду на текущий угол. Чтобы сделать это, мы используем обратное значение угла, и повернём звезду на этот угол. И так как мы повернули звезду на 10 градусов, то, поворачивая обратно на -10 градусов мы делаем звезду повернутой к наблюдателю по y. Первая строчка ниже отменяет вращение по оси y. Потом мы должны отменить вращение по оси x. Чтобы сделать это мы вращаем звезду на угол -tilt. После всех этих операций звезда полностью повернута к наблюдателю.

                glRotatef(-star[loop].angle,0.0f,1.0f,0.0f);
                // Отменяет текущий поворот звезды
                glRotatef(-tilt,1.0f,0.0f,0.0f);        // Отменяет поворот экрана

Если twinkle равно TRUE, мы рисуем не вращающуюся звезду на экране. Для того чтобы получить, различные цвета, мы берём максимальное количество звезд (num) и вычитаем текущий номер (loop), потом вычитаем единицу, так как наш цикл начинается с 0 и идет до num-1. Если результат будет 10, то мы используем цвет 10- ой звезды. Вследствие того, что цвет двух звезд обычно различный. Не совсем хороший способ, но зато эффективный. Последнее значение это альфа (alpha). Чем меньше значение, тем прозрачнее звезда (т.е. темнее, т.к. экран черный).

Если twinkle включен, то каждая звезда будет отрисована дважды. Программы будет работать медленнее в зависимости от типа вашего компьютера. Если twinkle включен, цвета двух звезд будут смешиваться вместе для создания реально красивых цветов. Так как это звезды не вращаются, то это проявлялось бы как будто звёзды анимировались, когда twinkling включен. (Если не понятно, то просто посмотрите).

Заметьте, как просто добавить цвет в текстуру. Даже если текстура черно-белая, она будет окрашена в цвет, который мы выбрали, перед тем как нарисовать текстуру. Также возьмите на заметку что мы используем байты для значений цвета, а не числа с плавающей запятой. Даже альфа тоже представляется байтом.

                if (twinkle)                            // Если Twinkling включен
                {
                        // Данный цвет использует байты
                        glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,
                        star[(num-loop)-1].b,255);
                        glBegin(GL_QUADS);// Начинаем рисовать текстурированый квадрат
                                glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
                                glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
                                glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
                                glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
                        glEnd();                                // Закончили рисовать
                }

Теперь мы рисуем основную звезду. Разница между кодом выше только в том, что звезда всегда рисуется, и вращается по оси z.

                glRotatef(spin,0.0f,0.0f,1.0f);// Поворачиваем звезду по оси z
                // Цвет использует байты
                glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
                glBegin(GL_QUADS);              // Начинаем рисовать текстурный квадрат
                        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
                        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
                        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
                        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
                glEnd();                                        // Закончили рисовать

Здесь мы делаем все движения. Мы вращаем звезду увеличением значения spin. Потом мы меняем угол каждой звезды. Угол каждой звезды увеличивается на loop/num. Что ускоряет вращение звезды с отдалением от центра. Чем ближе к центру, тем медленнее вращается звезда. Наконец мы уменьшаем расстояние до центра для каждой звезды. Это создаёт эффект засасывания звезд в центр экрана.

                spin+=0.01f;                    // Используется для вращения звезды
                star[loop].angle+=float(loop)/num;// Меняем угол звезды
                star[loop].dist-=0.01f; // Меняем растояние до центра

Нижеследующие строки проверяют видимость звезд. Попала звезда в центр экрана или нет. Если попала, то даём звезде новый цвет и двигаем на 5 единиц от центра. И так она может снова начать своё путешествие к центру как новая звезда.

                if (star[loop].dist<0.0f)    // Звезда в центре экрана?
                {
                        star[loop].dist+=5.0f;  // Перемещаем на 5 единиц от центра
                        // Новое значение красной компоненты цвета
                        star[loop].r=rand()%256;
                        // Новое значение зеленной компоненты цвета
                        star[loop].g=rand()%256;
                        // Новое значение синей компоненты цвета
                        star[loop].b=rand()%256;
                }
        }
        return TRUE;                                            // Всё ок
}

Теперь мы добавляем код для проверки нажатия клавиш. Идем ниже в WinMain(). Ищите строку с SwapBuffers(hDC). Мы добавляем код для нашей клавиши прямо под этой строкой.

Строкой ниже мы проверяем нажатие клавиши 'T'. Если нажата, и не была до этого нажата, то идем дальше. Если twinkle равно FALSE, то она станет TRUE. Если была TRUE, то станет FALSE. Одно нажатие 'T' установит tp равное TRUE. Это предотвращает постоянное выполнение этого кода, если клавиша 'T' удерживается.

                SwapBuffers(hDC);               // Смена буфера (Double Buffering)
                if (keys['T'] && !tp)           // Если 'T' нажата и tp равно FALSE
                {
                        tp=TRUE;                // то делаем tp равным TRUE
                        twinkle=!twinkle;       // Меняем значение twinkle на обратное
                }

Код ниже проверяет "выключение" (повторное нажатие) клавиши 'T'. Если да, то делаем tp=FALSE. Нажатие 'T' делает ничего кроме установки tp равной FALSE, так что эта часть кода очень важная.

                if (!keys['T'])                 // Клавиша 'T' была отключена
                {
                        tp=FALSE;               // Делаем tp равное FALSE
                }

Следующий код проверяет, нажаты ли клавиши 'стрелка вверх', 'стрелка вниз', 'page up', 'page down'.

                if (keys[VK_UP])                // Стрелка вверх была нажата?
                {
                        tilt-=0.5f;             // Вращаем экран вверх
                }

                if (keys[VK_DOWN])              // Стрелка вниз нажата?
                {
                        tilt+=0.5f;             // Вращаем экран вниз
                }

                if (keys[VK_PRIOR])             // Page Up нажат?
                {
                        zoom-=0.2f;             // Уменьшаем
                }

                if (keys[VK_NEXT])              // Page Down нажата?
                {
                        zoom+=0.2f;             // Увеличиваем
                }

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

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

В этом уроке я попытался объяснить, как можно детальнее как загружать черно-белое изображение, удалять черный фон на изображении (используя смешивание), добавлять цвет в картинку, и двигать ее по экрану в трехмерном пространстве. Я также попытался показать, как сделать красивые цвета и анимацию путем наложения второй копии изображения поверх оригинала. Теперь вы имеете хорошее представление обо всем, что я хотел вам рассказать на данный момент. У вас теперь не должно возникнуть проблем с созданием своих собственных демок в 3D. Все начальные данные изложены тут.

© Jeff Molofee (NeHe)

 19 октября 2001 (c)  Mike Samsonov