Создание 3D игр на C++
Часть1. Урок 4
Инициализация OpenGL
Если вы используете C++ Builder 6.0 или более поздний, и у вас НЕ установлен BCB5.0, то при компиляции проекта выскочит ошибка линкера, что не найден файл VCLE50.lib. Дело в том, что NukeDX, поставляемая с курсом, компилировалась в C++Builder 5.0 и требует эту библиотеку. Я обошел эту беду так: открыл поисковик Windows (Win-F, где Win - это клавиша между Ctrl и Alt), и ввел в нем VCLE*.lib. Он нашел два таких файла в папке с C++Buider. Для обоих файлов надо создать копии в их каталогах и переименовать эти копии в VCLE50.lib, тогда все будет работать.
Итак, инициализация OpenGL. Перво-наперво создайте новый модуль:
File->New->Unit
Сохраните его как col_gl.cpp
В col_gl.h добавьте строки:
#include <windows.hpp>
#include <gl.h>
#include <glu.h>
это библиотеки, которые мы будем использовать. gl.h - это основная библиотека. Она работает с библиотекой OpenGL32.dll. glu.h - это быблиотека утилит, она упрощает и расширяет работу с OpenGL посредством некоторых очень удобных функций, содержащихся в библиотеки glu32.dll.
Обе библиотеки должны находиться в папке Windows/System вашего компьютера.
Первое, что нам надо сделать перед работой с OpenGL - это инициализировать его.
Для этого создадим две функции: extglInit и SetWindowPixelFormat. Сразу объявите их прототипы в модуле col_gl.h:
int SetWindowPixelFormat (HDC hdc);
bool extglInit (HWND Handle);
Первая Функция будет принимать один параметр - HDC формы, и возвращать 0, если все прошло гладко и что-либо другое в остальных случаях.
Вторая функция будет принимать handle формы и возвращать true в случае успешной инициализации OpenGL и false в противном случае.
Объявите две глобальные переменные в col_gl.cpp и подгрузите консоль.
Вот как должен начинаться код модуля:
#pragma hdrstop
#include "col_gl.h"
#include "col_con.h"
#pragma package(smart_init)
//==============================================================================
// Глобальные переменные
HGLRC hGLRC;
HDC hDC;
Сначала первая функция. Она устанавливает так называемый PFD (Pixel Format Descriptor) без него OpenGL не будет работать с формой.
Вот текст:
//==============================================================================
// *** SetWindowPixelFormat ***
// Установка PIXELFORMATDESCRIPTOR
// Возвращает 0, если все прошло гладко
// 1, если установить PFD не удалось
//==============================================================================
int SetWindowPixelFormat(HDC hdc)
{
int PixelFormat;
PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
24,
0,0,0,0,0,0,
0,0,
0,0,0,0,0,
32,
0,
0,
PFD_MAIN_PLANE,
0,
0,0,
};
try
{
PixelFormat = ChoosePixelFormat (hdc, &pfd);
}
catch (...){;}
if (PixelFormat == 0)
{
PixelFormat = 1;
if(DescribePixelFormat(hDC, PixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pfd) == 0)
return 1;
}
if (SetPixelFormat (hdc, PixelFormat, &pfd) == FALSE)
return 1;
return 0;
}
Структура PFD имеет, как вы видите, множество параметров. Со многими из них разбираться не стоит вовсе. Здесь установлены довольно неплохие значения (многие их используют). Если вам интересно, то вы можете найти определение структуры в help'е по C++Builder и ознакомиться с ней, но вряд ли вам придется что-то менять. Эту функцию можно как шаблонную добавлять во все свои модули и разработки.
Теперь сама инициализация:
//==============================================================================
// *** extglInit ***
// Инициализация
// Возвращает true, если все прошло гладко
// false, в случае каких либо ошибок
// Все ошибки автоматически будут отображениы в консоле
//==============================================================================
bool extglInit (HWND Handle)
{
// Это константы освещения. Об освещении будет отдельный урок
GLfloat _mainlight[] = {0.3, 0.3, 0.3, 1.0};
GLfloat _lposition[] = {0.0, 0.0, 0.0, 1.0};
GLfloat _lambient[] = {0.5, 0.5, 0.4, 1.0};
GLfloat _ldiffuse[] = {1.0, 1.0, 0.8, 1.0}; // слегка желтоватое освещение
GLfloat _lspecular[] = {0.2, 0.2, 0.2, 1.0};
// Весь ход инициализации будем писать в консоль!
AddString ("Initializatig OpenGL...", true);
AddString (" Setting Pixel Format Descriptor...", true);
hDC = GetDC(Handle);
if (SetWindowPixelFormat(hDC) == 1)
{
AddString ("OpenGL Error: Cannot set PFD", true);
return false;
}
AddString (" Setting HRC...", true);
hGLRC = wglCreateContext(hDC);
if(hGLRC == NULL)
{
AddString ("OpenGL Error: HRC = NULL", true);
return false;
}
AddString (" Making Current...", true);
if(wglMakeCurrent(hDC, hGLRC) == false)
{
AddString ("OpenGL Error: Cannot MakeCurrent", true);
return false;
}
AddString (" Configuring OpenGL...", true);
glEnable (GL_ALPHA_TEST);
glEnable (GL_DEPTH_TEST);
glEnable (GL_NORMALIZE);
// Настраиваем освещение
glEnable (GL_COLOR_MATERIAL);
glEnable (GL_LIGHTING);
glEnable (GL_LIGHT0);
glLightModelfv (GL_LIGHT_MODEL_AMBIENT, _mainlight);
glLightfv (GL_LIGHT0, GL_POSITION, _lposition);
glLightfv (GL_LIGHT0, GL_AMBIENT, _lambient);
glLightfv (GL_LIGHT0, GL_DIFFUSE, _ldiffuse);
glLightfv (GL_LIGHT0, GL_SPECULAR, _lspecular);
glViewport (0, 0, 800, 600);
glClearColor (0.0, 0.0, 0.0, 1.0);
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
gluPerspective (90, GLdouble(4.0/3.0), 300.0, 1600.0);
glMatrixMode (GL_MODELVIEW);
glLoadIdentity ();
gluLookAt (0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -1.0, 0.0);
AddString ("OpenGL initializated successfully", false);
AddString (EXT_SEPARATOR, true);
return true;
}
Вплоть до строки AddString (" Configuring OpenGL...", true); можете ни во что не вникать. Это инициализация постоянна и должна быть такой всегда. Она связывает OpenGL с вашей формой.
Разве что ошибки, которые мы выводим в консоль можно выводить как-нибудь иначе в ваших проектах.
Затем мы устанавливаем три переменных состояния с помощью функции glEnable:
GL_ALPHA_TEST - разрешить прозрачность.
Если установлено, то цвета можно будет задавать не тремя компонентами (красный, зеленый, синий), а четырьмя, добавив еще альфа-компонент, который будет говорить о прозрачности объекта. Единица будет говорить об абсолютно непрозрачном объекте, а 0 - об абсолютно прозрачном.
GL_DEPTH_TEST - разрешить тест глубины
Говорит о необходимости теста глубины. Если отключен, то объекты будут выводиться на экран в том порядке, в каком их рисуют. Например, если нарисовать перед самым экраном большой квадрат, а потом далеко-далеко шарик, полностью этим квадратом скрытый, то шарик будет нарисован поверх квадрата (абсурд, скажете?). Если же включить тест глубины, то рисоваться объекты будут в зависимости от удаления от экрана, то есть более близкие объекты будут рисоваться поверх более удаленных.
GL_NORMALIZE - разрешить нормализацию
Между делом нам предстоит работа с векторами (нормали), и задавать их надо строго единичными. Поскольку рассчитывать каждый раз единичный вектор вам, наверное, не захочется, мы включаем эту опцию, которая будет делать эту работу (называемую нормализацией) за нас.
Существует множество переменных состояния. Чтобы установить любую из них, следует вызвать команду glEnable, передав эту переменную в качестве параметра (что мы и сделали три раза), чтобы снять любую из них, следует вызвать функцию glDisable, опять же передав эту переменную.
Далее мы инициализируем освещение. О нем мы еще поговорим в будущем. Далее мы устанавливаем порт просмотра (ViewPort) Здесь мы выбираем, в какую часть формы будет выводиться сцена (нам надо на всю форму). Для этого служит функция glViewPort:
glViewPort (left, top, right, bottom);
Вы задаете координаты прямоугольника внутри формы, в который будет выводиться результат работы OpenGL. Мы, как уже говорилось, развернем на всю форму. Хочется заметить одно расхождение с привычным нам видением вещей. Точкой с координатой (0, 0) для функции glViewPort является вовсе не левая верхняя точка. Для glViewPort точка (0, 0) - это левая НИЖНЯЯ точка. И вообще следует привыкнуть, что по оси y мы движемся снизу вверх, а не сверху вниз, как это принято в спрайтовой графике.
Далее мы устанавливаем цвет фона, то есть цвет, которым мы будем красись сцену перед выводом объектов на сцену. Функиця glClearColor только устанавливает этот цвет, но не красит ничего сама (для окраски служит функция glClear. О ней тоже в будущем).
Матрица
В каждый момент времени вы работаете с какой-то определенной матрицей. Мы будем работать только с двумя из них:
GL_PROJECTION - если текущей матрицей является GL_PROJECTION, то все последующие матричные преобразрвания будут влиять на проекцию сцены на экран, то есть устанавливать границы экрана в трехмерном представлении, угол обозрения (fov - field of view) или его отсутствие (когда объекты все одного размера в независимоти от удаления). Такие матричные преобразования осуществляются командами glFrustum, glPerspective, glOrtho.
GL_MODELVIEW - если текущей матрицей является GL_MODELVIEW, то все последующие матричные преобразования влияют на положение камеры в пространстве. Такие преобразования осуществляются командами glRotate*, glTranslate* и glScale*.
В начале мы выбираем для изменения матрицу проекции (GL_PROJECTION).
Выбирается матрица функцией glMatrixMode.
Функция LoadIdentity сбрасывает текущую матрицу в ее изначальное состояние.
Как уже говорилось, первая матрица (проекции) служит для установки размеров и положения сцены на экране. Для этого мы будем использовать функцию gluPerspective. Она принимает следующие параметры:
gluPerspective (fovy, aspect, near, far);
На картинке показано наглядно значение всех параметров функции.
Люди, играющие в Quake или Unreal, должно быть, знают о команде консоля FOV, которая так мило увеличивает обзор. FOV (Field Of View - поле зрения) - это угол обзора. Первый параметр - fovy - это как раз угол обозрения по оси y (потому fovY), именно его и меняет команда консоля FOV во всех 3D Action. aspect - это отношение ширины к высоте видимой части сцены. Near - это координата z экрана, а far - это координата z самой дальней точки, которую предстоит показывать.
В нашем случае мы ставим угол 90°, стандартный, и аспект 4/3 (800/600). Ближнуюю точку мы ставим в 1. Дальнюю в просто очень большое положение.
Дальше мы меняем текущую матрицу на матрицу видового преобразования (MODELVIEW).
Функция gluLookAt устанавливает камеру. Она принимает 9 параметров с плавающей точкой. Первая тройка говорит о координатах положения камеры. Мы ее ставим в начало координат. Вторая тройка - это координаты точки, на которую мы смотрим (которая в центре обзора). Последняя тройка - это коориднаты точки, которая прямо над нами. Мы смотрим вдоль отрицательной полуоси oz, а вектор направления вверх берем стандартный, направленый вдоль положительной полуоси oy.
Итак, эта вся инициализаяция. Теперь надо подумать о деиницализации. OpenGL оставляет за собой мусор, который надо вычистить, завершая программу.
Создадим функцию extglFree, в которую и пропишем всю деинициализацию:
void extglFree (HWND Handle)
{
// удаляем контекст воспроизведения OpenGL
if (hGLRC)
{
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hGLRC);
hGLRC = NULL;
}
// освобождаем контекст устройства окна
if (hDC)
{
ReleaseDC(Handle, hDC);
hDC = NULL;
}
}
Не забудьте объявить ее прототип в файле col_gl.h:
void extglFree (HWND Handle);
Вот и все. Теперь в конструктор формы в модуле col_main добавляем:
...
// Покажем консоль
frmCon = new TfrmCon (Application);
frmCon->Show();
// Инициализируем поддержку DirectX
CanRun = extdxInit (Handle) && CanRun;
// Перепоказываем консоль с новыми координатами
frmCon->Hide ();
frmCon->Left = 0;
frmCon->Top = 0;
frmCon->Show ();
frmCon->TControl::Refresh ();
// Инициализируем поддержку OpenGL
// СЛЕДУЮЩУЮ СТРОКУ НАДО ДОБАВИТЬ!!!
CanRun = extglInit (Handle) && CanRun;
AddString ("Software Initializating...", true);
AddString (" Initializating Software Input...", true);
extinInit ();
...
А в обрабоитчик события OnDestroy припишем:
extdxFree (); // Эта строка есть
extglFree (Handle); // А эту надо добавить!