Инициализация в Windows
Setting Up An OpenGL Window
Я начинаю это пособие непосредственного с кода, разбитого на секции, каждая из которых будет подробно комментироваться. Первое, что вы должны сделать - это создать проект в Visual C++. Если это для вас затруднительно, то вам стоит для начала изучить C++, а уже затем переходить к OpenGL.
После того как вы создадите новое приложение в Visual C++, Вам надо будет добавить для сборки проекта библиотеки OpenGL. В меню Project/setting, выберите закладку LINK. В строке "Object/Library Modules" добавьте "OpenGL32.lib GLu32.lib GLaux.lib". Затем кликните по OK. Теперь все готово для создания программы с OpenGL.
Первые четыре строки, которые вы введете, сообщают компилятору какие библиотечные файлы использовать. Они должны выглядят так:
#include <windows.h> // Заголовочный файл для Windows
#include <gl\gl.h> // Заголовочный файл для OpenGL32 библиотеки
#include <gl\glu.h> // Заголовочный файл для GLu32 библиотеки
#include <gl\glaux.h> // Заголовочный файл для GLaux библиотеки
Далее, необходимо инициализировать все переменные, которые будут использованы в вашей программе. Эта программа будет создавать пустое OpenGL окно, поэтому мы не будем нуждаться в большом количестве переменных. То немногое, что мы устанавливаем - очень важно, и будет использоваться в каждой программе с OpenGL, которую вы напишите с использованием этого кода.
Первые две строки устанавливают Контексты Рендеринга, которые связывает вызовы OpenGL с окном Windows. Контекст Рендеринга OpenGL определен как hRC. Для того чтобы рисовать в окне, вам необходимо создать Контекст Устройства Windows, который определен как hDC. DC соединяет окно с GDI. RC соединяет OpenGL с DC.
static HGLRC hRC; // Постоянный контекст рендеринга
static HDC hDC; // Приватный контекст устройства GDI
Последняя переменная, в которой мы будем нуждаться, это массив, который мы используем для отслеживания нажатия клавиш на клавиатуре. Есть много путей следить за нажатиями на клавиатуре, но я использую этот путь. При этом можно отслеживать нажатие более чем одной клавиши одновременно.
BOOL keys[256]; // Массив для процедуры обработки клавиатуры
В следующей секции кода будут произведены все настройки для OpenGL. Мы установим цвет для очистки экрана, включим глубину буфера, разрешим плавное сглаживание цветов, и что наиболее важно, мы установим рендеринг на экран в перспективе, используя ширину и высоту окна. Эта процедура не должна быть вызвана до тех пор, пока OpenGL окно будет сделано.
GLvoid InitGL(GLsizei Width, GLsizei Height) // Вызвать после создания окна GL
{
В следующей строке устанавливается цвет, которым будет очищен экран. Для тех, кто не знает, как устроены цвета, я постараюсь кратко объяснять. Все значения могут быть в диапазоне от 0.0f до 1.0f, при этом 0.0 самый темный, а 1.0 самый светлый. Первое число в glClearColor - это интенсивность красного, второе – зеленного, третье – синего. Наибольшее значение – 1.0f, является самым ярким значением данного цвета. Последние число - для альфа значения. Когда начинается очистка экрана, я не когда не волнуюсь о четвертом числе. Пока оно будет 0.0f. Как его использовать, я объясню в другом уроке.
Поэтому, если вы вызвали glClearColor(0.0f,0.0f,1.0f,0.0f) вы произведете очистку экрана, с последующим закрашиванием его в ярко синий цвет. Если вы вызвали glClearColor(0.5f,0.0f,0.0f,0.0f) экран будет заполнен умеренно красным цветом. Не очень ярким (1.0f) и не темным (0.0f), а именно умеренно красным. Для того чтобы сделать белый фон, вы должны установить все цвета в (1.0f). Черный - как можно ниже (0.0f).
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Очистка экрана в черный цвет
Следующие три строки создают Буфер Глубины. Думайте о буфере глубины как о слоях на экране. Буфер глубины указывает, как далеко объекты находятся от экрана. Мы не будем реально использовать буфер глубины в этой программе, но любая программа с OpenGL, которая рисует на экране в 3D будет его использовать. Он позволяет сортировать объекты для отрисовки, поэтому квадрат расположенный под кругом не изображен будет поверх круга. Буфер глубины очень важная часть OpenGL.
glClearDepth(1.0); // Разрешить очистку буфера глубины
glDepthFunc(GL_LESS); // Тип теста глубины
glEnable(GL_DEPTH_TEST); // разрешить тест глубины
Следующие пять строк разрешают плавное сглаживание (антиалиасинг - antialiasing)(которое я буду объяснять позднее) и установку экрана для перспективного просмотра. Отдаленные предметы на экране кажутся меньшими, чем ближние. Это придает сцене реалистичный вид. Перспектива вычисляется под углом просмотра 45 градусов на основе ширины и высоты окна. 0.1f, 100.0f глубина экрана.
glMatrixMode(GL_PROJECTION) сообщает о том, что следующие команды будут воздействовать на матрицу проекции. glLoadIdentity() – это функция работает подобно сбросу. Раз сцена сброшена, перспектива вычисляется для сцены. glMatrixMode(GL_MODELVIEW) сообщает, что любые новые трансформации будут воздействовать на матрицу просмотра модели. Не волнуйтесь, если вы что-то не понимаете этот материал, я буду обучать всему этому в дальнейших уроках. Только запомините, что НАДО сделать, если вы хотите красивую перспективную сцену.
glShadeModel(GL_SMOOTH); // разрешить плавное цветовое сглаживание
glMatrixMode(GL_PROJECTION); // Выбор матрицы проекции
glLoadIdentity(); // Сброс матрицы проекции
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
// Вычислить соотношение геометрических размеров для окна
glMatrixMode(GL_MODELVIEW); // Выбор матрицы просмотра модели
}
Следующая секция кода очень простая, по сравнению с предыдущим кодом. Это функция масштабирования сцены, вызываемая OpenGL всякий раз, когда вы изменяете размер окна (допустим, что вы используете окна чаще, чем полноэкранный режим, который мы не рассматриваем). Даже если вы не делаете изменение размеров окна (например, если вы находитесь в полноэкранном режиме), эта процедура все равно должна быть вызвана хоть один раз, обычно во время запуска программы. Замечу, что сцена масштабируется, основываясь на ширине и высоте окна, которое отображается.
GLvoid ReSizeGLScene(GLsizei Width, GLsizei Height)
{
if (Height==0) // Предотвращение деления на ноль, если окно слишком мало
Height=1;
glViewport(0, 0, Width, Height);
// Сброс текущей области вывода и перспективных преобразований
glMatrixMode(GL_PROJECTION); // Выбор матрицы проекций
glLoadIdentity(); // Сброс матрицы проекции
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
// Вычисление соотношения геометрических размеров для окна
glMatrixMode(GL_MODELVIEW); // Выбор матрицы просмотра модели
}
В этой секции содержится весь код для рисования. Все, что вы планируете для отрисовки на экране, будет содержатся в этой секции кода. В каждом уроке, после этого будет добавлять код в эту секцию программы. Если вы уже понимаете OpenGL, вы можете попробовать добавить в код простейшие формы на OpenGL, ниже вызова glLoadIdentity(). Если вы новичок в OpenGL, подождите до следующего моего урока. Сейчас все что мы сделаем, это очистка экрана цветом, который мы определили выше, очистка буфера глубины и сброс сцены.
GLvoid DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// очистка Экрана и буфера глубины
glLoadIdentity();
// Сброс просмотра
}
Следующая секция кода наиболее важная секция в этой программе. Это установка окна Windows, установка формата пикселя, обработка при изменении размеров, при нажатии на клавиатуру, и закрытие программы.
Первые четыре строки делают следующее: переменная hWnd – является указателем на окно. Переменная message – сообщения, передаваемые вашей программе системой. Переменные wParam и lParam содержат информацию, которая посылается вместе с сообщением, например такая как ширина и высота окна.
LRESULT CALLBACK WndProc( HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
Код между скобками устанавливает формат пикселей. Я предпочитаю не использовать режим индексации цвета. Если вы не знаете, что это означает, не заботьтесь об этом. Формат описания пикселя описывает, как OpenGL будет выводить в окно. Большинство кода игнорируется, но зачастую это необходимо. Я буду помещать короткий комментарий для каждой строки. Знак вопроса означает, что я не уверен, что это строка кода делает (я только человек!).
{
RECT Screen; // используется позднее для размеров окна
GLuint PixelFormat;
static PIXELFORMATDESCRIPTOR pfd=
{
sizeof(PIXELFORMATDESCRIPTOR), // Размер этой структуры
1, // Номер версии (?)
PFD_DRAW_TO_WINDOW | // Формат для Окна
PFD_SUPPORT_OPENGL | // Формат для OpenGL
PFD_DOUBLEBUFFER, // Формат для двойного буфера
PFD_TYPE_RGBA, // Требуется RGBA формат
16, // Выбор 16 бит глубины цвета
0, 0, 0, 0, 0, 0, // Игнорирование цветовых битов (?)
0, // нет буфера прозрачности
0, // Сдвиговый бит игнорируется (?)
0, // Нет буфера аккумуляции
0, 0, 0, 0, // Биты аккумуляции игнорируются (?)
16, // 16 битный Z-буфер (буфер глубины)
0, // Нет буфера траффарета
0, // Нет вспомогательных буферов (?)
PFD_MAIN_PLANE, // Главный слой рисования
0, // Резерв (?)
0, 0, 0 // Маски слоя игнорируются (?)
};
Эта секция кода обрабатывает системные сообщения. Они генерируются, когда вы выходите из программы, нажимаете на клавиши, передвигаете окно, и так далее, каждая секция "case" обрабатывает свой тип сообщения. Если вы что вставите в эту секцию, не ожидайте, что ваш код будет работать должным образом, или вообще работать.
switch (message) // Тип сообщения
{
WM_CREATE сообщает программе, что оно должно быть создано. Вначале мы запросим DC (контекст устройства) для вашего окна. Помните, без него мы не можем рисовать в окно. Затем мы запрашиваем формат пикселя. Компьютер будет выбирать формат, который совпадает или наиболее близок к формату, который мы запрашиваем. Я не делаю здесь множества проверок на ошибки, чтобы сократить код, но это неправильно. Если что-то не работает, я просто добавляю необходимый код. Возможно, вы захотите посмотреть, как работают другие форматы пикселей.
case WM_CREATE:
hDC = GetDC(hWnd); // Получить контекст устройства для окна
PixelFormat = ChoosePixelFormat(hDC, &pfd);
// Найти ближайшее совпадение для нашего формата пикселов
Если подходящий формат пикселя не может быть найден, будет выведено сообщение об ошибке с соответствующем уведомлением. Оно будет ждать, когда вы нажмете на OK, до выхода из программы.
if (!PixelFormat)
{
MessageBox(0,"Can't Find A Suitable
PixelFormat.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0);
// Это сообщение говорит, что программа должна завершится
break; // Предтовращение повтора кода
}
Если подходящий формат найден, компьютер будет пытаться установить формат пикселя для контекста устройства. Если формат пикселя не может быть установлен по какой-то причине, выскочит сообщение об ошибке, что формат пикселя не найден, и будет ожидать, пока Вы не нажмете кнопку OK, до выхода из программы.
if(!SetPixelFormat(hDC,PixelFormat,&pfd))
{
MessageBox(0,"Can't Set The
PixelFormat.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0);
break;
}
Если код сделан, как показано выше, будет создан DC (контекст устройства), и установлен подходящий формат пикселя. Сейчас мы создадим Контекст Рендеринга, для этого OpenGL использует DC. wglCreateContext будет захватывать Контекст Рендеринга и сохранять его в переменной hRC. Если по какой-то причине Контекст Рендеринга не доступен, выскочит сообщение об ошибке. Нажмите OK для вызова программы.
hRC = wglCreateContext(hDC);
if(!hRC)
{
MessageBox(0,"Can't Create A GL Rendering
Context.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0);
break;
}
Сейчас мы имеем Контекст Рендеринга, и нам необходимо сделать его активным, для того чтобы OpenGL мог рисовать в окно. Снова, если по не которой причине это не может быть сделано, выскочит сообщение об ошибке. Кликните OK в окошке ошибки для выхода из программы.
if(!wglMakeCurrent(hDC, hRC))
{
MessageBox(0,"Can't activate GLRC.","Error",MB_OK|MB_ICONERROR);
PostQuitMessage(0);
break;
}
Если все прошло удачно, то у нас есть все для того, чтобы создать область рисования OpenGL. GetClientRect возвратит нам ширину и высоту окна. Мы запомним ширину справа, и высоту снизу. После того как мы получили ширину и высоту, инициализируем экран OpenGL. Мы делаем это при помощи вызова InitGL, передавая в параметрах право и низ (ширину и высоту).
GetClientRect(hWnd, &Screen);
InitGL(Screen.right, Screen.bottom);
break;
WM_DESTROY и WM_CLOSE очень похожи. Программа будет посылать это сообщение каждый раз, когда вы выходите из программы, нажав ALT-F4, или если вы послали PostQuitMessage(0) также как мы делали, когда происходила ошибка.
ChangeDisplaySettings(NULL,0) будет переключать разрешение рабочего стола обратно, делая его таким, каким мы переключались из него в полноэкранный режим. ReleaseDC(hWnd,hDC) уничтожает контекст устройства окна. По существу это уничтожает окно OpenGL.
case WM_DESTROY:
case WM_CLOSE:
ChangeDisplaySettings(NULL, 0);
wglMakeCurrent(hDC,NULL);
wglDeleteContext(hRC);
ReleaseDC(hWnd,hDC);
PostQuitMessage(0);
break;
WM_KEYDOWN вызывается всякий раз при нажатии клавиши. Клавиша, которая была нажата, сохраняется в переменной wParam. Итак, что же делает следующий код... Скажем, я нажал 'A'. Буква фактически – это число, которое ее представляет. Поэтому в ячейку, которая представляет 'A' заносится TRUE. Позднее, в коде, если я проверю состояние ячейки и увижу TRUE, то я знаю, что клавиша 'A' действительно в этот момент нажата.
case WM_KEYDOWN:
keys[wParam] = TRUE;
break;
WM_KEYUP вызывается всякий раз, когда клавиша отпускается. Клавиша, которая отжата, также сохраняется в переменной wParam. Поэтому, когда я отпускаю клавишу 'A', это делает ячейку для клавиши 'A' равной FALSE. Когда я проверю ячейку, для того чтобы увидеть нажата ли клавиша 'A', она вернет FALSE, что означает "нет, она не нажата".
case WM_KEYUP:
keys[wParam] = FALSE;
break;
И последнее, что я сделаю - обработаю изменение размеров окна. Возможно, кажется, что бесмыслено добавлять этот код, когда программа запущена в полноэкранном режиме, но без этого кода, экран OpenGL не появится. Поверьте, мне это очень важно.
Всякий раз сообщение WM_SIZE посылается Windows с двумя параметрами - новая ширина, и новая высота экрана. Эти параметры сохранены в LOWORD(lParam) и HIWORD(lParam). Поэтому вызов ReSizeGLScene изменяет размеры экрана. Это передает высоту и ширину в эту секцию кода.
case WM_SIZE:
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));
break;
Затем, дадим Windows обработать все сообщения, которые мы не обрабатываем и завершим процедуру.
default:
return (DefWindowProc(hWnd, message, wParam, lParam));
}
return (0);
}
Это то место, где начинается программа, где создается окно, где делается практически все, кроме рисования. Мы начинаем с создания окна.
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
MSG msg; // Структура сообщения Windows
WNDCLASS wc; // Структура класса Windows для установки типа окна
HWND hWnd; // Сохранение дискриптора окна
Флаги стиля CS_HREDRAW и CS_VREDRAW принуждают перерисовать окно всякий раз, когда оно перемещается. CS_OWNDC создает скрытый DC для окна. Это означает, что DC не используется совместно нескольким приложениями. WndProc - процедура, которая перехватывает сообщения для программы. hIcon установлен равным нулю, это означает, что мы не хотим ICON в окне, и для мыши используем стандартный указатель. Фоновый цвет не имеет значения (мы установим его в GL). Мы не хотим меню в этом окне, поэтому мы используем установку его в NULL, и имя класса – это любое имя которое вы хотите.
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL WinClass";
Сейчас мы регистрируем класс. Если произошла ошибка, появится соответствующее сообщение. Кликните на OK в коробочку об ошибке и будете выкинуты из программы.
if(!RegisterClass(&wc))
{
MessageBox(0,"Failed To Register The Window
Class.","Error",MB_OK|MB_ICONERROR);
return FALSE;
}
Сейчас мы сделаем окно. Не смотря на то, что мы делаем окно здесь, это не вызовет OpenGL до тех пор, пока сообщение WM_CREATE не послано. Флаги WS_CLIPCHILDREN и WS_CLIPSIBLINGS требуются для OpenGL. Очень важно, чтобы вы добавили их здесь. Я люблю использовать всплывающее окно, оно хорошо работает в полноэкранном режиме.
hWnd = CreateWindow(
"OpenGL WinClass",
"Jeff Molofee's GL Code Tutorial ... NeHe '99", // Заголовок вверху окна
WS_POPUP |
WS_CLIPCHILDREN |
WS_CLIPSIBLINGS,
0, 0, // Позиция окна на экране
640, 480, // Ширина и высота окна
NULL,
NULL,
hInstance,
NULL);
Далее - обычная проверка на ошибки. Если окно не было создано по какой-то причине, сообщение об ошибке выскочит на экран. Давите OK и завершайте программу.
if(!hWnd)
{
MessageBox(0,"Window Creation Error.","Error",MB_OK|MB_ICONERROR);
return FALSE;
}
Следующая секция кода вызывает у многих людей массу проблем … переход в полноэкранный режим. Здесь важна одна вещь, которую вы должны запомнить, когда переключаетесь в полноэкранный режим - сделать ширину и высоту в полноэкранном режиме необходимо туже самую, что и ширина и высота, которую вы сделали в своем окне.
Я не устанавливаю глубину цвета, когда я переключаю полноэкранный режим. Всякий раз, когда я пробовал переключать глубину цвета, я получал сверхъестественные запросы от Windows чтобы сделать перезагрузку компьютера для переключения нового режима цвета. Я не уверен, надо ли удовлетворять это сообщение, но я решил оставлять компьютер с той глубиной цвета, которая была до запуска GL программы.
Важно отметить, что этот код не будет скомпилирован на Cи. Это файл должен быть сохранен как .CPP файл.
DEVMODE dmScreenSettings; // Режим работы
memset(&dmScreenSettings, 0, sizeof(DEVMODE)); // Очистка для хранения установок
dmScreenSettings.dmSize = sizeof(DEVMODE); // Размер структуры Devmode
dmScreenSettings.dmPelsWidth = 640; // Ширина экрана
dmScreenSettings.dmPelsHeight = 480; // Высота экрана
dmScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT; // Режим Пиксела
ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);
// Переключение в полный экран
ShowWindow название этой функции говорит само за себя - она показывает окно, которое вы создали на экране. Я люблю это делать, после того как я переключусь в полноэкранный режим, хотя я не уверен, что это имеет значения. UpdateWindow обновляет окно, SetFocus делает окно активным, и вызывает wglMakeCurrent(hDC,hRC) чтобы убедиться, что Контекст рендеринга не освобожден.
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
SetFocus(hWnd);
Теперь мы создадим бесконечный цикл. Есть только один момент выхода из цикла, - когда нажали ESC. При этом программе будет отправлено сообщение о выходе, и она прервется.
while (1)
{
// Обработка всех сообщений
while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
return TRUE;
}
}
DrawGLScene вызывает ту часть программы, которая фактически рисует объекты OpenGL. В этой программе мы оставим эту часть секции пустой, все что будет сделано - очистка экрана черным цветом. В следующих уроках я покажу, как применять OpenGL для рисования.
SwapBuffers(hDC) очень важная команда. Мы имеем окно с установленной двойной буферизацией. Это означает, что изображение рисуется на скрытом окне (называемым буфером). Затем, мы говорим компьютеру переключить буфера, скрытый буфер копируется на экран. При этом получается плавная анимация без рывков, и зритель не замечает отрисовку объектов.
DrawGLScene(); // Нарисовать сцену
SwapBuffers(hDC); // Переключить буфер экрана
if (keys[VK_ESCAPE]) SendMessage(hWnd,WM_CLOSE,0,0); // Если ESC - выйти
}
}
В этом уроке я попытался объяснить как можно больше деталей каждого шага запутанной установки, и создания ваших собственных полноэкранных OpenGL программ, которые будут завершаться при нажатии ESC. Я потратил 3 дня и 13 часов для написания этого урока. Если вы имеете любые комментарии или вопросы, пожалуйста, пошлите их мне по электронной почте. Если вы ощущаете, что я некорректно комментировал что-то или что код должен быть лучше в некоторых секциях по некоторым причинам, пожалуйста, дайте мне знать. Я хочу сделать уроки по OpenGL хорошими насколько смогу. Я заинтересован в обратной связи.
© Jeff Molofee (NeHe)
PMG
6 марта 2001 (c)
Сергей Анисимов
d=document;rn=Math.random(); y=""; d.write(y);
Отображение полигонов
Your First Polygon
В предыдущем уроке было рассмотрено создание OpenGL окна. Теперь мы изучим создание таких фигур как треугольники и квадраты, при помощи GL_TRIANGLES и GL_QUADS.
Для создания приложения мы будем использовать код предыдущего примера, только добавим код в функцию DrawGLScene. Все ее изменения приводятся ниже. Если вы планируете менять предыдущий урок, просто замените функцию DrawGLScene следующим кодом, или просто добавьте те строки, которые там отсутствуют.
GLvoid DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана
// и буфера глубины
glLoadIdentity(); // Сброс просмотра
Вызов функции gLoadIdentity() устанавливает начало системы координат в центр экрана, причем ось X идет слева направо, ось Y вверх и вниз, а ось Z к и от наблюдателя. Центр OpenGL экрана находится в точке 0, 0, 0. Координаты, расположенные слева, снизу и вглубь от него, имеют отрицательное значение, расположенные справа, сверху и по направлению к наблюдателю – положительное.
Функция glTranslate(x, y, z) перемещает оси координат на указанные значения. Следующая строчка кода перемещает ось X на 1.5 единиц и ось Z на 6 единиц. Следует заметить, что перевод осей координат осуществляется не относительно центра экрана, а от их текущего расположения.
glTranslatef(-1.5f,0.0f,-6.0f); // Сдвинемся влево на 1.5 единицы и
// в экран на 6.0
Теперь, когда мы переместились в левую часть экрана и установили более удобный вид (в глубину на 6 единиц), отобразим геометрическую фигуру. Функция glBegin(GL_TRIANGLES) означает, что мы начинаем рисовать треугольник, и далее следует перечисление его вершин. После указания всех вершин, производится вызов функции glEnd(). Обычно треугольники на большинстве видеокарт отображаются наиболее быстро, но если есть желание отобразить иную фигуру, вместо GL_TRIANGLE используется другое значение, например: для создания четырехугольника указывается GL_QUADS, если вершин больше чем 4, то GL_POLYGONS.
В нашем примере рисуем только один треугольник. Если есть желание изобразить еще один треугольник, добавьте часть кода, следующую за описанием первого треугольника. В это случае все шесть строк кода следует размещать между glBegin(GL_TRIANGLES) и glEnd(). Нет нужды выделять каждый треугольник этими командами, после обработки трех вершин OpenGL сам перейдет к созданию новой фигуры. Это же относится и к четырехугольникам. Полигоны (GL_POLYGONS) в свою очередь могут иметь любое количество вершин, и поэтому нет разницы, сколько описателей располагалось между строками glBegin(GL_POLYGONS) и glEnd().
Первая строка после glBegin описывает первую вершину нашего полигона. Функция glVertex3f() получает в качестве параметров ее X, Y и Z координаты. Первая вершина треугольника смещена только от оси Y на 1, таким образом, мы расположим ее точно в центре и она будет самой верхней. Следующая вершина будет располагаться на оси Х слева от центра и на оси Y вниз от центра. Эта вершина будет расположена внизу слева. Третья вершина будет справа и снизу от центра. Функция glEnd() указывает OpenGL, что вершин больше не будет. Результатом всего этого будет залитый цветом по умолчанию треугольник.
glBegin(GL_TRIANGLES);
glVertex3f( 0.0f, 1.0f, 0.0f); // Вверх
glVertex3f(-1.0f,-1.0f, 0.0f); // Слева снизу
glVertex3f( 1.0f,-1.0f, 0.0f); // Справа снизу
glEnd();
Теперь у нас есть треугольник, изображенный в правой части экрана. Нам нужно
переместиться в левую часть, для этого снова используем функцию glTranslate(). Так как
мы в прошлый раз перемещались влево на 1.5 единицы, необходимо переместиться на 3.0
единицы вправо (1.5 единицы – это будет центр, плюс еще 1.5 единицы для правого края).
glTranslatef(3.0f,0.0f,0.0f); // Сдвинем вправо на 3 единицы
Здесь мы изобразим квадрат. Так как он является четырехсторонним полигоном, мы будем использовать GL_QUADS. Создание квадрата напоминает создание треугольника, правда указывать нужно четыре вершины. Они будут идти в следующем порядке – левая вверху, правая вверху, правая снизу и левая снизу.
glBegin(GL_QUADS);
glVertex3f(-1.0f, 1.0f, 0.0f); // Слева вверху
glVertex3f( 1.0f, 1.0f, 0.0f); // Справа вверху
glVertex3f( 1.0f,-1.0f, 0.0f); // Справа внизу
glVertex3f(-1.0f,-1.0f, 0.0f); // Слева внизу
glEnd();
}
В этом примере я попытался описать как можно больше деталей, указать каждый шаг создания полигонов на экране с использованием OpenGL. Если у вас есть, какие либо замечания, вопросы или комментарии, я жду ваших писем. Если вы нашли неправильно описание или ошибки в коде, пожалуйста, сообщите мне об этом, мне очень хотелось бы, чтобы эти описания были лучше.
© Jeff Molofee (NeHe)
PMG
26 марта 2001 (c)
Тимур Салихов
d=document;rn=Math.random(); y=""; d.write(y);
Отображение цветов
Colors
В предыдущем уроке я показал, как можно отобразить на экране треугольник и квадрат. В этом уроке я научу вас отображать эти же фигуры, но в разных цветах. Квадрат мы зальем одним цветом, а вот треугольник будет залит тремя разными цветами (по одному на каждую вершину) с гладкими переходами.
Вы можете использовать код из предыдущего урока, изменив лишь процедуру DrawGLScene(). Я переписал ее содержимое, и, если вы планируете изменять предыдущий урок, вам нужно полностью ее заменить, или добавить те строки, которые там отсутствуют.
GLvoid DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glTranslatef(-1.5f,0.0f,-6.0f);
glBegin(GL_TRIANGLES);
Если вы еще помните предыдущий урок, в этой секции мы рисовали треугольник в левой части экрана. Следующие строки кода используют команду glColor3f(r, g, b). Три ее параметра указывают насыщенность цвета красной, синей и зеленой составляющей. Каждый из них может принимать значение от 0.0f до 1.0f.
Мы установим красный цвет (полный красный, без зеленого и синего). Следующие строки кода указывают, что первая вершина (верхняя) будет иметь красный цвет. Все что мы будем рисовать дальше красным, до тех пор, пока мы его не сменим цвет.
glColor3f(1.0f,0.0f,0.0f); // Красный цвет
glVertex3f( 0.0f, 1.0f, 0.0f);
Мы отобразили первую вершину, установив для нее красный цвет. Теперь добавим следующую вершину (левую нижнею), но установим для нее уже зеленый цвет.
glColor3f(0.0f,1.0f,0.0f); // Зеленный цвет
glVertex3f(-1.0f,-1.0f, 0.0f);
Пришло время третьей и последней вершины (правый нижний угол). Перед тем как отобразить ее, мы установим синий цвет. После выполнения команды glEnd() треугольник будет залит указанными цветами. Так как мы указали для каждой вершины свой цвет, каждая часть фигуры будет залита по-разному. При переходе в другую вершину заливка будет плавно изменять свой цвет на цвет вершины. В середине треугольника все три цвета будут слиты в один.
glColor3f(0.0f,0.0f,1.0f); // Синий цвет
glVertex3f( 1.0f,-1.0f, 0.0f);
glEnd();
glTranslatef(3.0f,0.0f,0.0f);
Теперь мы отобразим квадрат, но зальем его одним цветом. Очень важно помнить, что если вы установили какой-либо цвет, все примитивы в дальнейшем будет отображаться именно им. Каждый последующий проект, который вы будете создавать, так или иначе, будет использовать цвета. Если вы, например, создает сцену, где все фигуры текстурированы, цвет будет использоваться для тона текстур, и т.д.
Так как мы рисуем наш квадрат в одном цвете (для примера – в синем), для начала установим этот цвет, а затем отобразим саму фигуру. Синий цвет будет использоваться OpenGL для каждой вершины, так как мы не меняем его. В итоге мы получим синий квадрат.
glColor3f(0.5f,0.5f,1.0f); // Установим синий цвет только один раз
glBegin(GL_QUADS);
glVertex3f(-1.0f, 1.0f, 0.0f);
glVertex3f( 1.0f, 1.0f, 0.0f);
glVertex3f( 1.0f,-1.0f, 0.0f);
glVertex3f(-1.0f,-1.0f, 0.0f);
glEnd();
}
В этом примере я пытался, как можно детальнее описать процедуру установки цветов для вершин, показать различие между однотонной заливкой и разноцветной сглаженной. Попробуйте, для тренировки, изменять составляющие красного, зеленого и синего компонентов цвета. Посмотрите, какие будут результаты ваших экспериментов. Если у вас есть, какие либо замечания, вопросы или комментарии, я жду ваших писем. Если вы нашли неправильно описание или ошибки в коде, пожалуйста, сообщите мне об этом, мне очень хотелось бы, чтобы эти описания были лучше.
Вращение полигонов
Rotation
В прошлом уроке я научил Вас как закрашивать треугольники и четырехугольники. В этом уроке я научу Вас как вращать эти закрашенные объекты вдоль их осей.
Мы будем использовать код из последнего урока, добавляя новые строчки кода. Я перепишу целую секцию кода ниже, чтобы вы могли понять, что добавлено, а что заменено.
Вначале мы добавим две переменные для хранения угла вращения каждого объекта. Мы сделаем это вначале программы. Посмотрите ниже, я добавил две строки после объявления переменной BOOL keys[256]. В этих строках объявляются две переменные с плавающей запятой, которые мы можем использовать для очень точного поворота объектов. Числа с плавающей запятой учитывают значения меньше единицы. Вместо использования 1, 2, 3 для угла, мы можем использовать 1.1, 1.7, 2.3 или даже 1.015 для точности. Вы увидете, что числа с плавающей запятой неотемлимая часть программирования на OpenGL.
#include <windows.h> // Заголовочный файл для Windows
#include <gl\gl.h> // Заголовочный файл для OpenGL32 библиотеки
#include <gl\glu.h> // Заголовочный файл для GLu32 библиотеки
#include <gl\glaux.h> // Заголовочный файл для GLaux библиотеки
static HGLRC hRC; // Постоянный контекст рендеринга
static HDC hDC; // Приватный контекст устройства GDI
BOOL keys[256]; // Массив для процедуры обработки клавиатуры
GLfloat rtri; // Угол для треугольник
GLfloat rquad; // Угол для четырехугольника
Необходимо модифицировать код в DrawGLScene(). Я буду переписывать всю процедуру. Это будет сделать легко для Вас, так как Вы увидите какие изменения я сделал. Я объясню почему некоторые строки были модифицированы, и какие линии добавлены. Следующая секция кода, такая же как в последнем уроке.
GLvoid DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана
// и буфера глубины
glLoadIdentity(); // Сброс просмотра
glTranslatef(-1.5f,0.0f,-6.0f); // Сдвиг в глубь экрана и влево
Следующая строка новая. glRotatef(Angle,Xtrue,Ytrue,Ztrue) отвечает за вращения объекта вдоль оси. Вы многое получите от использования этой команды. Угол некоторое число (обычно переменная), которое задает насколько Вы хотите повернуть объект. Xtrue, Ytrue и Ztrue или 0.0f или 1.0f. Если один из параметров равен 1.0f, OpenGL будет вращать объект вдоль соответствующей оси. Поэтому если Вы имеете glRotatef(10.0f,0.0f,1.0f,0.0f), объект будет поварачиваться на 10 градусов по оси Y. Если glRotatef(5.0f,1.0f,0.0f,1.0f), объект будет поварачиваться на 5 градусов по обеим осям X и Z.
Чтобы лучше понять вращения по осям X, Y и Z я объясню это на примерах.
Ось X - предположим Вы работаете за токарным станком. Заготовка перемещается слева направо (также как ось X в OpenGL). Заготовка вращается вокруг оси X. Также мы вращаем что-то вокруг оси X в OpenGL.
Ось Y- Представьте что Вы стоите посреди поля. Огромный торнадо приближается к Вам. Центр торнадо перемещается от земли в небо (верх и низ, подобно оси Y в OpenGL). Предметы захваченные торнадо кружаться вдоль оси Y (центр торнадо) слева направо или справо налево. Когда вы вращаете что-то вокруг оси Y в OpenGL, это что-то будет вращаться также.
Ось Z - Вы смотрите на переднюю часть вентилятора. Передняя часть вентилятора ближе к Вам, а дальняя часть дальше от Вас (также как ось Z в OpenGL). Лопасти вентилятора вращаются вдоль оси Z (центр вентилятора) по часовой или против часовой стрелки. Когда Вы вращаете что-то вокруг оси Z в OpenGL, это что-то будет вращаться также.
В следующей строке кода, если rtri равно 7, мы будем вращать на 7 градусов по оси Y (слева направо). Вы можете поэксперементировать с кодом. Изменяйте от 0.0f до 1.0f, и от 1.0f до 0.0f вращение треугольника по осям X и Y одновременно.
glRotatef(rtri,0.0f,1.0f,0.0f); // Вращение треугольника по оси Y
Следующая секция кода не изменена. Здесь будет нарисован закрашенный сглаженный треугольник. Треугольник будет нарисован с левой стороны экрана, и будет вращаться по оси Y слева направо.
glBegin(GL_TRIANGLES); // Начало рисования треугольника
glColor3f(1.0f,0.0f,0.0f); // Верхняя точка - красная
glVertex3f( 0.0f, 1.0f, 0.0f); // Первая точка
glColor3f(0.0f,1.0f,0.0f); // Левая точка - зеленная
glVertex3f(-1.0f,-1.0f, 0.0f); // Вторая
glColor3f(0.0f,0.0f,1.0f); // Правая - синия
glVertex3f( 1.0f,-1.0f, 0.0f); // Третья
glEnd(); // Конец рисования
Посмотрите на код ниже, там мы добавим вызов glLoadIdentity(). Мы сделаем это для инициализации просмотра. Что будет если мы не сбросим просмотр? Если мы сдвинули объект после вращения, Вы получите очень неожиданные результаты. Поскольку оси вращаются, они будут указывать не в тех направлениях, о каких Вы думаете. Поэтому если мы сдвинулись влево по оси X (для установки треугольника), мы можем переместить квадрат в глубь экрана или вперед, в зависимости от того как много мы вращали по оси Y. Попробуйте убрать glLoadIdentity() и вы поймете о чем я говорю. Квадрат будет вращаться вокруг своей оси X, но и вокруг центра координат синхронно вращению треугольника.
Так как сцена сброшена, поэтому X идет слева направо, Y сверху вниз, Z от нас и далее. Теперь мы перемещаем. Как Вы видите мы сдвигаем на 1.5 вправо, вместо 3.0, как мы делали в последнем уроке. Когда мы сбрасываем экран, наш фокус перемещается в центр экрана, это означает, что мы находимся не 1.5 единицы слева, мы вернулись в 0.0. Поэтому мы не должны сдвигаться на 3.0 единицы вправо (если бы не было сброса), мы должны только сдвинуться от центра вправо на 1.5 единицы.
После того как мы сдвинулись в новое место на правой стороне экрана, мы вращаем квадрат по оси X. Квадрат будет вращаться верх и вниз.
glLoadIdentity();
glTranslatef(1.5f,0.0f,-6.0f); // Сдвиг вправо на 1.5
glRotatef(rquad,1.0f,0.0f,0.0f); // Вращение по оси X
Эта секция кода завершение предыдущей. Рисуем синий квадрат из одного четырехугольника. Квадрат будет справа на экране и там же будет вращаться.
glColor3f(0.5f,0.5f,1.0f); // Синий цвет
glBegin(GL_QUADS); // Начнем
glVertex3f(-1.0f, 1.0f, 0.0f); // Верх лево
glVertex3f( 1.0f, 1.0f, 0.0f); // Верх право
glVertex3f( 1.0f,-1.0f, 0.0f); // Низ право
glVertex3f(-1.0f,-1.0f, 0.0f); // Низ лево
glEnd(); // Окончим
Следующие две строки новые. Думайте о rtri и rquad как о контейнерах. Вначале нашей программы мы сделали контейнеры (GLfloat rtri и GLfloat rquad). Когда мы построили контейнеры они были пусты. В первой строке ниже ДОБАВЛЯЕМ 0.2 в контейнер. Если мы проверим значение контейнера rtri после этой секции кода, мы увидим что оно увеличилось на 0.2. Контейнер rquad уменьшиться на 0.15. Если мы проверим значение контейнера rquad после этой секции кода, мы увидим что оно уменьшилось на 0.15. Отрицательные значения вращения приводят к тому, что объект вращается в противоположную сторону. Как если бы значения были положительные.
Попробуйте изменить + на - в строке ниже и объект будет вращаться в другом направлении. Попробуйте изменить значение с 0.2 до 1.0. С увеличением значения объект будет вращаться быстрее. С уменьшением значения будет вращаться медленее.
rtri+=0.2f; // Увеличение переменной вращения для треугольника
rquad-=0.15f; // Уменьшение переменной вращения для квадрата
}
В этом уроке я попробывал рассказать как можно детальнее о том как вращаются объекты вокруг осей. Поиграйте с этим кодом, попробуйте вращать объекты по оси Z, X & Y, или по всем трем ;). Если у Вас есть комментарии или вопросы пожалуйста вышлите мне письмо. Если Вы нашли ошибки или улучшения дайте мне об этом знать. Я хочу сделать уроки по OpenGL хорошими насколько смогу. Я заинтересован в обратной связи.
© Jeff Molofee (NeHe)
PMG
21 сентября 2001 (c)
Сергей Анисимов
d=document;rn=Math.random(); y=""; d.write(y);
Создание фигур в 3D
Solid Objects
Продолжая последний урок, мы сделаем объект, как ИСТИННЫЙ трехмерный объект, а не 2D объекты в 3D мире. Мы будем делать это добавлением с левой, задней и правой сторон треугольника, и с левой, правой, верхней и нижней сторон квадрата. Сделав это, мы превращаем треугольник в пирамиду с четырьмя гранями и квадрат в куб.
Мы будем смешивать цвета на пирамиде, создавая сглаженный закрашенный объект, а для квадрата мы назначим каждой грани свой цвет.
GLvoid DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Очистка экрана и буфера глубины
glLoadIdentity(); // Сброс просмотра
glTranslatef(-1.5f,0.0f,-6.0f); // Сдвиг влево и вглубь экрана
glRotatef(rtri,0.0f,1.0f,0.0f); // Вращение пирамиды по оси Y
glBegin(GL_TRIANGLES); // Начало рисования пирамиды
Некоторые из Вас взяли код из последнего урока и сделали свои собственные 3D объекты. Вот один вопрос, который вы задали : "как сделать, чтобы мои объекты не вращались по своим осям? Потому что кажется, что они вращаются на весь экран". Чтобы объект вращался вокруг оси, он должен быть разработан для вращения ВОКРУГ оси. Вы должны помнить, что центр любого объекта должен быть в 0 для X, 0 для Y, 0 для Z.
Следующий код создаст пирамиду вокруг центральной оси. Верх пирамиды на единицу выше центра, низ пирамиды на единицу ниже центра. Верхняя точка как раз в середине (ноль), а нижние точки одна слева от центра, а одна справа от центра.
Отмечу, что все треугольники рисуются с вращением против часовой стрелки. Это важно, и будет объяснено в следующих уроках, сейчас, только запомните, что отличной привычкой будет делать объекты или по часовой или против часовой стрелки, и вы не должны смешивать эти два метода, если на то нет веской причины.
Мы начинаем рисовать с Передней Грани. Поскольку во все грани входит верхняя точка, мы будем делать эту точку красной во всех треугольниках. Цвет нижних двух точек треугольника будет другим. Передняя грань будет зеленной в левой точке и синей в правой точке. Треугольник с правой стороны будет синим в левой точке и зеленным в правой точке. При помощи чередования двух нижних цветов на каждой грани, мы сделаем общие закрашенные точки снизу на каждой грани.
glColor3f(1.0f,0.0f,0.0f); // Красный
glVertex3f( 0.0f, 1.0f, 0.0f); // Верх треугольника (Передняя)
glColor3f(0.0f,1.0f,0.0f); // Зеленный
glVertex3f(-1.0f,-1.0f, 1.0f); // Левая точка
glColor3f(0.0f,0.0f,1.0f); // Синий
glVertex3f( 1.0f,-1.0f, 1.0f); // Правая точка
Сейчас мы нарисуем правую грань. Отметим, что две нижних точки нарисованы на единицу справа от центра, верхняя точка нарисована на единицу выше оси Y, и справа от середины оси X. Поэтому эта грань имеет наклон от центральной точки сверху вниз с правой стороны.
Замечу, что левая точка нарисована синим цветом в этот раз. Так как она нарисована синей, это будет тот же самый цвет, какой у точки правого нижнего угла лицевой грани. Градиент синего цвета идет от одного угла вдоль лицевой и правой граней пирамиды.
Замечу, что все четыре грани включены внутрь тех же самых glBegin(GL_TRIANGLES) и glEnd(), словно одна сторона. Поскольку мы делаем целый объект из треугольников, OpenGL знает, что каждые три точки мы рисуем как три точки одного треугольника. Треугольник рисуется из трех точек, если больше трех точек, то OpenGL поймет, что надо рисовать другой треугольник. Если вы выведете четыре точки вместо трех, OpenGL будет рисовать первые три точки и примет четвертую точку как начальную точку нового треугольника. Но не будет нарисован Четырехугольник. Поэтому проверьте, что вы не добавили любые дополнительные точки нечаяно.
glColor3f(1.0f,0.0f,0.0f); // Красная
glVertex3f( 0.0f, 1.0f, 0.0f); // Верх треугольника (Правая)
glColor3f(0.0f,0.0f,1.0f); // Синия
glVertex3f( 1.0f,-1.0f, 1.0f); // Лево треугольника (Правая)
glColor3f(0.0f,1.0f,0.0f); // Зеленная
glVertex3f( 1.0f,-1.0f, -1.0f); // Право треугольника (Правая)
Сейчас задняя сторона. Снова переключим цвета. Левая точка – зеленного цвета, поскольку этот угол так же и зеленный угол правой грани.
glColor3f(1.0f,0.0f,0.0f); // Красный
glVertex3f( 0.0f, 1.0f, 0.0f); // Низ треугольника (Сзади)
glColor3f(0.0f,1.0f,0.0f); // Зеленный
glVertex3f( 1.0f,-1.0f, -1.0f); // Лево треугольника (Сзади)
glColor3f(0.0f,0.0f,1.0f); // Синий
glVertex3f(-1.0f,-1.0f, -1.0f); // Право треугольника (Сзади)
В завершении мы рисуем левую грань. Цвета переключаются в последний раз. Левая точка синего цвета, и смешивается с правой точкой на задней грани. Правая точка зеленного цвета, и смешивается с левой точкой на передней грани.
Мы закончили рисовать пирамиду. Поскольку пирамида только крутиться вдоль оси Y, мы не когда не увидим низ, поэтому нет необходимости выводить низ пирамиды. Если вы чувствуете потребность в экспериментах, попробуйте добавить низ используя четырехугольник, тогда вращайте по оси X для того чтобы увидеть низ, если конечно вы сделали все корректно. Проверьте, что цвет каждого угла в четырехугольнике совпадает с цветом, который использован в каждом из четырех углов пирамиды.
glColor3f(1.0f,0.0f,0.0f); // Красный
glVertex3f( 0.0f, 1.0f, 0.0f); // Верх треугольника (Лево)
glColor3f(0.0f,0.0f,1.0f); // Синий
glVertex3f(-1.0f,-1.0f,-1.0f); // Лево треугольника (Лево)
glColor3f(0.0f,1.0f,0.0f); // Зеленный
glVertex3f(-1.0f,-1.0f, 1.0f); // Право треугольника (Лево)
glEnd(); // Кончили рисовать пирамиду
Сейчас мы будем рисовать куб. Чтобы сделать это надо шесть квадратов. Все квадраты рисуются против часовой стрелке. Примем, что первая точка справа вверху, вторая точка слева вверху, третья точка слева внизу, и последняя слева внизу. Когда мы рисуем заднюю грань, то будет казаться, что мы рисуем по часовой стрелке, но вы помните, что если мы были позади куба и смотрели на него, то левая сторона экрана, фактически была бы с правой стороны квадрата, и правая сторона экрана была бы фактически с левой стороны квадрата.
Замечу, что мы сдвинули куб немного вглубь экрана в этом уроке. Поэтому размер куба будет казаться меньше размера пирамиды. Если мы переместили бы куб на 6 единиц к экрану, то куб будет казаться больше чем пирамида, и часть куба будет за пределами экрана. Вы можете поиграться с этим настройками, и сдвинув куб дальше от экрана он будет казаться меньше, а придвинув к экрану он будет казаться больше. Это происходит из-за переспективы. Объекты на расстоянии кажутся меньше :).
glLoadIdentity();
glTranslatef(1.5f,0.0f,-7.0f); // Сдвинуть вправо и вглубь экрана
glRotatef(rquad,1.0f,1.0f,1.0f); // Вращение куба по X, Y & Z
glBegin(GL_QUADS); // Рисуем куб
Мы начнем рисовать куб сверху. Мы сдвигаемся на одну единицу от центра куба. Отметим, что по оси Y всегда единица. Затем мы рисуем квадрат на Z плоскости. Мы начинаем рисовать с правой точки вверху экрана. Правая верхняя точка должна быть на одну единицу справа, и на одну единицу вглубь экрана. Вторая точка будет на одну единицу влево и на единицу вглубь экрана. Сейчас мы нарисуем ту часть квадрата, которая ближе к зрителю. Поэтому для того чтобы сделать это, вместо смещения вглубь экрана, мы сдвигаемся на одну единицу к экрану. Улавливаете смысл?
glColor3f(0.0f,1.0f,0.0f); // Синий
glVertex3f( 1.0f, 1.0f,-1.0f); // Право верх квадрата (Верх)
glVertex3f(-1.0f, 1.0f,-1.0f); // Лево верх
glVertex3f(-1.0f, 1.0f, 1.0f); // Лево низ
glVertex3f( 1.0f, 1.0f, 1.0f); // Право низ
Нижняя часть квадрата рисуется таким же образом, как и верхняя, но поскольку это низ, сдвигаемся вниз на одну единицу от центра куба. Замечу, что ось Y всегда минус единица. Если мы окажемся под кубом, и взглянем на квадрат, который снизу, вы заметите, что правый верхний угол – это угол ближний к зрителю. Поэтому вместо того чтобы рисовать дальше от зрителя в начале, мы рисуем ближе к зрителю, тогда левая сторона ближе к зрителю. И затем мы движемся вглубь экрана, для того чтобы нарисовать дальние две точки.
Если Вы действительно не заботитесь о порядке рисования полигонов (по часовой или против), вы должны скопировать код для верхнего квадрата, сдвинуть вниз по оси Y на единицу, и это будет работать, но игнорируя порядок рисования квадрата можно получить неверный результат, если вы захотите сделать, например, наложение текстуры.
glColor3f(1.0f,0.5f,0.0f); // Оранжевый
glVertex3f( 1.0f,-1.0f, 1.0f); // Верх право квадрата (Низ)
glVertex3f(-1.0f,-1.0f, 1.0f); // Верх лево
glVertex3f(-1.0f,-1.0f,-1.0f); // Низ лево
glVertex3f( 1.0f,-1.0f,-1.0f); // Низ право
Сейчас мы рисуем передний квадрат. Мы сдвигаемся на одну единицу ближе к экрану, и дальше от центра для того чтобы нарисовать переднею грань. Заметим, что ось Z всегда равна единице. В гранях пирамиды ось Z не всегда единица. Вверху, ось Z равна нулю. Если Вы попробуете установить ось Z равной нулю в привиденом ниже коде, вы увидите, что угол, который вы изменили наклонен к экрану. Но это не то, что мы хотим сейчас сделать ;).
glColor3f(1.0f,0.0f,0.0f); // Красный
glVertex3f( 1.0f, 1.0f, 1.0f); // Верх право квадрата (Перед)
glVertex3f(-1.0f, 1.0f, 1.0f); // Верх лево
glVertex3f(-1.0f,-1.0f, 1.0f); // Низ лево
glVertex3f( 1.0f,-1.0f, 1.0f); // Низ право
Задняя грань квадрата такая же ка передняя грань, но сдвинута вглубь экрана. Отметим, что ось Z всегда минус один во всех точках.
glColor3f(1.0f,1.0f,0.0f); // Желтый
glVertex3f( 1.0f,-1.0f,-1.0f); // Верх право квадрата (Зад)
glVertex3f(-1.0f,-1.0f,-1.0f); // Верх лево
glVertex3f(-1.0f, 1.0f,-1.0f); // Низ лево
glVertex3f( 1.0f, 1.0f,-1.0f); // Низ право
Сейчас нам осталось нарисовать только два квадрата. Как вы уже успели заметить одна ось всегда имеет тоже самое значение у всех точек квадрата. В этом случае ось X всегда равна минус один. Поскольку мы рисуем левую грань.
glColor3f(0.0f,0.0f,1.0f); // Синий
glVertex3f(-1.0f, 1.0f, 1.0f); // Верх право квадрата (Лево)
glVertex3f(-1.0f, 1.0f,-1.0f); // Верх лево
glVertex3f(-1.0f,-1.0f,-1.0f); // Низ лево
glVertex3f(-1.0f,-1.0f, 1.0f); // Низ право
И последняя грань завершит куб. Для нее ось X всегда равна единице. Рисуем против часовой стрелки. Если вы хотите, то вы можете не рисовать эту грань и получите коробку ;).
Если вы хотите поэксперементировать, вы всегда можете изменить цвет каждой точки в кубе для того чтобы сделать градиент, как в пирамиде. Вы можете посмотреть пример интерполяционной заливки куба скопировав первую демонстрацию Evil с моего сайта. Запустите ее и нажмите TAB. Вы увидите чудесный цветной куб, с изменяющимися цветами вдоль всех граней.
glColor3f(1.0f,0.0f,1.0f); // Фиолетовый
glVertex3f( 1.0f, 1.0f,-1.0f); // Верх право квадрата (Право)
glVertex3f( 1.0f, 1.0f, 1.0f); // Верх лево
glVertex3f( 1.0f,-1.0f, 1.0f); // Низ лево
glVertex3f( 1.0f,-1.0f,-1.0f); // Низ право
glEnd(); // Закончили квадраты
rtri+=0.2f; // Увеличим переменную вращения для треугольника
rquad-=0.15f; // Уменьшим переменную вращения для квадрата
}
В конце этого урока, вы должны лучше понимать как объекты создаются в 3D пространстве. Вы должны представлять экран OpenGL, как гиганская милиметровка, с множеством прозрачных слоев за ней. Это похоже на гигантский куб сделаный из точек. Некоторые точки двигаются слева направо, некоторые двигаются верх и вниз, и некоторые точки двигаются взад и вперед в кубе. Если вы может представить глубину экрана, вы не будете иметь проблем с разработкой новых 3D объектов.
Если вы с трудом понимаете 3D пространство, то это не бесполезно. Это может быть сложным только вначале. Объекты подобные кубу хороший пример для обучения. Если вы заметили задняя грань рисуется также как передняя грань, только дальше от экрана. Поиграйте с этим кодом, и если вы не можете понять это, спросите меня, и я вам отвечу.
Наложение текстуры
Texture Mapping
Из наложения текстуры можно извлечь много полезного. Предположим, что вы хотите, чтобы ракета пролетела через экран. До этого урока мы попытались бы сделать ракету из полигонов и фантастических цветов. С помощью наложения текстуры, мы можем получить реальную картинку ракеты и заставить ее летать по экрану. Как вы думаете, что будет выглядеть лучше? Фотография или объект сделанный их треугольников и четырехугольников? Используя наложение текстуры, и выглядеть будет лучше, и ваша программа будет работать быстрее. Ракета с наложением текстуры - это всего лишь четырехугольник, движущийся по экрану. Ракета сделанная из полигонов может иметь сотни или тысячи полигонов. Отрисовка простого четырехугольника будет отнимать меньше процессорного времени.
Давайте начнем с добавления четырех новых строк в начало кода первого урока. Первые три строки задают четыре вещественных переменных - xrot, yrot и zrot. Эти переменные будут использованы для вращения куба по осям x, y, z. В четвертой строке резервируется место для одной текстуры. Если вы хотите загрузить более чем одну текстуру, измените, число один на число текстур, которые вы хотите загрузить.
#include <windows.h> // Заголовочный файл для Windows #include <gl\gl.h> // Заголовочный файл для OpenGL32 библиотеки #include <gl\glu.h> // Заголовочный файл для GLu32 библиотеки #include <gl\glaux.h> // Заголовочный файл для GLaux библиотеки
static HGLRC hRC; // Постоянный контекст рендеринга static HDC hDC; // Приватный контекст устройства GDI
BOOL keys[256]; // Массив для процедуры обработки клавиатуры
GLfloat xrot; // Вращение X GLfloat yrot; // Y GLfloat zrot; // Z
GLuint texture[1]; // Место для одной текстуры
Теперь сразу же после этого кода, до InitGL, мы добавим следующую секцию кода. Этот код загружает файл картинки, и конвертирует его в текстуру. Прежде чем я начну объяснять этот код, я сделаю нескольких ВАЖНЫХ замечаний, которые вы должны знать об изображениях, которые вы используете как текстуры. Такое изображение ДОЛЖНО иметь высоту и ширину кратной двум. При этом высота и ширина изображения должна быть не меньше чем 64 пикселя, и по причинам совместимости, не более 256 пикселов. Если изображение, которое вы используете не 64, 128 или 256 пикселов в ширину и высоту, измените его размер в программе для рисования. Имеются возможность обойти эти ограничения, но мы пока будем придерживаться стандартных размеров текстуры.
AUX_RGBImageRec * texture1 задает указатель на структуру для хранения первой картинки, которую мы загрузим и используем как текстуру. Структура содержит красную, зеленную и синею компоненты цвета, которые используются при создании изображения. Обычно так размещается в памяти загруженная картинка. Структура AUX_RGBImageRec определена в библиотеке glAux, и делает возможной загрузку картинки в память. В следующей строке происходит непосредственная загрузка. Файл картинки "NeHe.bmp" из каталога "Data" будет загружен и сохранен в структуре texture1, которую мы задали выше с помощью AUX_RGBImageRec.
// Загрузка картинки и конвертирование в текстуру GLvoid LoadGLTextures() { // Загрузка картинки AUX_RGBImageRec *texture1; texture1 = auxDIBImageLoad("Data/NeHe.bmp");
Сейчас мы загрузили изображение как данные компонент цветов красного, зеленного и синего, далее мы построим текстуру используя эти данные. Вызовом glGenTextures(1, &texture[0]) мы скажем OpenGL, что мы хотим построить текстуру в нулевом элементе массива texture[]. Помните, в начале урока мы зарезервировали место для одной текстуры с помощью GLuint texture[1]. Хотя вы, возможно, подумали, что мы сохраним текстуру в &texture[1], но это не так. Первая действительная область для сохранения имеет номер 0. Если вы хотите две текстуры, надо задать GLuint texture[2] и вторая текстура будет сохранена в texture[1].
Во второй строке вызов glBindTexture(GL_TEXTURE_2D, texture[0]) говорит OpenGL, что texture[0] (первая текстура) будет 2D текстурой. 2D текстуры имееют и высоту (по оси Y) и ширину (по оси X). Основная задача glGenTexture указать OpenGL на доступную память. В этом случае мы говорим OpenGL, что память доступна в &texture[0]. Затем мы создаем текстуру, и она будет сохранена в этой памяти. Далее, если мы привязываемся к памяти, в которой уже находиться текстура, мы говорим OpenGL захватить данные текстуры из этой области памяти. Обычно это указатель на доступную память, или память, в которой содержиться текстура.
// Создание текстуры glGenTextures(1, &texture[0]); glBindTexture(GL_TEXTURE_2D, texture[0]);
В следующих двух строках мы сообщим OpenGL какой тип фильтрации надо использовать, когда изображение больше на экране, чем оригинальная текстура (GL_TEXTURE_MAG_FILTER), или когда оно меньше на экране, чем текстура (GL_TEXTURE_MIN_FILTER). я обычно использую GL_LINEAR для обоих случаев. При этом текстура выглядит сглаженной на расстоянии, и вблизи. Использование GL_LINEAR требует много работы для процессора/видеокарты, поэтому если ваша система медленная, вы можете захотеть использовать GL_NEAREST. Текстура, которая фильтруется с GL_NEAREST состоит из хорошо видимых цветовых прямоугольников, когда она вблизи. Вы можете попробовать комбинировать оба способа. Сделайте одну фильтрацию вблизи, а другую вдали.
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
В завершении мы создаем фактическую текстуру. В следующей строке мы говорим OpenGL, что текстура будет двухмерной (GL_TEXTURE_2D). Ноль задает уровень детализации, это обычно ноль. Три - число компонент цветовых данных, так как изображение сделано из трех цветовых компонент (красный, зеленный, синий). texture1- >sizeX - это ширина текстуры, автоматически. Если вы знаете ширину, вы можете указать ее тут, но проще дать компьютеру сделать это за вас. texture1->sizeY - высота текстуры. Ноль - это бордюр. Он обычно остается нулем. GL_RGB сообщает OpenGL, что данные изображения представлены в порядке следования красных, зеленных и голубых компонент цвета. GL_UNSIGNED_BYTE означает, что данные из которых состоит изображение имеют размер байта и все числа без знака, и в конце texture1->data сообщает OpenGL, где брать сами данные. В этом случае указатель на данные в записи texture1.
glTexImage2D(GL_TEXTURE_2D, 0, 3, texture1->sizeX, texture1->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, texture1->data); }
Мы добавим две строчки в код InitGL. я повторно привожу эту секцию кода, для того чтобы было легко увидеть строчки, которые я добавил, и где они идут в коде.
В первой строке происходит вызов процедуры LoadGLTextures(), которая загружает изображение и делает из него текстуру. Вторая строка glEnable(GL_TEXTURE_2D) разрешает наложение текстуры. Если вы не делаете доступной наложение текстуры, ваш объект будет закрашен сплошным белым цветом, который точно не очень хорош.
GLvoid InitGL(GLsizei Width, GLsizei Height) { LoadGLTextures(); // Загрузка текстур glEnable(GL_TEXTURE_2D); // Разрешение наложение текстуры glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClearDepth(1.0); glDepthFunc(GL_LESS); glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH);
glMatrixMode(GL_PROJECTION); glLoadIdentity();
gluPerspective(45.0f,(GLfloat)Width/(GLfloat)Height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW); }
Сейчас мы нарисуем куб с текстурой. Мы можете заменить код DrawGLScene на код ниже, или вы можете добавить новый код в оригинальный код первого урока. Эта секция будет сильно прокомментирована, поэтому легка для понимания. Первые две строки кода glClear() и glLoadIdentity() взяты из оригинального кода первого урока. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) очищает экран цветом, который мы выбрали в InitGL(). В этом случае, экран будет очищен в синий цвет. Буфер глубины будет также очищен. Просмотр будет сброшен с помощью glLoadIdentity().
GLvoid DrawGLScene(GLvoid) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glTranslatef(0.0f,0.0f,-5.0f);
Следующие три строки кода будут вращать куб по оси X, затем по оси Y, и в конце по оси Z. Насколько велико будет вращение (угол) по каждой оси будет зависеть от значения указанного в xrot, yrot и zrot.
glRotatef(xrot,1.0f,0.0f,0.0f); // Вращение по оси X glRotatef(yrot,0.0f,1.0f,0.0f); // Вращение по оси Y glRotatef(zrot,0.0f,0.0f,1.0f); // Вращение по оси Z
В следующей строке кода происходит выбор какую текстуру мы хотим использовать для наложения текстуры. Если вы хотите использовать более чем одну текстуру в вашей сцене, вы должны выбрать текстуру glBindTexture(GL_TEXTURE_2D, texture[номер текстуры для использования]). Вы должны затем нарисовать несколько четырехугольников используя эту текстуру. Каждый раз, когда Вы захотите сменить текстуру, Вы должны привязать новую текстуру. Одно замечание: вы НЕ должны связывать текстуру внутри glBegin() и glEnd().
glBindTexture(GL_TEXTURE_2D, texture[0]);
Для того чтобы правильно отобразить текстуру на четырехугольник, вы должны отобразить правый верхний угол текстуры на правую верхнею вершину четырехугольника. Левый верхний угол текстуры отображается в левую верхнею вершину четырехугольника, правый нижний угол текстуры отображается в правую нижнею вершину четырехугольника, и в завершении, левый нижний угол текстуры отображается в левую нижнею вершину четырехугольника. Если углы текстуры не совпадают с углами четырехугольника, изображение может быть сдвинуто вниз, в сторону, или вообще отсутствовать.
Первый аргумент glTexCoord2f - координата X. 0.0f - левая сторона текстуры. 0.5f - середина текстуры, и 1.0f - правая сторона текстуры. Втрое значение glTexCoord2f - это Y координата. 0.0f - низ текстуры. 0.5f - середина текстуры, и 1.0f - верх текстуры.
Теперь мы знаем, что левая верхняя координата текстуры 0.0f по X и 1.0f по Y, и левая верхняя вершина четырехугольника -1.0f по X, и 1.0f по Y. Теперь осталось сделать так, чтобы оставшиеся три координаты совпали с тремя углами четырехугольника.
Попробуйте поиграться со значениями x и y в glTexCoord2f. Изменение 1.0f на 0.5f будет только рисовать левую половину текстуры от 0.5f (середина) до 1.0f (право).
glBegin(GL_QUADS);
// Передняя грань glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Низ лево glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Низ право glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Верх право glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Верх лево
// Задняя грань glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Низ право glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Верх право glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Верх лево glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Низ лево
// Верхняя грань glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Верх лево glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Низ лево glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Низ право glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Верх право
// Нижняя грань glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Верх право glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Верх лево glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Низ лево glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Низ право
// Правая грань glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Низ право glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Верх право glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Верх лево glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Низ лево
// Левая грань glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Низ лево glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Низ право glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Верх право glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Верх лево
glEnd();
Сейчас мы увеличим значения xrot, yrot и zrot. Попробуйте изменить значения каждой переменной, замедляя или ускоряя вращение куба, или изменяя ' +' на '-' заставляя куб вращаться в другом направлении.
xrot+=0.3f; // Ось вращения X yrot+=0.2f; // Ось вращения Y zrot+=0.4f; // Ось вращения Z }
Теперь Вы должны лучше понимать наложение текстуры. Вы научились накладывать текстуру на поверхность любого четырехугольника с изображением по вашему выбору. Как только вы лучше поймете наложение текстуры, попробуйте наложить на куб шесть разных текстур.
Наложение текстуры не трудно для понимания, если Вы понимаете координаты текстуры. Если Вы имеете проблемы с пониманием любой части этого урока, дайте мне знать. Или я изменю этот урок, и я отвечу Вам по почте. Развлекайтесь созданием наложения текстуры в Ваших сценах.
© Jeff Molofee (NeHe)
PMG
13 сентября 2001 (c)
Сергей Анисимов
d=document;rn=Math.random(); y=""; d.write(y);
Режимы фильтрации текстур, освещение и обработка клавиатуры
Texture Filters, Lighting & Keyboard Control
В этом уроке я научу вас, как использовать три разных режима фильтрации текстур. Я научу вас, как перемещать объект, используя клавиши на клавиатуре, и я также преподам вам, как применить простое освещение в вашей OpenGL сцене. В этом уроке много материала, поэтому, если предыдущие уроки вам непонятны, вернитесь и посмотрите их вновь. Важно иметь хорошее понимание основ прежде, чем Вы перепрыгнете на этот код.
Мы собираемся снова изменить код первого урока. Как обычно, если много меняется, я привожу полную секцию кода, который был изменен. Мы начнем, добавив несколько новых переменных к программе.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартного ввода/вывода (ДОБАВИЛИ)
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
HDC hDC=NULL; // Служебный контекст GDI устройства
HGLRC hRC=NULL; // Постоянный контекст для визуализации
HWND hWnd=NULL; // Содержит дискриптор для окна
HINSTANCE hInstance; // Содержит данные для нашей программы
bool keys[256]; // Массив, использующийся для сохранения состояния клавиатуры
bool active=TRUE; // Флаг состояния активности приложения (по умолчанию: TRUE)
bool fullscreen=TRUE; // Флаг полноэкранного режима (по умолчанию: полноэкранное)
Строки ниже новые. Мы собираемся добавлять три логических переменных. Тип BOOL означает, что переменная может только быть ИСТИННА (TRUE) или ЛОЖЬ (FALSE). Мы создаем переменную называемую light, чтобы отслеживать, действительно ли освещение включено или выключено. Переменные lp и fp используются, для отслеживания нажатия клавиш 'L' и 'F'. Я объясню, почему нам нужны эти переменные позже. Пока, запомните, что они необходимы.
BOOL light; // Свет ВКЛ / ВЫКЛ
BOOL lp; // L нажата?
BOOL fp; // F нажата?
Теперь нам нужны пять переменных, которые будут управлять следующими параметрами: углом по оси X (xrot), углом по оси Y (yrot), скоростью вращения ящика по оси X (xspeed), и скоростью вращения ящика по оси Y (yspeed). Мы также создадим переменную z, которая будет управлять, погружением ящика в экран (по оси Z).
GLfloat xrot; // X вращение
GLfloat yrot; // Y вращение
GLfloat xspeed; // X скорость вращения
GLfloat yspeed; // Y скорость вращения
GLfloat z=-5.0f; // Сдвиг вглубь экрана
Теперь мы зададим массивы, которые мы будем использовать для освещения. Мы будем использовать два разных типа света. Первый тип света называется фоновым светом. Фоновый свет не имеет никакого определенного направления. Все объекты в вашей сцене будут освещены фоновым светом. Второй тип света - диффузный свет. Диффузный свет создается при помощи вашего источника света и отражается от поверхности объекта в вашей сцене. Любая поверхность объекта, на которую падает прямо свет, будет очень яркой и области, куда свет падает под углом, будут темнее. Это создает хороший эффект оттенения на сторонах нашей корзины.
Свет создается так же как цвет. Если первое число - 1.0f, а следующие два - 0.0f, мы получим ярко красный свет. Если третье число - 1.0f, и первые два - 0.0f, мы будем иметь ярко синий свет. Последнее число - альфа значение. Мы оставим его пока 1.0f.
Поэтому в строке ниже, мы зададим значение белого фонового света половиной интенсивности (0.5f). Поскольку все числа - 0.5f, мы получим свет средней яркости между черным (выключен свет) и белым (полная яркость). Смешанные в равных значениях красный, синий и зеленый дадут оттенки от черного (0.0f) до белого (1.0f). Без фонового света пятна, где нет диффузного света, будут очень темными.
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f }; // Значения фонового света ( НОВОЕ )
В следующей строке мы зададим значение для супер-яркой, полной интенсивности диффузного света. Все значения - 1.0f. Это означает, что свет настолько яркий, насколько мы можем получить его. Диффузный свет эти яркое пятно спереди ящика.
GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; // Значения диффузного света ( НОВОЕ )
Наконец мы зададим позицию света. Первые три числа совпадают с тремя первыми аргументами функции glTranslate. Первое число смещение влево или вправо по плоскости x, второе число - для перемещения вверх или вниз по плоскости y, и третье число для перемещения к или от экрана по плоскости z. Поскольку мы хотим чтобы наш свет, падал прямо на переднею часть ящика, мы не сдвигаемся влево или вправо, поэтому первое значение - 0.0f (нет движения по x), мы не хотим двигаться вверх или вниз, поэтому второе значение - 0.0f. Третье значение мы зададим так, чтобы свет был всегда перед ящиком. Поэтому мы поместим свет вне экрана по отношению к наблюдателю. Давайте примем, что стекло на вашем мониторе - 0.0f по плоскости z. Мы позиционируем свет в 2.0f по плоскости z. Если бы Вы могли бы фактически видеть свет, он бы плавал перед стеклом вашего монитора. Делая, таким образом, единственный способ, когда бы свет оказался позади ящика, был бы тот, если бы ящик был также перед стеклом вашего монитора. Конечно, если бы ящик был уже не позади стекла вашего монитора, Вы больше не видели ящик, поэтому тогда не имеет значения, где свет. Это имеет смысл?
Нет никакого простого реального способа разъяснить третий параметр. Вы должны понять, что -2.0f ближе к Вам чем -5.0f и что -100.0f ДАЛЕКО в экране. Как только Вы берете 0.0f, изображение становится настолько большим, это заполняет весь монитор. Как только Вы устанавливаете положительные значения, изображение больше не появляется на экране по той причине, что объект "пропал с экрана". Вот это я подразумеваю, когда я говорю вне экрана. Объект - все еще есть, но Вы уже не можете больше его видеть.
Оставьте последнее число в 1.0f. Это говорит OpenGL о том, что данные координаты - позиция источника света. Более подробно об этом я расскажу в одном из следующих уроков.
GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f }; // Позиция света ( НОВОЕ )
Переменная filter должна отслеживать, каким образом будут отображаться текстуры. Первая текстура (texture[0]) использует gl_nearest (без сглаживания). Вторая текстура (texture[1]) использует фильтрацию gl_linear, которая немного сглаживает изображение. Третья текстура (texture[2]) использует текстуры с мип-наложением (mipmapped, или множественное наложение), что повышает качество отображения. Переменная filter будет равняться 0, 1 или 2 в зависимости от текстуры, которую мы хотим использовать. Мы начинаем с первой текстуры.
Объявление GLuint texture[3] создает место для хранения трех разных текстур. Текстуры будут сохранены в texture[0], texture[1] и texture[2].
GLuint filter; // Какой фильтр использовать
GLuint texture[3]; // Место для хранения 3 текстур
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Декларация WndProc
Сейчас мы загрузим картинку (bitmap, или растровый (побитный) образ изображения), и создадим три различных текстуры из нее. В этом уроке используется библиотека glaux для загрузки картинки, поэтому проверьте, что Вы подключили библиотеку glaux прежде, чем Вы пробуете скомпилировать код. Я знаю, что Delphi, и Visual C++ имеют библиотеку glaux. Я не уверен, что она есть в других языках. Я собираюсь объяснить только новые строки кода, если Вы видите не прокомментированную строку, и Вы не понимаете, что она делает, посмотрите шестой урок. Там объясняется загрузка, и формирования образов текстуры из картинок очень подробно.
Сразу же после кода выше, и до ReSizeGLScene (), мы добавим следующую секцию кода. Это - тот же самый код, который мы использовали в уроке 6 для загрузки картинки. Ничего не изменилось. Если Вы не понимаете их, прочитайте шестой урок. Там этот код объяснен подробнее.
AUX_RGBImageRec *LoadBMP(char *Filename) // Загрузка картинки
{
FILE *File=NULL; // Индекс файла
if (!Filename) // Проверка имени файла
{
return NULL; // Если нет вернем NULL
}
File=fopen(Filename,"r"); // Проверим существует ли файл
if (File) // Файл существует?
{
fclose(File); // Закрыть файл
return auxDIBImageLoad(Filename); // Загрузка картинки и вернем на нее указатель
}
return NULL; // Если загрузка не удалась вернем NULL
}
В этой секции кода загружается картинка (вызов кода выше) и производится конвертирование ее в 3 текстуры. Переменная Status используется, чтобы следить, действительно ли текстура была загружена и создана.
int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру
{
int Status=FALSE; // Индикатор состояния
AUX_RGBImageRec *TextureImage[1]; // Создать место для текстуры
memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL
Теперь мы загружаем картинку и конвертируем ее в текстуру. Выражение TextureImage[0]=LoadBMP ("Data/Crate.bmp ") будет вызывать наш код LoadBMP(). Файл по имени Crate.bmp в каталоге Data будет загружен. Если все пройдет хорошо, данные изображения сохранены в TextureImage[0], Переменная Status установлена в TRUE, и мы начинаем строить нашу текстуру.
// Загрузка картинки, проверка на ошибки, если картинка не найдена - выход
if (TextureImage[0]=LoadBMP("Data/Crate.bmp"))
{
Status=TRUE; // Установим Status в TRUE
Теперь, мы загрузили данные изображения в TextureImage [0], мы будем использовать эти данные для построения 3 текстур. Строка ниже сообщает OpenGL, что мы хотим построить три текстуры, и мы хотим, чтобы текстура была сохранена в texture[0], texture[1] и texture[2].
glGenTextures(3, &texture[0]); // Создание трех текстур
В шестом уроке, мы использовали линейную фильтрацию образов текстур. Это способ фильтрации требует много мощности процессора, но текстуры при этом выглядят реалистичными. Первый тип текстуры, которую мы собираемся создать в этом уроке, использует GL_NEAREST. Этот тип текстуры не использует фильтрацию. Требуется очень мало мощности процессора, и качество плохое. Если вы когда-нибудь видели игру, где текстуры как будто состоят из блоков, они, вероятно, используют этот тип текстуры. Единственное применение этого типа текстуры для проектов, которые будут запускаться на медленных компьютерах.
Заметьте, что мы используем GL_NEAREST, и для MIN и для MAG. Вы можете смешивать использование GL_NEAREST с GL_LINEAR, и текстура будет смотреться немного лучше, но мы заинтересованы в быстродействии, поэтому мы будем использовать везде низкое качество. Фильтр MIN_FILTER используется, когда изображение рисуемого полигона меньше, чем первоначальный размер текстуры. Фильтр MAG_FILTER используется, когда изображение рисуемого полигона больше, чем первоначальный размер текстуры.
// Создание текстуры с фильтром по соседним пикселям
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); // ( НОВОЕ )
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); // ( НОВОЕ )
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
Следующая текстура, которую мы построим, имеет тот же самый тип текстуры, которую мы использовали в уроке шесть. Линейный режим фильтрации. Изменилось только то, что мы сохраняем эту текстуру в texture[1] вместо texture[0], потому что здесь она вторая текстура. Если бы мы сохранили ее в texture[0] как ранее, она затерла бы текстуру GL_NEAREST (первая текстура).
// Создание текстуры с линейной фильтрацией
glBindTexture(GL_TEXTURE_2D, texture[1]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
Теперь новый способ создания текстуры. Мип-наложение! Вы, возможно, замечали, что, когда изображение на экране очень маленькое, пропадает множество мелких деталей. Орнаменты, которые выглядят вначале отлично, смотрятся отвратительно. Когда Вы говорите OpenGL построить текстуру с мип-наложением, OpenGL будет строить разного размера высококачественные текстуры. Когда Вы выводите текстуру с мип-наложением на экран, OpenGL будет выбирать НАИБОЛЕЕ ЛУЧШУЮ для отображения текстуру из построенных текстур (текстура с таким размером, как размер полигона на экране, т.е. наиболее детальная) и нарисует ее на экран вместо масштабирования первоначального изображения (что и вызывает потерю качества).
Я рассказал в уроке шесть про ограничение на размер текстур в OpenGL по ширине и высоте текстуры в 64,128,256, и т.д. Для gluBuild2DMipmaps Вы можете использовать картинки любых размеров при формировании текстуры с мип-наложением. OpenGL будет автоматически изменять размер ее задавая правильные ширину и высоту.
Поскольку это - текстура номер три, мы собираемся хранить эту текстуру в texture[2]. Поэтому, теперь мы имеем texture[0], которая не имеет никакой фильтрации, texture[1], которая использует линейную фильтрацию, и texture[2], которая использует текстуру с мин-наложением. Мы закончили построение текстур в этом уроке.
// Создание Текстуры с Мип-Наложением
glBindTexture(GL_TEXTURE_2D, texture[2]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST); // ( НОВОЕ )
Следующая строка строит текстуру с мип-наложением. Мы создаем 2D текстуру, используя три цвета (красный, зеленый, синий) (red, green, blue). TextureImage[0]->sizeX - ширина картинки, TextureImage[0]->sizeY - высота картинки, GL_RGB означает, что мы используем цвета в порядке Красный, Зеленый, Синий (Red, Green, Blue). GL_UNSIGNED_BYTE означает что данные, из которых состоит текстура из байтов, и TextureImage[0]->data указатель на растр картинки, из которого мы строим текстуру.
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); // ( НОВОЕ )
}
Теперь мы можем освободить память, которую мы использовали для картинок. Мы проверяем, была ли картинка сохранена в TextureImage[0]. Если да, то мы проверяем, были ли данные сохранены. Если данные были сохранены, мы удаляем их. Тогда мы освобождаем структуру изображения, уверенные, что любая используемая память освобождена.
if (TextureImage[0]) // Если текстура существует
{
if (TextureImage[0]->data) // Если изображение текстуры существует
{
free(TextureImage[0]->data); // Освобождение памяти изображения текстуры
}
free(TextureImage[0]); // Освобождение памяти под структуру
}
Наконец мы возвращаем статус. Если все прошло OK, переменная Status будет TRUE. Если что-нибудь прошло не так, как надо, Status будет FALSE.
return Status; // Возвращаем статус
}
Теперь мы загружаем текстуры, и инициализируем параметры настройки OpenGL. В первой строке InitGL загружаются текстуры, используя код выше. После того, как текстуры созданы, мы разрешаем 2D наложение текстуры с помощью glEnable (GL_TEXTURE_2D). Режим закрашивания (shade) задается как сглаженное закрашивание. Цвет фона задан как черный, мы разрешаем тест глубины, затем мы разрешаем хорошие перспективные вычисления.
int InitGL(GLvoid) // Все настройки для OpenGL делаются здесь
{
if (!LoadGLTextures()) // Переход на процедуру загрузки текстуры
{
return FALSE; // Если текстура не загружена возвращаем FALSE
}
glEnable(GL_TEXTURE_2D); // Разрешить наложение текстуры
glShadeModel(GL_SMOOTH); // Разрешение сглаженного закрашивания
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Установка буфера глубины
glEnable(GL_DEPTH_TEST); // Разрешить тест глубины
glDepthFunc(GL_LEQUAL); // Тип теста глубины
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Улучшенные вычисления перспективы
Теперь мы задаем освещение. Строка ниже задает интенсивность фонового света, которое light1 будет давать. В начале этого урока мы задали интенсивность фонового света в LightAmbient. Значения, которые мы поместили в массив, будут использованы (фоновый свет половиной интенсивности).
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); // Установка Фонового Света
Затем мы задаем интенсивность диффузного света, который источник света номер один будет давать. Мы задали интенсивность диффузного света в LightDiffuse. Значения, которые мы поместили в этот массив, будут использованы (белый свет полной интенсивности).
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); // Установка Диффузного Света
Теперь мы задаем позицию источника света. Мы поместили позицию в LightPosition. Значения, которые мы поместили в этот массив, будут использованы (справо в центре передней грани, 0.0f по x, 0.0f по y, и 2 единицы вперед к наблюдателю {выходит за экран} по плоскости z).
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); // Позиция света
Наконец, мы разрешаем источник света номер один. Мы не разрешили GL_LIGHTING, поэтому Вы еще не увидите никакого освещения. Свет установлен, и позиционирован, и даже разрешен, но пока мы не разрешили GL_LIGHTING, освещение не будет работать.
glEnable(GL_LIGHT1); // Разрешение источника света номер один
return TRUE; // Инициализация прошла OK
}
В следующей секции кода, мы нарисуем текстуру, наложенную на куб. Я буду комментировать только несколько строк, потому что они новые. Если Вы не понимаете не прокомментированные строки, вернитесь к уроку номер шесть.
int DrawGLScene(GLvoid) // Здесь мы делаем все рисование
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка Экрана и Буфера Глубины
glLoadIdentity(); // Сброс Просмотра
Следующих трех строках кода куб с наложенной текстурой позиционируется и вращается. Вызов glTranslatef (0.0f, 0.0f, z) перемещает куб на значение z по плоскости z (от наблюдателя или к наблюдателю). Вызов glRotatef (xrot, 1.0f, 0.0f, 0.0f) использует переменную xrot, чтобы вращать куб по оси X. Вызов glRotatef (yrot, 1.0f, 0.0f, 0.0f) использует переменную yrot, чтобы вращать куб по оси Y.
glTranslatef(0.0f,0.0f,z); // Перенос В/Вне экрана по z
glRotatef(xrot,1.0f,0.0f,0.0f); // Вращение по оси X на xrot
glRotatef(yrot,0.0f,1.0f,0.0f); // Вращение по оси Y по yrot
Следующая строка подобна строке, которую мы использовали в уроке шесть, но вместо связывания к texture[0], мы привязываем texture[filter]. Если мы нажимаем клавишу 'F', значение в filter увеличится. Если значение больше чем два, переменная filter сбрасывается в ноль. Когда программа запускается, filter будет установлена в ноль. Это все равно, что glBindTexture (GL_TEXTURE_2D, texture[0]). Если мы нажимаем "F" еще раз, переменная filter будет равна единице, что фактически glBindTexture (GL_TEXTURE_2D, texture[1]). Используя, переменную filter мы можем выбирать любую из трех текстур, которые мы создали.
glBindTexture(GL_TEXTURE_2D, texture[filter]); // Выбор текстуры основываясь на filter
glBegin(GL_QUADS); // Начало рисования четырехугольников
glNormal3f новая функция в моих уроках. Нормаль - линия, берущая начало из середины полигона под 90 углом градусов. Когда Вы используете освещение, Вы должны задать нормали. Нормаль сообщает OpenGL, в каком направлении у полигона лицевая часть, какое направление верхнее. Если Вы не задали нормали, могут происходить сверхъестественные вещи. Грань, которая не освещена, будет освещена, неверная сторона полигона будет освещена, и т.д. Нормаль должна указывать вовне полигона.
Посмотрев на переднюю грань, Вы заметите, что нормаль имеет положительное направление по оси Z. Это означает, что нормаль указывает на наблюдателя. Это точно, то направление, которое мы хотим указать. На обратной грани, нормаль указывает от наблюдателя вглубь экрана. Снова это точно то, что мы хотим. Если куб повернут на 180 градусов или по оси X или по оси Y, передняя грань ящика будет лицом вглубь экрана, и задняя грань ящика будет лицом к наблюдателю. Независимо от того, какую грань видит наблюдатель, нормаль этой грани будет также направлена на наблюдателя. Поскольку свет - близко к наблюдателю всегда нормаль, указывающая на наблюдателя, также указывает на свет. Если это сделать, то грань будет освещена. Чем больше нормалей указывает на свет, тем более яркая будет грань. Если Вы переместились в центр куба, Вы заметите, что там темно. Нормали – указывают вовне, а не во внутрь, поэтому нет освещения внутри куба.
// Передняя грань
glNormal3f( 0.0f, 0.0f, 1.0f); // Нормаль указывает на наблюдателя
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Точка 1 (Перед)
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Точка 2 (Перед)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Точка 3 (Перед)
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Точка 4 (Перед)
// Задняя грань
glNormal3f( 0.0f, 0.0f,-1.0f); // Нормаль указывает от наблюдателя
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Точка 1 (Зад)
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Точка 2 (Зад)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Точка 3 (Зад)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Точка 4 (Зад)
// Верхняя грань
glNormal3f( 0.0f, 1.0f, 0.0f); // Нормаль указывает вверх
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Точка 1 (Верх)
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Точка 2 (Верх)
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Точка 3 (Верх)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Точка 4 (Верх)
// Нижняя грань
glNormal3f( 0.0f,-1.0f, 0.0f); // Нормаль указывает вниз
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Точка 1 (Низ)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Точка 2 (Низ)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Точка 3 (Низ)
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Точка 4 (Низ)
// Правая грань
glNormal3f( 1.0f, 0.0f, 0.0f); // Нормаль указывает вправо
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Точка 1 (Право)
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Точка 2 (Право)
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Точка 3 (Право)
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Точка 4 (Право)
// Левая грань
glNormal3f(-1.0f, 0.0f, 0.0f); // Нормаль указывает влево
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Точка 1 (Лево)
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Точка 2 (Лево)
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Точка 3 (Лево)
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Точка 4 (Лево)
glEnd(); // Кончили рисовать четырехугольник
В следующих двух строках увеличиваются значения xrot и yrot на значения, сохраненные в xspeed, и yspeed. Если значение в xspeed или yspeed большое, xrot и yrot увеличивается быстро. Чем быстрее увеличение xrot, или yrot, тем быстрее куб вращается по соответствующей оси.
xrot+=xspeed; // Добавить в xspeed значение xrot
yrot+=yspeed; // Добавить в yspeed значение yrot
return TRUE; // Выйти
}
Теперь мы спускаемся до WinMain (). Здесь добавим код для включения или выключения освещения, вращения корзины, смены фильтра и перемещения ящика ближе или дальше от экрана. Ближе к концу WinMain () Вы увидите вызов SwapBuffers (hDC). Сразу после этой строки, добавьте следующий код.
Этот код отслеживает нажатие клавиши 'L'. В первой строке проверяется, нажата ли 'L'. Если 'L' нажата, но lp - не ложь, что значит клавиша 'L' уже была нажата, или она удерживается нажатой, то тогда ничего не происходит.
SwapBuffers(hDC); // Переключение буферов (Двойная буферизация)
if (keys['L'] && !lp) // Клавиша 'L' нажата и не удерживается?
{
Если lp – ложь то, это означает, что клавиша 'L' не нажата, иначе, если она уже отпущена, lp - истина. Это гарантирует, что клавишу 'L' отпустят прежде, чем этот код выполнится снова. Если мы не будет проверять удержание клавиши, освещение будет мерцать, постоянно включаясь и выключаясь много раз, поскольку программа думала бы, что Вы нажимает клавишу 'L' снова и снова, при этом вызывая этот раздел кода.
Переменная lp будучи равной истине, сообщает, что 'L' отпущена, мы включаем или выключаем освещение. Переменная light может только быть истина или ложь. Поэтому, если мы говорим light=!light, то мы фактически говорим, что свет, не равен свету. По-русски это будет значит, что, если light равена истине, то light не будет истина (ложь), и если light равена ложь, то light не будет ложь (истина). Поэтому, если переменная light была истина, она станет ложь, и если light была ложь, она станет истина.
lp=TRUE; // lp присвоили TRUE
light=!light; // Переключение света TRUE/FALSE
Теперь мы проверим, какое значение light получилось в конце. Первая строка, переведенная на русский означает: если light равняется ложь. Поэтому, если Вы совместите все строки вместе, то они делают следующее: если light равняется ложь, то надо запретить освещение. Это выключает все освещение. Команда 'else' означает: если light не ложь. Поэтому, если light не была ложь то, она истинна, поэтому мы включаем освещение.
if (!light) // Если не свет
{
glDisable(GL_LIGHTING); // Запрет освещения
}
else // В противном случае
{
glEnable(GL_LIGHTING); // Разрешить освещение
}
}
Следующая строка отслеживает отжатие клавиши 'L'. Если мы присвоили переменной lp значение ложь, то это, означает, что клавиша 'L' не нажата. Если мы не будем отслеживать отжатие клавиши, мы могли бы включить освещение, но поскольку компьютер считал бы, что 'L' нажата, поэтому он не позволит нам выключить освещение.
if (!keys['L']) // Клавиша 'L' Отжата?
{
lp=FALSE; // Если так, то lp равно FALSE
}
Теперь мы сделаем что-то подобное с клавишей 'F'. Если клавиша нажата, и она не удерживается, или она не была нажата до этого, тогда присвоим значение переменной fp равное истине, что значит клавиша 'F' нажата и удерживается. При этом увеличится значение переменной filter. Если filter больше чем 2 (т.е. texture[3], а такой текстуры не существует), мы сбрасываем значение переменной texture назад в ноль.
if (keys['F'] && !fp) // Клавиша 'F' нажата?
{
fp=TRUE; // fp равно TRUE
filter+=1; // значение filter увеличивается на один
if (filter>2) // Значение больше чем 2?
{
filter=0; // Если так, то установим filter в 0
}
}
if (!keys['F']) // Клавиша 'F' отжата?
{
fp=FALSE; // Если так, то fp равно FALSE
}
В следующих четырех строках проверяем, нажали ли мы клавишу 'Page up'. Если так, то уменьшим значение переменной z. Если эта переменная уменьшается, куб будет двигаться вглубь экрана, поскольку мы используем glTranslatef (0.0f, 0.0f, z) в процедуре DrawGLScene.
if (keys[VK_PRIOR]) // Клавиша 'Page Up' нажата?
{
z-=0.02f; // Если так, то сдвинем вглубь экрана
}
В этих четырех строках проверяется, нажали ли мы клавишу 'Page down'. Если так, то увеличим значение переменной z и сместим куб к наблюдателю, поскольку мы используем glTranslatef (0.0f, 0.0f, z) в процедуре DrawGLScene.
if (keys[VK_NEXT]) // Клавиша 'Page Down' нажата?
{
z+=0.02f; // Если так, то придвинем к наблюдателю
}
Теперь все, что мы должны проверить - клавиши курсора. Нажимая влево или вправо, xspeed увеличивается или уменьшается. Нажимая вверх или вниз, yspeed увеличивается или уменьшается. Помните выше в этом уроке, я говорил, что, если значения xspeed или yspeed большие, куб вращается быстрее. Чем дольше Вы удерживаете одну из клавиш курсора, тем, быстрее куб будет вращаться в соответствующем направлении.
if (keys[VK_UP]) // Клавиша стрелка вверх нажата?
{
xspeed-=0.01f; // Если так, то уменьшим xspeed
}
if (keys[VK_DOWN]) // Клавиша стрелка вниз нажата?
{
xspeed+=0.01f; // Если так, то увеличим xspeed
}
if (keys[VK_RIGHT]) // Клавиша стрелка вправо нажата?
{
yspeed+=0.01f; // Если так, то увеличим yspeed
}
if (keys[VK_LEFT]) // Клавиша стрелка влево нажата?
{
yspeed-=0.01f; // Если так, то уменьшим yspeed
}
Как и во всех предыдущих уроках, удостоверитесь, что заголовок наверху окна правильный.
if (keys[VK_F1]) // Клавиша 'F1' нажата?
{
keys[VK_F1]=FALSE; // Если так, то сделаем Key FALSE
KillGLWindow(); // Уничтожим наше текущее окно
fullscreen=!fullscreen; // Переключение между режимами Полноэкранный/Оконный
// Повторное создание нашего окна OpenGL
if (!CreateGLWindow("Урок NeHe Текстуры, Свет & Обработка Клавиатуры",640,480,16,fullscreen))
{
return 0; // Выход, если окно не создано
}
}
}
}
}
// Сброс
KillGLWindow(); // Уничтожение окна
return (msg.wParam); // Выход из программы
}
После освоения этого урока Вы должны уметь создавать и оперировать объектами из четырехугольников с высококачественным, реалистичным наложением текстур. Вы должны понять преимущества каждого из трех фильтров, используемых в этом уроке. Нажимая определенные клавиши на клавиатуре Вы сможете взаимодействовать с объектом на экране, и наконец, Вы должны знать, как применить простое освещение на сцене, делая сцену более реалистичной.
© Jeff Molofee (NeHe)
PMG
22 апреля 2002 (c) Сергей Анисимов
d=document;rn=Math.random(); y=""; d.write(y);
Смешивание
Blending
Простая прозрачность
Большинство специальных эффектов в OpenGL основано на так называемом смешивании (blending). Смешивание используется, чтобы комбинировать цвет данного пикселя, который должен быть выведен с пикселем, уже находящемся на экране. Как будут объединены цвета, будет зависеть от альфа-канала, и/или функции смешивания, которая задается. Альфа - 4-й компонент цвета, который обычно указывается в конце. В прошлом Вы использовали GL_RGB, чтобы определить цвет с тремя компонентами. GL_RGBA может использоваться, чтобы также определить альфа-канал. Теперь мы можем использовать glColor4f() вместо glColor3f().
Большинство людей думает об альфе, как о непрозрачности материала. Альфа- значение 0.0 подразумевает, что материал полностью прозрачен. Значение 1.0 означает полную непрозрачность.
Уравнение смешивания
Если Вы не дружите с математикой, и только хотите уметь просто использовать эффект прозрачности, то пропустите этот раздел. Если Вы хотите понять, как работает смешивание, этот раздел - для Вас.
(Rs Sr + Rd Dr, Gs Sg + Gd Dg, Bs Sb + Bd Db, As Sa + Ad Da)
OpenGL вычислит результат смешивания двух пикселей, основанный на уравнении выше. Буквы s и r в нижнем регистре задают пиксели источника и приемника. S и D компоненты – коэффициенты смешивания (R,G,B,A – составляющие цвета). Их значения указывают, как бы Вы хотели смешать пиксели. Обычные значения для S и D: (As, As, As, As) (так называемые альфа источника (source alpha)) для S и (1, 1, 1, 1) - (As, As, As, As) (один минус альфа источника) для D. Отсюда уравнение смешивания будет выглядеть следующим образом:
(Rs As + Rd (1 - As), Gs As + Gd (1 - As), Bs As + Bs (1 - As), As As + Ad (1 - As))
Это уравнение определит стиль прозрачности/полу прозрачности.
Смешивание в OpenGL
Мы включаем эффект смешивания так же как что-либо другое. Затем мы задаем уравнение и выключаем буфер глубины на запись при рисовании прозрачных объектов, поскольку мы хотим увидеть объекты позади нарисованных полупрозрачных фигур. Это не идеальный способ смешивания, но в нашем простом проекте он будет работать достаточно хорошо.
Комментарий Rui Martins: более правильный путь состоит в том, чтобы вывести все прозрачные (с альфой < 1.0) полигоны после того, как Вы вывели полную сцену, и выводить их в обратном порядке глубины (сначала – дальние).
Это оттого, что смешивание двух полигонов (1 и 2) в различном порядке дает различные результаты, то есть если полигон 1 - самый близкий к наблюдателю, то нужно вывести полигон 2 и затем 1. Если провести аналогию с реальностью, то свет, проходящий сквозь эти два полигона (которые являются прозрачными), должен пройти сначала 2-й полигон и затем полигон 1 до того как достигнет глаза наблюдателя.
Вы должны СОРТИРОВАТЬ ПРОЗРАЧНЫЕ ПОЛИГОНЫ ПО ГЛУБИНЕ и выводить их ПОСЛЕ ТОГО, КАК БЫЛА ОТРИСОВАНА ПОЛНАЯ СЦЕНА с ВЫКЛЮЧЕННЫМ БУФЕРОМ ГЛУБИНЫ, или получится неправильно. Я знаю, что это иногда трудно, но это - правильный способ делать это.
Мы будем использовать код от урока семь. Мы начинаем, прибавляя две новых переменных в начало кода. Я приведу повторно полный раздел кода для ясности.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартного ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
HDC hDC=NULL; // приватный контекст устройства GDI
HGLRC hRC=NULL; // постоянный контекст рендеринга
HWND hWnd=NULL; // содержит дискриптор нашего окна
HINSTANCE hInstance; // содержит экземпляр нашего приложения
bool keys[256]; // массив для процедуры обработки клавиатуры
bool active=TRUE; // флаг активности окна, по умолчанию TRUE
bool fullscreen=TRUE; // флаг полноэкранного режима, по умолчанию полный экран
bool blend; // Смешивание НЕТ/ДА? (НОВОЕ)
bool light; // Освещение Вкл./Выкл.
bool lp; // L Нажата?
bool fp; // F Нажата?
bool bp; // B Нажата? ( Новое )
GLfloat xrot; // Вращение вдоль оси X
GLfloat yrot; // Вращение вдоль оси Y
GLfloat xspeed; // Скорость вращения вдоль оси X
GLfloat yspeed; // Скорость вращения вдоль оси X
GLfloat z=-5.0f; // Глубина в экран.
// Задаем параметры освещения
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };
GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat LightPosition[]={ 0.0f, 0.0f, 2.0f, 1.0f };
GLuint filter; // Используемый фильтр для текстур
GLuint texture[3]; // Хранит 3 текстуры
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);// Объявление для WndProc
Спускаемся вниз до LoadGLTextures(). Найдите строку, которая говорит texture1 = auxDIBImageLoad ("Data/crate.bmp"), измените ее на строку ниже. Мы используем текстуру окрашенного стекла для этого урока вместо текстуры ящика.
// Загрузка текстуры стекла (МОДИФИЦИРОВАННО)
texture1 = auxDIBImageLoad("Data/glass.bmp");
Прибавьте следующие две строки где-нибудь в InitGL(). Первая строка задает яркость для отрисовки объекта, равную полной яркости с альфой 50 % (непрозрачность). Это означает, когда включается смешивание, объект будет на 50% прозрачный. Вторая строка задает тип смешивания.
Комментарий Rui Martins: альфа, равное 0.0 подразумевало бы, что материал полностью прозрачен. Значение 1.0 было бы сиречь полностью непрозрачно.
glColor4f(1.0f,1.0f,1.0f,0.5f); // Полная яркость, 50% альфа (НОВОЕ)
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Функция смешивания для непрозрачности,
// базирующаяся на значении альфы(НОВОЕ)
Найдите следующий раздел кода, он должен быть в самом конце урока семь.
if (keys[VK_LEFT]) // Нажата левая стрелка?
{
yspeed-=0.01f; // уменьшаем скорость
}
Под вышеупомянутым кодом мы прибавляем следующие строки. Отслеживаем нажатие ‘B’. Если она была нажато, компьютер проверит, включено ли смешивание. Если смешивание задано, компьютер выключает его. И наоборот, если смешивание выключено, включает его.
if (keys['B'] && !bp)
{
bp=TRUE;
blend = !blend; // Инвертируем blend
if(blend) // blend TRUE?
{
glEnable(GL_BLEND); // Включаем смешивание
glDisable(GL_DEPTH_TEST); // Выключаем тест глубины
}
else
{
glDisable(GL_BLEND); // Выключаем смешивание
glEnable(GL_DEPTH_TEST); // Включаем тест глубины
}
}
if (!keys['B']) // ’B’ отжата?
{
bp=FALSE; // тогда bp возвращает ложь
}
Но как мы можем определить цвет, если используем картинку текстуры? Просто, в режиме модулирования текстуры, каждый пиксель, который отображен из текстуры умножается на текущий цвет. Поэтому, если цвет для отображения (0.5, 0.6, 0.4), мы умножаем его на цвет и получаем (0.5, 0.6, 0.4, 0.2) (альфа принимается равной 1.0, если не задается явно).
Отлично! Смешивание, действительно просто сделать в OpenGL.
Примечание от 13.11.99
Я (NeHe) модифицировал код смешивания, поэтому выводимые объекты выглядят лучше. Использование альфа значений для источника и приемника приводит к появлению артефактов при смешивании. От того, что задние грани выглядят более темными, чем боковые грани. От этого объект будет выглядеть очень странно. Способ, с помощью которого я делаю смешивание, может быть не лучший, но он работает, и объекты выглядят хорошо, даже когда включено освещение. Спасибо Тому за начальный код, способ, с помощью которого он смешивал , был правильный способ для смешивания с альфа значениями, но не выглядел привлекательно и как ожидают этого люди ;).
Код был модифицирован еще раз, для того чтобы исключить проблему, которая возникала при использовании glDepthMask(). Это команда не эффективно разрешала и запрещала тест буфера глубины на некоторых видеокартах, поэтому я восстановил старый код с glEnable и glDisable теста глубины.
Альфа из картинки текстуры
Альфа значение, которое используется для прозрачности, может быть получено из картинки текстуры точно также как цвет, для того чтобы сделать это, вы должны извлечь альфа из изображения, которое Вы хотите загрузить, и затем использовать GL_RGBA для формата цвета при вызове glTexImage2D().
Вопросы?
Если у Вас есть вопросы, не стесняйтесь послать их по адресу stanis@cs.wisc.edu <mailto:stanis@cs.wisc.edu>.
© Tom Stanis
PMG
27 февраля 2002 (c) Vlad Tushevskij
d=document;rn=Math.random(); y=""; d.write(y);
Передвижение изображений в 3D
Moving Bitmaps In 3D Space
Добро пожаловать на 9-й урок. На данный момент вы должны уже хорошо понимать суть OpenGL. Вы уже научились всему, начиная от создания окна и установки контекста OpenGL, до текстурирования вращающихся объектов с использованием освещения и смешивания (blending). Этот урок будет первым из серии "продвинутых" уроков. Вы научитесь следующему: перемещать изображение (bitmap) по экрану в 3D, удаляя, черные пикселы (pixels) из изображения (используя смешивание), дополнять цветность в черно-белые текстуры и, наконец, узнаете, как создавать красивые цвета и простую анимацию путём смешивания различных цветных текстур вместе.
Мы изменим код из первого урока. И начнем с добавления некоторых новых переменных в начале программы. Я переписал начальную секцию программы, для того чтобы было легче определять, где произведены изменения.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартного ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
HDC hDC=NULL; // Служебный контекст GDI устройства
HGLRC hRC=NULL; // Постоянный контекст для визуализации
HWND hWnd=NULL; // Содержит дискриптор для окна
HINSTANCE hInstance; // Содержит данные для нашей программы
bool keys[256]; // Массив, использующийся для сохранения состояния клавиатуры
bool active=TRUE; // Флаг состояния активности приложения (по умолчанию: TRUE)
bool fullscreen=TRUE;// Флаг полноэкранного режима (по умолчанию: полноэкранное)
Следующие строчки новые. twinkle и tp логические переменные, которые могут быть TRUE (истина) или FALSE (ложь). twinkle будет говорить о включении/выключении эффекта twinkle. tp используется для определения состояния клавиши 'T' (была ли нажата или нет). Если нажата, то tp=TRUE, иначе tp=FALSE.
BOOL twinkle; // Twinkling Stars (Вращающиеся звезды)
BOOL tp; // 'T' клавиша нажата?
num переменная, хранит информацию о количестве звезд, которые мы рисуем на экране. Она определена как константа. Это значит, в коде программы мы не может поменять её значение. Причина, по которой мы определяем её как константу, в том, что мы не можем переопределить (увеличить/уменьшить) массив. Так или иначе, если мы задаём массив только на 50 звезд и хотим увеличить num до 51 звезды где-нибудь в программе, то массив не сможет увеличиться, и выдаст ошибку. Вы можете изменить num только в этой строчке программы. И не пытайтесь изменить значение num где-то в другом месте, если вы не хотите, чтобы случилось страшное :).
const num=50; // Количество рисуемых звезд
Сейчас мы создадим структуру (structure). Слово структура звучит глобально, но это не так на самом деле. Структура это совокупность простых данных (переменных, и т.д.) сгрупированых по какому-либо признаку в одну группу. Мы знаем, что мы будем хранить цепочку звезд. Вы увидите, что 7-ая строчка ниже это stars;. Мы знаем, что каждая звезда имеет 3 значения для цвета, и все эти значения целые числа: 3-я строчка: int r,g,b задаёт эти значения. Одно для красного (red) (r), одно для зелёного (green) (g), и одно для голубого (blue) (b). Мы знаем, что каждая звезда будет иметь разное расстояние от центра экрана, и расположена на одном из 360 углов от центра. Если вы посмотрите на 4-ую строчку ниже, вы увидите это. Мы создаём переменную типа число с плавающей точкой (floating point value) называется dist. Она означает расстояние. 5-ая строчка создаёт переменную того же типа с именем angle. Она будет отвечать за угол звезды.
И так мы имеем группу данных, которая содержит цвет, расстояние и угол звезды на экране. К сожалению, у нас больше чем одна звезда, и вместо того чтобы создавать 50 переменных для красного цвета, 50 переменных для зеленого цвета, 50 переменных для синего цвета, 50 переменных для расстояния, 50 переменных для угла мы просто создадим массив и назовем его star. Под каждым номером в массиве star содержится информация о нашей структуре stars. Это мы делаем в 8-ой строке ниже: stars star[num]. Тип элемента массива будет stars. stars это структура. И массив содержит всю информацию в структурах. Массив называется star. Количество элементов - [num]. И так как num=50, мы имеем массив с именем star. Наш массив содержит элементы типа структура stars. Намного проще, чем хранить каждую звезду в отдельных переменных. Что было бы большой глупостью и не позволило добавлять или уменьшать количество звезд с помощью переменной num.
(Прим. перев. - Такое ощущение, что объясняешь слону, как ходить :). Можно было одной строчкой это объяснить.)
typedef struct // Создаём структуру для звезд
{
int r, g, b; // Цвет звезды
GLfloat dist; // Расстояние от центра
GLfloat angle; // Текущий угол звезды
}
stars; // Имя структуры - Stars
stars star[num]; // Делаем массив 'star' длинной 'num',
// где элементом является структура 'stars'
Далее мы задаём переменную для хранения расстояния от наблюдателя до звезд (zoom), и какой будет начальный угол(tilt). Также мы делаем переменную spin, которая будет вращать звезды по оси z, и это будет выглядеть как вращение вокруг их текущей позиции.
loop это переменная, которую мы используем в программе для отрисовки всех 50-ти звезд, и texture[1] будет использоваться для хранения одной черно-белой текстуры, которую мы загружаем. Если вы хотите больше текстур, вы должны увеличить длину массива, с одного до нужной длины.
GLfloat zoom=-15.0f; // Расстояние от наблюдателя до звезд
GLfloat tilt=90.0f; // Начальный угол
GLfloat spin; // Для вращения звезд
GLuint loop; // Используется для циклов
GLuint texture[1]; // Массив для одной текстуры
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявления для WndProc
Сразу же за переменными мы добавляем код для загрузки текстур. Я не буду объяснять этот код в деталях, это тот же код что был в 6, 7 и 8 уроке. Изображение, которое мы загружает называется star.bmp. Мы генерируем только одну текстуру используя glGenTextures(1, &texture[0]). Текстура будет иметь линейную фильтрацию (linear filtering).
AUX_RGBImageRec *LoadBMP(char *Filename)// Функция для загрузки bmp файлов
{
FILE *File=NULL; // Переменная для файла
if (!Filename) // Нужно убедиться в правильности переданого имени
{
return NULL; // Если неправильное имя, то возвращаем NULL
}
File=fopen(Filename,"r"); // Открываем и проверяем на наличие файла
if (File) // Файл существует?
{
fclose(File); // Если да, то закрываем файл
// И загружаем его с помощью библиотеки AUX, возращая ссылку на изображение
return auxDIBImageLoad(Filename);
}
// Если загрузить не удалось или файл не найден, то возращаем NULL
return NULL;
}
Эта секция кода загружает изображение (описанным выше кодом) и конвертирует в текстуру. Status хранит информацию об успехе операции.
int LoadGLTextures() // Функция загрузки изображения и конвертирования в текстуру
{
int Status=FALSE; // Индикатор статуса
AUX_RGBImageRec *TextureImage[1];// Создаём место для хранения текстуры
memset(TextureImage,0,sizeof(void *)*1);// устанавливаем ссылку на NULL
// Загружаем изображение, Проверяем на ошибки, Если файл не найден то выходим
if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
{
Status=TRUE; // Ставим статус в TRUE
glGenTextures(1, &texture[0]); // Генерируем один индификатор текстуры
// Создаём текстуру с линейной фильтрацией (Linear Filtered)
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX,
TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
}
if (TextureImage[0]) // Если текстура существует
{
if (TextureImage[0]->data) // Если изображение существует
{
// Освобождаем место выделенное под изображение
free(TextureImage[0]->data);
}
free(TextureImage[0]); // Освобождаем структуры изображения
}
return Status; // Возвращаем статус
}
Теперь мы настраиваем OpenGL для отрисовки того, что будет нужно. Нам не нужен Z-буфер (тест глубины) для нашего проекта, убедитесь что удалены строчки из первого урока: glDepthFunc(GL_LEQUAL); и glEnable(GL_DEPTH_TEST); иначе вы не получите нужного результата Мы используем текстурирование в этом коде, значит надо добавить все необходимые строки, которых нет в первом уроке. Включаем текстурирование и смешивание.
int InitGL(GLvoid) // Всё установки OpenGL будут здесь
{
if (!LoadGLTextures()) // Загружаем текстуру
{
return FALSE; // Если не загрузилась, то возвращаем FALSE
}
glEnable(GL_TEXTURE_2D); // Включаем текстурирование
// Включаем плавную ракраску (интерполирование по вершинам)
glShadeModel(GL_SMOOTH);
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Фоном будет черный цвет
glClearDepth(1.0f); // Установки буфера глубины (Depth Buffer)
// Максимальное качество перспективной коррекции
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
// Устанавливаем функцию смешивания
glBlendFunc(GL_SRC_ALPHA,GL_ONE);
glEnable(GL_BLEND); // Включаем смешивание
Следующий фрагмент кода новый. Он устанавливает начальные углы, расстояние и цвет для каждой звезды. Заметьте, как просто менять информацию в структуре. Цикл по всем 50 звездам. Чтобы изменить угол star[1] все, что мы должны написать - это star[1].angle={некоторое значение}. Это просто!
for (loop=0; loop<num; loop++) // Делаем цикл и бежим по всем звездам
{
star[loop].angle=0.0f; // Устанавливаем всё углы в 0
Я рассчитываю дистанцию взяв текущий номер звезды (это значение loop) и разделив на максимальное значение звезд. Потом я умножаю результат на 5.0f. Коротко, что это даёт - это отодвигает каждую звезду немного дальше, чем предыдущую. Когда loop равен 50 (последняя звезда), loop разделенный на num будет равен 1.0f. Я умножаю на 5 потому, что 1.0f*5.0f будет 5.0f. 5.0f это почти на границе экрана. Я не хочу, что бы звезды уходили за пределы экрана, так что 5.0f это идеально. Если вы установите zoom подальше в экран, вы должны использовать большее число, чем 5.0f, но ваши звезды должны быть немного меньше (из-за перспективы).
Заметьте, что цвета для каждой звезды задаются случайным образом от 0 до 255. Вы можете спросить, как мы может использовать эти числа, когда нормальное значение цвета от 0.0f до 1.0f. Отвечаю. Когда мы устанавливаем цвет, мы используем glColor4ub вместо glColor4f. ub значит Unsigned Byte (беззнаковый байт). И байт может иметь значения от 0 до 255. В этой программе легче использовать байты, чем работать с со случайными числами с плавающей запятой.
// Вычисляем растояние до центра
star[loop].dist=(float(loop)/num)*5.0f;
// Присваиваем star[loop] случайное значение (красный).
star[loop].r=rand()%256;
// Присваиваем star[loop] случайное значение (зеленый)
star[loop].g=rand()%256;
// Присваиваем star[loop] случайное значение (голубой)
star[loop].b=rand()%256;
}
return TRUE; // Инициализация прошла нормально.
}
Код функции Resize тот же самый, так что рассмотрим код отрисовки сцены. Если вы используете код из первого урока, то удалите весь код из DrawGLScene и просто скопируйте, то, что написано ниже. Здесь только две строки из первого урока, вообщем удалять немного придется.
int DrawGLScene(GLvoid) // Здесь мы всё рисуем
{
// Очищаем буфер цвета и глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Выбираем нашу текстуру
glBindTexture(GL_TEXTURE_2D, texture[0]);
for (loop=0; loop<num; loop++) // Цикл по всем звездам
{
// Обнуляем видовую матрицу (Model Matrix) перед каждой звездой
glLoadIdentity();
// Переносим по оси z на 'zoom'
glTranslatef(0.0f,0.0f,zoom);
// Вращаем вокруг оси x на угол 'tilt'
glRotatef(tilt,1.0f,0.0f,0.0f);
Теперь мы двигаем звезды! :) Звезда появляется в середине экрана. Первым делом мы вращаем сцену вокруг оси y. Если угол 90 градусов, то ось x будет лежать не слева направо, а наоборот и выходить за пределы экрана. В качестве примера: представьте, что вы стоите в центре комнаты. Теперь представьте, что слева на стене написано -x, впереди на стене написано -z, справа написано +x, в сзади написано +z. Если повернуться налево на 90 градусов, но не двигаться с места, то на стене впереди будет не -z, а -x. Все стены поменяются. -z будет справа, +z будет слева, -x впереди, и +x сзади. Проясняется? Вращая сцену, мы изменяем направления осей x и z.
Во второй строчке мы сдвигаем позицию по плоскости x. Обычно положительное значение по x двигает нас вправую сторону экрана (где обычно +x), но так как мы повернулись вокруг оси y, +x может быть хоть где. Если мы повернём на 180 градусов, +x будет с левой стороны экрана вместо правой. И так, когда мы двигаемся вперед по оси x, мы можем подвинуться влево, вправо, вперед и даже назад.
// Поворачиваем на угол звезды вокруг оси y
glRotatef(star[loop].angle,0.0f,1.0f,0.0f);
// Двигаемся вперед по оси x
glTranslatef(star[loop].dist,0.0f,0.0f);
Теперь немного хитрого кода. Звезда это всего лишь плоская текстура. И если мы нарисовали плоский квадрат в середине экрана с наложенной текстурой - это будет выглядеть отлично. Он будет, повернут к вам, как и должен быть. Но если повернёте его вокруг оси y на 90 градусов, текстура будет направлена вправо или влево экрана. Всё что вы увидите это тонкую линию. Нам не нужно чтобы это случилось. Мы хотим, чтобы звезды были направлены на наблюдателя всё время, не важно как они вращаются и двигаются по экрану.
Мы добиваемся этого путем отмены вращения, которое мы делаем, перед тем как нарисовать звезду. Мы отменяем вращение в обратном порядке. Выше мы повернули экран, когда сделали вращение на угол звезды. В обратном порядке, мы должны повернуть обратно звезду на текущий угол. Чтобы сделать это, мы используем обратное значение угла, и повернём звезду на этот угол. И так как мы повернули звезду на 10 градусов, то, поворачивая обратно на -10 градусов мы делаем звезду повернутой к наблюдателю по y. Первая строчка ниже отменяет вращение по оси y. Потом мы должны отменить вращение по оси x. Чтобы сделать это мы вращаем звезду на угол -tilt. После всех этих операций звезда полностью повернута к наблюдателю.
glRotatef(-star[loop].angle,0.0f,1.0f,0.0f);
// Отменяет текущий поворот звезды
glRotatef(-tilt,1.0f,0.0f,0.0f); // Отменяет поворот экрана
Если twinkle равно TRUE, мы рисуем не вращающуюся звезду на экране. Для того чтобы получить, различные цвета, мы берём максимальное количество звезд (num) и вычитаем текущий номер (loop), потом вычитаем единицу, так как наш цикл начинается с 0 и идет до num-1. Если результат будет 10, то мы используем цвет 10- ой звезды. Вследствие того, что цвет двух звезд обычно различный. Не совсем хороший способ, но зато эффективный. Последнее значение это альфа (alpha). Чем меньше значение, тем прозрачнее звезда (т.е. темнее, т.к. экран черный).
Если twinkle включен, то каждая звезда будет отрисована дважды. Программы будет работать медленнее в зависимости от типа вашего компьютера. Если twinkle включен, цвета двух звезд будут смешиваться вместе для создания реально красивых цветов. Так как это звезды не вращаются, то это проявлялось бы как будто звёзды анимировались, когда twinkling включен. (Если не понятно, то просто посмотрите).
Заметьте, как просто добавить цвет в текстуру. Даже если текстура черно-белая, она будет окрашена в цвет, который мы выбрали, перед тем как нарисовать текстуру. Также возьмите на заметку что мы используем байты для значений цвета, а не числа с плавающей запятой. Даже альфа тоже представляется байтом.
if (twinkle) // Если Twinkling включен
{
// Данный цвет использует байты
glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,
star[(num-loop)-1].b,255);
glBegin(GL_QUADS);// Начинаем рисовать текстурированый квадрат
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
glEnd(); // Закончили рисовать
}
Теперь мы рисуем основную звезду. Разница между кодом выше только в том, что звезда всегда рисуется, и вращается по оси z.
glRotatef(spin,0.0f,0.0f,1.0f);// Поворачиваем звезду по оси z
// Цвет использует байты
glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
glBegin(GL_QUADS); // Начинаем рисовать текстурный квадрат
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
glEnd(); // Закончили рисовать
Здесь мы делаем все движения. Мы вращаем звезду увеличением значения spin. Потом мы меняем угол каждой звезды. Угол каждой звезды увеличивается на loop/num. Что ускоряет вращение звезды с отдалением от центра. Чем ближе к центру, тем медленнее вращается звезда. Наконец мы уменьшаем расстояние до центра для каждой звезды. Это создаёт эффект засасывания звезд в центр экрана.
spin+=0.01f; // Используется для вращения звезды
star[loop].angle+=float(loop)/num;// Меняем угол звезды
star[loop].dist-=0.01f; // Меняем растояние до центра
Нижеследующие строки проверяют видимость звезд. Попала звезда в центр экрана или нет. Если попала, то даём звезде новый цвет и двигаем на 5 единиц от центра. И так она может снова начать своё путешествие к центру как новая звезда.
if (star[loop].dist<0.0f) // Звезда в центре экрана?
{
star[loop].dist+=5.0f; // Перемещаем на 5 единиц от центра
// Новое значение красной компоненты цвета
star[loop].r=rand()%256;
// Новое значение зеленной компоненты цвета
star[loop].g=rand()%256;
// Новое значение синей компоненты цвета
star[loop].b=rand()%256;
}
}
return TRUE; // Всё ок
}
Теперь мы добавляем код для проверки нажатия клавиш. Идем ниже в WinMain(). Ищите строку с SwapBuffers(hDC). Мы добавляем код для нашей клавиши прямо под этой строкой.
Строкой ниже мы проверяем нажатие клавиши 'T'. Если нажата, и не была до этого нажата, то идем дальше. Если twinkle равно FALSE, то она станет TRUE. Если была TRUE, то станет FALSE. Одно нажатие 'T' установит tp равное TRUE. Это предотвращает постоянное выполнение этого кода, если клавиша 'T' удерживается.
SwapBuffers(hDC); // Смена буфера (Double Buffering)
if (keys['T'] && !tp) // Если 'T' нажата и tp равно FALSE
{
tp=TRUE; // то делаем tp равным TRUE
twinkle=!twinkle; // Меняем значение twinkle на обратное
}
Код ниже проверяет "выключение" (повторное нажатие) клавиши 'T'. Если да, то делаем tp=FALSE. Нажатие 'T' делает ничего кроме установки tp равной FALSE, так что эта часть кода очень важная.
if (!keys['T']) // Клавиша 'T' была отключена
{
tp=FALSE; // Делаем tp равное FALSE
}
Следующий код проверяет, нажаты ли клавиши 'стрелка вверх', 'стрелка вниз', 'page up', 'page down'.
if (keys[VK_UP]) // Стрелка вверх была нажата?
{
tilt-=0.5f; // Вращаем экран вверх
}
if (keys[VK_DOWN]) // Стрелка вниз нажата?
{
tilt+=0.5f; // Вращаем экран вниз
}
if (keys[VK_PRIOR]) // Page Up нажат?
{
zoom-=0.2f; // Уменьшаем
}
if (keys[VK_NEXT]) // Page Down нажата?
{
zoom+=0.2f; // Увеличиваем
}
Как и других предыдущих уроках, убедимся, что название окна корректно.
if (keys[VK_F1]) // Если F1 нажата?
{
keys[VK_F1]=FALSE; // Делаем клавишу равной FALSE
KillGLWindow(); // Закрываем текущее окно
fullscreen=!fullscreen;
// Переключаем режимы Fullscreen (полноэкранный) / Windowed (обычный)
// Пересоздаём OpenGL окно
if (!CreateGLWindow("NeHe's Textures,
Lighting & Keyboard Tutorial",640,480,16,fullscreen))
{
return 0; //Выходим если не получилось
}
}
}
}
В этом уроке я попытался объяснить, как можно детальнее как загружать черно-белое изображение, удалять черный фон на изображении (используя смешивание), добавлять цвет в картинку, и двигать ее по экрану в трехмерном пространстве. Я также попытался показать, как сделать красивые цвета и анимацию путем наложения второй копии изображения поверх оригинала. Теперь вы имеете хорошее представление обо всем, что я хотел вам рассказать на данный момент. У вас теперь не должно возникнуть проблем с созданием своих собственных демок в 3D. Все начальные данные изложены тут.
Загрузка и перемещение в трехмерном мире
Loading And Moving Through A 3D World
Этот урок был написан человеком по имени Lionel Brits (Betelgeuse). Урок содержит только те части кода, которые нужно добавить. Но если просто добавите строки кода, описанные ниже, программа не будет работать. Если вы хотите знать, где должна идти каждая строка кода, скачайте исходник и просмотрите его так же, как вы прочитали этот урок.
Добро пожаловать в непопулярный Урок 10. Сейчас у вас есть вращающийся куб или цепочка звёзд и некоторая базовая ориентация в 3D программировании. Но стойте! Не убегайте сразу же и не начинайте создание Quake IV, пока что. Просто, вращающиеся кубики не доставят вам много удовольствий в десматче J. Вместо этого вам нужен большой, сложный и динамический 3D мир со свободным взглядом во все 6 сторон и с причудливыми эффектами такими, как зеркала, порталы, искривления и, конечно же, с высокой частотой кадров в секунду. В этом уроке представлена базовая «структура» 3D мира и, конечно же, способы перемещения по нему.
Структура данных
До сих пор было легко определять среду 3D мира, используя длинные комбинации чисел, но это становится чрезвычайно нелегко, когда сложность среды начинает возрастать. По этой причине мы должны организовать данные в более мощные структуры. Прежде всего обсудим понятие сектора. Каждый 3Д мир базируется на некотором количестве секторов. Сектор может быть комнатой, кубом или любым другим замкнутым пространством.
typedef struct tagSECTOR // Создаём структуру нашего сектора
{
int numtriangles; // Кол-во треугольников в секторе
TRIANGLE* triangle // Ссылка на массив треугольников
} SECTOR; // Обзовём структуру словом SECTOR
Сектор содержит ряд многоугольников, однако мы будем использовать треугольники, потому что их проще всего запрограммировать.
typedef struct tagTRIANGLE // Создаём стр-ру нашего треугольника
{
VERTEX vertex[3]; // Массив трёх вершин
} TRIANGLE; // Обзовём это TRIANGLE
Треугольник, как и любой многоугольник, определяется вершинами. Вершина содержит реальные для использования OpenGL’ом данные. Мы определяем каждую точку треугольника её расположением в 3D пространстве (x, y, z) и координатами на текстуре (u, v).
typedef struct tagVERTEX // Создаём стр-ру нашей вершины
{
float x, y, z; // 3D координаты
float u, v; // Координаты на текстуре
} VERTEX; // Обзовём это VERTEX
Загрузка файлов
Сохранение данных нашего мира внутри программы делает её слишком статичной и скучной. Загрузка миров с диска, тем не менее, даёт больше гибкости в тестировании различных миров, избавляя от перекомпиляции нашей программы. Другое преимущество в том, что пользователь может использовать новые уровни и модифицировать их, не задумываясь о коде нашей программы. Тип файла данных, который мы собираемся использовать будет текстовым. Это сделано для того, чтобы облегчить редактирование мира и уменьшить код программы. Оставим двоичные файлы на дальнейшее рассмотрение.
Естественный вопрос: как мы извлечем данные из нашего файла? Во-первых, мы создадим новую функцию SetupWorld(). Определим наш файл как filein и откроем его в режиме только чтение. А так же, когда закончим, мы должны не забыть закрыть наш файл. Давайте посмотрим на следующий код:
// Декларация выше: char* worldfile = "data\\world.txt";
void SetupWorld() // Установка нашего мира
{
FILE *filein; // Файл для работы
filein = fopen(worldfile, "rt"); // Открываем наш файл
...
(считываем наши данные)
...
fclose(filein); // Закрываем наш файл
return; // Возвращаемся назад
}
Следующее, чему мы уделим внимание, будет собственно считывание каждой строки текста в переменную. Это можно выполнить очень многими способами. Одна проблема в том, что не все строки содержат значимую информацию. Пустые линии и комментарии не должны быть считаны. Теперь создадим функцию readstr(). Она будет считывать одну значимую строку в инициализированную строку. Вот этот код:
void readstr(FILE *f,char *string) // Считать в строку
{
do // Начинаем цикл
{
fgets(string, 255, f); // Считываем одну линию
// Проверяем её на условие повт. цикла
} while ((string[0] == '/') || (string[0] == '\n'));
return; // Возврат
}
Далее мы должны считать данные сектора. В этом уроке мы будем иметь дело только с одним сектором, но достаточно просто реализовать многосекторный движок. Давайте вернёмся к SetupWorld(). Наша программа должна знать сколько треугольников в секторе. В нашем файле данных мы обозначим количество треугольников следующим образом:
NUMPOLLIES n
Вот код для чтения количества треугольников:
int numtriangles; // Кол-во треугольников в секторе
char oneline[255]; // Строка для сохранения данных
...
readstr(filein,oneline); // Считать одну линию данных
// Считать кол-во треугольников
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);
Остальная часть нашего процесса загрузки мира будет использовать тот же процесс. Далее мы инициализируем наш сектор и считываем в него некоторые данные:
// Декларация выше: SECTOR sector1;
char oneline[255]; // Строка для сохранения данных
int numtriangles; // Кол-во треугольников в секторе
float x, y, z, u, v; // 3D и текстурные координаты
...
// Выделяем память для numtriangles и устанавливаем ссылку
sector1.triangle = new TRIANGLE[numtriangles];
// Определяем кол-во треугольников в Секторе 1
sector1.numtriangles = numtriangles;
// Цикл для всех треугольников
// За каждый шаг цикла – один треугольник в секторе
for (int triloop = 0; triloop < numtriangles; triloop++)
{
// Цикл для всех вершин
// За каждый шаг цикла – одна вершина в треугольнике
for (int vertloop = 0; vertloop < 3; vertloop++) {
readstr(filein,oneline); // Считать строку для работы
// Считать данные в соответствующие переменные вершин
sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
// Сохранить эти данные
sector1.triangle[triloop].vertex[vertloop].x = x;
// Сектор 1, Треугольник triloop, Вершина vertloop, Значение x = x
sector1.triangle[triloop].vertex[vertloop].y = y;
// Сектор 1, Треугольник triloop, Вершина vertloop, Значение y = y
sector1.triangle[triloop].vertex[vertloop].z = z;
// Сектор 1, Треугольник triloop, Вершина vertloop, Значение z = z
sector1.triangle[triloop].vertex[vertloop].u = u;
// Сектор 1, Треугольник triloop, Вершина vertloop, Значение u = u
sector1.triangle[triloop].vertex[vertloop].v = v;
// Сектор 1, Треугольник triloop, Вершина vertloop, Значение y = v
}
}
Каждый треугольник в нашем файле данных имеет следующую структуру:
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3
Отображение миров
Теперь, когда мы можем загружать наш сектор в память, нам нужно вывести его на экран. Итак, мы уже умеем делать немного простых вращений и проекций, но наша камера всегда находилась в начальном положении (0, 0, 0). Любой хороший 3D движок должен предоставлять пользователю возможность ходить и исследовать мир, так мы и сделаем. Одна из возможностей сделать это – перемещать камеру и перерисовывать 3D среду относительно её положения. Это медленно выполняется и тяжело запрограммировать. Поэтому мы будем делать так:
1) Вращать и проецировать позицию камеры следуя командам пользователя.
2) Вращать мир вокруг начала координат противоположно вращению камеры (это даёт иллюзию того, что повернулась камера).
3) Переместить мир способом, противоположным перемещению камеры (опять-таки, это даёт иллюзию того, что переместилась камера).
Это красиво и легко реализовывается. Давайте начнём с первого этапа (Вращение и проецирование камеры).
if (keys[VK_RIGHT]) // Была ли нажата правая стрелка?
{
yrot -= 1.5f; // Вращать сцену влево
}
if (keys[VK_LEFT]) // Была ли нажата левая стрелка?
{
yrot += 1.5f; // Вращать сцену вправо
}
if (keys[VK_UP]) // Была ли нажата стрелка вверх?
{
// Переместиться на X-плоскости, базируемой на направлении игрока
xpos -= (float)sin(heading*piover180) * 0.05f;
// Переместиться на Z-плоскости, базируемой на направлении игрока
zpos -= (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle >= 359.0f)// walkbiasangle>=359?
{
walkbiasangle = 0.0f; // Присвоить walkbiasangle 0
}
else // В противном случае
{
// Если walkbiasangle < 359 увеличить его на 10
walkbiasangle+= 10;
}
// Иммитация походки человека
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
if (keys[VK_DOWN]) // Была ли нажата стрелка вниз?
{
// Переместиться на X-плоскости, базируемой на направлении игрока
xpos += (float)sin(heading*piover180) * 0.05f;
// Переместиться на Z-плоскости, базируемой на направлении игрока
zpos += (float)cos(heading*piover180) * 0.05f;
if (walkbiasangle <= 1.0f) // walkbiasangle<=1?
{
walkbiasangle = 359.0f; // Присвоить walkbiasangle 359
}
else // В противном случае
{
// Если walkbiasangle >1 уменьшить его на 10
walkbiasangle-= 10;
}
// Иммитация походки человека
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;
}
Это было довольно просто. Когда нажата стрелка влево или стрелка вправо, переменная вращения yrot увеличивает или уменьшает своё значение. Когда нажата стрелка вверх или стрелка вниз, новое положение камеры высчитывается с использованием синуса и косинуса (требуется немного тригонометрии J). Piover180 это просто коэффициент преобразования для перевода градусов в радианы.
Далее вы спросите меня: что такое walkbias (дословно: смещение походки)? Это слово, которое я изобрёл J. Оно представляет собой смещение, которое происходит, когда персона идёт (голова смещается вверх и вниз как буй). Это легко устанавливается изменением Y позиции камеры по синусоиде. Я решил использовать это, потому что простое перемещение вперёд и назад выглядит не реально.
Теперь когда эти переменные получили свои значения, мы можем сделать второй и третий шаги. Они будут сделаны в цикле отображения, так как наша программа на сложная, чтобы заслужить для этого отдельную функцию.
int DrawGLScene(GLvoid) // Нарисовать сцену OpenGL
{
// Очистить сцену и буфер глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // Сбросить текущую матрицу
// Вещ. перем. для временных X, Y, Z, U и V
GLfloat x_m, y_m, z_m, u_m, v_m;
GLfloat xtrans = -xpos; // Проекция игрока на ось X
GLfloat ztrans = -zpos; // Проекция игрока на ось Z
// Для смещения изображения вверх и вниз
GLfloat ytrans = -walkbias-0.25f;
// 360 градусный угол для поворота игрока
GLfloat sceneroty = 360.0f - yrot;
int numtriangles; // Количество треугольников
glRotatef(lookupdown,1.0f,0,0);// Вращать вверх и вниз
// Вращать в соответствии с направлением взгляда игрока
glRotatef(sceneroty,0,1.0f,0);
// Проецировать сцену относительно игрока
glTranslatef(xtrans, ytrans, ztrans);
// Выбрать текстуру filter
glBindTexture(GL_TEXTURE_2D, texture[filter]);
// Получить кол-во треугольников Сектора 1
numtriangles = sector1.numtriangles;
// Процесс для каждого треугольника
// Цикл по треугольникам
for (int loop_m = 0; loop_m < numtriangles; loop_m++)
{
glBegin(GL_TRIANGLES); // Начинаем рисовать треугольники
// Нормализованный указатель вперёд
glNormal3f( 0.0f, 0.0f, 1.0f);
x_m = sector1.triangle[loop_m].vertex[0].x;// X 1-ой точки
y_m = sector1.triangle[loop_m].vertex[0].y;// Y 1-ой точки
z_m = sector1.triangle[loop_m].vertex[0].z;// Z 1-ой точки
// U текстурная координата
u_m = sector1.triangle[loop_m].vertex[0].u;
// V текстурная координата
v_m = sector1.triangle[loop_m].vertex[0].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
// Установить TexCoord и грань
x_m = sector1.triangle[loop_m].vertex[1].x;// X 2-ой точки
y_m = sector1.triangle[loop_m].vertex[1].y;// Y 2-ой точки
z_m = sector1.triangle[loop_m].vertex[1].z;// Z 2-ой точки
// U текстурная координата
u_m = sector1.triangle[loop_m].vertex[1].u;
// V текстурная координата
v_m = sector1.triangle[loop_m].vertex[1].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
// Установить TexCoord и грань
x_m = sector1.triangle[loop_m].vertex[2].x;// X 3-ой точки
y_m = sector1.triangle[loop_m].vertex[2].y;// Y 3-ой точки
z_m = sector1.triangle[loop_m].vertex[2].z;// Z 3-ой точки
// U текстурная координата
u_m = sector1.triangle[loop_m].vertex[2].u;
// V текстурная координата
v_m = sector1.triangle[loop_m].vertex[2].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);
// Установить TexCoord и грань
glEnd(); // Заканчиваем рисовать треугольники
}
return TRUE; // Возвращаемся
}
И вуаля! Мы только что нарисовали наш первый фрейм. Это не совсем Quake, но эй, мы не совсем Carmack или Abrash. Когда вы запустите программу, можете нажать F, B, PgUp и PgDown и увидеть дополнительные эффекты. PgUp/Down просто наклоняет камеру вверх и вниз (наподобие панорамирования из стороны в сторону). Текстура – это моя обработанная школьная фотография J, если конечно NeHe сохранит ее.
Эффект "флага" на OpenGL
OpenGL Flag Effect
Всем привет. Для тех, кто хочет узнать, чем же мы тут занимаемся: эту вещь можно увидеть в конце моего демо/хака "Worthless!". Меня зовут Боско (Bosco), и я сделаю все, что в моих силах, чтобы научить вас, парни, делать анимированную картинку с синусоидальной волной по ней. Этот урок основан на уроке №6 от NeHe, и вы должны, по крайней мере, знать и уметь делать то, что в нем описано. Вы должны скачать архив с исходным кодом, распаковать его куда-нибудь, взять картинку из папки data и поместить в подпапку data той папки, где находится ваш исходник :). Ну, или использовать свою текстуру, если она подходящего для OpenGL размера.
Перво-наперво откройте урок №6 для Visual C++ и добавьте этот #include сразу за другими. Данный #include позволит нам использовать различные библиотечные математические функции, как, например, синус и косинус.
#include <math.h> // для функции Sin()
Мы будем использовать массив точек (points) для хранения отдельных x, y и z - координат нашей сетки. Размер сетки 45x45, и она в свою очередь образует 44x44 квадрата. wiggle_count будет использоваться для определения того, насколько быстро "развевается" текстура. Каждые три кадра выглядят достаточно хорошо, и переменная hold будет содержать число с плавающей запятой для сглаживания волн. Эти строки можно добавить в начале программы, где-нибудь под последней строчкой с #include и перед строкой GLuint texture[1]:
float points[ 45 ][ 45 ][3]; // массив точек сетки нашей "волны"
int wiggle_count = 0; // счетчик для контроля быстроты развевания флага
GLfloat hold; // временно содержит число с плавающей запятой
Перейдем вниз, к функции LoadGLTextures(). Мы хотим использовать текстуру с именем Tim.bmp. Найдите LoadBMP("Data/NeHe.bmp") и поменяйте на LoadBMP("Data/Tim.bmp").
if (TextureImage[0]=LoadBMP("Data/Tim.bmp")) // загружаем изображение
Теперь добавьте приведенный ниже код в конец функции InitGL() перед return TRUE:
glPolygonMode( GL_BACK, GL_FILL ); // нижняя (задняя) сторона заполнена
glPolygonMode( GL_FRONT, GL_LINE ); // верхняя (передняя) сторона прорисована линиями
Этот код просто означает, что мы хотим, чтобы полигоны нижней (задней) стороны были зарисованы полностью и чтобы полигоны верхней (передней) стороны были лишь очерчены.
Это мои личные предпочтения. Причина – ориентация полигона или направления граней. Загляните в Красную Книгу (по OpenGL), если хотите узнать больше. Пользуясь случаем, хочу сказать, что эта книга как раз из числа того, что помогает мне в изучении OpenGL, не считая сайта NeHe :). Спасибо NeHe. Купите книгу The Programmer's Guide to OpenGL издательства Addison-Wesley. Она – неисчерпаемый ресурс по части программирования на OpenGL. Ладно, вернемся к уроку.
Прямо под предыдущим кодом и выше return TRUE добавьте следующие строки:
// пройдемся по плоскости X
for(int x=0; x<45; x++)
{
// пройдемся по плоскости Y
for(int y=0; y<45; y++)
{
// применим волну к нашей сетке
points[x][y][0]=float((x/5.0f)-4.5f);
points[x][y][1]=float((y/5.0f)-4.5f);
points[x][y][2]=float(sin((((x/5.0f)*40.0f)/360.0f)*3.141592654*2.0f));
}
}
Благодарю Грэма Гиббонса (Graham Gibbons) за замечание об целочисленном цикле для того чтобы избежать пиков на волне.
Два цикла вверху инициализируют точки на нашей сетке. Я инициализирую переменные в моем цикле, чтобы помнить, что они просто переменные цикла. Не уверен, что это хорошо. Мы используем целочисленные циклы, дабы предупредить глюки, которые появляются при вычислениях с плавающей запятой. Мы делим переменные x и y на 5 (т.е. 45 / 9 = 5) и отнимаем 4.5 от каждой из них, чтобы отцентрировать "волну". Того же эффекта можно достигнуть переносом (translate), но этот способ мне больше нравится.
Окончательное значение points[x][y][2] - это наше значение синуса. Функция sin() принимает параметр в радианах. Мы берем наши градусы, которые получились умножением float_x (x/5.0f) на 40.0f, и, чтобы конвертировать их в радианы делением, мы берем градусы, делим на 360.0f, умножаем на число пи (или на аппроксимацию) и на 2.0f.
Я заново перепишу функцию DrawGLScene, так что удалите ее текст и замените его текстом, приведенным ниже.
int DrawGLScene(GLvoid) // рисуем нашу сцену
{
int x, y; // переменные циклов
// для разбиения флага на маленькие квадраты
float float_x, float_y, float_xb, float_yb;
Различные переменные используются для контроля в циклах. Посмотрите на код ниже – большинство из них служит лишь для контролирования циклов и хранения временных значений.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очистить экран и буфер глубины
glLoadIdentity(); // сброс текущей матрицы
glTranslatef(0.0f,0.0f,-12.0f); // перенести 17 единиц в глубь экрана
glRotatef(xrot,1.0f,0.0f,0.0f); // вращение по оси X
glRotatef(yrot,0.0f,1.0f,0.0f); // вращение по оси Y
glRotatef(zrot,0.0f,0.0f,1.0f); // вращение по оси Z
glBindTexture(GL_TEXTURE_2D, texture[0]); // выбрать нашу текстуру
Вы могли видеть это все раньше. Все тут так же, как в уроке №6, кроме того, что я просто отвожу сцену от камеры чуть дальше.
glBegin(GL_QUADS); // начинаем рисовать квадраты
for( x = 0; x < 44; x++ ) // пройдемся по плоскости X 0-44 (45 точек)
{
for( y = 0; y < 44; y++ ) // пройдемся по плоскости Y 0-44 (45 точек)
{
Просто запускаем цикл рисования наших полигонов. Я здесь использую целые числа, чтобы избежать использования функции int(), которой я пользовался ранее для получения индекса ссылки массива как целое значение.
float_x = float(x)/44.0f; // создать значение X как float
float_y = float(y)/44.0f; // создать значение Y как float
float_xb = float(x+1)/44.0f; // создать значение X как float плюс 0.0227f
float_yb = float(y+1)/44.0f; // создать значение Y как float плюс 0.0227f
Мы используем четыре переменных выше для координат текстуры. Каждый наш полигон (квадрат в сетке) имеет секцию размером 1/44 x 1/44 с отображенной на нее текстурой. В начале циклов задается нижняя левая вершина секции, и потом мы просто добавляем к этой вершине соответственно 0 или 1 для получения трех остальных вершин (т.е. x+1 или y+1 будет правая верхняя вершина).
// первая координата текстуры (нижняя левая)
glTexCoord2f( float_x, float_y);
glVertex3f( points[x][y][0], points[x][y][1], points[x][y][2] );
// вторая координата текстуры (верхняя левая)
glTexCoord2f( float_x, float_yb );
glVertex3f( points[x][y+1][0], points[x][y+1][1], points[x][y+1][2]);
// третья координата текстуры (верхняя правая)
glTexCoord2f( float_xb, float_yb );
glVertex3f( points[x+1][y+1][0], points[x+1][y+1][1], points[x+1][y+1][2]);
// четвертая координата текстуры (нижняя правая)
glTexCoord2f( float_xb, float_y );
glVertex3f( points[x+1][y][0], points[x+1][y][1], points[x+1][y][2]);
}
}
glEnd(); // закончили с квадратами
Строки выше просто делают OpenGL-вызовы для передачи всех данных, о которых мы говорили. Четыре отдельных вызова каждой функции glTexCoord2f() и glVertex3f(). Продолжим. Заметьте – квадраты рисуются по часовой стрелке. Это означает, что сторона, которую вы видите, вначале будет задней. Задняя заполнена. Передняя состоит из линий.
Если бы вы рисовали против часовой стрелки, то сторона, которую вы бы видели вначале, была бы передней. Значит, вы увидели бы текстуру, выглядящую как сетка, (ты написал: типа сетки) вместо заполненной текстурой поверхности.
if( wiggle_count == 2 ) // для замедления волны (только каждый второй кадр)
{
Теперь мы хотим повторять значения синуса, тем самым создавая "движение".
for( y = 0; y < 45; y++ ) // пройдемся по плоскости Y
{
// сохраним текущее значение одной точки левой стороны волны
hold=points[0][y][2];
for( x = 0; x < 44; x++) // пройдемся по плоскости X
{
// текущее значение волны равно значению справа
points[x][y][2] = points[x+1][y][2];
}
// последнее значение берется из дальнего левого сохраненного значения
points[44][y][2]=hold;
}
wiggle_count = 0; // снова сбросить счетчик
}
wiggle_count++; // увеличить счетчик
Вот что мы делаем: сохраняем первое значение каждой линии, затем двигаем волну к левому, заставляя текстуру развеваться. Значение, которое мы сохранили, потом вставляется в конец, чтобы создать бесконечную волну, идущую по поверхности текстуры. Затем мы обнуляем счетчик wiggle_count для продолжения анимации.
Код вверху был изменен NeHe (в феврале 2000) для исправления недостатка волны. Волна теперь гладкая.
xrot+=0.3f; // увеличить значение переменной вращения по X
yrot+=0.2f; // увеличить значение переменной вращения по Y
zrot+=0.4f; // увеличить значение переменной вращения по Z
return TRUE; // возврат из функции
}
Обычные значения поворота у NeHe. :) Вот и все. Скомпилируйте, и у вас должна появиться миленькая вращающаяся "волна". Я не знаю, что еще сказать, фу-у-ух… Это было так ДОЛГО! Но я надеюсь, вы, парни, сможете последовать этому, либо приобрести что-то полезное для себя. Если у вас есть вопросы, если вы хотите, чтобы я что-нибудь подправил или сказать мне, как, все-таки, жутко я пишу программы :), то пришлите мне письмецо.
Это был экспромт, но он очень сэкономил время и силы. Он заставил меня ценить теперь взгляды NeHe гораздо больше. Спасибо всем.
Использование списков отображения
Display Lists
В этом уроке я научу вас, как использовать Списки Отображения (Display Lists). Использование списков отображения не только ускоряет ваш код, списки также позволяют уменьшить количество строк, которые необходимо написать для создания простой GL сцены.
Например. Скажем, вы создаете в игре астероиды. Каждый уровень начинается как минимум с двух астероидов. Итак, вы сидите с вашей графической бумагой ( :)) ) и рисуете, как можно создать 3D астероиды. После того, как вы все нарисовали, вы создаете астероид в OpenGL, используя полигоны или четырехугольники. Скажем астероид - это октагон (восемь сторон). Если вы умны, вы создаете цикл, и рисуете астероид один раз в цикле. Вы сделаете это, написав 18 или более небрежных строк кода, чтобы создать астероид. Создание астероида, перерисовка его на экран трудоемка для вашей системы. Когда вы возьмете более сложные объекты вы увидите, что я имел ввиду.
Ну и какое решение? Списки Отображения!!! Используя список отображения, вы создаете объект лишь один раз. Вы можете текстурировать его, раскрасить его, все что хотите. Вы даете списку название. Поскольку это астероид мы назовем список 'asteroid'. Теперь, всякий раз, когда я хочу нарисовать текстурированный/раскрашенный астероид на экране, все, что мне требуется сделать это вызов glCallList(asteroid). Предварительно созданный астероид немедленно появится на экране. Так как астероид уже создан в списке отображения, не нужно указывать OpenGL, как создать его. Он уже создан в памяти. Это снижает нагрузку на процессор и позволяет вашим программам выполняться быстрее.
Итак, вы готовы учиться? :). Мы назовем эту демонстрацию списков отображения Q- Bert. В этой демке мы создадим 15 кубиков на экране Q-Bert'а. Каждый кубик изготавливается из крышки (TOP) и коробки (BOX). Крышка будет отдельным списком отображения, так мы сможем закрасить ее в более темные цвета (как тень). Коробка - это куб без крышки :).
Этот код основан на уроке 6. Я перепишу большую часть программы - так будет легче видеть, где я внес изменения. Следующие строки - код, стандартно используемый во всех уроках.
#include <windows.h> //Заголовочный файл для Windows
#include <stdio.h> //Заголовочный файл стандартного ввода/вывода
#include <gl\gl.h> //Заголовочный файл библиотеки OpenGL32
#include <gl\glu.h> //Заголовочный файл библиотеки GLu32
#include <gl\glaux.h> //Заголовочный файл библиотеки GLaux
HDC hDC=NULL; //Приватный контекст GDI устройства
HGLRC hRC=NULL; //Постоянный контекст отображения
HWND hWnd=NULL; //Содержит дискриптор нашего окна
HINSTANCE hInstance; //Содержит экземпляр приложения
bool keys[256]; //Массив применяемый для подпрограммы клавиатуры
bool active=TRUE; //Флаг "Активное" окна устанавливается истинным (TRUE)
// по умолчанию.
bool fullscreen=TRUE; //Флаг "На полный экран" устанавливается в полноэкранный
// режим по умолчанию.
Теперь объявим наши переменные. Первым делом зарезервируем место для одной текстуры. Затем создадим две новых переменных для наших двух списков отображения. Эти переменные - указатели на списки отображения, хранящиеся в ОЗУ. Назовем их коробка (box) и крышка (top).
Затем заведем еще 2 переменные xloop и yloop, которые используем для задания позиций кубов на экране и 2 переменные xrot и yrot для вращения по осям x и y.
GLuint texture[1]; // Память для одной текстуры
GLuint box; // Память для списка отображения box (коробка)
GLuint top; // Память для второго списка отображения top (крышка)
GLuint xloop; // Цикл для оси x
GLuint yloop; // Цикл для оси y
GLfloat xrot; // Вращает куб на оси x
GLfloat yrot; // Вращает куб на оси y
Далее создаем два цветовых массива. Первый, назовем его boxcol, хранит величины для следующих цветов: ярко-красного (Bright Red), оранжевого (Orange), желтого (Yellow), зеленого (Green) и голубого (Blue). Каждое значение внутри фигурных скобок представляет значения красного, зеленого и голубого цветов. Каждая группа внутри фигурных скобок - конкретный цвет.
Второй массив цветов мы создаем для следующих цветов: темно-красного (Dark Red), темно-оранжевого (Dark Orange), темно-желтого (Dark Yellow), темно-зеленого (Dark Green) и темно-голубого (Dark Blue). Темные цвета будем использовать для окрашивания крышки коробок. Мы хотим, чтобы крышка была темнее коробки.
static GLfloat boxcol[5][3]= //Массив для цветов коробки
{
//Яркие: Красный, Оранжевый, Желтый, Зеленый, Голубой
{1.0f,0.0f,0.0f},{1.0f,0.5f,0.0f},{1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f}
};
static GLfloat topcol[5][3]= //Массив для цветов верха
{
//Темные: Красный, Оранжевый, Желтый, Зеленый, Голубой
{.5f,0.0f,0.0f},{0.5f,0.25f,0.0f},{0.5f,0.5f,0.0f},{0.0f,0.5f,0.0f},{0.0f,0.5f,0.5f}
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //Объявление для WndProc
Теперь мы создадим собственно список отображения. Если вы помните, весь код для создания коробки в первом списке, а для создания верха - в другом списке. Я попытаюсь разъяснить эту часть более детально.
GLvoid BuildLists() //создаем список отображения
{
Мы начинаем, указывая OpenGL создать 2 списка. glGenLists(2) создает место для двух списков, и возвращает указатель на первый из них. 'box' будет хранить расположение первого списка. Всякий раз при вызове box будет нарисован первый список.
box=glGenLists(2); //создаем два списка
Теперь мы начнем создавать первый список. Мы уже выделили память для двух списков, и мы знаем, что box указывает на место, где мы храним первый список. Все, что нам теперь надо сделать - это сказать OpenGL, где должен начаться список, какой тип списка сделать.
Мы используем для этого команду glNewList(). Укажите box в качестве первого параметра. Этим вы укажете OpenGL место хранения списка, на которое указывает переменная box. Второй параметр GL_COMPILE говорит OpenGL, что мы хотим предварительно создать список в памяти, таким образом, OpenGL не будет вычислять, как создать объект, всякий раз как мы хотим нарисовать его.
С флагом GL_COMPILE как в программирование. Если вы пишете программу и транслируете ее компилятором вы компилируете ее всякий раз, когда хотите запустить ее. Если же она уже скомпилирована в EXE файл, все, что вам нужно сделать - это запустить ее, кликнув на файле. Нет необходимости перекомпилировать ее заново. OpenGL компилирует список отображения только один раз, после этого он готов к применению, больше компилировать не надо. Поэтому мы получаем увеличение скорости при использовании списков отображения.
glNewList(box,GL_COMPILE); // Новый откомпилированный список отображения box
В следующей части кода нарисуем коробку без верха. Она не окажется на экране. Она будет сохранена в списке отображения.
Вы можете написать любую команду, какую только захотите, между glNewList() и glEndList(). Вы можете задавать цвета, менять текстуры и т.д. Единственный тип кода, который вы не можете использовать - это код, который изменит список отображения "на лету". Однажды создав список отображения, вы не можете его изменить.
Если вы добавите строку glColor3ub(rand()%255,rand()%255,rand()%255) в нижеследующий код, вы можете подумать, что каждый раз, когда вы рисуете объект на экране, он будет другого цвета. Но так как список создается лишь однажды, цвет не будет изменяться всякий раз, как вы рисуете его на экране. Каким бы ни был цвет объекта, когда он создается первый раз, таким он и останется.
Если вы хотите изменить цвет списка отображения, вам необходимо изменить его ДО того как список отображения будет выведен на экран. Я больше расскажу об этом позже.
glBegin(GL_QUADS); // Начинаем рисование четырехугольников (quads)
// Нижняя поверхность
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f); // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f); // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); // Нижний правый угол текстуры и четырехугольник
// Передняя поверхность
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f); // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f); // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); // Верхний левый угол текстуры и четырехугольник
// Задняя поверхность
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f); // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f); // Нижний левый угол текстуры и четырехугольник
// Правая поверхность
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f); // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f); // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f); // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, 1.0f); // Нижний левый угол текстуры и четырехугольник
// Левая поверхность
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, -1.0f); // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); // Верхний правый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); // Верхний левый угол текстуры и четырехугольник
glEnd(); // Закончили рисование четырехугольников
При помощи команды glEndList() мы сказали OpenGL, что мы создали список. Все, что находится между glNewList() и glEndList - часть списка отображения, все, что находится до glNewList() или после glEndList() не является частью списка отображения.
glEndList(); // Закончили создание списка box
Теперь создадим наш второй список отображения. Чтобы найти, где список отображения хранится в памяти, мы возьмем значение старого списка отображения (box) и добавим к нему еще единицу. Нижеследующий код присваивает переменной 'top' местоположение второго списка отображения.
top=box+1; // Значение top это значение box + 1
Теперь, когда мы знаем, где хранится второй список отображения, мы можем создать его. Мы сделаем это, так же как и первый список, но в этот раз мы укажем OpenGL хранить этот список под названием 'top' в отличие от предыдущего 'box'.
glNewList(top,GL_COMPILE);// Новый откомпилированный список отображения 'top'
Следующая часть кода рисует верх коробки. Это просто четырехугольник нарисованный на плоскости z.
glBegin(GL_QUADS); // Начинаем рисование четырехугольника
// Верхняя поверхность
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f); // Верхний левый угол текстуры и четырехугольник
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); // Нижний левый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 0.0f);
glVertex3f( 1.0f, 1.0f, 1.0f); // Нижний правый угол текстуры и четырехугольник
glTexCoord2f(1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f); // Верхний правый угол текстуры и четырехугольник
glEnd(); // Заканчиваем рисование четырехугольника
Снова говорим OpenGL, что мы закончили создание списка отображения с помощью команды glEndList(). Вот. Мы успешно создали два списка отображения.
glEndList(); // Закончили создание списка отображения 'top'
}
Код создания текстур тот же, который мы использовали в предыдущих уроках для загрузки и создания текстур. Мы хотим получить текстуру, которую сможем наложить на все шесть сторон каждого куба. Я, бесспорно, буду использовать мипмаппинг, чтобы сделать действительно сглаженные текстуры. Я ненавижу глядеть на пиксели :). Текстура для загрузки называется 'cube.bmp'. Она хранится в каталоге под названием 'data'. Найдите LoadBMP и измените эту строку таким образом, чтобы она выглядела как нижеследующая строка.
if (TextureImage[0]=LoadBMP("Data/Cube.bmp")) // Загрузить картинку.
Код изменения размеров такой же, как в Уроке N6.
Только в коде инициализации есть несколько изменений. Я добавлю строку BuildList(). Это будет переход к части кода, в которой создаются списки отображения. Обратите внимание что BuildList() находится после LoagGLTextures(). Важно знать, что порядок должен быть именно таким. Сначала мы создаем текстуру, а затем создаем наши списки отображения, где уже созданные текстуру мы можем наложить на куб.
int InitGL(GLvoid)// Все настройки OpenGL начинаются здесь
{
if (!LoadGLTextures())// Переход к процедуре загрузки текстуры
{
return FALSE;// Если текстура не загружена возращает FALSE
}
BuildLists();// Переход к коду, который создает наши списки отображения
glEnable(GL_TEXTURE_2D);// Включение нанесения текстур
glShadeModel(GL_SMOOTH);// Включение гладкой закраски (smooth shading)
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);// Черный фон
glClearDepth(1.0f);// Установка буфера глубины
glEnable(GL_DEPTH_TEST);// Включение проверки глубины
glDepthFunc(GL_LEQUAL); // Тип выполняемой проверки глубины
Следующие три строки кода включают быстрое и простое освещение. Light0 предустановлен на большинстве видеоплат, и это спасет нас от трудностей установки источников освещения. После того как мы включили light0, мы включили освещение. Если light0 не работает на вашей видео плате (вы видите темноту), просто отключите освещение.
Еще одна строка GL_COLOR_MATERIAL позволяет нам добавить цвет к текстурам. Если мы, не включили закрашивание материала, текстуры всегда будут своего первоначального цвета. glColor3f(r,g,b) не будет действовать на расцвечивание. Поэтому важно включить это.
glEnable(GL_LIGHT0); // Быстрое простое освещение
// (устанавливает в качестве источника освещения Light0)
glEnable(GL_LIGHTING); // Включает освещение
glEnable(GL_COLOR_MATERIAL); // Включает раскрашивание материала
Наконец, мы устанавливаем коррекцию перспективы для улучшения внешнего вида и возвращаем TRUE, давая знать нашей программе, что инициализация прошла OK.
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Изящная коррекция перспективы
return TRUE; // Инициализация прошла OK
}
Теперь часть программы для рисования. Как обычно, я немного шизею с математики. Нет SIN и COS, но это все еще непривычно :). Мы начнем, как обычно, с очистки экрана и глубины буфера.
Затем мы привяжем текстуру к кубу. Я мог добавить эту строку в коде списка отображения, но, оставляя ее вне списка, я могу изменить текстуру, когда захочу. Если бы я добавил строку glBindTexture(GL_TEXTURE_2D, texture[0]) внутри кода списка отображения, список был бы создан с этой текстурой постоянно нанесенной на него.
int DrawGLScene(GLvoid) // Здесь мы выполняем все рисование
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очищаем экран и буфер глубины
glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбираем текстуру
Теперь немного позабавимся. Мы имеем цикл по yloop. Этот цикл используется для позиционирования кубов по оси Y (вверх и вниз). Мы хотим иметь 5 строк кубиков вверх и вниз, итак мы создадим цикл от 1 до 6 (то есть 5).
for (yloop=1;yloop
{
Создадим другой цикл по xloop. Он используется для позиционирования кубиков по оси x (слева направо). Количество кубов зависит от того, в какой они рисуются строке. В верхней строчке xloop будет изменяться только от 0 до 1 (рисование одного кубика). В следующей строке xloop изменяется от 0 до 2 (рисование 2-х кубиков) и так далее.
for (xloop=0;xloop< yloop;xloop++) // Цикл по плоскости X
{
Мы очищаем наше окно с помощью glLoadIdentity().
glLoadIdentity(); // Очистка вида
Следующая строка задает особую точку на экране. Это может сбить с толку, но не пугайтесь. На оси X происходит следующее:
Мы смещаемся вправо на 1. 4 единицы так, чтобы центр пирамиды оказался в центре экрана. Умножим xloop на 2.8 и добавим к ней 1.4 (мы умножаем на 2.8, так как кубики стоят друг на друге, 2.8 это приближенная ширина куба, когда он повернут на 45 градусов, по диагонали куба). Наконец, вычитаем yloop*1.4. Это перемещение кубов влево в зависимости от того, в какой строке он находится. Если мы не сместим влево, пирамида начнется у левого края экрана (не будет выглядеть как пирамида).
На оси Y мы вычитаем yloop из 6, иначе пирамида будет нарисована сверху вниз. Результат умножим на 2.4. Иначе кубики будут находиться друг на друге. (2.4 примерная высота куба). Затем мы отнимем 7, чтобы пирамида начиналась от низа экрана и строилась вверх.
Наконец по оси Z мы переместимся на 20 единиц в глубь экрана. Так мы добьемся того, что бы пирамида хорошо вписалась в экран.
// Размещение кубиков на экране
glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),
((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);
Теперь мы повернемся вокруг оси X. Таким образом, мы наклоним кубики вперед на 45 градусов минус 2 умноженное на yloop. Режим перспективы наклоняет кубики автоматически, и я вычитаю, чтобы скомпенсировать наклон. Не лучший способ, но это работает.
Наконец добавим xrot. Это дает нам управлять углом с клавиатуры. (Получите удовольствие, играя с ним).
После этого повернем кубики на 45 градусов вокруг оси Y, и добавим yrot чтобы управлять с клавиатуры поворотом вокруг оси Y.
glRotatef(45.0f-(2.0f*yloop)+xrot,1.0f,0.0f,0.0f); // Наклонять кубы вверх и вниз
glRotatef(45.0f+yrot,0.0f,1.0f,0.0f); // Вращать кубы вправо и влево
Далее мы выберем цвет коробки (яркий) перед тем как действительно нарисовать эту часть куба. Мы используем команду glColor3fv(), которая выполняет загрузку всех трех значений цвета (красный, зеленый, голубой). Аббревиатура 3fv означает 3 значения, числа с плавающей точкой, v указатель на массив. Цвет мы выбираем по переменной yloop-1, что дает нам разные цвета для каждого ряда кубов. Если мы используем xloop-1, мы получим различные цвета для каждого столбца.
}
if (keys[VK_DOWN]) // Была нажата стрелка вниз?
{
xrot+=0.2f; // Если так, то наклоним кубы вниз
}
Как во всех предыдущих уроках, убедимся, что заголовок на верху окна правильный.
if (keys[VK_F1]) // Была нажата кнопка F1?
{
keys[VK_F1]=FALSE;// Если так - установим значение FALSE
KillGLWindow(); // Закроем текущее окно OpenGL
// Переключим режим "Полный экран"/"Оконный"
fullscreen=!fullscreen;
// Заново создадим наше окно OpenGL
if (!CreateGLWindow("NeHe's Display List Tutorial",
640,480,16,fullscreen))
{
return 0;// Выйти, если окно не было создано
}
}
}
}
В результате этого урока вы усвоили, как работает список отображения, как его создавать, и как выводить его на экран. Списки отображения восхитительны. Не только потому, что они упрощают программирование сложных проектов, они также дают небольшое увеличение необходимое для поддержания высокой частоты обновления экрана.
Я надеюсь, вам понравился этот урок. Если у вас есть вопросы или вы чувствуете, что что-нибудь не ясно, пожалуйста, напишите мне и дайте мне знать.
Растровые шрифты
Bitmap Fonts
Добро пожаловать еще на один урок. На сей раз, я буду учить Вас, как использовать растровые шрифты. Вы можете сказать себе "разве так трудно вывести текст на экран". Если Вы когда-либо пробовали это, то вы знаете, что это не просто!
Уверен, Вы сможете запустить графический редактор, написать текст на изображении, загрузить изображение в вашу программу OpenGL, включить смешивание, затем отобразить текст на экран. Но это съедает время, конечный результат обычно выглядит расплывчатым или грубым в зависимости от типа фильтрации, который Вы использовали, и если ваше изображение имеет альфа канал, ваш текст будет в довершении прозрачным (смешанный с объектами на экране) после наложения на экран.
Если Вы когда-либо использовали Wordpad, Microsoft Word или другой текстовый процессор, Вы, возможно, заметили, сколько разных типов доступных шрифтов. В этом уроке Вы научитесь, как использовать те же самые шрифты в ваших собственных программах OpenGL. Фактически, любой шрифт, который Вы установлен на вашем компьютере, может использоваться в ваших примерах.
Растровые шрифты не только в 100 раз лучше выглядят, чем графические шрифты (текстуры). Вы можете изменять текст на лету. Не нужно делать текстуры для каждого слова или символа, которые Вы хочет вывести на экран. Только позиционируйте текст, и используйте мою удобную новую gl команду, чтобы отобразить текст на экране.
Я попробовал сделать команду настолько простой насколько это возможно. Все, что Вы должны сделать, так это, набрать glPrint ("Привет"). Это легко. Как бы то ни было, Вы можете сказать, что здесь довольно длинное введение, которое я счастлив, дать Вам в этом уроке. Мне потребуется около полутора часов, чтобы создать программу. Почему так долго? Поскольку нет почти никакой информации, доступной по использованию растровых шрифтов, если конечно Вы не наслаждаетесь кодом MFC. Чтобы сохранить по возможности полученный код как можно проще, я решил, что будет хорошо, если я напишу все это в простом для понимания коде Cи :).
Небольшое примечание, этот код применим только в Windows. Он использует функции wgl Windows, для построения шрифтов. Очевидно, Apple имеет функции agl, которые должны делать то же самое, а X имеет glx. К сожалению, я не могу гарантировать, что этот код переносим. Если кто-нибудь имеет платформо-незавизимый код для вывода шрифтов на экран, пришлите мне его, и я напишу другой урок по шрифтам.
Мы начнем с такого же кода как в уроке 1. Мы будем добавлять заголовочный файл stdio.h для стандартных операций ввода/вывода; stdarg.h для разбора текста и конвертирования переменных в текст, и, наконец math.h, для того чтобы перемещать текст по экрану, используя SIN и COS.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
#include <math.h> // Заголовочный файл для математической библиотеки ( НОВОЕ )
#include <stdarg.h> // Заголовочный файл для функций для работы с переменным
// количеством аргументов ( НОВОЕ )
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст рендеринга
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
Мы также собираемся добавить 3 новых переменных. В base будет сохранен номер первого списка отображения, который мы создаем. Каждому символу требуется собственный список отображения. Символ 'A' - 65 список отображения, 'B' - 66, 'C' - 67, и т.д. Поэтому 'A' будет сохранен в списке отображения base+65.
Затем мы добавляем два счетчика (cnt1 и cnt2). Эти счетчики будут изменяться с разной частотой, и используются для перемещения текста по экрану, используя SIN и COS. Это будет создавать эффект хаотичного движения строки текста по экрану. Мы будем также использовать эти счетчики, чтобы изменять цвет символов (но об этом чуть позже).
GLuint base; // База списка отображения для фонта
GLfloat cnt1; // Первый счетчик для передвижения и закрашивания текста
GLfloat cnt2; // Второй счетчик для передвижения и закрашивания текста
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE;// Флаг полноэкранного режима
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
В следующей секции кода происходит построение шрифта. Это наиболее трудная часть кода. Объявление 'HFONT font' задает шрифт в Windows.
Затем мы определяем base. Мы создаем группу из 96 списков отображения, используя glGenLists(96). После того, как списки отображения созданы, переменная base будет содержать номер первого списка.
GLvoid BuildFont(GLvoid) // Построение нашего растрового шрифта
{
HFONT font; // Идентификатор фонта
base = glGenLists(96); // Выделим место для 96 символов ( НОВОЕ )
Теперь позабавимся. Мы собираемся создать наш шрифт. Мы начинаем, задавая размер шрифта. Вы заметили, что это отрицательное число. Вставляя минус, мы сообщаем Windows, что надо найти нам шрифт, основанный на высоте СИМВОЛОВ. Если мы используем положительное число, мы выбираем шрифт, основанный на высоте ЯЧЕЙКИ.
font = CreateFont( -24, // Высота фонта ( НОВОЕ )
Затем мы определяем ширину ячейки. Вы увидите, что я установил ее в 0. При помощи установки значения в 0, Windows будет использовать значение по умолчанию. Вы можете поиграть с этим значением, если Вы хотите. Сделайте шрифт широким, и т.д.
0, // Ширина фонта
Угол отношения (Angle of Escapement) позволяет вращать шрифт. К сожалению, это - не очень полезная возможность. Исключая 0, 90, 180, и 270 градусов, у шрифта будет обрезаться то, что не попало внутрь невидимой квадратной границы. Угол наклона (Orientation Angle), цитируя справку MSDN, определяет угол, в десятых долях градуса, между базовой линией символа и осью X устройства. К сожалению, я не имею понятия о том, что это означает :(.
0, // Угол отношения
0, // Угол наклона
Ширина шрифта – отличный параметр. Вы можете использовать числа от 0 - 1000, или Вы можете использовать одно из предопределенных значений. FW_DONTCARE - 0, FW_NORMAL - 400, FW_BOLD - 700, и FW_BLACK - 900. Есть множество других предопределенные значений, но и эти 4 дают хорошее разнообразие. Чем выше значение, тем более толстый шрифт (более жирный).
FW_BOLD, // Ширина шрифта
Курсив, подчеркивание и перечеркивание может быть или TRUE или FALSE. Если подчеркивание TRUE, шрифт будет подчеркнут. Если FALSE то, нет. Довольно просто :).
FALSE, // Курсив
FALSE, // Подчеркивание
FALSE, // Перечеркивание
Идентификатор набора символов описывает тип набора символов, который Вы хотите использовать. Есть множество типов, и обо всех их не рассказать в этом уроке. CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET, и т.д. ANSI – тот набор, который я использую, хотя ЗНАЧЕНИЕ ПО УМОЛЧАНИЮ, вероятно, работало бы точно также.
Если Вы хотите использовать шрифт типа Webdings или Wingdings, Вы должны использовать SYMBOL_CHARSET вместо ANSI_CHARSET.
ANSI_CHARSET, // Идентификатор набора символов
Точность вывода очень важна. Этот параметр сообщает Windows какой из наборов символов использовать, если их доступно больше чем один. OUT_TT_PRECIS сообщает Windows что, если доступен больше чем один тип шрифта, то выбрать с тем же самым названием Truetype версию шрифта. Truetype шрифты всегда смотрят лучше, особенно когда Вы сделаете их большими по размеру. Вы можете также использовать OUT_TT_ONLY_PRECIS, при этом ВСЕГДА используется Truetype шрифт.
OUT_TT_PRECIS, // Точность вывода
Точность отсечения - тип отсечения, который применяется, когда вывод символов идет вне области отсечения. Об этом много говорить нечего, оставьте значение по умолчанию.
CLIP_DEFAULT_PRECIS, // Точность отсечения
Качество вывода - очень важный параметр. Вы можете выбрать PROOF, DRAFT, NONANTIALIASED, DEFAULT или ANTIALIASED. Всем известно, что при ANTIALIASED шрифты выглядят отлично :). Сглаживание (Antialiasing) шрифта – это тот же самый эффект, который Вы получаете, когда Вы включаете сглаживание шрифта в Windows. При этом буквы выглядят менее ступенчато.
ANTIALIASED_QUALITY, // Качество вывода
Затем идут настройка шага и семейство. Для настройки шага Вы можете выбрать DEFAULT_PITCH, FIXED_PITCH и VARIABLE_PITCH, а для настройки семейства, Вы можете выбрать FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Проиграйтесь с этими константами, чтобы выяснить, что они делают. Я оставил их по умолчанию.
FF_DONTCARE|DEFAULT_PITCH, // Семейство и шаг
Наконец... Фактическое название шрифта. Загрузите Microsoft Word или другой текстовый редактор. Щелчок по шрифту - выпадет вниз меню, и ищите шрифт, который Вам нравится. Чтобы его использовать, замените 'Courier New' на название шрифта, который Вы хотите использовать.
"Courier New"); // Имя шрифта
Теперь мы выберем шрифт, привязав его к нашему DC, и построим 96 списков отображения, начиная с символа 32 (который является пробелом). Вы можете построить все 256 символов, если Вы хотите. Проверьте, что Вы удаляете все 256 списков отображения, когда Вы выходите из программы, и проверьте, что Вы задаете вместо 32 значение 0 и вместо 96 значение 255 в строке кода ниже.
SelectObject(hDC, font); // Выбрать шрифт, созданный нами ( НОВОЕ )
wglUseFontBitmaps(hDC, 32, 96, base); // Построить 96 символов начиная с пробела ( НОВОЕ )
}
Следующий код очень прост. Он удаляет 96 списков отображения из памяти, начиная с первого списка, заданного 'base'. Я не уверен, что windows сделала бы это за Вас, поэтому лучше быть осмотрительным, чем потом жалеть :).
GLvoid KillFont(GLvoid) // Удаление шрифта
{
glDeleteLists(base, 96); // Удаление всех 96 списков отображения ( НОВОЕ )
}
Теперь моя удобная первоклассная функция вывода текста GL. Вы вызываете этот раздел кода по команде glPrint ("здесь сообщение"). Текст находится в строке символов *fmt.
GLvoid glPrint(const char *fmt, ...) // Заказная функция «Печати» GL
{
В первой строке кода ниже выделяется память для строки на 256 символов. text – это строка, которую мы хотим напечатать на экране. Во второй строке ниже создается указатель, который указывает на список параметров, которые мы передаем наряду со строкой. Если мы посылаем переменные вместе с текстом, она укажет на них.
char text[256]; // Место для нашей строки
va_list ap; // Указатель на список аргументов
В следующих двух строках кода проверяется, если, что-нибудь для вывода? Если нет текста, fmt не будет равняться ничему (NULL), и ничего не будет выведено на экран.
if (fmt == NULL) // Если нет текста
return; // Ничего не делать
va_start(ap, fmt); // Разбор строки переменных
vsprintf(text, fmt, ap); // И конвертирование символов в реальные коды
va_end(ap); // Результат помещается в строку
Затем мы проталкиваем в стек GL_LIST_BIT, это защищает другие списки отображения, которые мы можем использовать в нашей программе, от влияния glListBase.
Команду glListBase(base-32) немного трудно объяснить. Скажем, что мы рисуем символ 'A', он задан номером 65. Без glListBase(base-32) OpenGL, не понял бы, где найти этот символ. Он стал бы искать этот символ в 65 списке отображения, но если бы база была равна 1000, 'A' был бы фактически сохранен в 1065 списке отображения. Поэтому при помощи, установки базовой отправной точки, OpenGL знает, где находится нужный список отображения. Причина, по которой мы вычитаем 32, состоит в том, что мы не сделали первые 32 списка отображения. Мы опустили их. Так что мы должны указать OpenGL про это, вычитая 32 из базового значения. Символы, кодируются, начиная с нуля, код символа пробела имеет значение 32, поэтому если, мы хотим вывести пробел, то мы должны иметь тридцать второй список отображения, а у нас он нулевой. Поэтому мы искусственно занижаем значение базы, с тем, чтобы OpenGL брал нужные списки. Я надеюсь, что это понятно.
glPushAttrib(GL_LIST_BIT); // Протолкнуть биты списка отображения ( НОВОЕ )
glListBase(base - 32); // Задать базу символа в 32 ( НОВОЕ )
Теперь, когда OpenGL знает, где находятся Символы, мы можем сообщить ему, что пора выводить текст на экран. glCallLists - очень интересная команда. Она может вывести больше, чем один список отображения на экран одновременно.
В строке ниже делает следующее. Сначала она сообщает OpenGL, что мы собираемся показывать на экране списки отображения. При вызове функции strlen(text) вычисляется, сколько символов мы собираемся отобразить на экране. Затем необходимо указать максимальное значение посылаемых символов. Мы не посылаем больше чем 255 символов. Так что мы можем использовать UNSIGNED_BYTE. (Вспомните, что байт - любое значение от 0 - 255). Наконец мы сообщаем, что надо вывести, передачей строки 'text'.
Возможно, Вы задаетесь вопросом, почему символы не наезжают друг на друга. Каждый список отображения для каждого символа знает, где правая сторона у символа. После того, как символ выведен, OpenGL переходит на правую сторону выведенного символа. Следующий символ или выведенный объект будут выведены, начиная с последнего места вывода GL, которое будет справа от последнего символа.
Наконец мы возвращаем настройки GL GL_LIST_BIT обратно, как было прежде, чем мы установили нашу базу, используя glListBase(base-32).
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);// Текст списками отображения(НОВОЕ)
glPopAttrib(); // Возврат битов списка отображения ( НОВОЕ )
}
В коде Init изменилось только одно: добавлена строчка BuildFont(). Она вызывает код выше, для построения шрифта, чтобы OpenGL мог использовать его позже.
int InitGL(GLvoid) // Все начальные настройки OpenGL здесь
{
glShadeModel(GL_SMOOTH); // Разрешить плавное затенение
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Установка буфера глубины
glEnable(GL_DEPTH_TEST); // Разрешение теста глубины
glDepthFunc(GL_LEQUAL); // Тип теста глубины
// Действительно хорошие вычисления перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
BuildFont(); // Построить шрифт
return TRUE; // Инициализация окончена
}
Теперь о коде для отрисовки. Вначале мы, очищаем экран и буфер глубины. Мы вызываем glLoadIdentity() чтобы все сбросить. Затем мы перемещаемся на одну единицу вглубь экрана. Если не сделать перемещения, текст не будет отображен. Растровые шрифты лучше применять с ортографической проекцией, а не с перспективной, но ортографическая проекция выглядит плохо, поэтому, когда работаем в этой проекции, перемещаем.
Вы можете заметить, что, если бы Вы переместили текст даже вглубь экрана, размер шрифта не уменьшиться, как бы Вы этого ожидали. Что реально происходит, когда Вы глубже перемещаете текст, то, что Вы имеете возможность контролировать, где текст находится на экране. Если Вы переместили на 1 единицу в экран, Вы можете расположить текст, где-нибудь от -0.5 до +0.5 по оси X. Если Вы переместите на 10 единиц в экран, то Вы должны располагать текст от -5 до +5. Это даст Вам возможность лучше контролировать точное позиционирование текста, не используя десятичные разряды. При этом размер текста не измениться. И даже с помощью glScalef(x,y,z). Если Вы хотите шрифт, больше или меньше, сделайте его большим или маленьким во время его создания!
int DrawGLScene(GLvoid) // Здесь мы будем рисовать все
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экран и буфера глубины
glLoadIdentity(); // Сброс просмотра
glTranslatef(0.0f,0.0f,-1.0f); // Передвижение на одну единицу вглубь
Теперь мы воспользуемся нестандартными вычислениями, чтобы сделать цветовую пульсацию. Не волнуйтесь, если Вы не понимаете то, что я делаю. Я люблю пользоваться множеством переменных, и дурацкими уловками, чтобы достигнуть результата :).
На этот раз, я использую два счетчика, которые мы создали для перемещения текста по экрану, и для манипулирования красным, зеленым и синим цветом. Красный меняется от -1.0 до 1.0 используя COS и счетчик 1. Зеленый меняется от -1.0 до 1.0 используя SIN и счетчик 2. Синий меняется от 0.5 до 1.5 используя COS и счетчики 1 + 2. Тем самым синий никогда не будет равен 0, и текст не должен никогда полностью исчезнуть. Глупо, но это работает :).
// Цветовая пульсация, основанная на положении текста
glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)));
Теперь новая команда. glRasterPos2f(x,y) будет позиционировать растровый шрифт на экране. Центр экрана как прежде в 0,0. Заметьте, что нет координаты Z. Растровые шрифты используют только ось X (лево/право) и ось Y (вверх/вниз). Поскольку мы перемещаем на одну единицу в экран, левый край равен -0.5, и правый край равен +0.5. Вы увидите, что я перемещаю на 0.45 пикселей влево по оси X. Это устанавливает текст в центр экрана. Иначе он было бы правее на экране, потому что текст будет выводиться от центра направо.
Нестандартные вычисления делают в большой степени то же самое, как и при вычислении цвета. Происходит перемещение текста по оси X от -0.50 до -0.40 (вспомните, мы вычли справа от начала 0.45). При этом текст на экране будет всегда. Текст будет ритмично раскачиваться влево и вправо, используя COS и счетчик 1. Текст будет перемещаться от -0.35 до +0.35 по оси Y, используя SIN и счетчик 2.
// Позиционирование текста на экране
glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2)));
Теперь моя любимая часть... Реальный вывод текста на экран. Я попробовал сделать его очень простым, и крайне дружелюбным способом. Вы увидите, что вывод текста выглядит как большинство команд OpenGL, при этом в комбинации с командой Print, сделанной на старый добрый манер :). Все, что Вам надо сделать, чтобы ввести текст на экран - glPrint ("{любой текст, который Вы хотите}"). Это очень просто. Текст будет выведен на экран точно в том месте, где Вы установили его.
Shawn T. прислал мне модифицированный код, который позволяет glPrint передавать переменные для вывода на экран. Например, Вы можете увеличить счетчик и отобразить результат на экране! Это работает примерно так... В линии ниже Вы увидите нормальный текст. Затем идет пробел, тире, пробел, затем "символ" (%7.2f). Посмотрев на %7.2f Вы можете сказать, что эта рогулька означает. Все очень просто. Символ % - подобен метке, которая говорит, не печатать 7.2f на экран, потому что здесь будет напечатано значение переменной. При этом 7 означает, что максимум 7 цифр будут отображены слева от десятичной точки. Затем десятичная точка, и справа после десятичной точки - 2. 2 означает, что только две цифры будут отображены справа от десятичной точки. Наконец, f. f означает, что число, которое мы хотим отобразить - число с плавающей запятой. Мы хотим вывести значение cnt1 на экран. Предположим, что cnt1 равен 300.12345f, окончательно мы бы увидели на экране 300.12. Цифры 3, 4, и 5 после десятичной точки были бы обрезаны, потому что мы хотим, чтобы появились только 2 цифры после десятичной точки.
Конечно, если Вы профессиональный программист на Си, то, это ненужный рассказ, но этот урок могут читать люди, которые и не использовали printf. Если Вы хотите больше узнать о маркерах, купите книгу, или посмотрите MSDN.
glPrint("Active OpenGL Text With NeHe - %7.2f", cnt1); // Печать текста GL на экран
И в завершении увеличим значение обоих счетчиков на разную величину, чтобы была цветовая пульсация и передвижение текста.
cnt1+=0.051f; // Увеличение первого счетчика
cnt2+=0.005f; // Увеличение второго счетчика
return TRUE; // Все отлично
}
Также необходимо добавить KillFont() в конец KillGLWindow() как, показано ниже. Важно добавить эту строку. При этом списки отображения очищаются прежде, чем мы выходим из нашей программы.
if (!UnregisterClass("OpenGL",hInstance)) // Если класс не зарегистрирован
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Установить копию приложения в ноль
}
KillFont(); // Уничтожить шрифт
}
Вот и все... Все, что Вы должны знать, чтобы использовать растровые шрифты в ваших собственных проектах OpenGL. Я поискал в сети подобный материал, и ничего похожего не нашел. Возможно мой сайт первый раскрывает эту тему на простом понятном коде Cи? Возможно. Получайте удовольствие от этого урока, и счастливого кодирования!
Векторные шрифты
Outline Fonts
Этот урок представляет собой продолжение предыдущего, 13-го урока, в котором я показал вам, как использовать растровые шрифты, и будет посвящен векторным шрифтам.
Путь, которым мы пойдем при построения векторного шрифта, подобен тому, как мы строили растровый шрифт в уроке 13. И все-таки:
Векторные шрифты во сто раз круче: вы можете менять их размер, перемещать по экрану в трехмерной проекции и они при этом могут быть объемными! Никаких плоских двумерных букв. В качестве трехмерного шрифта для OpenGL- приложения вы сможете использовать любой шрифт, имеющийся на вашем компьютере. И, наконец, - собственные нормали, помогающие так мило зажечь буквы, что просто приятно смотреть, как они светятся :).
Небольшое замечание - приведенный код предназначен для Windows. Для построения шрифтов в нем используются WGL-функции из набора Windows API. Очевидно, Apple имеет в AGL поддержку тех же вещей, и X - в GLX. К сожалению, я не могу гарантировать переносимость этого кода. Если кто-нибудь имеет независимый от платформы код для отрисовки шрифтов на экране, пришлите их мне и я напишу другое руководство.
Начнем со стандартного кода из урока номер 1. Добавим STDIO.H для стандартных операций ввода-вывода; STDARG.H - для разбора текста и конвертации в текст числовых переменных, и, MATH.H - для вызова функций SIN и COS, которые понадобятся, когда мы захотим вращать текст на экране :).
#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; // Копия нашего приложения
Теперь добавим 2 новые переменные. Переменная base будет содержать номер первого списка отображения ('display list' по-английски, представляет собой последовательность команд OpenGL, часто выполняемую и хранящуюся в специальном формате, оптимизированном для скорости выполнения - прим. перев.), который мы создадим. Каждому символу требуется свой список отображения. Например, символ 'A' имеет 65-й номер списка отображения, 'B' - 66, 'C' - 67, и т.д. То есть символ 'A' будет хранится в списке отображения base+65.
Кроме этой добавим переменную rot. Она будет использоваться при вычислениях для вращения текста на экране через функции синуса и косинуса, И еще она будет использована при пульсации цветов.
GLuint base; // База отображаемого списка для набора символов (добавлено)
GLfloat rot; // Используется для вращения текста (добавлено)
bool keys[256]; // Массив для манипуляций с клавиатурой
bool active=TRUE; // Флаг активности окна, по умолчанию=TRUE
bool fullscreen=TRUE; // Флаг полноэкранного режима, по умолчанию=TRUE
GLYPHMETRICSFLOAT gmf[256] будет содержать информацию о местоположении и ориентации каждого из 256 списков отображения нашего векторного шрифта. Чтобы получить доступ к нужной букве просто напишем gmf[num], где num - это номер списка отображения, соответствующий требуемой букве. Позже в программе я покажу вам, как узнать ширину каждого символа для того, чтобы вы смогли автоматически центрировать текст на экране. Имейте в виду, что каждый символ имеет свою ширину. Метрика шрифта (glyphmetrics) на порядок облегчит вам жизнь.
GLYPHMETRICSFLOAT gmf[256]; // Массив с информацией о нашем шрифте
LRESULT CALLBACK WndProc(
HWND, UINT, WPARAM, LPARAM); // Объявление оконной процедуры
Следующая часть программы создает шрифт так же, как и при построении растрового шрифта. Как и в уроке №13, это место в программе было для меня самым трудным для объяснения.
Переменная HFONT font будет содержать идентификатор шрифта Windows.
Далее заполним переменную base, создав набор из 256-ти списков отображения, вызвав функцию glGenLists(256). После этого переменная base будет содержать номер первого списка отображения.
GLvoid BuildFont(GLvoid) // Строим растровый шрифт
{
HFONT font; // Идентификатор шрифта Windows
base = glGenLists(256); // массив для 256 букв
Теперь будет интереснее :). Делаем наш векторный шрифт. Во-первых, определим его размер. В приведенной ниже строке кода вы можете заметить отрицательное значение. Этим минусом мы говорим Windows, что наш шрифт основан на высоте СИМВОЛА. А положительное значение дало бы нам шрифт, основанный на высоте ЗНАКОМЕСТА.
font = CreateFont( -12, // высота шрифта
Определим ширину знакоместа. Вы видите, что ее значение установлено в ноль. Этим мы заставляем Windows взять значение по умолчанию. Если хотите, поиграйтесь с этим значением, сделайте, например, широкие буквы.
0, // ширина знакоместа
Угол отношения (Angle of Escapement) позволяет вращать шрифт. Угол наклона (Orientation Angle), если сослаться на 'MSDN help', определяет угол, в десятых долях градуса, между базовой линией символа и осью Х экрана. Я, к сожалению, не имею идей насчет того, зачем это :(.
0, //Угол перехода
0, //Угол направления
Ширина шрифта - важный параметр. Может быть в пределах от 0 до 1000. Также можно использовать предопределенные значения: FW_DONTCARE = 0, FW_NORMAL = 400, FW_BOLD = 700 и FW_BLACK = 900. Таких значений много, но эти дают наилучший результат. Понятно, что чем выше значение, тем толще (жирнее) шрифт.
FW_BOLD, //Ширина шрифта
Параметры Italic, Underline и Strikeout (наклонный, подчеркнутый и зачеркнутый) могут иметь значения TRUE или FALSE. Хотите, к примеру, чтобы шрифт был подчеркнутым, в параметре подчеркнутости поставьте значение TRUE, не хотите - FALSE. Просто :).
FALSE, // Курсив
FALSE, // Подчеркивание
FALSE, // Перечеркивание
Идентификатор набора символов определяет, соответственно, набор символов (кодировку), который мы хотим использовать. Их, конечно, очень много: CHINESEBIG5_CHARSET, GREEK_CHARSET, RUSSIAN_CHARSET, DEFAULT_CHARSET и т.д. Я, к примеру, использую ANSI. Хотя, DEFAULT тоже может подойти. Если интересуетесь такими шрифтами, как Webdings или Wingdings, вам надо использовать значение SYMBOL_CHARSET вместо ANSI_CHARSET.
ANSI_CHARSET, //Идентификатор кодировки
Точность вывода - тоже важный параметр. Он говорит Windows о том, какой тип символа использовать, если их более одного. Например, значение OUT_TT_PRECIS означает, что надо взять TRUETYPE - версию шрифта.Truetype - шрифт обычно смотрится лучше, чем другие, особенно когда буквы большие. Можно также использовать значение OUT_TT_ONLY_PRECIS, которое означает, что всегда следует брать, если возможно, шрифт TRUETYPE.
OUT_TT_PRECIS, // Точность вывода
Точность отсечения - этот параметр указывает вид отсечения шрифта при попадании букв вне определенной области. Сказать о нем больше нечего, просто оставьте его по умолчанию.
IP_DEFAULT_PRECIS, //Точность отсечения
Качество вывода - очень важный параметр. Можете поставить PROOF, DRAFT, NONANTIALIASED, DEFAULT или ANTIALIASED. Мы с вами знаем, что ANTIALIASED будет лучше смотреться :). Сглаживание (Antialiasing) шрифта - это эффект, позволяющий сгладить шрифт в Windows. Он делает вид букв менее ступенчатым.
ANTIALIASED_QUALITY,// Качество вывода
Следующими идут значения семейства (Family) и шага (Pitch). Шаг может принимать значения DEFAULT_PITCH, FIXED_PITCH и VARIABLE_PITCH. Семейство может быть FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE. Поиграйте этими значениями, чтобы понять, что они делают. Здесь оба параметра установлены по умолчанию.
FF_DONTCARE|DEFAULT_PITCH, // Семейство и Шаг
И последнее... Нам нужно настоящее имя используемого нами шрифта. Его можно увидеть, например в Word при выборе шрифта для текста. Зайдите в Word или другой текстовый редактор, выберите шрифт, который требуется, запомните, как он называется, и вставьте его название вместо значения 'Comic Sans MS' в приведенной ниже строке.
"Comic Sans MS"); // Имя шрифта
Теперь выберем этот шрифт, связав его с нашим контекстом устройста (DC).
SelectObject(hDC, font); //Выбрать шрифт, созданный нами
Подошло время добавить новые строки в программу. Мы построим наш векторный шрифт, используя новую команду - wglUseFontOutlines. Выберем контекст устройства (DC), начальный символ, количество создаваемых символов, и базовое значение списка отображения. Все очень похоже на то, как мы строили растровый шрифт.
wglUseFontOutlines( hDC, // Выбрать текущий контекст устройства (DC)
0, // Стартовый символ
255, // Количество создаваемых списков отображения
base, // Стартовое значение списка отображения
Но это еще не все. Здесь мы установим уровень отклонения. Уже при значении 0.0f сглаживание будет видимо. После установки отклонения идет определение толщины шрифта (ширины по оси Z). Толщина, равная 0.0f, понятно, даст нам плоские буквы, то есть двумерные. А вот при значении 1.0f уже будет виден некоторый объем.
Параметр WGL_FONT_POLYGONS говорит OpenGL создать "твердый" шрифт при помощи полигонов. Если вместо него вы укажете WGL_FONT_LINES, шрифт будет каркасным, или "проволочным" (составленным из линий). Стоит заметить, если вы укажете значение GL_FONT_LINES, не будут сгенерированы нормали, что сделает невозможным свечение букв.
Последний параметр, gmf указывает на буфер адреса для данных списка отображения.
0.0f, //Отклонение от настоящего контура
0.2f, //Толщина шрифта по оси Z
WGL_FONT_POLYGONS, //Использовать полигоны, а не линии
gmf), //буфер адреса для данных списка отображения
}
Следующий код очень простой. Он удаляет 256 списков отображения из памяти, начиная с первого списка, номер которого записан в base. Не уверен, что Windows сделает это за нас, поэтому лучше позаботиться самим, чем потом иметь глюки в системе.
GLvoid KillFont(GLvoid) // Удаление шрифта
{
glDeleteLists(base, 256); // Удаление всех 256 списков отображения
}
Теперь функция вывода текста в OpenGL. Ее мы будем вызывать не иначе как glPrint("Выводимый текст"), точно также, как и в варианте с растровым шрифтом из урока N 13. Текст при этом помещается в символьную строку fmt.
GLvoid glPrint(const char *fmt, ...) // Функция вывода текста в OpenGL
{
В первой строке, приведенной ниже, объявляется и инициализируется переменная length. Она будет использована при вычислении того, какой из нашей строки выйдет текст. Во второй строке создается пустой массив для текстовой строки длиной в 256 символов. text - строка, из которой будет происходить вывод текста на экран. В третьей строке описывается указатель на список аргументов, передаваемый со строкой в функцию. Если мы пошлем с текстом какие-нибудь переменные, этот указатель будет указывать на них.
float length=0; // Переменная для нахождения
// физической длины текста
char text[256]; // Здесь наша строка
va_list ap; // Указатель на переменный список аргументов
Проверим, а может нет никакого текста :). Тогда выводить нечего, и - return.
if (fmt == NULL) // Если нет текста,
return; // ничего не делаем
В следующих трех строчках программа конвертирует любые символы в переданной строке в представляющие их числа. Проще говоря, мы можем использовать эту функцию, как С-функцию printf. Если в переданном постоянном аргументе мы послали программе строку форматирования, то в этих трех строках она будет читать переменный список аргументов-переменных и в соответствии со строкой форматирования преобразовать их в конечный понятный для программы текст, сохраняемый в переменной text. Подробности идут дальше.
va_start(ap, fmt); // Анализ строки на переменные
vsprintf(text, fmt, ap); // И конвертация символов в реальные коды
va_end(ap); // Результат сохраняется в text
Стоит сказать отдельное спасибо Джиму Вильямсу (Jim Williams) за предложенный ниже код. До этого я центрировал текст вручную, но его метод немного лучше :). Начнем с создания цикла, который проходит весь текст символ за символом. Длину текста вычисляем в выражении strlen(text). После обработки данных цикла (проверки условия завершения) идет его тело, где к значению переменной lenght добавляется физическая ширина каждого символа. После завершения цикла величина, находящаяся в lenght, будет равна ширине всей строки. Так, если мы напечатаем при помощи данной функции слово "hello", и по каким-то причинам каждый символ в этом слове будет иметь ширину ровно в 10 единиц, то, понятно, что в итоге у нас получится значение lenght=10+10+10+10+10=50 единиц.
Значение ширины каждого символа получается из выражения gmf[text[loop]].gmfCellIncX. Помните, что gmf хранит информацию о каждом списке отображения. Если loop будет равна 0, то text[loop] - это будет первый символ нашей с вами строки. Соответственно, при loop, равной 1, то text[loop] будет означать второй символ этой же строки. Ширину символа дает нам gmfCellIncX. На самом деле gmfCellIncX - это расстояние, на которое смещается вправо позиция графического вывода после отображения очередного символа для того, чтобы символы не наложились друг на друга. Так уж вышло, что это расстояние и наша ширина символа - одно и то же значение.
Высоту символа можно узнать при помощи команды gmfCellIncY. Это может пригодиться при выводе вертикального текста.
for (unsigned int loop=0;loop//Цикл поиска размера строки
{
length+=gmf[text[loop]].gmfCellIncX;
// Увеличение размера на ширину символа
}
Полученную величину размера строки преобразуем в отрицательную (потому что мы будем перемещаться влево от центра для центровки текста). Затем мы делим длину на 2: нам не нужно перемещать весь текст влево - только его половину.
glTranslatef(-length/2,0.0f,0.0f); //Центровка на экране нашей строки
Дальше мы сохраним в стеке значение GL_LIST_BIT, это сохранит glListBase от воздействия любых других списков отображения, используемых в нашей программе.
Команда glListBase(base) говорит OpenGL, где искать для каждого символа соответствующий список отображения.
glPushAttrib(GL_LIST_BIT); // Сохраняет в стеке значения битов списка отображения
glListBase(base); // Устанавливает базовый символ в 0
Теперь, когда OpenGL знает расположение всех символов, мы можем сказать ей написать текст на экране. glCallLists выводит всю строку текста на экран сразу, создавая для вас многочисленные списки отображения.
Строка, приведенная ниже, делает следующее. Сначала она сообщает OpenGL, где на экране будут находиться списки отображения. strlen(text) находит количество букв, которые мы хотим послать на экран. Далее ему потребуется знать, какой наибольший будет номер списка из подготавливаемых к посылке. Пока что мы не посылаем более 255 символов. Так что мы можем использовать значение GL_UNSIGNED_BYTE (байт позволяет хранить целые числа от 0 до 255). Наконец, мы ему скажем, что отображать при помощи передачи строки text.
На тот случай, если вас удивит, что буквы не сваливаются в кучу одна над другой. Каждый список отображения каждого символа знает, где находится правая сторона предыдущего символа. После того, как буква отобразилась на экране, OpenGL перемещает вывод к правой стороне нарисованной буквы. Следующая буква или объект рисования начнет рисоваться с последней позиции OpenGL после перемещения, находящейся справа от последней буквы.
Наконец, мы восстанавливаем из стека GL_LIST_BIT - установки OpenGL обратно по состоянию на тот момент. Как они были перед установкой базовых значений командой glCallLists(base).
// Создает списки отображения текста
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib(); // Восстанавливает значение Display List Bits
}
Участок программы, отвечающий за размеры в окне OpenGL, точно такой же, как и в уроке N 1, поэтому здесь мы его пропустим.
В конец функции InitGL добавилось несколько новых строк. Строка с выражением BuildFont() из 13 урока осталась прежней, вместе с новым кодом, который создает быстрое и черновое освещение. Источник света Light0 встроен в большинство видеокарт, поэтому достаточно приемлемое освещение сцены не потребует особых усилий с нашей стороны.
Еще я добавил команду glEnable(GL_Color_Material). Поскольку символы являются 3D-объектами, вам понадобится раскраска материалов (Material Coloring). В противном случае смена цвета с помощью glColor3f(r,g,b) не изменит цвет текста. Если у вас на экран выводятся кроме текста другие фигуры-объекты 3D-сцены, включайте раскраску материалов перед выводом текста и отключайте сразу после того, как текст будет нарисован, иначе будут раскрашены все объекты на экране.
int InitGL(GLvoid) // Здесь будут все настройки для OpenGL
{
glShadeModel(GL_SMOOTH); // Включить плавное затенение
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Настройка буфера глубины
glEnable(GL_DEPTH_TEST); // Разрешить проверку глубины
glDepthFunc(GL_LEQUAL); // Тип проверки глубины
// Действительно хорошие вычисления перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glEnable(GL_LIGHT0); // Включить встроенное освещение (черновое) (новая)
glEnable(GL_LIGHTING); // Разрешить освещение (новая)
glEnable(GL_COLOR_MATERIAL); // Включить раскраску материалов (новая)
BuildFont(); // Построить шрифт (добавлена)
return TRUE; // Инициализация прошла успешно
}
Теперь рисующий код. Начнем с очистки экрана и буфера глубины. Для полного сброса вызовем функцию glLoadIdentity(). Затем мы смещаемся на 10 единиц вглубь экрана. Векторный шрифт великолепно смотрится в режиме перспективы. Чем дальше в экран смещаемся, тем меньше шрифт. Чем ближе, тем шрифт больше.
Управлять векторными шрифтами можно также при помощи команды glScalef(x,y,z). Если захотите сделать буквы в два раза выше, дайте команду glScalef(1.0f,2.0f,1.0f). Значение 2.0f здесь относится к оси Y и сообщает OpenGL, что список отображения нужно нарисовать в двойной высоте. Если это значение поставить на место первого аргумента (Х), то буквы будут в два раза шире. Ну, третий аргумент, естественно, касается оси Z.
int DrawGLScene(GLvoid) // Здесь весь вывод на экран
{
// Очистка экрана и буфера глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // Сброс вида
glTranslatef(0.0f,0.0f,-10.0f); // Смещение на 10 единиц в экран
После сдвига вглубь экрана мы можем повращать текст. Следующие три строки поворачивают изображение по трем осям. Умножением переменной rot на различные значения я пытался добиться как можно более различающихся скоростей вращения.
glRotatef(rot,1.0f,0.0f,0.0f); // Поворот по оси X
glRotatef(rot*1.5f,0.0f,1.0f,0.0f); // Поворот по оси Y
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Поворот по оси Z
Теперь займемся цветовым циклом. Как обычно, использую здесь переменную-счетчик (rot). Возгорание и затухание цветов получается при помощи функций SIN и COS. Я делил переменную rot на разные числа, так что бы каждый цвет не возрастал с такой же скоростью. Результат впечатляющий.
// Цветовая пульсация основанная на вращении
glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)),
1.0f-0.5f*float(cos(rot/17.0f)));
Моя любимая часть... Запись текста на экран. Мною были использованы несколько команд, которые мы применяли также при выводе на экран растровых шрифтов. Сейчас вы уже знаете, как вывести текст в команде glPrint("Ваш текст"). Это так просто!
В коде, приведенном ниже, мы печатаем "NeHe", пробел, тире, пробел и число из переменной rot и разделенное на 50, чтобы немного его уменьшить. Если число больше 999.99, разряды слева игнорируются (так как в команде мы указали 3 разряда на целую часть числа и 2 - на дробную после запятой).
glPrint("NeHe - %3.2f",rot/50); // Печать текста на экране
Затем увеличиваем переменную rot для дальнейшей пульсации цвета и вращения текста.
rot+=0.5f; // Увеличить переменную вращения
return TRUE; // Все прошло успешно
}
И последняя вещь, которую мы должны сделать, это добавить строку KillFont() в конец функции KillGLWindow(), так как я это сделал ниже. Очень важно это сделать. Она очистит все, что касалось шрифта, прежде чем мы выйдем из нашей программы.
if (!UnregisterClass("OpenGL",hInstance))// Если класс незарегистрирован
{
MessageBox(NULL,"Could Not Unregister Class.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Установить копию приложения в ноль
}
KillFont(); // Уничтожить шрифт
Под конец урока вы уже должны уметь использовать векторные шрифты в ваших проектах, использующих OpenGL. Как и в случае с уроком N 13, я пытался найти в сети подобные руководства, но, к сожалению, ничего не нашел. Возможно мой сайт - первый в раскрытии данной темы в подробном рассмотрении для всех, понимающих язык Си? Читайте руководство и удачного вам программирования!