Урок 25. Морфинг и загрузка объектов из файла.
Добро пожаловать в еще один потрясающий урок! На этот раз мы сосредоточимся не на графике, а на эффекте, хотя результат все равно будет выглядеть очень круто. В этом уроке Вы научитесь осуществлять морфинг — плавное "превращение" одного объекта в другой. Подобный эффект я использую в демонстрации dolphin. Надо сделать несколько замечаний. Прежде всего, стоит отметить, что каждый объект должен состоять из одинакового количества точек. Очень редко удается получить три объекта, содержащих точно одно и тоже количество вершин, но, к счастью, в этом уроке у нас имеются три объекта с одинаковым количеством точек :). Не поймите меня неправильно, — Вы можете использовать объекты с разным количеством вершин, но тогда переход одного объекта в другой не будет выглядеть так четко и плавно.
Также Вы научитесь считывать объект из файла. Формат файла подобен формату, используемому в уроке 10, хотя код легко можно изменить для чтения .ASC файлов или других текстовых файлов. В общем, это действительно крутой эффект и действительно крутой урок. Итак, приступим!
Начинаем как обычно. Подключаем заголовочные файлы, в том числе необходимые для работы с математическими функциями и стандартной библиотекой ввода/вывода. Заметьте, что мы не подключаем библиотеку GLAUX. В этом уроке мы будем рисовать точки, а не текстуры. После того, как Вы поймете урок, Вы сможете поиграть с полигонами, линиями и текстурами!
#include <windows.h> // Заголовочный файл для Windows
#include <math.h> // Заголовочный файл для математической библиотеки
#include <stdio.h> // Заголовочный файл для стандартного ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
HDC hDC=NULL; // Контекст устройства
HGLRC hRC=NULL; // Контекст рендеринга
HWND hWnd=NULL; // Дескриптор окна
HINSTANCE hInstance; // Экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активности приложения
bool fullscreen=TRUE; // Флаг полноэкранного режима
После установки всех стандартных переменных, мы добавим несколько новых. Переменные xrot, yrot и zrot будут хранить текущие значения углов вращения экранного объекта по осям x, y и z. Переменные xspeed, yspeed и zspeed будут контролировать скорость вращения объекта по соответствующим осям. Переменные cx, cy и cz определяют позицию рисования объекта на экране (cx — слева направо, cy — снизу вверх и cz — в экран и от него).
Переменную key я включил для того, чтобы убедиться в том, что пользователь не будет пытаться сделать морфинг первой формы на себя. Это было бы красивой бессмысленностью и вызвало бы задержку, за счет морфинга точек в позицию, в которой они уже находятся.
step — это переменная-счетчик, которая постепенно увеличивается до значения steps. Если Вы увеличите значение переменной steps, то морфинг объекта займет больше времени, зато морфинг будет осуществлен более плавно. Как только переменная step станет равной steps, мы поймем, что морфинг завершился.
Последняя переменная morph дает знать нашей программе, нужно ли осуществить морфинг точек или же не трогать их. Если она установлена в TRUE, то объект находится в процессе морфинга из одной фигуры в другую.
Lfloat xrot, yrot, zrot, // углы вращения по X, Y и Z
xspeed, yspeed, zspeed, // скорость вращения по X, Y и Z
cx, cy, cz=-15; // положение на X, Y и Z
int key=1; // Используется для проверки морфинга
int step=0, steps=200; // Счетчик шага и максимальное число шагов
bool morph=FALSE; // По умолчанию morph равен False (морфинг выключен)
Здесь мы создаем структуру для хранения вершин. В ней будут храниться координаты x, y и z любой точки на экране. Переменные x, y и z — вещественные, поэтому мы можем позиционировать точку в любом месте экрана с большой точностью.
typedef struct // Структура для вершины
{
float x, y, z; // X, Y и Z координаты
} VERTEX; // Назовем ее VERTEX
Итак, у нас есть структура для хранения вершин. Нам известно, что объект состоит из множества вершин, тогда давайте создадим структуру OBJECT. Первая переменная verts является целым числом, определяющим количество вершин необходимое для создания объекта. Таким образом, если наш объект состоит из 5 вершин, то verts будет равно 5. Мы установим значение позже в коде. Все что Вам нужно знать сейчас — это то, что verts следит за тем, сколько точек будет использовано для создания объекта.
Переменная points будет указывать на переменную типа VERTEX(значения x, y и z). Это даст нам возможность захватывать значения x, y и z координат любой точки, используя выражение points[{точка, к которой нужно осуществить доступ}].{x, y или z}.
Имя этой структуры… Вы угадали… OBJECT!
typedef struct // Структура для объекта
{
int verts; // Количество вершин в объекте
VERTEX* points; // Одна вершина
} OBJECT; // Назовем ее OBJECT
Теперь, когда мы создали структуру VERTEX и структуру OBJECT, мы можем определить некоторые объекты.
Переменная maxver будет хранить самое большое количество вершин, из всех используемых в объектах. Если один объект содержит всего 5 точек, другой — 20, а последний объект — 15, то значение maxver будет равно самому большему из них. Таким образом, значение maxver будет равно 20.
После того, как мы определили переменную maxver, мы можем определить объекты. morph1, morph2, morph3, morph4 и helper — все определены как переменные типа OBJECT. *sour и *dest — определены как переменные типа OBJECT* (указатели на объект). Объект состоит из вершин (VERTEX). Первые четыре переменных morph{номер} будут хранить объекты, которые мы и будем подвергать морфингу. helper будет использоваться для отслеживания изменений морфинга объекта. *sour будет указывать на объект-источник, а *dest будет указывать на объект, в который мы хотим осуществить морфинг (объект-назначение).
int maxver; // Хранит максимум числа вершин объектов
OBJECT morph1,morph2,morph3,morph4, // Наши объекты для морфинга (morph1, 2, 3 и 4)
helper,*sour,*dest; // Вспомогательный объект, Источник и Назначение
Так же, как всегда, объявляем WndProc().
LRESULTCALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
В коде ниже происходит выделение памяти для каждого объекта, основанное на числе вершин которое мы передаем параметром n. *k указывает на объект, для которого мы хотим выделить память.
В строке кода расположенной между скобками выделяется память для переменной points объекта k. Размер выделенной памяти будет равен размеру структуры VERTEX (3 переменных типа float) умноженному на количество вершин (n). Так, если бы было 10 вершин (n=10), то мы выделили бы место для 30 вещественных значений (3 переменных типа float * 10 вершин).
void objallocate(OBJECT *k, int n) // Выделение памяти для каждого объекта
{ // И определение точек
k->points=(VERTEX*)malloc(sizeof(VERTEX)*n); // points = размер(VERTEX)* число вершин
} // (3 точки для каждой вершины)
Следующий код удаляет объект и освобождает память, используемую объектом. Объект задается параметром k. Функция free() говорит нашей программе освободить память, занимаемую вершинами, которые были использованы для создания нашего объекта (k).
void objfree(OBJECT *k) // Удаление объекта (Освобождение памяти)
{
free(k->points); // Освобождаем память, занимаемую points
}
Код ниже считывает строку из текстового файла. Указатель на нашу файловую структуру задается параметром *f. Считанная строка будет помещена в переменную string.
Начинаем с создания do-while цикла. Функция fgets() прочитает 255 символов из нашего файла f и сохранит их в переменной *string. Если считанная строка пуста (перевод строки \n), то цикл будет продолжать искать строку с текстом. Оператор while() проверяет наличие пустых строк, и, в случае успеха, начинает цикл заново.
После того, как строка будет прочитана, функция возвратит управление.
void readstr(FILE *f, char *string) // Считывает строку из файла (f)
{
do // Повторять
{
fgets(string, 255, f); // Считывание 255 символов из файла f в переменную string
} while ((string[0] == '/') || (string[0] == '\n')); // Пока строка пуста
return; // Возврат
}
Здесь мы загружаем объект из файла. *name указывает на имя файла. *k указывает на объект, в который мы хотим загрузить данные.
Мы начинаем с переменной целого типа ver. В ver будет храниться количество вершин, используемое для построения объекта.
В переменных rx, ry и rz будут храниться значения x, y и z каждой вершины.
Переменная filein является указателем на нашу файловую структуру, массив oneline[] будет использоваться для хранения 255 текстовых символов.
Мы открываем файл на чтение как текстовый (значение символа CTRL-Z означает конец строки). Затем мы считываем строку, используя readstr(filein, oneline). Строка текста будет сохранена в массиве oneline.
После того, как строка считана, мы сканируем ее (oneline) пока не найдем фразу "Vertices: {какое-то число}{\n}". Если фраза найдена, то число сохраняется в переменной ver. Это число является количеством вершин, используемых для построения объекта. Если Вы посмотрите в текстовые файлы описания объектов, то Вы увидите, что первой строкой является: Vertices: {какое-то число}.
Теперь, когда нам известно количество используемых вершин, мы сохраняем его в переменных verts объектов. Объекты могут иметь различное количество вершин, поэтому их значения verts могут отличаться.
Последнее, что мы делаем в этой секции кода — это выделение памяти для объекта. Это делается вызовом objallocate({имя объекта}, {количество вершин}).
void objload(char *name, OBJECT *k) // Загружает объект из файла (name)
{
int ver; // Будет хранить количество вершин
float rx, ry, rz; // Будут хранить x, y и z координаты вершины
FILE *filein; // Файл для работы
char oneline[255]; // Хранит одну строку текста (до 255 символов)
filein = fopen(name, "rt"); // Открываем файл на чтение (CTRL-Z означает конец файла)
readstr(filein, oneline); // Считываем одну строку текста из файла
// Сканируем текст на "Vertices: ".
// Число вершин сохраняем в ver
sscanf(oneline, "Vertices: %d\n", &ver);
k->verts=ver; // Устанавливаем переменные verts объектов
// равными значению ver
objallocate(k, ver); // Выделяем память для хранения объектов
Мы знаем, из скольких вершин состоит объект. Память выделена. Все, что нам осталось сделать — это считать вершины. Мы создаем цикл по всем вершинам, используя переменную i.
Далее, мы считываем строку текста. Это будет первая строка корректного текста, следующая за строкой "Vertices: {какое-то число}". В конечном итоге мы считываем строку с вещественными значениями координат x, y и z.
Строка анализируется функцией sscanf() и три вещественных значения извлекаются и сохраняются в переменных rx, ry и rz.
for (int i=0; i<ver; i++) // Цикл по всем вершинам
{
readstr(filein, oneline); // Считывание следующей строки
// Поиск 3 вещественных чисел и сохранение их в rx, ry и rz
sscanf(oneline, "%f %f %f", &rx, &ry, &rz);
Следующие три строки кода сложно объяснить, если Вы не понимаете что такое структуры, и т.п., но я попытаюсь :)
Строку k->points[i].x=rx можно разобрать следующим образом:
rx — это значение координаты x одной из вершин.
points[i].x — это координата x вершины points[i]. Если i = 0, то мы устанавливаем значение координаты x вершины 1, если i = 1, то мы устанавливаем значение координаты x вершины 2, и т. д.
points[i] является частью нашего объекта (представленного как k).
Таким образом, если i = 0, то мы говорим: координата x вершины 1 (points[0].x) нашего объекта (k) равняется значению координаты x, считанному нами из файла (rx).
Оставшиеся две строки устанавливают значения координат y и z каждой вершины нашего объекта.
Цикл проходит по всем вершинам. Во избежание ошибки, в случае если вершин будет не достаточно, убедитесь в том, что текст в начале файла “Vertices: {какое-то число}” соответствует действительному числу вершин в файле. То есть, если верхняя строка файла говорит “Vertices: 10”, то за ней должно следовать описание 10 вершин (значения x, y и z).
После считывания всех вершин, мы закрываем файл и проверяем больше ли переменная ver, чем переменная maxver. Если ver больше maxver, то мы делаем maxver равной ver. Таким образом, если мы считали первый объект, состоящий из 20 вершин, то maxver станет равной 20. Далее, если мы считали следующий объект, состоящий из 40 вершин, то maxver станет равной 40. Таким образом, мы узнаем, сколько вершин содержит наш самый большой объект.
k->points[i].x = rx; // Устанавливаем значение points.x объекта (k) равным rx
k->points[i].y = ry; // Устанавливаем значение points.y объекта (k) равным ry
k->points[i].z = rz; // Устанавливаем значение points.z объекта (k) равным rz
}
fclose(filein); // Закрываем файл
if(ver > maxver) maxver=ver;// Если ver больше чем maxver, устанавливаем maxver равным ver
} // Следим за максимальным числом используемых вершин
Следующий кусок кода может показаться немного пугающим… это не так :). Я объясню его настолько подробно, что Вы будете смеяться, когда в следующий раз посмотрите на это.
Код ниже всего лишь вычисляет новую позицию для каждой точки, когда включен морфинг. Номер вычисляемой вершины храниться в i. Возвращаемый результат вычислений будет иметь тип VERTEX.
Мы создаем переменную a типа VERTEX, что позволяет нам работать с a как с совокупностью значений x, y и z.
Давайте посмотрим на первую строку. Значению x вершины a присваивается разность значений x вершин sour->points[i].x объекта-ИСТОЧНИКА и dest->points[i].x объекта-НАЗНАЧЕНИЯ, деленная на steps.
Давайте для примера возьмем какие-нибудь числа. Пусть значение x нашего объекта-источника равняется 20, а значение x объекта-назначения равняется 40. Мы знаем, что steps равно 200! Тогда a.x=(40-20)/200=(20)/200=0.1
Это значит, что для перемещения из точки 40 в точку 20 за 200 шагов, мы должны перемещаться на 0.1 единиц за один раз. Для доказательства этого умножьте 0.1 на 200, и Вы получите 20. 40-20=20 :)
То же самое мы делаем для вычисления количества единиц, на которые нужно перемещаться по осям y и z, для каждой точки. Если Вы увеличите значение steps, то превращение будет выглядеть более красиво (плавно), но морфинг из одной позиции в другую займет больше времени.
VERTEX calculate(int i) // Вычисление перемещения точек в процессе морфинга
{
VERTEX a; // Временная вершина a
// a.x равно x Источника - x Назначения делить на Steps
a.x=(sour->points[i].x-dest->points[i].x)/steps;
// a.y равно y Источника - y Назначения делить на Steps
a.y=(sour->points[i].y-dest->points[i].y)/steps;
// a.z равно z Источника - z Назначения делить на Steps
a.z=(sour->points[i].z-dest->points[i].z)/steps;
return a; // Возвращаем результат
} // Возвращаем вычисленные точки
Код функции ReSizeGLScene() не изменился, поэтому мы пропускаем его.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Изменение размеров и инициализация GL окна
В коде ниже мы устанавливаем смешивание для эффекта прозрачности. Это позволит нам создать красиво смотрящиеся следы от перемещающихся точек.
int InitGL(GLvoid) // Здесь задаются все установки для OpenGL
{
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Устанавливаем функцию смешивания
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Очищаем фон в черный цвет
glClearDepth(1.0); // Очищаем буфер глубины
glDepthFunc(GL_LESS); // Устанавливаем тип теста глубины
glEnable(GL_DEPTH_TEST); // Разрешаем тест глубины
glShadeModel(GL_SMOOTH); // Разрешаем плавное цветовое сглаживание
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Улучшенные вычисления перспективы
Для начала мы устанавливаем переменную maxver в 0. Мы не считывали объекты, поэтому нам неизвестно какое будет максимальное количество вершин.
Затем мы загружаем три объекта. Первый объект — сфера. Данные для сферы находятся в файле sphere.txt. Данные будут загружены в объект, который называется morph1. Также мы загружаем тор и трубку в объекты morph2 и morph3.
maxver=0; // Устанавливаем максимум вершин в 0 по умолчанию
objload("data/sphere.txt", &morph1); // Загружаем первый объект в morph1 из файла sphere.txt
objload("data/torus.txt", &morph2); // Загружаем второй объект в morph2 из файла torus.txt
objload("data/tube.txt", &morph3); // Загружаем третий объект в morph3 из файла tube.txt
Четвертый объект не считывается из файла. Это множество точек, произвольно разбросанных по экрану. Поскольку данные не считываются из файла, постольку мы должны вручную выделить память, используя вызов objallocate(&morph4, 486). Цифра 486 означает то, что мы хотим выделить достаточное количество памяти для хранения 486 вершин (то же количество вершин, из которого состоят остальные три объекта).
После выделения памяти мы создаем цикл, который назначает случайные значения x, y и z каждой вершине. Случайное значение является вещественным числом из интервала от -7 до 7. (14000/1000=14… минус 7 даст нам максимальное значение +7… если случайным числом будет 0, то мы получим минимум 0-7 или -7).
objallocate(&morph4, 486); // Резервируем память для 486 вершин четвертого объекта (morph4)
for(int i=0; i<486; i++) // Цикл по всем 486 вершинам
{
// Точка x объекта morph4 принимает случайное значение от -7 до 7
morph4.points[i].x=((float)(rand()%14000)/1000)-7;
// Точка y объекта morph4 принимает случайное значение от -7 до 7
morph4.points[i].y=((float)(rand()%14000)/1000)-7;
// Точка z объекта morph4 принимает случайное значение от -7 до 7
morph4.points[i].z=((float)(rand()%14000)/1000)-7;
}
Затем мы загружаем sphere.txt во вспомогательный объект helper. Мы никогда не будем модифицировать объектные данные в morph{1/2/3/4} непосредственно. Мы будем модифицировать данные, хранящиеся в helper, для превращения его в одну из 4 фигур. Мы начинаем с отображения morph1 (сфера), поэтому во вспомогательный объект helper мы также поместили сферу.
После того, как все объекты загружены, мы устанавливаем объекты источник и назначение (sour и dest) равными объекту morph1, который является сферой. Таким образом, все будет начинаться со сферы.
// Загружаем sphere.txt в helper (используется как отправная точка)
objload("data/sphere.txt",&helper);
sour=dest=&morph1; // Источник и Направление приравниваются к первому объекту (morph1)
return TRUE; // Инициализация прошла успешно
}
А теперь позабавимся! Фактический код рисования :)
Начинаем как обычно. Очищаем экран и буфер глубины, сбрасываем матрицу просмотра модели. Затем мы позиционируем объект на экране, используя значения, хранящиеся в переменных cx, cy и cz.
Осуществляем вращение, используя переменные xrot, yrot и zrot.
Углы вращения увеличиваются за счет xpseed, yspeed и zspeed.
Наконец, создаем три временные переменные tx, ty и tz вместе с новой вершиной q.
void DrawGLScene(GLvoid) // Здесь происходит все рисование
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очищаем экран и буфер глубины
glLoadIdentity(); // Сбрасываем просмотр
glTranslatef(cx, cy, cz); // Сдвигаем текущую позицию рисования
glRotatef(xrot, 1, 0, 0); // Вращаем по оси X на xrot
glRotatef(yrot, 0, 1, 0); // Вращаем по оси Y на yrot
glRotatef(zrot, 0, 0, 1); // Вращаем по оси Z на zrot
// Увеличиваем xrot, yrot и zrot на xspeed, yspeed и zspeed
xrot+=xspeed; yrot+=yspeed; zrot+=zspeed;
GLfloat tx, ty, tz; // Временные переменные X, Y и Z
VERTEX q; // Хранит вычисленные значения для одной вершины
Здесь мы рисуем точки и, если морфинг включен, производим наши вычисления. Команда glBegin(GL_POINTS) говорит OpenGL, что каждая вершина, которую мы определили, будет нарисована на экране как точка.
Мы создаем цикл для прохода по всем вершинам. Вы можете использовать переменную maxver, но, поскольку все объекты состоят из одинакового количества вершин, мы будем использовать morph1.verts.
В цикле мы проверяем, равняется ли значение morph TRUE. Если да, то мы вычисляем перемещение для текущей точки (i). Результат будет помещен в q.x, q.y и q.z. Если нет, то q.x, q.y и q.z будут сброшены в 0 (предотвращение перемещения).
Точки объекта helper перемещаются на основе результатов, полученных из calculate(i) (вспомните, как выше мы вычислили, что точка должна перемещаться по 0.1 единиц, для того, чтобы переместиться из 40 в 20 за 200 шагов).
Мы корректируем значения каждой точки по осям x, y и z, вычитая количество единиц перемещения из объекта helper.
Новая точка объекта helper сохраняется в tx, ty и tz (t{x/y/z}=helper.points[i].{x/y/z}).
glBegin(GL_POINTS); // Начало рисования точек
// Цикл по всем вершинам объекта morph1 (все объекты состоят из
for(int i=0; i<morph1.verts; i++)
{ // одинакового количества вершин, также можно использовать maxver)
// Если morph равно True, вычисляем перемещение, иначе перемещение = 0
if(morph) q=calculate(i); else q.x=q.y=q.z=0;
// Вычитание q.x единиц из helper.points[i].x (перемещение по оси X)
helper.points[i].x-=q.x;
// Вычитание q.y единиц из helper.points[i].y (перемещение по оси Y)
helper.points[i].y-=q.y;
// Вычитание q.z единиц из helper.points[i].z (перемещение по оси Z)
helper.points[i].z-=q.z;
// Делаем временную переменную X равной вспомогательной X
tx=helper.points[i].x;
// Делаем временную переменную Y равной вспомогательной Y
ty=helper.points[i].y;
// Делаем временную переменную Z равной вспомогательной Z
tz=helper.points[i].z;
Теперь, когда вычислена новая позиция, пришло время нарисовать наши точки. Устанавливаем ярко-голубой цвет и рисуем первую точку с помощью glVertex3f(tx, ty, tz). Эта команда нарисует точку на новой вычисленной позиции.
Затем мы делаем цвет более темным и перемещаемся на 2 единицы в вычисленном направлении, вместо одной. Это перемещает точку на новую вычисленную позицию, а затем перемещает ее опять в том же направлении. Таким образом, если она путешествовала влево на 0.1 единиц, то следующая точка окажется на 0.2 единиц. После вычисления 2 позиций вперед, мы рисуем вторую точку.
Наконец, мы устанавливаем темно-синий цвет и вычисляем будущее перемещение. На этот раз, используя наш пример, мы переместимся на 0.4 единиц влево вместо 0.1 или 0.2. Конечным результатом является небольшой хвост, движущийся за перемещающимися точками. С включенным смешиванием это дает очень крутой эффект!
glEnd() говорит OpenGL о том, что мы закончили рисовать точки.
glColor3f(0, 1, 1); // Установить цвет в ярко голубой
glVertex3f(tx, ty, tz); // Нарисовать точку
glColor3f(0, 0.5f, 1); // Темный цвет
tx-=2*q.x; ty-=2*q.y; ty-=2*q.y; // Вычислить на две позиции вперед
glVertex3f(tx, ty, tz); // Нарисовать вторую точку
glColor3f(0, 0, 1); // Очень темный цвет
tx-=2*q.x; ty-=2*q.y; ty-=2*q.y; // Вычислить еще на две позиции вперед
glVertex3f(tx, ty, tz); // Нарисовать третью точку
} // Это создает призрачный хвост, когда точки двигаются
glEnd(); // Закончим рисовать точки
Напоследок, мы проверяем, равняется ли morph значению TRUE и меньше ли step значения переменной steps (200). Если step меньше 200, то мы увеличиваем step на 1.
Если morph равно FALSE или step больше или равно steps (200), то morph устанавливается в FALSE, объект sour (источник) устанавливается равным объекту dest (назначение), и step сбрасывается в 0. Это говорит программе о том, что, либо морфинг не происходит, либо он уже завершился.
// Если делаем морфинг и не прошли все 200 шагов, то увеличим счетчик
// Иначе сделаем морфинг ложью, присвоим источник назначению и счетчик обратно в ноль
if(morph && step<=steps) step++; else { morph=FALSE; sour=dest; step=0; }
}
Код функции KillGLWindow() не сильно изменился. Единственное существенное отличие заключается в том, что мы освобождаем все объекты из памяти, перед тем как убить окно. Это предотвращает утечку памяти, да и просто является хорошим стилем программирования ;)
Lvoid KillGLWindow(GLvoid) // Правильное завершение работы окна
{
objfree(&morph1); // Освободим память
objfree(&morph2);
objfree(&morph3);
objfree(&morph4);
objfree(&helper);
if (fullscreen) // Полноэкранный режим?
{
ChangeDisplaySettings(NULL, 0); // Перейти обратно в режим рабочего стола
ShowCursor(TRUE); // Показать указатель мыши
}
if (hRC) // Есть контекст визуализации?
{
if (!wglMakeCurrent(NULL, NULL)) // Можем освободим контексты DC и RC?
{
MessageBox(NULL, "Release Of DC And RC Failed.", "SHUTDOWN ERROR", MB_OK |
MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC))
{
MessageBox(NULL, "Release Rendering Context Failed.", "SHUTDOWN ERROR", MB_OK |
MB_ICONINFORMATION);
}
hRC=NULL; // Установить RC в NULL
}
if (hDC && !ReleaseDC(hWnd, hDC))
{
MessageBox(NULL, "Release Device Context Failed.", "SHUTDOWN ERROR", MB_OK |
MB_ICONINFORMATION);
hDC=NULL;
}
if (hWnd && !DestroyWindow(hWnd)) // Можем удалить окно?
{
MessageBox(NULL, "Could Not Release hWnd.", "SHUTDOWN ERROR", MB_OK |
MB_ICONINFORMATION);
hWnd=NULL; // Установить hWnd в NULL
}
if (!UnregisterClass("OpenGL", hInstance))
{
MessageBox(NULL, "Could Not Unregister Class.", "SHUTDOWN ERROR", MB_OK |
MB_ICONINFORMATION);
hInstance=NULL;
}
}
Код функций CreateGLWindow() and WndProc() не изменился. Пропустим его.
BOOL CreateGLWindow() // Создает GL окно
LRESULT CALLBACK WndProc() // Дескриптор этого окна
Некоторые изменения произошли в функции WinMain(). Во-первых, изменилась надпись в заголовке окна.
int WINAPI WinMain( HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Состояние отображения окна
{
MSG msg; // Структура сообщений Windows
BOOL done=FALSE; // Переменная для выхода из цикла
// Спросить пользователя какой он предпочитает режим
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start
FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // Оконный режим
}
// Create Our OpenGL Window
if (!CreateGLWindow(
"Piotr Cieslak & NeHe's Morphing Points tutorial",640,480,16,fullscreen))
{
return 0; // Выходим если окно не было создано
}
while(!done) // Цикл, который продолжается пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение?
{
if (msg.message==WM_QUIT) // Мы получили сообщение о выходе?
{
done=TRUE; // Если так done=TRUE
}
else // Если нет, продолжаем работать с сообщениями окна
{
TranslateMessage(&msg); // Переводим сообщение
DispatchMessage(&msg); // Отсылаем сообщение
}
}
else // Если сообщений нет
{
// Рисуем сцену. Ожидаем нажатия кнопки ESC
// Активно? Было получено сообщение о выходе?
if (active && keys[VK_ESCAPE])
{
done=TRUE; // ESC просигналил "выход"
}
else // Не время выходить, обновляем экран
{
// Нарисовать сцену (Не рисовать, когда неактивно 1% использования CPU)
DrawGLScene();
SwapBuffers(hDC); // Переключаем буферы (Двойная буферизация)
Код ниже отслеживает нажатия клавиш. Оставшийся код довольно легок для понимания. Если нажата клавиша Page Up, то мы увеличиваем значение zspeed. Это приводит к тому, что объект начинает вращаться быстрее по оси Z в положительном направлении.
Если нажата клавиша Page Down, то мы уменьшаем значение zspeed. Это приводит к повышению скорости вращения объекта по оси Z в отрицательном направлении.
Если нажата клавиша Стрелка Вниз, то мы увеличиваем значение xspeed. Это приводит к повышению скорости вращения объекта по оси X в положительном направлении.
Если нажата клавиша Стрелка Вверх, то мы уменьшаем значение xspeed. Это приводит к повышению скорости вращения объекта по оси X в отрицательном направлении.
Если нажата клавиша Стрелка Вправо, то мы увеличиваем значение yspeed. Это приводит к повышению скорости вращения объекта по оси Y в положительном направлении.
Если нажата клавиша Стрелка Влево, то мы уменьшаем значение yspeed. Это приводит к повышению скорости вращения объекта по оси Y в отрицательном направлении.
if (keys[VK_PRIOR]) // Page Up нажата?
zspeed+=0.01f; // Увеличиваем zspeed
if (keys[VK_NEXT]) // Page Down нажата?
zspeed-=0.01f; // Уменьшаем zspeed
if (keys[VK_DOWN]) // Стрелка Вниз нажата?
xspeed+=0.01f; // Увеличиваем xspeed
if (keys[VK_UP]) // Стрелка Вверх нажата?
xspeed-=0.01f; // Уменьшаем xspeed
if (keys[VK_RIGHT]) // Стрелка Вправо нажата?
yspeed+=0.01f ; // Увеличиваем yspeed
if (keys[VK_LEFT]) // Стрелка Влево нажата?
yspeed-=0.01f; // Уменьшаем yspeed
Следующие клавиши физически перемещают объект. 'Q' перемещает его внутрь экрана, 'Z' перемещает его к зрителю, 'W' перемещает объект вверх, 'S' перемещает объект вниз, 'D' перемещает его вправо, и, наконец, 'A' перемещает его влево.
if (keys['Q']) // Клавиша Q нажата и удерживается?
cz-=0.01f; // Перемещение объекта прочь от зрителя
if (keys['Z']) // Клавиша Z нажата и удерживается?
cz+=0.01f; // Перемещение объекта к зрителю
if (keys['W']) // Клавиша W нажата и удерживается?
cy+=0.01f; // Перемещение объекта вверх
if (keys['S']) // Клавиша S нажата и удерживается?
cy-=0.01f; // Перемещение объекта вниз
if (keys['D']) // Клавиша D нажата и удерживается?
cx+=0.01f; // Перемещение объекта вправо
if (keys['A']) // Клавиша A нажата и удерживается?
cx-=0.01f; // Перемещение объекта влево
Здесь мы отслеживаем нажатие клавиш с 1 по 4. Если нажата клавиша 1, и переменная key не равна 1 (не является текущим объектом), и значение morph равно FALSE (в текущий момент не происходит морфинг), то мы устанавливаем key в 1, тем самым, сообщая нашей программе, что мы только что выбрали объект 1. Затем мы устанавливаем morph в TRUE, позволяя нашей программе начать морфинг, и, наконец, мы делаем объект-назначение (dest) равным объекту 1 (morph1).
Обработка нажатия клавиш 2, 3 и 4 аналогична. Если нажата клавиша 2, то мы делаем dest равной morph2 и устанавливаем key равной 2. Нажатие 3 устанавливает dest в morph3 и key в 3.
Устанавливая значение переменной key в значение только что нажатой нами клавиши, мы предотвращаем попытку пользователя сделать морфинг из сферы в сферу или из тора в тор!
if (keys['1'] && (key!=1) && !morph) // Если нажата 1, key не равно 1 и morph равен False
{
key=1; // Устанавливаем key в 1 (для предотвращения нажатия 1 два раза подряд)
morph=TRUE; // Устанавливаем morph в True (Начинаем процесс морфинга)
dest=&morph1; // Устанавливаем объект-назначение в morph1
}
if (keys['2'] && (key!=2) && !morph) // Если нажата 2, key не равно 2 и morph равен False
{
key=2; // Устанавливаем key в 2 (для предотвращения нажатия 2 два раза подряд)
morph=TRUE; // Устанавливаем morph в True (Начинаем процесс морфинга)
dest=&morph2; // Устанавливаем объект-назначение в morph2
}
if (keys['3'] && (key!=3) && !morph) // Если нажата 3, key не равно 3 и morph равен False
{
key=3; // Устанавливаем key в 3 (для предотвращения нажатия 3 два раза подряд)
morph=TRUE; // Устанавливаем morph в True (Начинаем процесс морфинга)
dest=&morph3; // Устанавливаем объект-назначение в morph3
}
if (keys['4'] && (key!=4) && !morph) // Если нажата 4, key не равно 4 и morph равен False
{
key=4; // Устанавливаем key в 4 (для предотвращения нажатия 4 два раза подряд)
morph=TRUE; // Устанавливаем morph в True (Начинаем процесс морфинга)
dest=&morph4; // Устанавливаем объект-назначение в morph4
}
Наконец, если нажата клавиша F1, то мы переключаемся из полноэкранного режима в оконный режим или наоборот!
if (keys[VK_F1]) // Нажата клавиша F1?
{
keys[VK_F1]=FALSE; // Если да, то устанавливаем ее в FALSE
KillGLWindow(); // Убиваем наше текущее окно
fullscreen=!fullscreen; // Переключаемся в Полноэкранный/Оконный режим
// Регенерируем наше OpenGL окно
if (!CreateGLWindow("Piotr Cieslak & NeHe's Morphing Points Tutorial",
640, 480, 16, fullscreen))
{
return 0; // Выход если окно не было создано
}
}
}
}
}
// Завершение
KillGLWindow(); // Убиваем окно
return (msg.wParam); // Выходим из программы
}
Я надеюсь, что Вам понравился этот урок. Это не сложный урок, но Вы можете извлечь из кода много полезного! Анимация в моей демонстрации dolphin осуществляется по правилам, аналогичным изложенным в этом уроке. Играя с этим кодом, Вы можете добиться реально крутых результатов! Превращение точек в слова. Анимация и др. Возможно, что Вы захотите использовать многоугольники или линии вместо точек. Эффект может получиться очень впечатляющим!
Piotr обновил код. Я надеюсь, что после прочтения этого урока Вы стали больше понимать о сохранении и загрузке объектов из файла, о манипулировании данными для достижения крутых GL эффектов в Ваших собственных программах! Написание файла .html этого урока заняло 3 дня. Если Вы обнаружили какие-либо ошибки, пожалуйста, дайте мне знать. Большая часть этого урока создана глубокой ночью, поэтому возможны некоторые ошибки. Я хочу улучшить свои уроки настолько, насколько это возможно. Обратная связь очень важна!
RabidHaMsTeR создал демо "Morph" до того как написал этот урок, в котором лучше показана более усовершенствованная версия этого эффекта. Вы можете проверить это сами на http://homepage.ntlworld.com/fj.williams/PgSoftware.html.
© Piotr Cieslak
© Jeff Molofee (NeHe)
5 августа 2003 (c) Popov Denis