Реалистичное отражение с использование буфера шаблона и отсечения.
Clipping & Reflections Using The Stencil Buffer
Добро пожаловать в следующий урок № 26, довольно интересный урок. Код для него написал Banu Cosmin. Автором, конечно же, являюсь я (NeHe). Здесь вы научитесь создавать исключительно реалистичные отражения безо всяких подделок. Отражаемые объекты не будут выглядеть как бы под полом или по другую сторону стены, нет. Отражение будет настоящим!
Очень важное замечание по теме этого занятия: поскольку Voodoo1, 2 и некоторые другие видеокарты не поддерживают буфер шаблона (stencil buffer, или буфер трафарета или стенсильный буфер), программа на них не заработает. Код, приведенный здесь, работает ТОЛЬКО на картах, поддерживающих буфер шаблона. Если вы не уверены, что ваша видеосистема его поддерживает, скачайте пример и попробуйте его запустить. Для программы не требуется мощный процессор и видеокарта. Даже на моем GeForce1 лишь временами я замечал некоторое замедление. Данная демонстрационная программа лучше всего работает в 32-битном цветовом режиме.
Поскольку видеокарты становятся все лучше, процессоры - быстрее, с моей точки зрения, поддержка буфера шаблона становится все более распространенной. Итак, если оборудование позволяет и вы готовы к отражению, приступим к занятию!
Первая часть программы довольно стандартна. Мы включаем необходимые заголовочные файлы, готовим наш контекст устройства (Device Context), контекст рендеринга (Rendering Context) и т.д.
#include <windows.h> // заголовочный файл для Windows
#include <math.h> // заголовочный файл для математической библиотеки Windows(добавлено)
#include <stdio.h> // заголовочный файл для стандартного ввода/вывода(добавлено)
#include <stdarg.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; // Копия нашего приложения
Далее идут стандартные переменные: keys[] – массив для работы с последовательностями нажатий клавиш, active – показывает, активна программа или нет, и индикатор полноэкранного режима – fullscreen.
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активности окна, TRUE по умолчанию
bool fullscreen=TRUE; // Флаг полноэкранного режима, TRUE по умолчанию
Далее мы настроим параметры нашего освещения. В LightAmb[] поместим настройки окружающего освещения. Возьмем его составляющие в пропорции 70% красного, 70% синего и 70% зеленого цвета, что даст нам белый свет с яркостью в 70%. LightDif[]-массив с настройками рассеянного освещения (это составляющая света, равномерно отражаемая поверхностью нашего объекта). В данном случае нам потребуется освещение максимальной интенсивности для наилучшего отражения. И массив LightPos[] используется для размещения источника освещения. Сместим его на 4 единицы вправо, 4 единицы вверх и на 6 единиц к наблюдателю. Для более актуального освещения источник размещается на переднем плане правого верхнего угла экрана.
//Параметры освещения
static Glfloat LightAmb[]={0.7f, 0.7f, 0.7f}; //Окружающий свет
static Glfloat LightDif[]={1.0f, 1.0f, 1.0f}; //Рассеянный свет
//Позиция источника освещения
static Glfloat LightPos[]={4.0f, 4.0f, 6.0f, 1.0f};
Настроим переменную q для нашего квадратичного объекта (квадратичным объект, по-видимому, называется по той причине, что его полигонами являются прямоугольники – прим.перев), xrot и yrot – для осуществления вращения. xrotspeed и yrotspeed – для управления скоростью вращения. Переменная zoom используется для приближения и отдаления нашей сцены (начальное значение = -7, при котором мы увидим полную сцену), и переменная height содержит значение высоты мяча над полом.
Затем выделим массив для трех текстур и определим WndProc().
GLUquadricObj *q; // Квадратичный объект для рисования сферы мяча
GLfloat xrot = 0.0f; // Вращение по Х
GLfloat yrot = 0.0f; // Вращение по Y
GLfloat xrotspeed = 0.0f;// Скорость вращения по X
GLfloat yrotspeed = 0.0f;// Скорость вращения по Y
GLfloat zoom = -7.0f; // Глубина сцены в экране
GLfloat height = 2.0f; // Высота мяча над полом
GLuint texture[3]; // 3 Текстуры
// Объявление WndProc
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
Функции ReSizeGLScene() и LoadBMP() не меняются, так что я их обе пропускаю.
// Функция изменения размера и инициализации OpenGL-окна
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
// Функция загрузки растрового рисунка
AUX_RGBImageRec *LoadBMP(char *Filename)
Код, загружающий текстуру довольно стандартный. Вы могли пользоваться им не раз при изучении предыдущих статей. Мы создали массив для трех текстур, затем мы загружаем три рисунка и создаем три текстуры с линеарной фильтрацией из данных рисунков. Файлы с растровыми рисунками мы ищем в каталоге DATA.
int LoadGLTextures() // Загрузка рисунков и создание текстур
{
int Status=FALSE; // Индикатор статуса
AUX_RGBImageRec *TextureImage[3]; // массив для текстур
memset(TextureImage,0,sizeof(void *)*3); // Обнуление
if ((TextureImage[0]=LoadBMP("Data/EnvWall.bmp")) && // Текстура пола
(TextureImage[1]=LoadBMP("Data/Ball.bmp")) && // Текстура света
(TextureImage[2]=LoadBMP("Data/EnvRoll.bmp"))) // Текстура стены
{
Status=TRUE; // Статус положительный
glGenTextures(3, &texture[0]); // Создание текстуры
for (int loop=0; loop<3; loop++) // Цикл для 3 текстур
{
glBindTexture(GL_TEXTURE_2D, texture[loop]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX,
TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE,
TextureImage[loop]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
for (loop=0; loop<3; loop++) // Цикл для 3 рисунков
{
if (TextureImage[loop]) // Если текстура существует
{
if (TextureImage[loop]->data) // Если рисунок есть
{
free(TextureImage[loop]->data);
}
free(TextureImage[loop]); // Очистить память из-под него
}
}
}
return Status; // Вернуть статус
}
В функции инициализации представлена новая команда OpenGL – glClearStencil. Значение параметра = 0 для этой команды говорит о том, что очищать буфер шаблона не надо. С остальной частью функции вы уже, должно быть, знакомы. Мы загружаем наши текстуры и включаем плавное закрашивание. Цвет очистки экрана - синий, значение очистки буфера глубины = 1.0f. Значение очистки буфера шаблона = 0. Мы включаем проверку глубины и устанавливаем значение проверки глубины меньше или равной установленному значению. Коррекция перспективы выбрана наилучшего качества и включается 2D-текстурирование.
int InitGL(GLvoid) // Инициализация OpenGL
{
if (!LoadGLTextures()) // Если текстуры не загружены, выход
{
return FALSE;
}
glShadeModel(GL_SMOOTH);//Включаем плавное закрашивание
glClearColor(0.2f, 0.5f, 1.0f, 1.0f);// Фон
glClearDepth(1.0f); // Значение для буфера глубины
glClearStencil(0); // Очистка буфера шаблона 0
glEnable(GL_DEPTH_TEST);//Включить проверку глубины
glDepthFunc(GL_LEQUAL); // Тип проверки глубины
// Наилучшая коррекция перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glEnable(GL_TEXTURE_2D);//Включить рисование 2D-текстур
Теперь пора настроить источник света GL_LIGHT0. Первая нижеприведенная строка говорит OpenGL использовать массив LightAmp для окружающего света. Если вы помните начало программы, RGB-компоненты в этом массиве были все равны 0.7f , что означает 70% интенсивности белого света. Затем мы при помощи массива LightDif настраиваем рассеянный свет, и местоположение источника света – значениями x,y,z из массива LightPos.
После настройки света мы включаем его командой glEnable(GL_LIGHT0). Хотя источник включен, вы его не увидите, пока не включите освещение командой glEnable(GL_LIGHTING).
Примечание: если вы хотите отключить все источники света в сцене, примените команду glDisable(GL_LIGHTING). Для отключения определенного источника света надо использовать команду glEnable(GL_LIGHT{0-7}). Эти команды позволяют нам контролировать освещение в целом, а также источники по отдельности. Еще раз запомните, пока не отработает команда glEnable(GL_LIGHTING), ничего вы на своей 3D-сцене не увидите.
// Фоновое освещение для источника LIGHT0
glLightfv(GL_LIGHT0, GL_AMBIENT, LightAmb);
// Рассеянное освещение для источника LIGHT0
glLightfv(GL_LIGHT0, GL_DIFFUSE, LightDif);
// Положение источника LIGHT0
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
// Включить Light0
glEnable(GL_LIGHT0);
// Включить освещение
glEnable(GL_LIGHTING);
В первой строке мы создаем новый квадратичный объект. Затем мы говорим OpenGL о типе генерируемых нормалей для нашего квадратичного объекта - нормали сглаживания. Третья строка включает генерацию координат текстуры для квадратичного объекта. Без этих строк – второй и третьей закрашивание объекта будет плоским и невозможно будет наложить на него текстуру.
Четвертая и пятая строки говорят OpenGL использовать алгоритм сферического наложения (Sphere Mapping) для генерации координат для текстуры. Это дает нам доступ к сферической поверхности квадратичного объекта.
q = gluNewQuadric(); // Создать квадратичный объект
// тип генерируемых нормалей для него – «сглаженные»
gluQuadricNormals(q, GL_SMOOTH);
// Включить текстурные координаты для объекта
gluQuadricTexture(q, GL_TRUE);
// Настройка сферического наложения
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
// Настройка отображения сферы
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
return TRUE; // Инициализация прошла успешно
}
Нижеприведенная функция рисует наш объект (в самом деле, неплохо смотрящийся пляжный мяч).
Устанавливаем цвет с максимальной интенсивностью белого и подключаем текстуру мяча (состоящую из последовательности красной, белой и синей полос).
После этого мы рисуем квадратичную сферу (Quadratic Sphere) с радиусом в 0.35f, 32 срезами (разбиениями вокруг оси Z) и 16 полосами (разбиениями вдоль оси Z) (вверх и вниз).
void DrawObject() // Рисование мяча
{
glColor3f(1.0f, 1.0f, 1.0f);// Цвет - белый
glBindTexture(GL_TEXTURE_2D, texture[1]);// Выбор текстуры 2 (1)
gluSphere(q, 0.35f, 32, 16);// Рисование первого мяча
Нарисовав первый мяч, выбираем новую текстуру (EnvRoll), устанавливаем значение прозрачности в 40% и разрешаем смешивание цветов, основанное на значении альфа (прозрачности). Команды glEnable(GL_TEXTURE_GEN_S) и glEnable(GL_TEXTURE_GEN_T) разрешают сферическое наложение.
После всего этого мы перерисовываем сферу, отключаем сферическое наложение и отключаем смешивание.
Конечный результат – это отражение, которое выглядит как блики на пляжном мяче. Так как мы включили сферическое наложение, текстура всегда повернута к зрителю, даже если мяч вращается. Поэтому мы применили смешивание, так что новая текстура не замещает старую (одна из форм мультитекстурирования).
glBindTexture(GL_TEXTURE_2D, texture[2]);// Выбор текстуры 3 (2)
glColor4f(1.0f, 1.0f, 1.0f, 0.4f);// Белый цвет с 40%-й прозрачностью
glEnable(GL_BLEND); // Включить смешивание
// Режим смешивания
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
// Разрешить сферическое наложение
glEnable(GL_TEXTURE_GEN_S);
// Разрешить сферическое наложение
glEnable(GL_TEXTURE_GEN_T);
// Нарисовать новую сферу при помощи новой текстуры
gluSphere(q, 0.35f, 32, 16);
// Текстура будет смешена с созданной для эффекта мультитекстурирования (Отражение)
glDisable(GL_TEXTURE_GEN_S); // Запретить сферическое наложение
glDisable(GL_TEXTURE_GEN_T); // Запретить сферическое наложение
glDisable(GL_BLEND); // Запретить смешивание
}
Следующая функция рисует пол, над которым парит наш мяч. Мы выбираем текстуру пола (EnvWall), и рисуем один текстурированный прямоугольник, расположенный вдоль оси Z.
void DrawFloor() // Рисование пола
{
glBindTexture(GL_TEXTURE_2D, texture[0]);// текстура 1 (0)
glBegin(GL_QUADS); // Начало рисования
glNormal3f(0.0, 1.0, 0.0); // «Верхняя» нормаль
glTexCoord2f(0.0f, 1.0f); // Нижняя левая сторона текстуры
glVertex3f(-2.0, 0.0, 2.0);//Нижний левый угол пола
glTexCoord2f(0.0f, 0.0f); //Верхняя левая сторона текстуры
glVertex3f(-2.0, 0.0,-2.0);//Верхний левый угол пола
glTexCoord2f(1.0f, 0.0f); //Верхняя правая сторона текстуры
glVertex3f( 2.0, 0.0,-2.0);//Верхний правый угол пола
glTexCoord2f(1.0f, 1.0f); //Нижняя правая сторона текстуры
glVertex3f( 2.0, 0.0, 2.0);//Нижний правый угол пола
glEnd(); // конец рисования
}
Теперь одна забавная вещь. Здесь мы скомбинируем все наши объекты и изображения для создания сцены с отражениями.
Начинаем мы с того, что очищаем экран (GL_COLOR_BUFFER_BIT) синим цветом (задан ранее в программе). Также очищаются буфер глубины (GL_DEPTH_BUFFER_BIT) и буфер шаблона (GL_STENCIL_BUFFER_BIT). Убедитесь в том, что вы включили команду очистки буфера шаблона, так как это новая команда для вас и ее легко пропустить. Важно отметить, что при очистке буфера шаблона мы заполняем его нулевыми значениями.
После очистки экрана и буферов мы определяем наше уравнение для плоскости отсечения. Плоскость отсечения нужна для отсечения отражаемого изображения.
Выражение eqr[]={0.0f,-1.0f, 0.0f, 0.0f} будет использовано, когда мы будем рисовать отраженное изображение. Как вы можете видеть, Y-компонента имеет отрицательное значение. Это значит, что мы увидим пиксели рисунка, если они появятся ниже пола, то есть с отрицательным значением по Y-оси. Любой другой графический вывод выше пола не будет отображаться, пока действует это уравнение.
Больше об отсечении будет позже… читайте дальше.
int DrawGLScene(GLvoid)// Рисование сцены
{
// Очистка экрана, буфера глубины и буфера шаблона
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// Уравнение плоскости отсечения для отсечения отраженных объектов
double eqr[] = {0.0f,-1.0f, 0.0f, 0.0f};
Итак, мы очистили экран и определили плоскости отсечения. Теперь перейдем к изюминке нашего руководства!
Сначала сбросим матрицу модели. После чего все процессы рисования будут начинаться из центра экрана. Затем мы перемещаемся вниз на 0.6f единиц (для наклонной перспективы пола) и в экран на значение zoom. Для лучшего понимания, для чего мы перемещаемся вниз на 6.0f единиц, я приведу вам простой пример. Если вы смотрите на лист бумаги на уровне своих глаз, вы его едва видите – так он скорее похож на тонкую полоску. Если вы переместите лист немного вниз, он перестанет быть похожим на линию. Вы увидите большую площадь бумаги, так как ваши глаза будут обращены вниз на лист вместо прямого взгляда на его ребро.
glLoadIdentity();// Сброс матрицы модели
// Отдаление и подъем камеры над полом (на 0.6 единиц)
glTranslatef(0.0f, -0.6f, zoom);
Далее мы настроим маску цвета – новую вещь в этом руководстве. Маска представлена 4 значениями : красный, зеленый, синий и альфа-значение(прозрачность). По умолчанию все значения устанавливаются в GL_TRUE.
Если бы значение красной компоненты в команде glColorMask({red},{green},{blue},{alpha}) было установлено в GL_TRUE и в то же самое время все другие значения были равны 0 (GL_FALSE), единственный цвет, который бы мы увидели на экране, это красный. Соответственно, если бы ситуация была обратной (красная компонента равна GL_FALSE, а все остальные равны GL_TRUE), то на экране мы бы увидели все цвета за исключением красного.
Нам не нужно что-либо выводить на экран в данный момент, поэтому установим все четыре значения в 0.
glColorMask(0,0,0,0); // Установить маску цвета
Именно сейчас пойдет речь об изюминке урока… Настроим буфер шаблона и проверку шаблона.
Сначала включается проверка шаблона. После того, как была включена проверка шаблона, мы можем изменять буфер шаблона.
Немного сложно объяснить работу команд, приводимых ниже, так что, пожалуйста, потерпите, а если у вас есть лучшее объяснение, пожалуйста, дайте мне знать. Строка glStencilFunc(GL_ALWAYS, 1, 1) сообщает OpenGL тип проверки, производимой для каждого пикселя выводимого на экран объекта (если пиксель не выводиться, то нет и проверки - прим.перев.).
Слово GL_ALWAYS говорит OpenGL, что тест работает все время. Второй параметр – это значение ссылки, которое мы проверяем в третьей строке, и третий параметр – это маска. Маска – это значение, поразрядно умножаемое операцией AND (логическое умножение из булевой алгебры – прим.перев.) на значение ссылки и сохраняемое в буфере шаблона в конце обработки. Значение маски равно 1, и значение ссылки тоже равно 1. Так что, если мы передадим OpenGL эти параметры, и тест пройдет успешно, то в буфере шаблона сохранится единица (так как 1(ссылка)&1(маска)=1).
Небольшое разъяснение: тест шаблона – это попиксельная проверка изображения объектов, выводимых на экран во время работы теста. Значение ссылки, обработанное операцией логического умножения AND на значение маски, сравнивается с текущим значением в буфере шаблона в соответствующем пикселе, также обработанным операцией AND на значение маски.
Третья строка проверяет три различных состояния, основываясь на функции проверки шаблона, которую мы решили использовать. Первые два параметра – GL_KEEP, а третий -GL_REPLACE.
Первый параметр говорит OpenGL что делать в случае если тест не прошел. Так как этот параметр у нас установлен в GL_KEEP, в случае неудачного завершения теста (что не может случится, так как у нас вид функции установлен в GL_ALWAYS), мы оставим состояние буфера в том виде, в котором оно было на это время.
Второй параметр сообщает OpenGL о том, что делать, если тест шаблона прошел успешно, а тест глубины – нет. Далее в программе мы так и так отключаем тест глубины, поэтому этот параметр может быть игнорирован.
И, наиболее важный, третий параметр. Он сообщает OpenGL о том, что надо делать в случае, если весь тест пройден успешно. В приведенном ниже участке программы мы говорим OpenGL, что надо заместить (GL_REPLACE) значение в буфере шаблона. Значение, которое мы помещаем в буфер шаблона, является результатом логического умножения (операция AND) нашего значения ссылки и маски, значение которой равно 1.
Следующее, что нам надо сделать после указания типа теста, это отключить тест глубины и перейти к рисованию пола.
А теперь расскажу обо всем этом попроще.
Мы указываем OpenGL, что не надо ничего не отображать на экране. Значит, во время рисования пола мы ничего не должны видеть на экране. При этом любая точка на экране в том месте, где должен нарисоваться наш объект (пол) будет проверена выбранным нами тестом шаблона. Сначала буфер шаблона пуст (нулевые значения). Нам требуется, чтобы в том, месте, где должен был бы появиться наш объект (пол), значение шаблона было равной единице. При этом нам незачем самим заботиться о проверке. В том месте, где пиксель должен был бы нарисоваться, буфер шаблона помечается единицей. Значение GL_ALWAYS это обеспечивает. Это также гарантируют значения ссылки и маски, установленные в 1. Во время данного процесса рисования на экран ничего не выводится, а функция шаблона проверяет каждый пиксель и устанавливает в нужном месте 1 вместо 0.
// Использовать буфер шаблона для «пометки» пола
glEnable(GL_STENCIL_TEST);
// Всегда проходит, 1 битовая плоскость, маска = 1
glStencilFunc(GL_ALWAYS, 1, 1); // 1, где рисуется хоть какой-нибудь полигон
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glDisable(GL_DEPTH_TEST);// Отключить проверку глубины
DrawFloor();// Рисование пола (только в буфере шаблона)
Теперь у нас есть невидимая шаблонная маска пола. Пока действует проверка шаблона, пиксели будут появляться, только в тех местах, где в буфере шаблона будет установлена 1. И он у нас устанавливается в 1 в том месте, где выводился невидимый пол. Это значит, что мы увидим рисунок лишь в том месте, где невидимый пол установил 1 в буфер шаблона. Этот трюк заставляет появиться отражение лишь на полу и нигде более!
Итак, теперь мы уверены, что отражение мяча нарисуется только на полу. Значит, пора рисовать само отражение! Включаем проверку глубины и отображение всех трех составляющих цвета.
Взамен использования значения GL_ALWAYS в выборе шаблонной функции мы станем использовать значение GL_EQUAL. Значение ссылки и маски оставим равными 1. Для всех операций с шаблоном установим все параметры в GL_KEEP. Проще говоря, теперь любой объект может быть отображен на экране (поскольку цветовая маска установлена в истину для каждого цвета). Во время работы проверки шаблона, выводимые пиксели будут отображаться лишь в том месте, где буфер шаблона установлен в 1 (значение ссылки AND значение маски (1&1) равно 1, что эквивалентно (GL_EQUAL) значению буфера шаблона AND значение маски, что также равно 1). Если в том месте, где рисуется пиксель, буфер шаблона не равен 1, пиксель не отобразится. Значение GL_KEEP запрещает модифицировать буфер шаблона вне зависимости от результата проверки шаблона.
glEnable(GL_DEPTH_TEST); // Включить проверку глубины
glColorMask(1,1,1,1); // Маска цвета = TRUE, TRUE, TRUE, TRUE
glStencilFunc(GL_EQUAL, 1, 1); // Рисуем по шаблону (где шаблон=1)
// (то есть в том месте, где был нарисован пол)
// Не изменять буфер шаблона
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
Теперь подключим плоскость отсечения для отражения. Эта плоскость задается массивом eqr, и разрешает рисовать только те объекты, которые выводятся в пространстве от центра экрана (где находится наш пол) и ниже. Это способ для того, чтобы не дать отражению мяча появиться выше центра пола. Будет некрасиво, если он это сделает. Если вы еще не поняли, что я имею ввиду, уберите первую строку в приведенном ниже коде и переместите клавишами исходный мяч (не отраженный) через пол.
После подключения плоскости отсечения plane0(обычно применяют от 0 до 5 плоскостей отсечения), мы определяем ее, передав параметры из массива eqr.
Сохраняем матрицу (относительно ее позиционируются все объекты на экране) и применяем команду glScalef(1.0f,-1.0f,1.0f) для поворота всех вещей сверху вниз (придавая отражению реальный вид). Негативное значение для Y-параметра в этой команде заставляет OpenGL рисовать все в положении с обратной координатой Y (то есть, «вниз головой» - прим.перев.). Это похоже на переворачивание картинки сверху вниз. Объект с положительным значением по оси Y появляется внизу экрана, а не вверху. Если вы поворачиваете объект к себе, он поворачивается от вас (словом, представьте себя Алисой – прим.перев.). Любая вещь будет перевернута, пока не будет восстановлена матрица или не отработает та же команда с положительным значением Y-параметра (1) (glScalef({x},1.0f,{z}).
glEnable(GL_CLIP_PLANE0);// Включить плоскость отсечения для удаления
// артефактов(когда объект пересекает пол)
glClipPlane(GL_CLIP_PLANE0, eqr);// Уравнение для отраженных объектов
glPushMatrix(); // Сохранить матрицу в стеке
glScalef(1.0f, -1.0f, 1.0f); // Перевернуть ось Y
Первая нижеприведенная строка перемещает наш источник света в позицию, заданную в массиве LightPos. Источник света должен освещать правую нижнюю часть отраженного мяча, имитируя почти реальный источник света. Позиция источника света также перевернута. При рисовании «настоящего» мяча (мяч над полом) свет освещает правую верхнюю часть экрана и создает блик на правой верхней стороне этого мяча. При рисовании отраженного мяча источник света будет расположен в правой нижней стороне экрана.
Затем мы перемещаемся вниз или вверх по оси Y на значение, определенное переменной height. Перемещение также переворачивается, так что если значение height =0.5f, позиция перемещения превратится в -5.0f. Мяч появится под полом вместо того, чтобы появиться над полом!
После перемещения нашего отраженного мяча, нам нужно повернуть его по осям X и Y на значения xrot и yrot соответственно. Запомните, что любые вращения по оси X также переворачиваются. Так, если верхний мяч поворачивается к вам по оси X, то отраженный мяч поворачивается от вас.
После перемещения и вращения мяча нарисуем его функцией DrawObject(), и восстановим матрицу из стека матриц, для восстановления ее состояния на момент до рисования мяча. Восстановленная матрица прекратит отражения по оси Y.
Затем отключаем плоскость отсечения (plan0), так как нам больше не надо ограничивать рисование нижней половиной экрана, и отключаем шаблонную проверку, так что теперь мы можем рисовать не только в тех точках экрана, где должен быть пол.
Заметьте, что мы рисуем отраженный мяч раньше пола.
// Настройка источника света Light0
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
glTranslatef(0.0f, height, 0.0f);// Перемещение объекта
// Вращение локальной координатной системы по X-оси
glRotatef(xrot, 1.0f, 0.0f, 0.0f);
// Вращение локальной координатной системы по Y-оси
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
DrawObject();// Рисование мяча (для отражения)
glPopMatrix(); // Восстановить матрицу
glDisable(GL_CLIP_PLANE0);// Отключить плоскость отсечения
// Отключение проверки шаблона
glDisable(GL_STENCIL_TEST);
Начнем эту секцию с позиционирования источника света. Так как ось Y больше не перевернута, свет будет освещать верхнюю часть экрана, а не нижнюю.
Включаем смешивание цветов, отключаем освещение и устанавливаем компоненту прозрачности в 80% в команде glColor4f(1.0f,1.0f,1.0f,0.8f). Режим смешивания настраивается командой glBlendFunc(), и полупрозрачный пол рисуется поверх отраженного мяча.
Если бы мы сначала нарисовали пол, а затем – мяч (как нам подсказывает логика – прим.перев.), результат выглядел бы не очень хорошо. Нарисовав мяч, а затем – пол, вы увидите небольшой участок пола, смешанный с рисунком мяча. Когда я посмотрю в синее зеркало, я предположу, что отражение будет немного синим.
Нарисовав сначала пол, последующим отображением пола мы придадим отраженному изображению мяча легкую окраску пола.
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);// Положение источника
// Включить смешивание (иначе не отразится мяч)
glEnable(GL_BLEND);
// В течение использования смешивания отключаем освещение
glDisable(GL_LIGHTING);
// Цвет белый, 80% прозрачности
glColor4f(1.0f, 1.0f, 1.0f, 0.8f);
// Смешивание, основанное на «Source Alpha And 1 Minus Dest Alpha»
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawFloor();// Нарисовать пол
Теперь нарисуем «настоящий» мяч (парящий над полом). При рисовании пола освещение было отключено, но теперь мы опять его включим.
Так как смешивание нам более не потребуется, мы его отключаем. Если мы этого не сделаем, изображение мяча смешается с изображением пола. Нам не нужно, чтобы мяч выглядел, как его отражение, поэтому мы и отключаем смешивание цветов.
Мы не будем отсекать «настоящий» мяч. Если мяч будет проходить через пол, мы должны видеть его выходящим из пола снизу. Если мы будем использовать отсечение, мяч снизу пола не появится. При возникновении необходимости запретить мячу появляться снизу пола вы можете применить значение плоскости отсечения, где будет указано положительное значение Y-координаты. При этом мяч будет виден, только когда он будет рисоваться в верхней части экрана, до той Y-координаты, которую вы укажете в выражении плоскости отсечения. В данном демонстрационном примере у нас нет необходимости этого делать, поэтому мяч будет виден по обе стороны пола.
Затем мы перемещаемся на позицию вывода, заданную в переменной heght. Только теперь ось Y не перевернута, поэтому мяч двигается в направлении, противоположном направлению движения отраженного мяча.
Мяч вращается, и, опять же, поскольку ось Y на данный момент не перевернута, мяч будет вращаться в направлении, обратном направлению вращения отраженного мяча. Если отраженный мяч вращается к вам, «реальный» мяч вращается от вас. Это дополняет иллюзию отражения.
После перемещения и поворота мы рисуем мяч.
glEnable(GL_LIGHTING);// Включить освещение
glDisable(GL_BLEND); // Отключить смешивание
glTranslatef(0.0f, height, 0.0f);// Перемещение мяча
glRotatef(xrot, 1.0f, 0.0f, 0.0f);// Поворот по оси X
glRotatef(yrot, 0.0f, 1.0f, 0.0f);// Поворот по оси Y
DrawObject(); // Рисование объекта
Следующий код служит для поворота мяча по осям X и Y. Для поворота по оси X увеличивается переменная xrot на значение переменной xrotspeed. Для поворота по оси Y увеличивается переменная yrot на значение переменной yrotspeed. Если xrotspeed имеет слишком большое позитивное или негативное значение, мяч будет крутиться быстрее, чем, если бы xrotspeed было близко к нулю. То же касается и yrotspeed. Чем больше yrotspeed, тем быстрее мяч крутится по оси Y.
Перед тем, как вернуть TRUE, выполняется команда glFlush(). Эта команда указывает OpenGL выполнить все команды, переданные ему в конвейер, что помогает предотвратить мерцание на медленных видеокартах.
xrot += xrotspeed; // Обновить угол вращения по X
yrot += yrotspeed; // Обновить угол вращения по Y
glFlush(); // Сброс конвейера OpenGL
return TRUE; // Нормальное завершение
}
Следующий код обрабатывает нажатия клавиш. Первые 4 строки проверяют нажатие вами 4 клавиш (для вращения мяча вправо, влево, вниз, вверх).
Следующие 2 строки проверяют нажатие вами клавиш ‘A’ или ‘Z’. Клавиша ‘A’ предназначена для приближения сцены, клавиша ‘Z’ – для отдаления.
Клавиши ‘PAGE UP’ и ’ PAGE UP’ предназначены для вертикального перемещения мяча.
void ProcessKeyboard() // Обработка клавиатуры
{
if (keys[VK_RIGHT]) yrotspeed += 0.08f;// Вправо
if (keys[VK_LEFT]) yrotspeed -= 0.08f; // Влево
if (keys[VK_DOWN]) xrotspeed += 0.08f; // Вверх
if (keys[VK_UP]) xrotspeed -= 0.08f; // Вниз
if (keys['A']) zoom +=0.05f; // Приближение
if (keys['Z']) zoom -=0.05f; // Отдаление
if (keys[VK_PRIOR]) height +=0.03f; // Подъем
if (keys[VK_NEXT]) height -=0.03f; // Спуск
}
Функция KillGLWindow() не меняется, поэтому пропущена.
GLvoid KillGLWindow(GLvoid)// Удаление окна
Также можно оставить и следующую функцию - CreateGLWindow(). Для большей уверенности я включил ее полностью, даже если поменялась всего одна строка в этой структуре:
static PIXELFORMATDESCRIPTOR pfd=
// pfd говорит Windows о наших запросах для формата пикселя
{
sizeof(PIXELFORMATDESCRIPTOR), // Размер структуры
1, // Номер версии
PFD_DRAW_TO_WINDOW | // Формат должен поддерживать Window
PFD_SUPPORT_OPENGL | // Формат должен поддерживать OpenGL
PFD_DOUBLEBUFFER, // Нужна двойная буферизация
PFD_TYPE_RGBA, // Формат данных- RGBA
bits, // Глубина цвета
0, 0, 0, 0, 0, 0, // Игнорируются биты цвета
0, // Нет альфа-буфера
0, // Игнорируется смещение бит
0, // Нет аккумулирующего буфера
0, 0, 0, 0, // Игнорируются биты аккумуляции
16, // 16-битный Z-буфер (глубины)
Только одно изменение в этой функции – в приведенной ниже строке. ОЧЕНЬ ВАЖНО: вы меняете значение с 0 на 1 или любое другое ненулевое значение. Во всех предыдущих уроках значение в строке ниже было равным 0. Для использования буфера шаблона это значение должно быть больше либо равным 1. Оно обозначает количество битовых планов буфера шаблона.
1, // Использовать буфер шаблона (* ВАЖНО *)
0, // Нет вспомогательного буфера
PFD_MAIN_PLANE, // Основной уровень рисования
0, // Не используются
0, 0, 0 , // Нет маски уровня
};
WndProc() не изменилась, поэтому здесь не приводится.
Здесь тоже ничего нового. Типичный запуск WinMain().
Меняется только заголовок окна, в котором содержится информация о названии урока и его авторах. Обратите внимание, что вместо обычных параметров экрана 640, 480, 16 в команду создания окна передаются переменные resx, resy и resbpp соответственно.
// Создание окна в Windows
if (!CreateGLWindow("Banu Octavian & NeHe's Stencil & Reflection Tutorial",
resx, resy, resbpp, fullscreen))
{
return 0;// Выход, если не создалось
}
while(!done)// Цикл, пока done=FALSE
{
// Выборка сообщений
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if (msg.message==WM_QUIT) // Выход?
{
done=TRUE;// Если да
}
else // Если нет, обработка сообщения
{
TranslateMessage(&msg); // Транслировать сообщение
DispatchMessage(&msg);
}
}
else // Если нет сообщений
{ // Отрисовка сцены. Ожидание клавиши ESC или
// сообщения о выходе от DrawGLScene()
if (active) // Программа активна?
{
if (keys[VK_ESCAPE])// ESC нажата?
{
done=TRUE;// Если да, выход
}
else// Иначе - рисование
{
DrawGLScene();// Рисование сцены
SwapBuffers(hDC);//Переключить буфера
Вместо проверки нажатия клавиш в WinMain(), мы переходим к нашей функции обработки клавиатуры ProcessKeyboard(). Запомните, что эта функция вызывается, только если программа активна!
ProcessKeyboard();// Обработка нажатий клавиш
}
}
}
} // Конец работы
KillGLWindow(); // Удалить окно
return (msg.wParam);// Выход из программы
}
Я надеюсь, что вам понравилось это руководство. Понимаю, что оно задаст вам немного работы. Это было одно из самых трудных руководств, написанных мною. Оно несложно для моего понимания того, что оно делает, и какие команды используются для создания эффектов, но когда вы попытаетесь объяснить это, понимая, что большинство программистов даже не слышали о буфере шаблона, это трудно.
Если вам что-то показалось лишним в программе или если вы обнаружили какие-либо ошибки в руководстве или программе, пожалуйста, дайте мне знать. Как обычно, я пытался сделать данное руководство наилучшим, насколько смог, и ваши отзывы будут наиболее ценны.
Определение столкновений и моделирование законов физики
Collision Detection
Исходный код на котором основан этот урок, взят из моей старой конкурсной работы (ее можно найти на OGLchallenge.dhs.org). Тема называлась “Сумасшедшие столкновения” и моя статья (которая, кстати, заняла первое место :)) была названа Магической Комнатой. Она освещала определение столкновений, моделирование законов физики и эффекты.
Определение столкновений
Трудная тема, и честно говоря, я не знаю до сих пор простых подходов для ее решения. Для каждого приложения существуют различные способы нахождения и проверки столкновений. Конечно, существуют обобщенные физические законы и они могут работать с любыми видами объектов, но они очень медленные.
Мы собираемся исследовать алгоритмы, которые очень быстрые, легкие для понимания и до некоторой степени гибкие. К тому же важно и должно быть рассмотрено, что сделать, когда столкновение определено, и то, как тогда перемещать объекты, в соответствии с законами физики. Мы имеет много материала для рассмотрения. Давайте просмотрим, что мы собираемся изучить:
1) Определение столкновений
Движущаяся сфера - Плоскость
Движущаяся сфера - Цилиндр
Движущаяся сфера - движущаяся сфера
2) Моделирование законов физики
Реакция на столкновение
Движение под действием гравитации с использованием уравнения Эйлера
3) Специальные эффекты
Моделирование взрыва с использованием метода Fin-Tree Billboard
Использование звуков с помощью The Windows Multimedia Library (только для Windows)
4) Разъяснение кода
Код, разделен на 5 файлов
Lesson30.cpp | : Основной код для этого учебника | ||||
Image.cpp, | Image.h | : Код загрузки текстур | |||
Tmatrix.cpp, | Tmatrix.h | : Классы обработки вращения | |||
Tray.cpp, | Tray.h | : Классы, обрабатывающие операции с лучами | |||
Tvector.cpp, | Tvector.h | : Классы, обрабатывающие операции с векторами |
В этом коде есть много удобного для дальнейшего использования! Классы операций с векторами, лучами и матрицами очень полезны. Я использую их до сих пор в собственных проектах.
1) Определение столкновений.
Для определения столкновений мы собираемся использовать алгоритмы метода трассировки лучей. Дадим сначала определение луча.
Луч задается с помощью вектора, он имеет начальную точку и вектор (обычно нормализованный), по направлению которого идет луч. По существу, луч исходит из начальной точки и движется по направлению направляющего вектора. Итак, наше уравнение луча:
PointOnRay = Raystart + t * Raydirection
t - точка, принимающая значения из [0, бесконечность).
При 0 мы получим начальную точку, используя другие значения, мы получаем соответствующие точки вдоль луча.
PointOnRay, Raystart, Raydirection - трехмерные вектора со значениями (x,y,z). Сейчас мы можем использовать это представление луча и вычислить пересечение с плоскостью или цилиндром.
Определение пересечения луча с плоскостью.
Плоскость представляется с помощью векторного представления таким образом:
Xn dot X = d
Xn, X - векторы, и d - значение с плавающей точкой.
Xn - ее нормаль.
X - точка на ее поверхности.
d - расстояние от центра системы координат до плоскости вдоль нормали.
По существу, плоскость обозначает половину пространства. Поэтому, все, что нам необходимо, чтобы определить плоскость, это 3D точка и нормаль в этой точке, которая является перпендикуляром к этой плоскости. Эти два вектора формируют плоскость, т.е. если мы возьмем для 3D точки вектор (0,0,0) и нормаль (0,1,0), мы по существу определяем плоскость через оси x и y. Поэтому, определения точки и нормали достаточно для вычисления векторного представления плоскости.
Согласно векторному уравнению плоскости, нормаль - Xn и 3D точка из которой исходит нормаль - X. Недостающие значение - d, которое легко вычисляется с помощью dot product (скалярного произведения).
(Замечание: Это векторное представление эквивалентно широко известной параметрической формуле плоскости Ax + By + Cz + D=0, для соответствия надо просто взять три значения нормали x,y,z как A,B,C и присвоить D=-d).
Вот два уравнения, которые мы пока что имеем:
PointOnRay = Raystart + t * Raydirection
Xn dot X = d
Если луч пересекает плоскость в некоторой точке, то тогда должна быть какая-то точка на луче, которая соответствует уравнению плоскости следующим образом:
Xn dot PointOnRay = d или (Xn dot Raystart) + t * (Xn dot Raydirection) = d
находя для t:
t = (d - Xn dot Raystart) / (Xn dot Raydirection)
заменяя d:
t= (Xn dot PointOnRay - Xn dot Raystart) / (Xn dot Raydirection)
сокращая его:
t= (Xn dot (PointOnRay - Raystart)) / (Xn dot Raydirection)
t представляет расстояние от начала луча до точки пересечения с плоскостью по направлению луча. Поэтому, подставляя t в уравнении луча, мы можем получить точку столкновения. Однако, существует несколько особых случаев. Если Xn dot Raydirection = 0, тогда эти два вектора перпендикулярны (луч идет паралельно плоскости), и столкновения не будет. Если t отрицателен, луч направлен в противоположную от плоскости сторону и не пересекает ее.
int TestIntersionPlane
(const Plane& plane,const TVector& position,
const TVector& direction, double& lamda, TVector& pNormal)
{
// Векторное произведение между нормалью плоскости и лучом
double DotProduct=direction.dot(plane._Normal);
double l2;
// Определить, параллелен ли луч плоскости
if ((DotProduct<ZERO)&&(DotProduct>-ZERO))
return 0;
// Определить расстояние до точки столкновения
l2=(plane._Normal.dot(plane._Position-position))/DotProduct;
if (l2<-ZERO) // Определить, пересекает ли луч плоскость
return 0;
pNormal=plane._Normal;
lamda=l2;
return 1;
}
Код, приведенный выше, вычисляет и возвращает пересечение. Он возвращает 1, если пересечение есть, иначе 0. Параметры: плоскость (plane), начало (position) и направление вектора луча (direction), lamda - расстояние до точки столкновения, если оно есть, и вычисляется нормаль от точки столкновения (pNormal).
Пересечение луча с цилиндром
Вычисление пересечения между бесконечным цилиндром и лучом настолько сложено, что я не хочу объяснять его здесь. Этот способ требует больших математических расчетов и его просто объяснить, но моя главная цель дать вам инструменты, без излишней детализации (это не класс геометрии). Если кто-то интересуется теорией, на которой основан код, смотрите Graphic Gems II Book (pp 35, intersection of a with a cylinder). Цилиндр представляется как луч, с началом и направляющим вектором (здесь он совпадает с как осью), и радиус (радиус вокруг оси цилиндра). Соответственно функция:
int TestIntersionCylinder
(const Cylinder& cylinder, const TVector& position, const TVector& direction,
double& lamda, TVector& pNormal, TVector& newposition)
Возвращает 1, если было обнаружено пересечение, иначе 0.
Параметры: структура, задающая цилиндр (смотрите в объяснении кода ниже), вектор начала и вектор направления луча. Значения, возвращаемые через параметры - расстояние, нормаль от точки пересечения и сама точка пересечения.
Столкновение сферы со сферой
Сфера задается с помощью ее центра и ее радиуса. Столкновение двух сфер определить легко. Находя расстояние между двумя центрами (метод dist класса TVector) мы можем это определить, пересекаются ли они, если расстояние меньше, чем сумма их радиусов.
Проблема лежит в определении, столкнуться ли две ДВИЖУЩИЕСЯ сферы. Ниже есть пример, где две сферы двигаются в течение временного шага из одной точки в другую. Их пути пересекаются, но этого недостаточно, чтобы подтвердить, что столкновение произошло (они могут пройти в различное время), да и точку столкновения определить невозможно.
Рисунок 1
Предыдущие методы пересечения были решением уравнений объектов для определения пересечения. Когда используются сложные формы объектов или эти уравнения не применимы или не могут быть решены, должны быть использованы другие методы. Начальные и конечные точки, временной шаг, скорость (направление сферы + скорость) сферы и метод вычисления пересечения неподвижных сфер уже известны. Чтобы вычислить пересечение, временной шаг должен быть разрезан на более мелкие части. Затем, мы перемещаем сферы в соответствии к этим разрезанным временным шагам, используя ее скорость, и проверяем столкновение. Если в какой-либо точке обнаруживается столкновение (это означает, что сферы уже проникли друг в друга), то мы берем предыдущую позицию как точку пересечения (мы можем начать интерполяцию между этими точками, чтобы точно определить позицию пересечения, но это в основном не требуется).
Чем меньше временной шаг, чем больше частей мы используем, тем точнее метод. Например, допустим временной шаг равен 1 и количество частей - 3. Мы бы проверили два шара на столкновение во время 0, 0.33, 0.66, 1. Легко!!!!
Код, который это выполняет:
/*** Определить, какой из текущих шаров ***/
/*** пересекает другой в текущем временном шаге ***/
/*** Возвращает индекс двух пересекающихся шаров, точку и время пересечения ***/
int FindBallCol
(TVector& point, double& TimePoint, double Time2,
int& BallNr1, int& BallNr2)
{
TVector RelativeV;
TRay rays;
double MyTime=0.0, Add=Time2/150.0, Timedummy=10000, Timedummy2=-1;
TVector posi;
// Проверка всех шаров один относительно других за 150 маленьких шагов
for (int i=0;i<NrOfBalls-1;i++)
{
for (int j=i+1;j<NrOfBalls;j++)
{
RelativeV=ArrayVel[i]-ArrayVel[j]; // Найти расстояние
rays=TRay(OldPos[i],TVector::unit(RelativeV));
MyTime=0.0;
// Если расстояние между центрами больше чем 2*радиус
if ( (rays.dist(OldPos[j])) > 40) continue;
// Произошло пересечение
// Цикл для точного определения точки пересечения
while (MyTime<Time2)
{
MyTime+=Add;
posi=OldPos[i]+RelativeV*MyTime;
if (posi.dist(OldPos[j])<=40)
{
point=posi;
if (Timedummy>(MyTime-Add)) Timedummy=MyTime-Add;
BallNr1=i;
BallNr2=j;
break;
}
}
}
}
if (Timedummy!=10000)
{
TimePoint=Timedummy;
return 1;
}
return 0;
}
Как использовать то, что мы только что изучили.
Поскольку сейчас мы можем определить точку пересечения между лучом и плоскостью/цилиндром, мы должны использовать это каким-нибудь образом для определения столкновения между сферой и одним из этих примитивов. Что мы могли сделать до этого, это определить точную точку столкновения между частицей (точкой) и плоскостью/цилиндром. Начало луча - расположение частицы, и направление луча - его вектор скорости (скорость и направление). Сделать это применительно к сферам довольно легко. Смотрите на рисунке 2а, как это может быть выполнено.
Рисунок 2a Рисунок 2b
Каждая сфера имеет радиус, берем центр сферы как частицу (точка) и сдвигаем поверхность вдоль нормали каждой интересующей нас плоскости/цилиндра. На рисунке 2а эти новые примитивы изображены пунктирными линиями. Наши настоящие примитивы изображены непрерывными линиями, но тест на столкновение делается с помощью сдвинутых примитивов (представленных пунктирными линиями). Фактически, мы выполняем тест на пересечение с помощью небольшой плоскости сдвига и увеличенным радиусом цилиндра. Используя эту маленькую хитрость, шар не проникает в поверхность, если пересечение обнаружено с помощью его центра. Иначе мы получаем ситуацию как на рисунке 2b, где сфера проникает в поверхность. Это происходит, потому что мы определяем пересечение между его центром и примитивом, что означает, что мы не изменяли наш первоначальный код!
Определив, где будет столкновение, мы должны определить, будет ли пересечение в нашем текущем временном шаге. Временной шаг это время, в течение которого мы перемещаем сферу из ее текущей точки в соответствии с ее скоростью. Из-за того, что мы тестируем с помощью бесконечных лучей, всегда существует возможность того, что точка столкновения будет позади нового расположения сферы. Чтобы определить это, мы перемещаем сферу, вычисляем ее новое расположение и находим расстояние между начальной и конечной точкой. Из нашей процедуры определения столкновений мы также можем взять расстояния от начальной точки до точки столкновения. Если это расстояние меньше чем расстояние между начальной и конечной точкой, тогда столкновение есть. Чтобы вычислить точное время, мы решаем следующее простое уравнение. Представляем расстояние между начальной и конечной точкой как Dst, расстояние между начальной точкой и точкой столкновения - Dsc, и временной шаг - Т. Время, когда происходит столкновение (Тс):
Tc= Dsc*T / Dst
Все это выполняется, конечно, если пересечение было определено. Возвращаемое время - часть от целого временного шага, если временной шаг был в 1 секунду, и мы обнаружили пересечение точно в середине расстояния, то вычисленное время столкновения будет 0.5 сек. Сейчас точка пересечения может быть вычислена только умножением Тс на текущую скорость и прибавлением к начальной точке.
Collision point= Start + Velocity*Tc
Это точка столкновения на расширенном примитиве, чтобы найти точку столкновения на настоящем примитиве мы добавляем к этой точке реверс нормали от этой точки (который также возвращается процедурой пересечения) с помощью радиуса сферы. Заметьте, что процедура пересечения цилиндра возвращает точку пересечения, если она существует, поэтому не нуждается в вычислении.
2) Моделирование законов физики
Реакция на столкновения
Определить, как отреагируют после удара неподвижные объекты, типа плоскостей, цилиндров также важно, как определить точку столкновения. Используя описанные алгоритмы и функции, можно обнаружить точную точку столкновения, нормаль от нее и время внутри временного шага, в течение которого происходит столкновение.
Чтобы определить, как отреагировать на столкновение, должны быть применены законы физики. Когда объект сталкивается с поверхностью, его направление меняется, т.е. он отскакивает. Угол нового направления (или вектор отражения) от нормали точки столкновения такой же, как у первоначального вектора. Рисунок 3 показывает столкновение со сферой.
Рисунок 3
R - новый направляющий вектор
I - старый направляющий вектор, до столкновения
N - нормаль от точки столкновения
Новый вектор R вычисляется следующим образом:
R= 2*(-I dot N)*N + I
Есть ограничение: вектора I и N должны быть единичными векторами. Вектор скорости, который мы использовали в наших примерах, представляет скорость и направление. Вектор скорости не может быть включен в уравнение за место I, без преобразования. Скорость должна быть исключена. Скорость исключается нахождением величины вектора. Когда величина вектора найдена, вектор может быть преобразован в единичный и включен в уравнение, вычисляющее вектор отражения R. R показывает нам направление луча отражения, но для того, чтобы использовать как вектор скорости, необходимо к нему подключить скорость. Берем его, умножаем на величину первоначального луча, получаем правильный вектор скорости.
В примере эта процедура применяется для вычисления реакции на столкновение, когда шар сталкивается с плоскостью или цилиндром. Но она работает также для любых поверхностей, их форма не имеет значения. Пока точка столкновения и нормаль могут быть вычислены, метод вычисления реакции на столкновение всегда будет тот же самый. Код, который выполняет эти операции:
rt2=ArrayVel[BallNr].mag(); // Найти величину скорости
ArrayVel[BallNr].unit(); // Нормализовать его
// Вычислить отражение
ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr]))) + ArrayVel[BallNr] );
// Умножить на величину скорости для получения вектора скорости
ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;
Когда сфера сталкивается с другой сферой
Определить реакцию на столкновение двух шаров намного труднее. Должны быть решены сложные уравнения динамики частиц, и поэтому я выдам только окончательное решение без каких-либо доказательств. Просто поверьте мне в этом :). Во время столкновения имеем ситуацию, как изображено на рисунке 4.
Рисунок 4
U1 и U2 векторы скорости двух сфер во время столкновения. Существует ось (X_Axis), вектор, которые соединяет центры двух сфер, и U1x, U2x проекции векторов скоростей U1,U2 ось (X_Axis).
U1y и U2y проекции векторов скорости U1,U2 на ось, перпендикулярную X_Axis. Чтобы найти эти вектора нужно просто произвести скалярное произведение. M1, M2 - массы двух сфер, соответственно. V1,V2 - новые скорости после столкновения, и V1x, V1y, V2x, V2y - проекции векторов скорости на X_Axis.
Более подробно:
a) Найти X_Axis
X_Axis = (center2 - center1);
Unify X_Axis, X_Axis.unit();
b) Найти проекции
U1x= X_Axis * (X_Axis dot U1)
U1y= U1 - U1x
U2x =-X_Axis * (-X_Axis dot U2)
U2y =U2 - U2x
c) Найти новые скорости
(U1x * M1)+(U2x*M2)-(U1x-U2x)*M2
V1x= --------------------------------
M1+M2
(U1x * M1)+(U2x*M2)-(U2x-U1x)*M1
V2x= --------------------------------
M1+M2
В нашем приложении мы установили M1=M2=1, поэтому уравнение получилось даже проще.
d) Найти окончательные скорости
V1y=U1y
V2y=U2y
V1=V1x+V1y
V2=V2x+V2y
Решение этих уравнений требует много работы, но раз они в той форме, как выше, они могут быть использованы совершенно легко. Код, который вычисляет действительную реакцию на столкновение:
TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
double a,b;
// Найти расположение первого шара
pb1=OldPos[BallColNr1]+ArrayVel[BallColNr1]*BallTime;
// Найти расположение второго шара
pb2=OldPos[BallColNr2]+ArrayVel[BallColNr2]*BallTime;
xaxis=(pb2-pb1).unit(); // Найти X-Axis
a=xaxis.dot(ArrayVel[BallColNr1]); // Найти проекцию
U1x=xaxis*a; // Найти спроецированные вектора
U1y=ArrayVel[BallColNr1]-U1x;
xaxis=(pb1-pb2).unit(); // Сделать также, как выше
b=xaxis.dot(ArrayVel[BallColNr2]); // Найти проекцию
U2x=xaxis*b; // Векторы для другого шара
U2y=ArrayVel[BallColNr2]-U2x;
V1x=(U1x+U2x-(U1x-U2x))*0.5; // Сейчас найти новые скорости
V2x=(U1x+U2x-(U2x-U1x))*0.5;
V1y=U1y;
V2y=U2y;
for (j=0;j<NrOfBalls;j++) // Обновить все новые расположения
ArrayPos[j]=OldPos[j]+ArrayVel[j]*BallTime;
ArrayVel[BallColNr1]=V1x+V1y; // Установить новые вектора скорости
ArrayVel[BallColNr2]=V2x+V2y; // столкнувшимся шарам
Движение под действием гравитации, с использованием уравнения Эйлера
Чтобы изобразить реалистичное движение со столкновениями, определение точки столкновения и вычисления реакции не достаточно. Движение основывается на физических законах и тоже должно быть смоделировано.
Наиболее широко используемый метод для этого - использование уравнения Эйлера. Как показано, все вычисления должны быть выполнены с использованием временного шага. Это означает, что все моделирование происходит в некоторых временных шагах, в течение которых происходит движение, и выполняются тесты на столкновения и реакцию. Как пример, мы можем произвести моделирование в течение 2 секунд на каждом фрейме. Основываясь на уравнении Эйлера, скорость и расположение в каждом нового временном шаге вычисляется следующим образом:
Velocity_New = Velovity_Old + Acceleration*TimeStep
Position_New = Position_Old + Velocity_New*TimeStep
Сейчас объекты перемещаются и тестируются на столкновения, используя новую скорость. Ускорение для каждого объекта вычисляется делением силы, действующей на него, на его массу, в соответствии с эти уравнением:
Force = mass * acceleration
Много физических формул :)
Но, в нашем случае, на объекты действует только сила тяжести, которая может быть представлена сейчас как вектор, указывающий ускорение. В нашем случае, что-либо отрицательное в направлении Y, типа (0,-0.5,0). Это означает, что в начале каждого временного шага, мы вычисляем новую скорость каждой сферы и перемещаем их, тестируя на столкновение. Если во время временного шага происходит столкновение (скажем после 0.5 сек. с временным шагом равным 1 сек.) мы передвигаем объект в эту позицию, вычисляем отражение (новый вектор скорости) и перемещаем объект за оставшееся время (0.5 в нашем примере) снова тестируя на столкновения в течение этого времени. Эта процедура выполняется пока не завершится временной шаг.
Когда присутствует много движущихся объектов, каждый движущийся объект тестируется на пересечение с неподвижными объектами и ближайшее пересечение записывается. Далее выполняется тест на пересечение среди движущихся объектов для определения столкновений, в котором каждый объект тестируется с каждым другим. Обнаруженные пересечения сравниваются с пересечениями со статическим объектами, и берется наиболее близкое из них. Все моделирование обновляется в этой точке, (т.е., если ближайшее пересечение было после 0.5 сек., мы должны переместить все объекты на 0.5 сек.), для столкнувшихся объектов вычисляется вектор отражения, и цикл снова выполняется за оставшееся время.
3) Специальные эффекты
Взрывы
Каждый раз, когда происходит столкновение, в точке столкновения происходит взрыв. Хороший способ моделировать взрывы - произвести смешивание двух перпендикулярных друг другу полигонов с центрами в интересующей точке (в точке пересечения). Полигоны уменьшаются и исчезают со временем. Исчезновение выполняется изменением для вершин в течение времени значения alpha от 1 до 0. Так как возникает много полупрозрачных полигонов, то это может вызвать проблемы, и они могут перекрывать друг друга (как указано в Red Book в главе о прозрачности и смешивании) из-за Z-буфера, мы заимствуем технику, используемую в рендеринге частиц. Чтобы реализовать корректно этот трюк (как это описано в Red Book), мы должны отсортировать полигоны в соответствии с их расположением по глубине, но с выключением записи в буфер глубины (не чтения). Заметьте, что число взрывов ограничено до 20 за один фрейм, если происходят дополнительные взрывы, буфер переполняется, и они сбрасываются. Код, который производит взрывы:
// Исполнение / смешивание взрывов
glEnable(GL_BLEND); // Включить смешивание
glDepthMask(GL_FALSE); // Отключить запись буфера глубины
glBindTexture(GL_TEXTURE_2D, texture[1]); // Подключение текстуры
for(i=0; i<20; i++) // Обновление и визуализация взрывов
{
if(ExplosionArray[i]._Alpha>=0)
{
glPushMatrix();
ExplosionArray[i]._Alpha-=0.01f; // Обновить альфу
ExplosionArray[i]._Scale+=0.03f; // Обновить размер
// Назначить прозрачным вершинам желтый цвет
glColor4f(1,1,0,ExplosionArray[i]._Alpha); // Размер
glScalef(ExplosionArray[i]._Scale,
ExplosionArray[i]._Scale,ExplosionArray[i]._Scale);
// Переместить в позицию с учетом масштабирования
glTranslatef(
(float)ExplosionArray[i]._Position.X()/ExplosionArray[i]._Scale,
(float)ExplosionArray[i]._Position.Y()/ExplosionArray[i]._Scale,
(float)ExplosionArray[i]._Position.Z()/ExplosionArray[i]._Scale);
glCallList(dlist);// Вызвать список изображений
glPopMatrix();
}
}
Звук
Для звука была использована мультимедийная функция окошек PlaySound(). Это быстрый и отвратительный способ проигрывания звуковых файлов быстро и без хлопот.
4) Разъяснение кода
Поздравляю...
Если вы еще со мной, значит, вы успешно пережили теоретическую часть ;). Перед тем как позабавиться с демкой, необходимы некоторые разъяснения исходного кода. Основные действия и шаги моделирования следующие (в псевдокоде):
Цикл (ВременнойШаг!=0)
{
Цикл по всем шарам
{
вычислить ближайшее столкновение с плоскостью
вычислить ближайшее столкновение с цилиндром
Сохранить и заменить, если это ближайшее пересечение
по времени вычисленное до сих пор;
}
Проверить на столкновение среди движущихся шаров;
Сохранить и заменить, если это ближайшее пересечение
по времени, вычисленное до сих пор;
If (Столкновение произошло)
{
Переместить все шары на время, равное времени столкновения;
(Мы уже вычислили точку, нормаль и время столкновения.)
Вычислить реакцию;
ВременнойШаг -=ВремяСтолкновения;
}
else
Переместить все шары на время, равное временному шагу
}
Настоящий код, выполняющий псевдокод выше - тяжелей для чтения, но, в сущности, точная реализация этого псевдокода.
// Пока не закончится временной шаг
while (RestTime>ZERO)
{
lamda=10000; // Инициализировать очень большое значение
// Для всех шаров найти ближайщее пересечение между шарами и плоскостями/цилиндрами
for (int i=0;i<NrOfBalls;i++)
{
// Вычислить новое расположение и расстояние
OldPos[i]=ArrayPos[i];
TVector::unit(ArrayVel[i],uveloc);
ArrayPos[i]=ArrayPos[i]+ArrayVel[i]*RestTime;
rt2=OldPos[i].dist(ArrayPos[i]);
// Проверить, произошло ли столкновение между шаром и всеми 5 плоскостями
if (TestIntersionPlane(pl1,OldPos[i],uveloc,rt,norm))
{
// Найти время пересечения
rt4=rt*RestTime/rt2;
// Если оно меньше, чем уже сохраненное во временном шаге, заменить
if (rt4<=lamda)
{
// Если время пересечения в текущем временном шаге
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=OldPos[i]+uveloc*rt;
lamda=rt4;
BallNr=i;
}
}
}
if (TestIntersionPlane(pl2,OldPos[i],uveloc,rt,norm))
{
// ...То же самое, что и выше
}
if (TestIntersionPlane(pl3,OldPos[i],uveloc,rt,norm))
{
// ...То же самое, что и выше
}
if (TestIntersionPlane(pl4,OldPos[i],uveloc,rt,norm))
{
// ...То же самое, что и выше
}
if (TestIntersionPlane(pl5,OldPos[i],uveloc,rt,norm))
{
// ...То же самое, что и выше
}
// Сейчас проверяем пересечения с 3 цилиндрами
if (TestIntersionCylinder(cyl1,OldPos[i],uveloc,rt,norm,Nc))
{
rt4=rt*RestTime/rt2;
if (rt4<=lamda)
{
if (rt4<=RestTime+ZERO)
if (! ((rt<=ZERO)&&(uveloc.dot(norm)>ZERO)) )
{
normal=norm;
point=Nc;
lamda=rt4;
BallNr=i;
}
}
}
if (TestIntersionCylinder(cyl2,OldPos[i],uveloc,rt,norm,Nc))
{
// ...То же самое, что и выше
}
if (TestIntersionCylinder(cyl3,OldPos[i],uveloc,rt,norm,Nc))
{
// ...То же самое, что и выше
}
}
// После того, как были проверены все шары на столкновение с плоскостями/цилиндрами
// Проверить между ними и записать наименьшее время столкновения
if (FindBallCol(Pos2,BallTime,RestTime,BallColNr1,BallColNr2))
{
if (sounds)
PlaySound("Explode.wav",NULL,SND_FILENAME|SND_ASYNC);
if ( (lamda==10000) || (lamda>BallTime) )
{
RestTime=RestTime-BallTime;
TVector pb1,pb2,xaxis,U1x,U1y,U2x,U2y,V1x,V1y,V2x,V2y;
double a,b;
.
.
Код опущен для экономии пространства
Код описан в разделе Моделирование физических законов
Столкновение между сферами
.
.
//Обновить массив взрывов и вставить взрыв
for(j=0;j<20;j++)
{
if (ExplosionArray[j]._Alpha<=0)
{
ExplosionArray[j]._Alpha=1;
ExplosionArray[j]._Position=ArrayPos[BallColNr1];
ExplosionArray[j]._Scale=1;
break;
}
}
continue;
}
}
// Конец проверок
// Если столкновение произошло, произвести моделирование для точного временного шага
// и вычислить реакцию для столкнувшихся шаров
if (lamda!=10000)
{
RestTime-=lamda;
for (j=0;j<NrOfBalls;j++)
ArrayPos[j]=OldPos[j]+ArrayVel[j]*lamda;
rt2=ArrayVel[BallNr].mag();
ArrayVel[BallNr].unit();
ArrayVel[BallNr]=TVector::unit( (normal*(2*normal.dot(-ArrayVel[BallNr])))
+ ArrayVel[BallNr] );
ArrayVel[BallNr]=ArrayVel[BallNr]*rt2;
// Обновить массив взрывов и вставить взрыв
for(j=0;j<20;j++)
{
if (ExplosionArray[j]._Alpha<=0)
{
ExplosionArray[j]._Alpha=1;
ExplosionArray[j]._Position=point;
ExplosionArray[j]._Scale=1;
break;
}
}
}
else RestTime=0;
}
Основные глобальные переменные, представляющие важность:
Представляет направление и расположение камеры. Камера перемещается, используя функцию LookAt. Как вы, возможно, заметите, в не hook моде (который я объясню позже), вся сцена вращается вокруг, camera_rotation - угол вращения. |
TVector dir Tvector pos(0,-50,1000); float camera_rotation=0; |
Представляет ускорение, приложенное к движущимся шарам. В приложении действует как гравитация. |
TVector accel(0, -0.05, 0); |
Массив, который содержит новые и старые расположения и векторы скорости каждого шара. Количество шаров жестко установлено равным 10. |
TVector ArrayVel[10]; TVector ArrayPos[10]; TVector OldPos[10]; int NrOfBalls=3; |
Временной шаг, который мы используем. |
double Time=0.6; |
Если 1, камера меняет вид и следует за шаром (шар с индексом 0 в массиве). Для того чтобы камера следовала за шаром, мы использовали его расположение и вектор скорости для расположения камеры точно за шаром, и установили ее вид вдоль вектора скорости шара. |
int hook_toball1=0; |
Структуры, содержащие данные по взрывам, плоскостям и цилиндрам. |
struct Plane struct Cylinder struct Explosion |
Взрывы, хранящиеся в массиве фиксированной длины. |
Explosion ExplosionArray[20]; |
Основные интересующие функции:
Выполняет тест на пересечение с примитивами |
Int TestIntersionPlane(….); int TestIntersionCylinder(...); |
Загружает текстуры из bmp файлов |
void LoadGLTextures(); |
Код визуализации. Визуализация шаров, стен, колонн и взрывов. |
void DrawGLScene(); |
Выполнение основной логики симуляции |
void idle(); |
Инициализация OpenGL |
void InitGL(); |
Поиск, если любой шар сталкивается с другим в текущее время |
int FindBallCol(...); |
Для большей информации смотрите исходный код. Я пытался прокомментировать его настолько хорошо, насколько смог. Сейчас, когда логика определения столкновения и реакции понята, исходный код должен стать ясным. Не стесняйтесь обращаться ко мне для получения большей информации.
Как я заявил в начале этого учебника, тема определения столкновений - очень сложная тема, чтобы ее охватить в одном учебнике. Вы многое изучите в этом учебнике, достаточное для создания своих собственных достаточно впечатляющих демок, но все еще есть много чего, что нужно изучить по этой теме. Сейчас, когда вы имеете основы, все другие исходники по определению столкновений и моделированию физических законов должны стать легче для понимания. С этими словами, я отправляю вас своей дорогой и желаю вам счастливых столкновений!!!
Немного информации о Dimitrios Christopoulos: в настоящее время он работает как программный инженер по виртуальной реальности в Foundation of the Hellenic World в Athens/Греция (www.fhw.gr). Хотя он родился в Германии, он учился в Греции в University of Patras на факультете Компьютерной инженерии и информатики. Он также имеет MSc степень в Университете Hull (UK) по Компьютерной Графике и Виртуальному окружению. Первые шаги по программированию игр он начинал на Basic на Commodore 64, и перешел на C/C++/Assembly на PC платформе, после того как стал студентом. В течение нескольких последних лет он перешел на OpenGL. Также смотри на его сайте http://members.xoom.com/D_Christop.
Визуализация моделей Milkshape 3D
Model Loading
В качестве источника этого проекта я взял PortaLib3D, библиотеку, которую я написал, чтобы тем, кто ей пользуется, было легко отображать модели, используя очень маленькую часть дополнительного кода. И хотя вы, конечно, можете доверить все библиотеке, вы должны понимать, что она делает, в этом вам и поможет данный урок.
Часть PortaLib3D включенная здесь содержит мое авторское право. Это не значит, что этот код не может быть использован вами - это значит, что если вы вырежете и вставите в свой проект часть кода, то вам придется сослаться на меня. Это все. Если вы сами разберете и переделаете код (то, что вы сделаете, если вы не используете библиотеку, и если вы не изучаете, что-то простое типа 'вырезать и вставить код'!), тогда вы освободитесь от обязательств. Давайте посмотрим, в коде нет ничего особенного! Ок, перейдем к кое-чему более интересному!
Основной OpenGL код.
Основной OpenGL код в файле Lesson31.cpp. Он, почти совпадает с уроком 6, с небольшими изменениями в секции загрузки текстур и рисования. Мы обсудим изменения позже.
Milkshape 3D
Модель, которую использованная в примере разработана в Milkshape 3D. Причина, по которой я использую ее в том, что этот пакет для моделирования чертовски хорош, и имеет свой собственный формат файлов, в котором легко разобраться и понять. В дальнейшем я планирую включить поддержку загрузки формата Anim8or, потому что он бесплатный и, конечно, загрузку 3DS.
Тем не менее, самое важное в формате файла, который здесь будет кратко обсуждаться, не просто загрузка модели. Вы должны создать свою собственную структуру, которая будет удобна для хранения данных, и потом читать файл в нее. Поэтому, в начале, обсудим структуры, необходимые для модели.
Структуры данных модели
Вот структуры данных модели представленные в классе Model в Model.h. Первое и самое важное, что нам надо - это вершины:
// Структура для вершины
struct Vertex
{
char m_boneID; // Для скелетной анимации
float m_location[3];
};
// Используемые вершины
int m_numVertices;
Vertex *m_pVertices;
Сейчас вы можете не обращать на переменную m_boneID внимания - рассмотрим ее в следующих уроках! Массив m_location представляет собой координаты точек (X, Y, Z). Две переменные хранят количество вершин и сами вершины в динамическом массиве, который создается загрузчиком.
Дальше нам надо сгруппировать вершины в треугольники:
// Структура треугольника
struct Triangle
{
float m_vertexNormals[3][3];
float m_s[3], m_t[3];
int m_vertexIndices[3];
};
// Используемые треугольники
int m_numTriangles;
Triangle *m_pTriangles;
Теперь 3 вершины составляют треугольник и хранятся в m_vertexIndices. Это смещения в массиве m_pVertices. При этом каждая вершина содержится в списке только один раз, что позволят сократить место в памяти (и в вычислениях, когда мы потом будем рассматривать анимацию). m_s и m_t - это координаты (s, t) в текстуре для каждой из 3-х вершин. Текстура используется только одна для данной сетки (которые будут описаны ниже). Наконец, у нас есть член m_vertexNormals, в котором хранится нормали к каждой из 3-х вершин. Каждая нормаль имеет 3 вещественные координаты, описывающие вектор.
Следующая структура, которую мы рассмотрим в модели, это сетка (mesh). Сетка - это группа треугольников, к которым применен одинаковый материал. Набор сеток составляет целую модель. Вот структура сетки:
// Сетка
struct Mesh
{
int m_materialIndex;
int m_numTriangles;
int *m_pTriangleIndices;
};
// Используемые сетки
int m_numMeshes;
Mesh *m_pMeshes;
На этот раз у нас есть m_pTriangleIndices, в котором хранится треугольники в сетке, в точности так же, как треугольники хранят индексы своих вершин. Этот массив будет выделен динамически, потому что количество треугольников в сетке в начала не известно, и определяется из m_num_Triangles. Наконец, m_materialIndex - это индекс материала (текстура и коэффициент освещения) используемый для сетки. я покажу структуру материала ниже:
// Свойства материала
struct Material
{
float m_ambient[4], m_diffuse[4], m_specular[4], m_emissive[4];
float m_shininess;
GLuint m_texture;
char *m_pTextureFilename;
};
// Используемые материалы
int m_numMaterials;
Material *m_pMaterials;
Здесь есть все стандартные коэффициенты освещения в таком же формате, как и в OpenGL: окружающий, рассеивающий, отражающий, испускающий и блестящий. У нас так же есть объект текстуры m_texture и имя файла (динамически располагаемое) текстуры, которые могут быть выгружены, если контекст OpenGL упадет.
Код - загрузка модели
Теперь займемся загрузкой модели. Вы увидите, что это чистая виртуальная функция, названная loadModelData, которая в качестве параметра имеет имя файла модели. Все что мы сделаем - это создадим производный класс MilkshapeModel, который использует эту функцию, которая заполняет защищенные структуры данных, упомянутые выше. Теперь посмотрим на функцию:
bool MilkshapeModel::loadModelData( const char *filename )
{
ifstream inputFile( filename, ios::in | ios::binary | ios::nocreate );
if ( inputFile.fail())
return false; // "Не можем открыть файл с моделью."
Для начала мы открыли файл. Это бинарный файл, поэтому используем ios::binary. Если файл не найден, функция возвратит false, что говорит об ошибке.
inputFile.seekg( 0, ios::end );
long fileSize = inputFile.tellg();
inputFile.seekg( 0, ios::beg );
Код дальше определяет размер файла в байтах.
byte *pBuffer = new byte[fileSize];
inputFile.read( pBuffer, fileSize );
inputFile.close();
Затем файл читается во временный буфер целиком.
const byte *pPtr = pBuffer;
MS3DHeader *pHeader = ( MS3DHeader* )pPtr;
pPtr += sizeof( MS3DHeader );
if ( strncmp( pHeader->m_ID, "MS3D000000", 10 ) != 0 )
return false; // "Не настоящий Milkshape3D файл."
if ( pHeader->m_version < 3 || pHeader->m_version > 4 )
return false; // "Не поддерживаемая версия.
// Поддерживается только Milkshape3D версии 1.3 и 1.4."
Теперь указатель pPtr будет указывать на текущую позицию. Сохраняем указатель на заголовок и устанавливаем pPtr на конец заголовка. Вы, наверное, заметили несколько структур MS3D, которые мы использовали. Они объявлены в начале MilkshapeModel.cpp и идут прямо из спецификации формата файла. Поля в заголовке проверяются, что бы убедиться, в правильности загружаемого файла.
int nVertices = *( word* )pPtr;
m_numVertices = nVertices;
m_pVertices = new Vertex[nVertices];
pPtr += sizeof( word );
int i;
for ( i = 0; i < nVertices; i++ )
{
MS3DVertex *pVertex = ( MS3DVertex* )pPtr;
m_pVertices[i].m_boneID = pVertex->m_boneID;
memcpy( m_pVertices[i].m_location, pVertex->m_vertex, sizeof( float )*3 );
pPtr += sizeof( MS3DVertex );
}
Текст выше читает каждую структуру вершины из файла. Начальная память для модели выделяется для вершин, а затем каждая вершина копируется, пока не будут обработаны все. В функции используются несколько вызовов memcpy которая просто копирует содержимое маленьких массивов. Член m_boneID пока по-прежнему игнорируется - он для скелетной анимации!
int nTriangles = *( word* )pPtr;
m_numTriangles = nTriangles;
m_pTriangles = new Triangle[nTriangles];
pPtr += sizeof( word );
for ( i = 0; i < nTriangles; i++ )
{
MS3DTriangle *pTriangle = ( MS3DTriangle* )pPtr;
int vertexIndices[3] = { pTriangle->m_vertexIndices[0],
pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };
float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1],
1.0f-pTriangle->m_t[2] };
memcpy( m_pTriangles[i].m_vertexNormals, pTriangle->m_vertexNormals,
sizeof( float )*3*3 );
memcpy( m_pTriangles[i].m_s, pTriangle->m_s, sizeof( float )*3 );
memcpy( m_pTriangles[i].m_t, t, sizeof( float )*3 );
memcpy( m_pTriangles[i].m_vertexIndices, vertexIndices, sizeof( int )*3 );
pPtr += sizeof( MS3DTriangle );
}
Так же как и для вершин, эта часть функции сохраняет все треугольники модели. Пока что она включает просто копирование массивов из одной структуры в другую, и вы увидите разницу между массивом vertexIndeces и t-массивами. В файле номера вершин хранятся как массив переменных типа word, в модели это переменные типа int для согласованности и простоты (при этом противное приведение не нужно). Итак просто нужно привести 3 значения к типу int. Все значения t задаются как 1.0 - (оригинальное значение). Причина этого в том, что OpenGL использует левую нижнюю систему координат, тогда как Milkshape использует правую верхнюю систему координат (прим.: имеется в виду расположение точки центра системы координат и ориентация) для работы с текстурой. Это меняет направление оси y.
int nGroups = *( word* )pPtr;
m_numMeshes = nGroups;
m_pMeshes = new Mesh[nGroups];
pPtr += sizeof( word );
for ( i = 0; i < nGroups; i++ )
{
pPtr += sizeof( byte ); // Флаги
pPtr += 32; // Имя
word nTriangles = *( word* )pPtr;
pPtr += sizeof( word );
int *pTriangleIndices = new int[nTriangles];
for ( int j = 0; j < nTriangles; j++ )
{
pTriangleIndices[j] = *( word* )pPtr;
pPtr += sizeof( word );
}
char materialIndex = *( char* )pPtr;
pPtr += sizeof( char );
m_pMeshes[i].m_materialIndex = materialIndex;
m_pMeshes[i].m_numTriangles = nTriangles;
m_pMeshes[i].m_pTriangleIndices = pTriangleIndices;
}
Текст выше загружает данные структуры сетки (в Milkshape3D они называется группами "groups"). Так как число треугольников меняется от сетки к сетке, нет никакой стандартной структуры чтения. Поэтому берется поле за полем. Память для индексов треугольников выделяется динамически внутри сетки и читается по очереди.
int nMaterials = *( word* )pPtr;
m_numMaterials = nMaterials;
m_pMaterials = new Material[nMaterials];
pPtr += sizeof( word );
for ( i = 0; i < nMaterials; i++ )
{
MS3DMaterial *pMaterial = ( MS3DMaterial* )pPtr;
memcpy( m_pMaterials[i].m_ambient, pMaterial->m_ambient, sizeof( float )*4 );
memcpy( m_pMaterials[i].m_diffuse, pMaterial->m_diffuse, sizeof( float )*4 );
memcpy( m_pMaterials[i].m_specular, pMaterial->m_specular,
sizeof( float )*4 );
memcpy( m_pMaterials[i].m_emissive, pMaterial->m_emissive,
sizeof( float )*4 );
m_pMaterials[i].m_shininess = pMaterial->m_shininess;
m_pMaterials[i].m_pTextureFilename = new char[strlen(
pMaterial->m_texture )+1];
strcpy( m_pMaterials[i].m_pTextureFilename, pMaterial->m_texture );
pPtr += sizeof( MS3DMaterial );
}
reloadTextures();
Наконец, из буфера берется информация о материале. Это происходит так же, как и раньше, копированием каждого коэффициента освещения в новую структуру. Так же выделяется новая память для названия файла, содержащего текстуру, и оно копируется в эту память. Последний вызов reloadTextures используется собственно для загрузки текстур и привязки ее к объекту текстуры OpenGL. Эта функция из базового класса Model описывается ниже.
delete[] pBuffer;
return true;
}
Последний фрагмент освобождает память временного буфера, когда вся информация уже скопирована и работа
процедуры завершена успешно.
Итак, в данный момент, защищенные члены класса Model заполнены информацией о модели. Заметьте, что это только код для MilkshapeModel, потому что все это относилось к специфике Milkshape3D. Теперь, перед тем как можно будет нарисовать модель, необходимо загрузить текстуры для всех материалов. Это мы сделаем в следующем куске кода:
void Model::reloadTextures()
{
for ( int i = 0; i < m_numMaterials; i++ )
if ( strlen( m_pMaterials[i].m_pTextureFilename ) > 0 )
m_pMaterials[i].m_texture = LoadGLTexture( m_pMaterials[i].m_pTextureFilename );
else
m_pMaterials[i].m_texture = 0;
}
Для каждого материала, текстура загружается, используя функцию из основных уроков NeHe (слегка измененных в отличие от предыдущих версий). Если имя файла с текстурой - пустая строка, то текстура не загружается, но взамен текстуре объекта присваивается 0, что означает, что нет никакой текстуры.
Код - рисование модели
Теперь можем начать код, рисующий модель! Теперь это совсем не сложно, когда у нас есть аккуратно расположенные структуры данных в памяти.
void Model::draw()
{
GLboolean texEnabled = glIsEnabled( GL_TEXTURE_2D );
Эта часть сохраняет состояние отображения текстур в OpenGL, поэтому функция не нарушит его. Заметьте, что она не сохраняет так же свойства материала.
Теперь цикл рисования каждой сетки по отдельности:
// Рисовать по группам
for ( int i = 0; i < m_numMeshes; i++ )
{
m_pMeshes[i] будет использован для ссылки на текущую сетку. Теперь, каждая сетка имеет свои свойства материала, поэтому мы устанавливаем соответствующее состояние OpenGL. Если, однако, materialIndex сетки равен -1, это значит, что материала для такой сетки нет, и она рисуется в стандартном виде OpenGL.
int materialIndex = m_pMeshes[i].m_materialIndex;
if ( materialIndex >= 0 )
{
glMaterialfv( GL_FRONT, GL_AMBIENT,
m_pMaterials[materialIndex].m_ambient );
glMaterialfv( GL_FRONT, GL_DIFFUSE,
m_pMaterials[materialIndex].m_diffuse );
glMaterialfv( GL_FRONT, GL_SPECULAR,
m_pMaterials[materialIndex].m_specular );
glMaterialfv( GL_FRONT, GL_EMISSION,
m_pMaterials[materialIndex].m_emissive );
glMaterialf( GL_FRONT, GL_SHININESS,
m_pMaterials[materialIndex].m_shininess );
if ( m_pMaterials[materialIndex].m_texture > 0 )
{
glBindTexture( GL_TEXTURE_2D,
m_pMaterials[materialIndex].m_texture );
glEnable( GL_TEXTURE_2D );
}
else
glDisable( GL_TEXTURE_2D );
}
else
{
glDisable( GL_TEXTURE_2D );
}
Свойства материала устанавливаются в соответствие со значением, сохраненным в модели. Заметим, что текстура используется и доступна, если ее индекс больше чем 0. Если поставить 0, то вы отказываетесь от текстуры, и текстура не используется. Так же текстура не используется, если для сетки вообще нет материала.
glBegin( GL_TRIANGLES );
{
for ( int j = 0; j < m_pMeshes[i].m_numTriangles; j++ )
{
int triangleIndex = m_pMeshes[i].m_pTriangleIndices[j];
const Triangle* pTri = &m_pTriangles[triangleIndex];
for ( int k = 0; k < 3; k++ )
{
int index = pTri->m_vertexIndices[k];
glNormal3fv( pTri->m_vertexNormals[k] );
glTexCoord2f( pTri->m_s[k], pTri->m_t[k] );
glVertex3fv( m_pVertices[index].m_location );
}
}
}
glEnd();
}
Секция выше производит рисование треугольников модели. Она проходит цикл для каждого из треугольников сетки и потом рисует каждую из 3-х вершин, используя нормали и координаты текстуры. Помните, что каждый треугольник в сетке, как и все вершины, пронумерованы в общих массивах модели (они используют 2 индексные переменные). pTri - указывает на текущий треугольник в сетке и используется для упрощения кода следующего за ним.
if ( texEnabled )
glEnable( GL_TEXTURE_2D );
else
glDisable( GL_TEXTURE_2D );
}
Заключительный фрагмент кода устанавливает режим отображения текстур в свое первоначальное состояние.
Другой важный кусок кода в классе Model - это конструктор и деструктор. Они сами все объясняют. Конструктор устанавливает все члены в 0-ое значение (или NULL для указателей), и деструктор удаляет динамические массивы из памяти для всех структур модели. Вы должны заметить, что если вызываете функцию loadModelData дважды, для объекта Model, то можете потерять часть памяти. Будьте осторожны!
Последняя тема, которую я буду здесь обсуждать, это изменение, в основном коде отображения используя новый класс модели, и прямо отсюда я планирую начать свой будущий урок о скелетной анимации.
Model *pModel = NULL; // Место для хранения данных модели
В начале кода lesson31.cpp была объявлена модель, но не инициализирована. Она создается в процедуре WinMain:
Функция должна быть вызвана вначале, но после сброса матрицы просмотра, таким образом, она работает.
Чтобы сделать сцену немного интереснее, постепенно вращаем ее вокруг оси y с помощью glRotatef.
Наконец, рисуем модель с помощью члена-функции рисования. Она рисуется в центре (она была создана в центре системы координат в Milkshape3D!), поэтому если вы хотите вращать ее, менять позицию или масштабировать, просто вызовите соответствующие функции GL перед рисованием. Вуаля! Чтобы проверить, сделайте свои собственные модели в Milkshape (или используйте функции импорта), и загрузите их, изменяя строки в функции WinMain. Или добавьте их в сцену и нарисуйте несколько объектов.
Что дальше?
В будущем уроке NeHe, я опишу, как расширить класс, чтобы объединить модель с анимационным скелетом. И если я вернусь к этому, то я напишу еще классы для загрузки, чтобы сделать программу более гибкой.
Шаг к скелетной анимации не такой уж и большой, как может показаться, хотя математика, привлекаемая для этого, достаточно хитра. Если вы что-то не понимаете в матрицах и векторах, пришло время прочитать что-нибудь по этой теме! Есть несколько источников в сети, которые вам в этом помогут.
Увидимся!
Информация о Brett Porter: родился в Австралии, был студентом University of Wollongong, недавно получил дипломы BCompSc и BMath. Он начал программировать на Бейсике в 12 лет на "клоне" Commandore 64, называемом VZ300, но скоро перешел на Паскаль, Intel ассемблер, C++ и ява. В течение последних нескольких лет его интересом стало 3-х мерное программирование, и его выбором в качестве API стал OpenGL. Для более подробной информации посетите его страницу http://rsn.gamedev.net/.
Продолжение этого урока Скелетной Анимацией можно найти на странице Brett'а.
Радиальное размытие и текстурный рендеринг
Radial Blur & Rendering To A Texture
Привет! Меня зовут Дарио Корно (Dario Corno), еще меня знают как rio of SpinningKids. Я занимаюсь построением сцен с 1989 года. Прежде всего, хочу объяснить, для чего я решил написать эту небольшую статью. Я бы хотел, чтобы все, пожелавшие скачать демонстрационную программу, смогли разобраться в ней и в эффектах, которые в ней реализованы.
Демонстрационные программы часто показывают, как путем простого, а иногда грубого программирования добиться неплохих художественных возможностей программы. Вы можете увидеть просто убойные эффекты в демонстрационных примерах, существующих на данный момент. Кучу таких программ вы найдете по адресу http://www.pouet.net или http://ftp.scene.org.
Теперь закончим со вступлением и приступим к изучению программы.
Я покажу, как создавать эффект «eye candy» (реализованный в программе), похожий на радиальное размытие. Иногда его относят к объемному освещению, но не верьте этому, это просто имитация радиального размытия! ;D
Радиальное размытие обычно достигалось (когда еще не было аппаратного рендеринга) смазыванием каждого пикселя исходного изображения в направлении от центра размытия.
На современной аппаратуре обеспечением пока довольно трудно реализовать размытие с помощью использования буфера цвета (по крайней мере, это относится ко всем «gfx»-видеокартам), поэтому для получения подобного эффекта мы пойдем на маленькую хитрость.
И как плюс, при изучении построения эффекта радиального размытия вы научитесь простому способу текстурного рендеринга!
В качестве трехмерного объекта я выбрал пружину, поскольку это нетривиальная фигура, да и обычные кубы мне уже порядком поднадоели :)
Хочу заметить, что эта статья является скорее руководством по созданию эффектов. Я не буду слишком вдаваться в подробности программы. Постарайтесь больше понимать все сердцем :)
Ниже приведены объявления переменных и необходимый заголовочный файл:
#include <math.h> // Нам потребуются некоторые математические операции
float angle; // Угол вращения спирали
float vertexes[3][3]; // Массив трех вершин
float normal[3]; // Массив нормали
Gluint BlurTexture; // Номер текстуры
Функция EmptyTexture() создает пустую текстуру и возвращает ее номер. Нам потребуется выделить некоторую свободную память (а точнее 128*128*4 беззнаковых целочисленных).
128*128 – размер текстуры (128 пикселей ширины и столько же высоты), цифра 4 означает, что для каждого пикселя нам нужно 4 байта для хранения компонент RED, GREEN, BLUE и ALPHA.
GLuint EmptyTexture() // Создание пустой текстуры
{
GLuint txtnumber; // Идентификатор текстуры
unsigned int* data; // Указатель на хранимые данные
// Выделение памяти под массив пустой текстуры (128x128x4 байт)
data = (unsigned int*)new GLuint[((128 * 128)* 4 * sizeof(unsigned int))];
После выделения памяти нам нужно обнулить ее при помощи функции ZeroMemory, передав ей указатель (data) и размер обнуляемой памяти.
Хочу обратить ваше внимание на то, что характеристики магнификации (увеличения) и минификации (уменьшения) текстуры (это методы, применяемые для изменения размера текстуры до размера объекта, на который эта текстура «натягивается» - прим. перев.) устанавливаются параметром GL_LINEAR (определяющим цвет точки как среднее арифметическое всех элементов текстуры, входящих в отображаемый пиксел - прим. перев.). А параметр GL_NEAREST при растяжении текстуры дал ба нам не очень красивую картинку.
// Очистка памяти массива
ZeroMemory(data,((128 * 128)* 4 * sizeof(unsigned int)));
glGenTextures(1, &txtnumber); // Создать 1 текстуру
glBindTexture(GL_TEXTURE_2D, txtnumber); // Связать текстуру
// Построить текстуру по информации в data
glTexImage2D(GL_TEXTURE_2D, 0, 4, 128, 128, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
delete [] data; // Удалить data
return txtnumber; // Вернуть идентификатор текстуры
}
Следующая функция нормализует длину векторов нормали. Векторы представлены массивом из 3-х элементов типа float, в котором 1-й элемент – это X, второй – Y и третий – Z. Нормализованный вектор (Nv) выражается через Vn = (Vox / |Vo| , Voy / |Vo|, Voz / |Vo|), где Vo – исходный вектор, |Vo| - его длина, и x, y, z – его компоненты. В программе необходимо сделать следующее: вычислить длину исходного вектора: sqrt(x^2 + y^2 + z^2) ,где x,y,z - это 3 компоненты вектора. Затем надо разделить все три компоненты вектора нормали на полученное значение длины.
// Преобразовать вектор нормали (3 координаты) в единичный вектор с единичной длиной
void ReduceToUnit(float vector[3]) {
float length; // Переменная для длины вектора
// Вычисление длины вектора
length = (float)sqrt((vector[0]*vector[0]) + (vector[1]*vector[1])
+ (vector[2]*vector[2]));
// Предотвращение деления на нуль
if(length == 0.0f) length = 1.0f;
vector[0] /= length; // Деление всех трех элементов
vector[1] /= length; // на длину единичного вектора
vector[2] /= length; // нормали
}
Следующая функция подсчитывает нормаль, заданную тремя вершинами (хранящимися в трех массивах типа float). У нас есть два параметра: v[3][3] и out[3], первый из них – матрица со значениями типа float, размерностью m=3 и n=3, где каждая строка из трех элементов является вершиной треугольника. out – это переменная, в которую мы поместим результат - вектор нормали.
Немного (простой) математики. Мы собираемся воспользоваться знаменитым векторным произведением, которое определяется как операция между двумя векторами, дающая в результате вектор, перпендикулярный обоим исходным векторам. Нормаль - это вектор, ортогональный к поверхности, но направленный в противоположную сторону (и имеющий нормализованную (единичную) длину). Теперь представьте себе, что два вектора расположены вдоль сторон одного треугольника, ортогональный вектор (полученный в результате умножения этих векторов) этих двух сторон треугольника является нормалью этого треугольника.
Осуществить это легче, чем понять.
Сначала найдем вектор, идущий от вершины 0 к вершине 1, затем вектор, идущий от вершины 1 к вершине 2, что просто осуществляется путем вычитания каждой компоненты (координаты) стартовой вершины из компоненты следующей вершины стороны треугольника (соответствующей искомому вектору) в порядке обхода треугольника. Так мы получим вектора сторон треугольника. А в результате векторного произведения этих двух векторов (V*W) получим вектор нормали треугольника (если быть точнее, - вектор нормали плоскости, образованной сторонами этого треугольника, так как известно, что для образования плоскости достаточно двух несовпадающих векторов – прим.перев.).
Давайте взглянем на программу.
V[0][] – это первая вершина, V[1][] – это вторая вершина, V[2][] – это третья вершина. Каждая вершина состоит из: V[][0] – x-координата вершины, V[][1] – y-координата вершины, V[][2] – z-координата вершины.
Простым вычитанием всех соответствующих координат первой вершины из координат следующей мы получим ВЕКТОР от первой вершины к следующей. v1[0] = v[0][0] - v[1][0] - это выражение подсчитывает X-компоненту ВЕКТОРА, идущего от ВЕРШИНЫ 0 к вершине 1. v1[1] = v[0][1] - v[1][1] - это выражение подсчитывает Y-компоненту, v1[2] = v[0][2] - v[1][2] подсчитывает Z компоненту и так далее.
Так мы получим два вектора, что даст нам возможность вычислить нормаль треугольника.
Вот формула векторного произведения:
out[x] = v1[y] * v2[z] - v1[z] * v2[y]
out[y] = v1[z] * v2[x] - v1[x] * v2[z]
out[z] = v1[x] * v2[y] - v1[y] * v2[x]
В итоге в массиве out[] у нас будет находиться нормаль треугольника.
void calcNormal(float v[3][3], float out[3])
// Вычислить нормаль для четырехугольников используя 3 точки
{
float v1[3],v2[3]; // Вектор 1 (x,y,z) & Вектор 2 (x,y,z)
static const int x = 0 // Определение X-координаты
static const int y = 1 // Определение Y-координаты
static const int z = 2; // Определение Z-координаты
// Вычисление вектора между двумя точками вычитанием
// x,y,z-координат одной точки из координат другой.
// Подсчет вектора из точки 1 в точку 0
v1[x] = v[0][x] - v[1][x]; // Vector 1.x=Vertex[0].x-Vertex[1].x
v1[y] = v[0][y] - v[1][y]; // Vector 1.y=Vertex[0].y-Vertex[1].y
v1[z] = v[0][z] - v[1][z]; // Vector 1.z=Vertex[0].y-Vertex[1].z
// Подсчет вектора из точки 2 в точку 1
v2[x] = v[1][x] - v[2][x]; // Vector 2.x=Vertex[0].x-Vertex[1].x
v2[y] = v[1][y] - v[2][y]; // Vector 2.y=Vertex[0].y-Vertex[1].y
v2[z] = v[1][z] - v[2][z]; // Vector 2.z=Vertex[0].z-Vertex[1].z
// Вычисление векторного произведения
out[x] = v1[y]*v2[z] - v1[z]*v2[y]; // для Y - Z
out[y] = v1[z]*v2[x] - v1[x]*v2[z]; // для X - Z
out[z] = v1[x]*v2[y] - v1[y]*v2[x]; // для X - Y
ReduceToUnit(out); // Нормализация векторов
}
Следующая функция задает точку наблюдения с использованием функции gluLookAt. Мы разместим эту точку в точке (0,5,50), она будет направлена на точку (0,0,0)(центр сцены), при этом верхний вектор будет задан с направлением вверх (0,1,0)! ;D
void ProcessHelix() // Рисование спирали (или пружины)
{
GLfloat x; // x-координата спирали
GLfloat y; // y-координата спирали
GLfloat z; // z-координата спирали
GLfloat phi; // Угол
GLfloat theta; // Угол
GLfloat v,u; // Углы
GLfloat r; // Радиус скручивания
int twists = 5; // пять витков
// Задание цвета материала
GLfloat glfMaterialColor[]={0.4f,0.2f,0.8f,1.0f};
// Настройка рассеянного освещения
GLfloat specular[]={1.0f,1.0f,1.0f,1.0f};
glLoadIdentity(); // Сброс матрицы модели
// Точка камеры (0,5,50) Центр сцены (0,0,0)
// Верх но оси Y
gluLookAt(0, 5, 50, 0, 0, 0, 0, 1, 0);
glPushMatrix(); // Сохранение матрицы модели
// Смещение позиции вывода на 50 единиц вглубь экрана
glTranslatef(0,0,-50);
glRotatef(angle/2.0f,1,0,0); // Поворот на angle/2 относительно X
glRotatef(angle/3.0f,0,1,0); // Поворот на angle/3 относительно Y
glMaterialfv(GL_FRONT_AND_BACK,
GL_AMBIENT_AND_DIFFUSE, glfMaterialColor);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,specular);
Далее займемся подсчетом формулы спирали и визуализацией пружины. Это совсем несложно, и я не хотел бы останавливаться на этих пунктах, так как «соль» данного урока совсем в другом. Участок программы, отвечающий за спираль, заимствован у друзей из «Listen Software» (и несколько оптимизирован). Он написан наиболее простым и не самым быстрым способом. Использование массивов вершин не делает его быстрее.
r=1.5f; // Радиус
glBegin(GL_QUADS); // Начать рисовать четырехугольник
for(phi=0; phi <= 360; phi+=20.0) // 360 градусов шагами по 20
{// 360 градусов * количество_витков шагами по 20
for(theta=0; theta<=360*twists; theta+=20.0)
{// Подсчет угла первой точки (0)
v=(phi/180.0f*3.142f);
// Подсчет угла первой точки (0)
u=(theta/180.0f*3.142f);
// Подсчет x-позиции (первая точка)
x=float(cos(u)*(2.0f+cos(v) ))*r;
// Подсчет y-позиции (первая точка)
y=float(sin(u)*(2.0f+cos(v) ))*r;
// Подсчет z-позиции (первая точка)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r);
vertexes[0][0]=x; // x первой вершины
vertexes[0][1]=y; // y первой вершины
vertexes[0][2]=z; // z первой вершины
// Подсчет угла второй точки (0)
v=(phi/180.0f*3.142f);
// Подсчет угла второй точки (20)
u=((theta+20)/180.0f*3.142f);
// Подсчет x-позиции (вторая точка)
x=float(cos(u)*(2.0f+cos(v) ))*r;
// Подсчет y-позиции (вторая точка)
y=float(sin(u)*(2.0f+cos(v) ))*r;
// Подсчет z-позиции (вторая точка)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r);
vertexes[1][0]=x; // x второй вершины
vertexes[1][1]=y; // y второй вершины
vertexes[1][2]=z; // z второй вершины
// Подсчет угла третьей точки ( 20 )
v=((phi+20)/180.0f*3.142f);
// Подсчет угла третьей точки ( 20 )
u=((theta+20)/180.0f*3.142f);
// Подсчет x-позиции (третья точка)
x=float(cos(u)*(2.0f+cos(v) ))*r;
// Подсчет y-позиции (третья точка)
y=float(sin(u)*(2.0f+cos(v) ))*r;
// Подсчет z-позиции (третья точка)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r);
vertexes[2][0]=x; // x третьей вершины
vertexes[2][1]=y; // y третьей вершины
vertexes[2][2]=z; // z третьей вершины
// Подсчет угла четвертой точки (20)
v=((phi+20)/180.0f*3.142f);
// Подсчет угла четвертой точки (0)
u=((theta)/180.0f*3.142f);
// Подсчет x-позиции (четвертая точка)
x=float(cos(u)*(2.0f+cos(v) ))*r;
// Подсчет y-позиции (четвертая точка)
y=float(sin(u)*(2.0f+cos(v) ))*r;
// Подсчет z-позиции (четвертая точка)
z=float((( u-(2.0f*3.142f)) + sin(v) ) * r);
vertexes[3][0]=x; // x четвертой вершины
vertexes[3][1]=y; // y четвертой вершины
vertexes[3][2]=z; // z четвертой вершины
// Вычисление нормали четырехугольника
calcNormal(vertexes,normal);
// Установка нормали
glNormal3f(normal[0],normal[1],normal[2]);
// Визуализация четырехугольника
glVertex3f(vertexes[0][0],vertexes[0][1],vertexes[0][2]);
glVertex3f(vertexes[1][0],vertexes[1][1],vertexes[1][2]);
glVertex3f(vertexes[2][0],vertexes[2][1],vertexes[2][2]);
glVertex3f(vertexes[3][0],vertexes[3][1],vertexes[3][2]);
}
}
glEnd(); // Конец визуализации четырехугольника
glPopMatrix(); // Восстанавливаем матрицу
}
Две функции (ViewOrtho и ViewPerspective) написаны для упрощения рисования в ортогональной проекции и возврата перспективную.
Функция ViewOrtho выбирает текущую матрицу проекции и сохраняет ее копию в стеке системы OpenGL. Затем в матрицу проекции грузится единичная матрица, и устанавливается ортогональный просмотр при текущем разрешении экрана.
После этого мы получим возможность рисовать в 2D-координатах от 0,0 в верхнем левом углу и 640,480 нижнем правом углу экрана.
И в конце, делается активной матрица модели для визуализации.
Функция ViewPerspective выбирает текущую матрицу проекции и восстанавливает из стека «неортогональную» матрицу проекции, которая была сохранена функцией ViewOrtho. Потом также выбирается с матрица модели, чтобы мы могли заняться визуализацией.
Советую применять эти две процедуры, позволяющие легко переключаться между 2D и 3D-рисованием и не волноваться об искажениях матрицы проекции и матрицы модели.
void ViewOrtho() // Установка ортогонального вида
{
glMatrixMode(GL_PROJECTION); // Выбор матрицы проекции
glPushMatrix(); // Сохранить матрицу
glLoadIdentity(); // Сбросить матрицу
glOrtho( 0, 640 , 480 , 0, -1, 1 ); // Ортогональный режим (640x480)
glMatrixMode(GL_MODELVIEW); // Выбор матрицы модели
glPushMatrix(); // Сохранить матрицу
glLoadIdentity(); // Сбросить матрицу
}
void ViewPerspective() // Установка вида перспективы
{
glMatrixMode( GL_PROJECTION ); // Выбор матрицы проекции
glPopMatrix(); // Восстановить матрицу
glMatrixMode( GL_MODELVIEW ); // Выбрать матрицу вида
glPopMatrix(); // Восстановить матрицу
}
Ну а теперь будем учиться «подделывать» эффект размытия.
Нам требуется нарисовать сцену размытой от центра во всех направлениях. Способ рисования не должен понизить быстродействие. Непосредственно считывать и записывать пиксели мы не можем, и если мы хотим сохранить совместимость с большинством видеокарт, различные специфические возможности видеокарт также неприменимы.
Так неужели все понапрасну …?
Ну нет, решение совсем близко. OpenGL дает нам возможность «размытия» текстур. Конечно, это не реальное размытие, просто в процессе масштабирования текстуры выполняется линейная фильтрация ее изображения, и, если напрячь воображение, результат будет похож на «размытие Гаусса».
Так что же произойдет, если мы поместим много растянутых текстур прямо сверху на 3D-сцену и затем промасштабируем их?
Ответ вы уже знаете …– радиальное размытие!
Здесь у нас три проблемы: как в реальном времени мы будем создавать текстуры и как мы будем точно совмещать 3D-объект и текстуру?
Решение проще, чем вы себе представляете!
Проблема первая: текстурный рендеринг.
Она просто решается форматом пикселов фонового буфера. Визуализация текстуры (да и визуализация вообще) без использования фонового буфера очень неприятна для созерцания!
Визуализация текстуры производится всего одной функцией! Нам надо нарисовать наш объект и затем скопировать результат (ПЕРЕД ТЕМ КАК ПЕРЕКЛЮЧИТЬ РАБОЧИЙ И ФОНОВЫЙ БУФЕР в текстуру с использованием функции glCopytexSubImage.
Проблема вторая: подгонка текстуры точно на передней стороне 3D-объекта.
Нам известно, что если мы поменяем параметры области просмотра без установки правильной перспективы, у нас получится растянутая визуализация наших объектов. Например, если мы установим слишком широкую область просмотра (экран), визуализация будет растянута по вертикали.
Решением этой проблемы будет во-первых то, что надо установить режим вида OpenGL, равной размеру нашей текстуры (128х128). После визуализации нашего объекта в изображение текстуры мы визуализируем эту текстуру в текущем разрешении OpenGL-экрана. Таким образом, OpenGL сначала переносит уменьшенную копию объекта в текстуру, затем растягивает ее на весь экран. Вывод текстуры происходит поверх всего экрана и поверх нашего 3D-объекта в том числе. Надеюсь, я ничего не забыл. Еще небольшое уточнение… Если вы возьмете содержимое экрана размером 640х480 и затем ужмете его до рисунка в 256х256 пикселей, то его также можно будет использовать в качестве текстуры экрана и растянуть на 640х480 пикселей. Качество окажется, скорее всего, не очень, но смотреться будет как исходное 640х480 изображение.
Забавно! Эта функция в самом деле совсем несложная и является одним из моих самых любимых «дизайнерских трюков». Она устанавливает размер области просмотра (или внутреннего окна) OpenGL равным размеру нашей текстуры (128х128), или нашего массива BlurTexture, в котором эта текстура хранится. Затем вызывается функция, рисующая спираль (наш 3D-объект). Спираль будет рисоваться в окне в 128х128 и поэтому будет иметь соответствующие размеры.
После того, как в 128х128- области визуализируется спираль, мы подключаем BlurTexture и копируем буфера цвета из области просмотра при помощи функции glCopyTexImage2D.
Параметры определяются следующим образом:
Слово GL_TEXTURE_2D показывает, что мы используем двумерную текстуру. 0 - уровень мип-мапа, с которым мы совершаем копирование буфера, это значение по умолчанию. Слово GL_LUMINANCE определяет формат копируемых данных. Я использовал GL_LUMINANCE, поскольку с этим результат гораздо красивее, это значение позволяет копировать в текстуру лишь светящуюся часть буфера. Тут же могут быть использованы значения GL_ALPHA, GL_RGB, GL_INTENSITY и так далее.
Следующие два параметра указывают OpenGL на угол экрана, с которого начнется копирование данных (0,0). Далее идут ширина и высота копируемого прямоугольника экрана. И последний параметр необходим, если мы хотим рамку, которая нам не нужна.
Теперь, когда у нас есть копия буфера цвета (с уменьшенной спиралью) в массиве BlurTexture, мы можем очистить буфер и вернуть области просмотра ее естественные размеры (640х480 – во весь экран).
ВАЖНОЕ ЗАМЕЧАНИЕ:
Этот трюк возможен лишь при поддержке двойной буферизации форматом пикселя. Причиной данного условия является то, что все эти подготовительные процедуры скрыты от пользователя, поскольку полностью совершаются в фоновом буфере.
void RenderToTexture() // Визуализация в текстуру
{
// Изменить область просмотра (в соответствии с размером текстуры)
glViewport(0,0,128,128);
ProcessHelix(); // Нарисовать спираль
glBindTexture(GL_TEXTURE_2D,BlurTexture); // Подключить нашу текстуру
// Копирование области просмотра в текстуру (от 0,0 до 128,128... без рамки)
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 128, 128, 0);
glClearColor(0.0f, 0.0f, 0.5f, 0.5); // Цвет фона
// Очистка экрана и фоновом буфера
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0 , 0,640 ,480);// Область просмотра = (0,0 - 640x480)
}
Функция просто рисует несколько смешенных полигонов-прямоугольников на переднем плане нашей 3D-сцены, с использованием текстуры BlurTexture, которую мы уже подготовили. Путем постепенного изменения прозрачности (альфа) полигона и масштабирования текстуры у нас получится нечто, похожее на радиальное размытие.
В первую очередь я отключаю флаги GEN_S и GEN_T (я фанатею по сферическому наложению, и мои программы обычно их включают :P).
Включаем 2D-текстурирование, отключаем проверку глубины, выбираем соответствующую функцию смешивания, включаем смешивание и подключаем BlurTexture.
Следующее, что мы делаем, это переключаемся на ортогональный вид сцены, что упрощает рисование полигонов, которые точно совпадают с размерами экрана. Это выстроит текстуры поверх 3D-объекта (с постепенным растяжением текстуры до размеров экрана). Таким образом, проблема два решена. Два зайца одной выстрелом! (первый «заяц» - рисование полигона в нужном месте экрана, второй «заяц» - вывод текстуры также в требуемом месте над «размываемой» спиралью, «выстрел» - переход на ортогональный вид, дающий возможность работать с обычными 2D-координатами – прим. перев.).
void DrawBlur(int times, float inc) // вывод размытого изображения
{
float spost = 0.0f; // Начальное смещение координат
float alphainc = 0.9f / times; // Скорость уменьшения прозрачности
float alpha = 0.2f; // Начальное значение прозрачности
// Отключить автоопределение координат текстуры
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_2D); // Включить наложение 2D-текстур
glDisable(GL_DEPTH_TEST); // Отключение проверки глубины
glBlendFunc(GL_SRC_ALPHA,GL_ONE);// Выбор режима смешивание
glEnable(GL_BLEND); // Разрешить смешивание
glBindTexture(GL_TEXTURE_2D,BlurTexture);// Подключить текстуру размытия
ViewOrtho(); // переключение на ортогональный вид
alphainc = alpha / times; // alphainc=0.2f / число_раз визуализации размытия
Мы много раз выводим рисунок текстуры для создания эффекта размытия, масштабируя его изменением координат текстуры, и тем самым увеличивая степень размытия. Всего рисуется 25 прямоугольников с текстурой, растягиваемой на 1.015 за каждый проход цикла.
glBegin(GL_QUADS); // Рисуем прямоугольники
for (int num = 0;num < times;num++)// Количество походов = times
{
// Установить значение alpha (начальное = 0.2)
glColor4f(1.0f, 1.0f, 1.0f, alpha);
glTexCoord2f(0+spost,1-spost); // Координаты текстуры (0,1)
glVertex2f(0,0); // Первая вершина(0,0)
glTexCoord2f(0+spost,0+spost); // Координаты текстуры (0,0)
glVertex2f(0,480); // Вторая вершина(0,480)
glTexCoord2f(1-spost,0+spost); // Координаты текстуры (1,0)
glVertex2f(640,480);// Третья вершина (640,480)
glTexCoord2f(1-spost,1-spost); // Координаты текстуры (1,1)
glVertex2f(640,0); // Четвертая вершина (640,0)
// Увеличение spost (Приближение к центру текстуры)
spost += inc;
// Уменьшение alpha (постепенное затухание рисунка)
alpha = alpha - alphainc;
}
glEnd(); // Конец рисования
ViewPerspective(); // Назад к перспективному виду
glEnable(GL_DEPTH_TEST); // Включить проверку глубины
glDisable(GL_TEXTURE_2D); // Отключить 2D-текстурирование
glDisable(GL_BLEND); // Отключить смешивание
glBindTexture(GL_TEXTURE_2D,0); // Отвязать текстуру
}
И вуаля: самая короткая функция Draw, когда-либо виданная, дающая возможность увидеть превосходный визуальный эффект.
Вызывается функция RenderToTexture. Здесь один раз рисуется растянутая пружина маленького размера, благодаря изменению параметров области вывода. Растянутая пружина копируется в нашу текстуру, затем буфер очищается.
Затем мы рисуем «настоящую» пружину (трехмерный объект, который вы видите на экране) при помощи функции ProcessHelix().
Наконец, мы рисуем последовательность «смешено-размазанных» полигонов-прямоугольников спереди пружины. То есть текстурированные прямоугольники будут растянуты и размазаны по изображению реальной 3D-пружины.
void Draw (void) // Визуализация 3D-сцены
{
glClearColor(0.0f, 0.0f, 0.0f, 0.5);// Очистка черным цветом
// Очистка экрана и фонового буфера
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // Сброс вида
RenderToTexture(); // Визуализация в текстуру
ProcessHelix(); // Рисование спирали
DrawBlur(25,0.02f); // Эффект размытия
glFlush (); // «Сброс» конвейера OpenGL
}
Надеюсь, данное руководство вам понравилось, хотя оно больше ничему и не учит, кроме визуализации в текстуру, но определенно может добавить красивый эффект в ваши 3D-программы.
Со всеми комментариями и предложениями по улучшению реализации этого эффекта прошу слать письма на rio@spinningkids.org. (а если вы хотите получить исправленную и улучшенную версию программы, пишите на lake@tut.by, будет интересно пообщаться :) - прим. перев. ).
Вы вольны использовать этот код бесплатно, тем не менее, если вы собираетесь использовать его в своих разработках, отметьте это и попытайтесь понять, как он работает, только так и никак иначе. Кроме того, если вы будете использовать этот код в своих коммерческих разработках (разработки, за которые вы потребуете плату), пожалуйста, выделите мне некоторый кредит.
И еще я хочу оставить вам всем небольшой список заданий для практики (домашнее задание) :D
1) Переделайте функцию DrawBlur для получения горизонтального размытия, вертикального размытия и каких-нибудь других интересных эффектов (круговое размытие, например).
2) Поменяйте параметры функции DrawBlur (увеличьте или уменьшите), чтобы получился красивый световой эффект под вашу музыку.
3) Поиграйте с функцией DrawBlur, нашей небольшой текстурой и значением GL_LUMINANCE (крутой блеск!).
4) Симитируйте объемные тени при помощи темных текстур взамен светящихся.
Ок. Теперь, должно быть, все.
X1. Улучшенная обработка ввода с использованием DirectInput и Windows
Вы должны использовать самые современные технологии, чтобы конкурировать с такими играми как Quake и Unreal. В этом уроке я научу вас, как подключить и использовать DirectInput и как использовать мышь в OpenGL под Windows. Код этого урока базируется на коде урока 10. Начнем.
Мышь
Первое, что нам понадобиться, это переменная для хранения X и Y позиции мыши.
typedef struct tagSECTOR
{
int numtriangles;
TRIANGLE* triangle;
} SECTOR;
SECTOR sector1; // Наша модель
POINT mpos; // Позиция мыши (Новое)
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Отлично, как вы видите, мы добавили новую переменную mpos. Структура POINT состоит из двух переменных – x и y, мы будем использовать их для того, чтобы вычислить вращение сцены. Далее мы изменим, часть функции CreateGLWindow() так, как показано ниже.
ShowCursor(FALSE); // Убрать указатель мыши (Изменено)
if (fullscreen) // Если полноэкранный режим?
{
dwExStyle=WS_EX_APPWINDOW;
dwStyle=WS_POPUP;
}
Выше мы переместили вызов ShowCursor(FALSE) так, чтобы курсора мыши не было видно не только в полноэкранном режиме, но и в оконном тоже. Теперь нам нужно получить и установить координаты мыши каждый кадр, поэтому измените функцию WinMain() так как показано ниже:
SwapBuffers(hDC); // Смена буферов (двойная буферизация)
GetCursorPos(&mpos); // Получить текущую позицию мыши (Новое)
SetCursorPos(320,240); // Установить мышь в центр окна (Новое)
heading += (float)(320 - mpos.x)/100 * 5;//Обновить направление движения (Новое)
yrot = heading; // Обновить вращение вокруг оси Y (Новое)
lookupdown -= (float)(240 - mpos.y)/100 * 5;//Обновить вращение вокруг X (Новое)
Сначала мы получили позицию мыши при помощи функции GetCursorPos(POINT p). Смещение от центра окна даст нам информацию о том, куда и насколько нужно вращать камеру. Затем мы устанавливаем мышь в центр окна для следующего прохода используя SetCursorPos(int X, int Y).
Замечание: Не устанавливайте позицию мыши в 0,0! Если вы сделаете это, то не сможете обработать перемещение мыши вверх и влево, потому что 0,0 это левый верхний угол окна. 320, 240 – это центр окна для режима 640х480.
После того как мы позаботились о мыши, нужно изменить часть кода для выполнения перемещения.
float = (P - CX) / U * S;
P – точка, в которую мы устанавливаем мышь каждый кадр
CX – текущая позиция мыши
U – единицы
S – скорость мыши (будучи истинным квакером я люблю в этом месте значение 12).
По этой формуле вычисляются значения переменных heading и lookupdown.
С мышью вроде как разобрались. Идем дальше.
Клавиатура (DirectX 7)
Теперь мы можем при помощи мыши вращать камеру в нашем мире. Следующий шаг использовать клавиатуру для перемещения вперед, атаки и приседания. Довольно болтовни, начнем кодировать.
Сначала я расскажу, как использовать DirectX7. Первый шаг – настройка компилятора. Я покажу, как сделать это на примере Visual C++, настройка других компиляторов может отличаться от предложенного способа.
Если у вас еще нет DirectX SDK, то вам придется его заиметь, например, скачать с сайта MicroSoft, и проинсталлировать его.
После этого, в VisualStudio зайдите в меню Project->Settings. Выберите закладку Link и в строке Object/libraty modules в начало строки добавьте dinput.lib dxguid.lib winmm.lib. Библиотеки DirectInput, DirectX GUID и Windows Multimedia соответственно, последняя необходима для таймера. Возможно, вам также понадобиться войти в меню Tools->Options и на закладке Directories добавить пути (Include files и Library files) к DirectX SDK и переместить их наверх списка.
Теперь DirectInput готов к использованию, можно начинать программировать!
Мы нуждаемся в подключении заголовочных файлов DirectInput, для того чтобы мы могли использовать некоторые его функции. Также мы нуждаемся в создании и добавлении новых переменных для DirectInput и для устройства DirectInput клавиатуры. Сделаем мы это следующим образом:
#include <stdio.h> // Заголовочный файл стандартного ввода/вывода
#include <gl\gl.h> // Заголовочный файл библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл библиотеки Glaux
#include <dinput.h> // DirectInput функции (Новое)
LPDIRECTINPUT7 g_DI; // DirectInput (Новое)
LPDIRECTINPUTDEVICE7 g_KDIDev; // Устройство клавиатуры (Новое)
В последних две строчках объявляются переменные для DirectInput (g_DI) и для устройства клавиатуры (g_KDIDev), последнее будет получать данные и обрабатывать их. Константы DirectInput не сильно отличаются от стандартных констант Windows.
Windows DirectInput
VK_LEFT DIK_LEFT
VK_RIGHT DIK_RIGHT
... и так далее
Основное отличие в замене VK на DIK. Хотя некоторые названия изменили существенно. Все DIK константы объявлены в файле dinput.h.
Теперь нужно написать функцию инициализации DirectInput’а и устройства клавиатуры. Под CreateGLWindow() добавьте следующее:
// Инициализация DirectInput (Новое)
int DI_Init()
{
// Создание DirectInput
if ( DirectInputCreateEx( hInstance, // Instance окна
DIRECTINPUT_VERSION, // Версия DirectInput
IID_IDirectInput7,
(void**)&g_DI, // DirectInput
NULL ) ) // NULL параметр
{
return(false); // Не создался DirectInput
}
// Создание устройства клавиатуры
if ( g_DI->CreateDeviceEx( GUID_SysKeyboard,
// Какое устройство создается (клавиатура, мышь или джойстик)
IID_IDirectInputDevice7,
(void**)&g_KDIDev, // Устройство клавиатуры
NULL ) ) // NULL параметр
{
return(false); // Не создалось устройство клавиатуры
}
// Установка формата данных для клавиатуры
if ( g_KDIDev->SetDataFormat(&c_dfDIKeyboard) )
{
return(false); // Не удалось установить формат данных
// здесь не хватает функций уничтожения устройства клавиатуры и DirectInput
}
// Установка уровня кооперации
if ( g_KDIDev->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_EXCLUSIVE) )
{
return(false); // Не удалось установить режим
// здесь не хватает функций уничтожения устройства клавиатуры и DirectInput
}
if (g_KDIDev) // Создано устройство клавиатуры? (лишняя проверка)
g_KDIDev->Acquire(); // Взять его под контроль
else // если нет
return(false); // возвращаем false
return(true); // все отлично
}
// Уничтожение DirectInput
void DX_End()
{
if (g_DI)
{
if (g_KDIDev)
{
g_KDIDev->Unacquire();
g_KDIDev->Release();
g_KDIDev = NULL;
}
g_DI->Release();
g_DI = NULL;
}
}
Этот код в достаточной мере снабжен комментариями и должен быть понятен. Первое – мы инициализируем DirectInput и при помощи него создаем устройство клавиатуры, которое затем берем под контроль. Можно также использовать DirectInput для мыши, но на данном этапе средств Windows вполне достаточно.
Теперь нужно заменить старый код обработки ввода с клавиатуры на новый, использующий DirectInput. Изменить предстоит много, приступим.
Удалите следующий код из WndProc():
case WM_KEYDOWN: // Клавиша была нажата?
{
keys[wParam] = TRUE; // Пометить ее как нажатую
return 0; // Вернуться
}
case WM_KEYUP: // Клавиши была отпущена?
{
keys[wParam] = FALSE; // Отменить пометку
return 0; // Вернуться
}
В начале программы необходимо сделать следующие изменения:
BYTE buffer[256]; // Новый буфер вместо Keys[] (Изменено)
bool active=TRUE; // Флаг активности окна
bool fullscreen=TRUE; // Флаг полноэкранного режима
bool blend; // Смешивание ON/OFF
bool bp; // Состояние кнопки смешивания
bool fp; // Состояние F1 (Изменено)
...
GLfloat lookupdown = 0.0f;
GLfloat z=0.0f; // Глубина в экран
GLuint filter; // Фильтр (Удалено)
GLuint texture[5]; // Для текстур (Изменено)
В функции WinMain()
// Создание окна OpenGL
if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced DirectInput Tutorial",640,480,16,fullscreen)) // (Изменено)
{
return 0; // Выйти, если не удалось создать окно
}
if (!DI_Init()) // Инициализация DirectInput (Новое)
{
return 0;
}
...
// Отрисовка сцены, пока окно активно и не была нажата клавиша Esc
if ((active && !DrawGLScene())) // (Изменено)
...
// Обновить состояние клавиатуры (Новое)
HRESULT hr = g_KDIDev->GetDeviceState(sizeof(buffer), &buffer);
if ( buffer[DIK_ESCAPE] & 0x80 ) // Тест клавиши Escape (Изменено)
{
done=TRUE;
}
if ( buffer[DIK_B] & 0x80) // Нажата клавиша B? (Изменено)
{
if (!bp)
{
bp = true; // Нажата клавиша смешения (Новое)
blend=!blend;
if (!blend)
{
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
else
{
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
}
}
}
else
{
bp = false;
}
if ( buffer[DIK_PRIOR] & 0x80 ) // Page Up? (Изменено)
{
z-=0.02f;
}
if ( buffer[DIK_NEXT] & 0x80 ) // Page Down? (Изменено)
{
z+=0.02f;
}
if ( buffer[DIK_UP] & 0x80 ) // Вверх? (Изменено)
{
xpos -= (float)sin(heading*piover180) * 0.05f;
zpos -= (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle >= 359.0f)
{
walkbiasangle = 0.0f;
}
else
{
walkbiasangle+= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
if ( buffer[DIK_DOWN] & 0x80 ) // Вниз? (Изменено)
{
xpos += (float)sin(heading*piover180) * 0.05f;
zpos += (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f;
}
else
{
walkbiasangle-= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
if ( buffer[DIK_LEFT] & 0x80 ) // Влево? (Изменено)
{
xpos += (float)sin((heading - 90)*piover180) * 0.05f; ( Modified )
zpos += (float)cos((heading - 90)*piover180) * 0.05f; ( Modified )
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f;
}
else
{
walkbiasangle-= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
if ( buffer[DIK_RIGHT] & 0x80 ) // Вправо? (Изменено)
{
xpos += (float)sin((heading + 90)*piover180) * 0.05f; ( Modified )
zpos += (float)cos((heading + 90)*piover180) * 0.05f; ( Modified )
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f;
}
else
{
walkbiasangle-= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
if ( buffer[DIK_F1] & 0x80) // F1 нажато? (Изменено)
{
if (!fp) // Если не была нажата (Новое)
{
fp = true; // F1 нажата (Новое)
KillGLWindow(); // Уничтожить текущее окно (Изменено)
fullscreen=!fullscreen; // Переключить режим (Изменено)
// Пересоздать окно
if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced Direct Input Tutorial",640,480,16,fullscreen)) (Изменено)
{
return 0; // Выйти, если не удалось создать окно (Изменено)
}
if (!DI_Init()) // Переинициализировать DirectInput (Новое)
{
return 0; // Выйти, если не удалось
}
}
}
else
{
fp = false; // F1 отпущена
}
// Shutdown
// Выход
DX_End(); // Уничтожить DirectInput (Новое)
KillGLWindow(); // Уничтожить окно
return (msg.wParam); // Выйти из программы
}
Функция DrawGLScene() изменилась следующим образом.
glTranslatef(xtrans, ytrans, ztrans);
numtriangles = sector1.numtriangles;
// Для каждого треугольника
for (int loop_m = 0; loop_m < numtriangles; loop_m++)
{
glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]); // (Изменено)
glBegin(GL_TRIANGLES);
Отлично, теперь обсудим все это немного. Мы заменили весь старый код, использующий ввод с клавиатуры средствами Windows на код использующий средства DirectInput. Так же были изменены и сами клавиши управления для удобства использования.
Теперь в нашей игре есть поддержка мыши и клавиатуры через DirectInput, что дальше? Нам нужен таймер, для того чтобы регулировать скорость. Без таймера мы обрабатываем клавиатуру каждый кадр, из-за этого скорость перемещения и вращения будет различной на различных компьютерах.
Добавим переменную для корректировки и структуру для работы с таймером.
POINT mpos;
int adjust = 5; // Корректировка скорости (Новое)
// информация для таймера (Новое)
struct
{
__int64 frequency; // Частота
float resolution; // Точность
unsigned long mm_timer_start; // Стартовое значение
unsigned long mm_timer_elapsed; // Прошедшее время
bool performance_timer; // Использовать эффективный таймер
__int64 performance_timer_start; // Стартовое значение эффективного таймера
__int64 performance_timer_elapsed; // Прошедшее время по эффективному таймеру
} timer;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
Этот код обсуждался в уроке 21. Рекомендую просмотреть его.
// Инициализация таймера (Новое)
void TimerInit(void)
{
memset(&timer, 0, sizeof(timer)); // Очистить структуру
// Проверить есть ли возможность использования эффективного таймера
if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))
{
// нет эффективного таймера
timer.performance_timer = FALSE;
timer.mm_timer_start = timeGetTime(); // Использовать timeGetTime()
timer.resolution = 1.0f/1000.0f; // Установить точность .001f
timer.frequency = 1000; // Установить частоту 1000
// Установить прошедшее время равным стартовому
timer.mm_timer_elapsed = timer.mm_timer_start;
}
else
{
// доступен эффективный таймер
QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);
timer.performance_timer = TRUE;
// Вычисление точности и частоты
timer.resolution = (float) (((double)1.0f)/((double)timer.frequency));
// Установить прошедшее время равным стартовому
timer.performance_timer_elapsed = timer.performance_timer_start;
}
}
// Получить время в миллисекундах (Новое)
float TimerGetTime()
{
__int64 time; // Время храниться в 64-битном целом
if (timer.performance_timer) // Если используется эффективный таймер
{
// Получить текущее время по эффективному таймеру
QueryPerformanceCounter((LARGE_INTEGER *) &time);
// вернуть текущее время мину стартовое с данной точностью и в миллисекундах
return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;
}
else
{
// вернуть текущее время минус стартовое с данной точностью и в миллисекундах
return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;
}
}
Напомню, что пояснения кода таймера есть в 21-ом уроке. Убедитесь, что к проекту добавлена библиотека winmm.lib.
Теперь мы добавим кое-что в функцию WinMain().
if (!DI_Init()) // Инициализация DirectInput (Новое)
{
return 0;
}
TimerInit(); // Инициализация таймера (Новое)
...
float start=TimerGetTime();
// Получить время перед отрисовкой (Новое)
// Отрисовка сцены. Esc – выход.
// Если окно активно и был выход (Изменено)
if ((active && !DrawGLScene()))
{
done=TRUE; // ESC DrawGLScene сигнализирует о выходе
}
else // обновить сцену
{
// Цикл ожидания для быстрых систем (Новое)
while(TimerGetTime()<start+float(adjust*2.0f)) {}
Теперь программа должна выполняться с корректной скоростью. Следующая часть кода предназначена для вывода уровня, как в уроке 10.
Если вы уже скачали код этого урока, то вы уже заметили, что я добавил несколько текстур для сцены. Новые текстуры находятся в папке Data.
Изменения структуры tagTriangle.
typedef struct tagTRIANGLE
{
int texture; // (Новое)
VERTEX vertex[3];
} TRIANGLE;
Изменения кода SetupWorld
for (int loop = 0; loop < numtriangles; loop++)
{
readstr(filein,oneline); // (Новое)
sscanf(oneline, "%i\n", §or1.triangle[loop].texture); // (Новое)
for (int vert = 0; vert < 3; vert++)
{
Изменения кода DrawGLScene
// Для каждого треугольника
for (int loop_m = 0; loop_m < numtriangles; loop_m++)
{
// (Модифицировано)
glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]);
glBegin(GL_TRIANGLES);
В функцию LoadGLTextures добавлена загрузка дополнительных текстур
int LoadGLTextures() // Загрузка картинок и конвертирование их в текстуры
{
int Status=FALSE; // Статус
AUX_RGBImageRec *TextureImage[5]; // Массив текстур
memset(TextureImage,0,sizeof(void *)*2); // Инициализация указателей NULL
if( (TextureImage[0]=LoadBMP("Data/floor1.bmp")) && // Загрузка текстуры пола
(TextureImage[1]=LoadBMP("Data/light1.bmp"))&& // Загрузка текстуры освещения
(TextureImage[2]=LoadBMP("Data/rustyblue.bmp"))&& // Стены
(TextureImage[3]=LoadBMP("Data/crate.bmp")) && // решетки
(TextureImage[4]=LoadBMP("Data/weirdbrick.bmp"))) // потолок
{
Status=TRUE;
glGenTextures(5, &texture[0]); // Создание текстур
for (int loop1=0; loop1<5; loop1++)
{
glBindTexture(GL_TEXTURE_2D, texture[loop1]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX,
TextureImage[loop1]->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
for (loop1=0; loop1<5; loop1++)
{
if (TextureImage[loop1]->data)
{
free(TextureImage[loop1]->data);
}
free(TextureImage[loop1]);
}
}
return Status; // Вернуть статус
}
Теперь вы можете использовать возможности DirectInput. Я потратил много времени на написание этого урока и надеюсь, что он вам пригодиться. Спасибо, что потратили свое время на чтение этого урока!