Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #98, ФЕВРАЛЬ 2007 г.

Х-лаба #1

Михаил Фленов

Хакер, номер #098, стр. 126


(www.vr-online.ru)

Создание изображений с использованием библиотеки OpenGL

Сегодня в самом первом выпуске нашего революционного Х-проекта :) мы рассмотрим лабу, которую задают на третьем курсе факультета прикладной математики в МАИ, (спецкурс «Компьютерная графика»). Я немного усложнил задание, так что читай - будет интересно.

Тема: Создание изображений с использованием библиотеки OpenGL.

Задание: аппроксимировать заданную поверхность полигональной сеткой и средствами OpenGL обеспечить для нее возможность

  • а) изображения в параллельной и перспективной проекции,
  • б) удаления невидимых линий и поверхностей,
  • в) реалистичного освещения,
  • г) каркасного изображения,
  • д) пространственных аффинных преобразований.

Исходные данные, определяющие поверхность, должны считываться из текстового файла. Формат представления исходных данных разрабатывается студентами самостоятельно.

В зависимости от номера студента в группе предоставляются на выбор следующие поверхности: сфера с вырезами, конус с вырезами, цилиндр с вырезами и т.д.

От редактора

Александр «Dr.Klouniz» Лозовский

Ура, товарищи! То, о чем так долго твердили большевики, меньшевики, эсеры, троцкисты и сталинисты, свершилось! Наверное, ты догадываешься, что члены коллектива нашего журнала были или являются весьма разгильдяйскими студентами (в основном в этом виновата злокачественная непосещаемость лекций и семинаров :)). Долгие годы проблем со сдачей зачетов, лаб и коллоквиумов наполнили наши сердца состраданием к братьям-студентам, поэтому идея сделать подрубрику «Х-лаба» пришла сама собой. Теперь ты можешь прислать нам свое задание, а мы решим его (обрати внимание, что слать свои просьбы и требования надо не на ящик Фленова, а на наш, специально для этого сделанный). Точнее, не только решим, но и опубликуем решение в нашем журнале. Итак, внимание, первый наш клиент – студент МАИ, Кислицын Никита ;).

Усложняем

Что такое «аппроксимировать поверхность»? Если посмотреть на фигуры, которые нам предлагают, то видно, что все они имеют форму с изгибами. Невозможно создать в компьютерной графике сферу, можно только рисовать точки и линии, а окружности создаются с помощью большого количества линий (трехмерные объекты - из треугольников). Чем больше линий, тем более округлой будет получаться форма объекта. Аппроксимировать означает создать объект, максимально приближенный к реальному. А насколько приближенным его нужно сделать в этом задании? Ладно, выберем степень соответствия на свое усмотрение.

Данные необходимо загружать из файла, но это же серьезное упущение! Отображение должно происходить полигональным методом, поэтому какая разница, какие данные в файле – сфера, цилиндр или пышные формы Памелы Андерсон? Достаточно одному студенту выполнить задание, а все остальные должны только чуть изменить формат файла и переменные в исходнике, чтобы идентичность кода не бросалась в глаза. После этого нужно создать необходимую фигуру в 3DS Max, сохранить ее в файле, и можно считать, что задание выполнено. Мы усложним задачу и будем генерить фигуру программно.

Инициализация

Итак, давай напишем программку, которая будет динамически формировать сферу. Для начала создадим пустое Win32-приложение и сразу же добавим необходимые заголовочные файлы, а именно:

#include <gl\gl.h>

#include <math.h>

Первый заголовочный файл подключает функции OpenGL, которые нам предстоит использовать для отображения сферы. Вторая строка подключает математические функции. Так как сфера будет генериться, нам понадобятся тригонометрические функции из math.h. Теперь идем в Свойства проекта и в Установках линкера в строке Additional Dependencies добавляем библиотеку opengl32.lib. Библиотеку расширенных функций glu мы использовать не будем (да, она упростила бы создание сферы, цилиндра и т.д., но, судя по заданию, мы не имеем права обращаться к ней).

Из глобальных переменных нам понадобится hrc типа HGLRC, в которой мы будем сохранять контекст рисования OpenGL.

Теперь движемся в функцию InitInstance, где создается окно. После его создания добавляем код из листинга 1. Я думаю, стоит рассмотреть этот листинг поподробнее, поскольку его понимание необходимо для выполнения задания.

После получения контекста рисования окна, мы должны задать формат пикселя. В данном случае я использую RGBA-формат в 24 бита. Помимо этого, для повышения скорости включается двойная буферизация (флаг PFD_DOUBLEBUFFER). В исходнике, который ты найдешь на DVD, задание формата пикселя я вынес в отдельную функцию SetPixelFormat.

Далее идет создание контекста рисования OpenGL с помощью функции wglCreateContext (он будет «текущим» - wglMakeCurrent). Это стандартная операция при инициализации OpenGL.

Теперь самое интересное. В задании определена необходимость удаления невидимых линий и поверхностей. Но как это сделать? В OpenGL для этого нужно всего лишь включить тест глубины. Без него выводится все подряд и объекты, находящиеся дальше, могут оказаться в одной позиции с более близкими объектами. Чтобы включить тест глубины, пишем:

glEnable(GL_DEPTH_TEST);

Существует множество вариантов тестов глубины. Их можно задать с помощью функции glDepthFunc. В этом примере я задействую тест GL_LESS, который используется по умолчанию. Про все остальные их виды ты можешь узнать из файла справки по функции glDepthFunc.

Освещение

Освещение – это отдельная история. Согласно заданию, мы должны обеспечить реалистичное освещение. Но как это сделать? Реалистичное освещение – это целая наука, для него существует великое множество алгоритмов и методов (OpenGL в этом плане может практически все :)). Самое реалистичное, на мой взгляд, освещение можно получить только с использованием вершинных шейдеров. Но я надеюсь, что составители задания не пошли так далеко в своих запросах и не испортили нам всю жизнь, и поэтому использую освещение, предоставляемое функциями OpenGL.

Итак, чтобы в нашей сцене появился источник освещения, необходимо его создать, выбрать модель и указать его положение. В OpenGL первый пункт достаточно прост, поскольку нам уже доступны источники с именами GL_LIGHTi, где i изменяется от 0 до GL_MAX_LIGHTS. В моем заголовочном файле эта константа равна 0x0D31. Я думаю, этого будет вполне достаточно.

Мы будем использовать один источник освещения - GL_LIGHT0. Чтобы задать его положение, используем следующий код:

GLfloat position[] = { 0.0, 1, -1.5, 0.0 };

glLightfv(GL_LIGHT0, GL_POSITION, position);

В первой строке мы задаем массив из четырех чисел типа GLfloat, которые определят позицию источника освещения в нашем мире. Во второй - вызываем функцию glLightfv, которая имеет 3 параметра:

  1. источник освещения;
  2. параметр, который нужно изменить; мы будем устанавливать позицию света, поэтому указываем константу GL_POSITION;
  3. вектор (массив из четырех значений), задающий позицию. Эта позиция будет трансформирована в матрицу modelview нашего мира.

Если не задать положение источника света, то по умолчанию будет использоваться вектор (0,0,1,0). Можно еще задать тип лампочки – рассеянный свет, прожектор и т.д., но это уже дело вкуса и цвета. В задании тип света не указан.

Итак, позицию лампочки мы задали, теперь необходимо включить свет. Нет, для этого не нужно вызывать электрика-алкаша, нужно просто написать две следующие строки:

glEnable(GL_LIGHTING);

glEnable(GL_LIGHT0);

В первой строке мы разрешаем освещение в целом, а во второй - включаем источник GL_LIGHT0.

Постинсталл

Чтобы освещение работало нормально, желательно включить нормализацию:

glEnable(GL_NORMALIZE);

glEnable(GL_AUTO_NORMAL);

Несмотря на то что при построении сферы я буду рассчитывать нормаль ручками, эти 2 флага включены, чтобы ты знал об их существовании. Первый флаг разрешает нормализацию, а второй - разрешает это делать автоматом.

По заданию необходимо предоставить возможность отображения фигуры в виде каркаса. Как это сделать? Очень просто. Наш полигон будет строиться из закрашенных треугольников. Чтобы убрать закраску, можно добавить следующую строку:

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

Здесь мы включаем отображение передних и задних поверхностей полигона линиями. Теперь это не закрашенные треугольники, а линии, а значит, OpenGL нарисует только каркас.

Проекция

В задании есть пункт, который требует от нас обеспечить возможность отображения в параллельной и перспективной проекции. Для начала определимся, что же это такое - параллельная и перспективная проекция. При параллельной проекции все объекты проецируются на плоскость просмотра параллельно, без каких-либо искажений. Однако наши глаза не обладают такой широтой обзора, поэтому нам присуще перспективное зрение и такая проекция воспринимается как более естественная.

Для работы с параллельной проекцией используется функция glOrtho:

void glOrtho(GLdouble left, GLdouble right,

GLdouble bottom, GLdouble top, GLdouble near, GLdouble far)

Уже по названиям переменных можно понять, что первые четыре задают прямоугольник просмотра, а последние две - расстояние до ближней и дальней плоскостей отсечения соответственно. Все, что ближе near и дальше far, отображаться не будет.

Чтобы задать перспективную проекцию, нужно использовать функцию gluPerspective:

void gluPerspective(GLdouble angley, GLdouble aspect,

GLdouble znear, GLdouble zfar)

Первый параметр – это угол обзора по оси Y. Второй параметр – соотношение сторон, в большинстве случаев его делают равным отношению ширины окна к высоте. Далее идут расстояния до ближней и дальней плоскостей отсечения.

Вполне логично будет задавать параметры проекции по событию изменения размеров окна, ведь от этого зависят и параметры отображения. Идем в функцию WndProc и добавляем туда обработчик события WM_SIZE, код которого можно увидеть в листинге 2. В этом примере мы будем использовать параллельное проецирование. Чтобы превратить его в перспективу, замени вызов glOrtho следующей строкой:

gluPerspective(45.0f, width/height, 1.0f, 100.0f);

Эта перспектива будет иметь следующие характеристики:

  1. Угол обзора в высоту равен 45 градусам.
  2. Соотношение сторон просмотра соответствует соотношению сторон окна.
  3. Все вершины/объекты ближе 1 и больше 100 не будут видны.

Перед установкой необходимой проекции необходимо выбрать режим матрицы GL_PROJECTION, а после этого загрузить единичную матрицу с помощью функции glLoadIdentity().

Отображение

Для отображения сцены по событию WM_PAINT мы добавим вызов функции DrawScene. Гигантский размер этой функции, обусловленный генерацией в ней сферы математическим методом, не позволяет мне привести ее здесь (если бы я загружал сферу из файла, то кода получилось бы несколько меньше). Впрочем, если ты умеешь работать с файлами, адаптировать пример не составит труда.

Кстати, в процессе изучения исходника обрати внимание, что для каждого треугольника я определяю нормали, без которых освещение станет невозможным. Просто сформировать полигон мало, необходимо нормализовать его, чтобы OpenGL знал, как должен вести себя источник освещения.

Помимо этого, объекту назначается материал. В зависимости от материала поверхности изменяется и освещение. Глянцевые поверхности должны отбрасывать блики, а матовые - просто равномерно рассеивать свет. Размер блика также может отличаться в зависимости от поверхности.

Полный код примера ты найдешь на DVD. Что в нем не хватает? Пространственных преобразований и выреза. Вырез сделать программно не так уж и сложно, а вот преобразования – достаточно интересная тема, и для ее раскрытия нужна отдельная статья. Про DirectX-преобразования я писал в своих книгах «Искусство программирования игр на C++» и «Direct и С++. Искусство программирования». Вторая книга имеет вариант и для Delphi.

Зачет!

Исходный код готового примера – это, конечно, хорошо, но нужно еще уметь объяснить этот код (а еще лучше - знать предмет :)). Существует множество вариантов тестов глубины, алгоритмов освещений, и, плавая в этой теме, будь готов к тому, что знающий препод может легко поставить тебя в тупик. Так что, если ты прогулял целый семестр и не знаешь OpenGL, лучше купи книгу и наверстай упущенное. Это необходимо не только для сдачи экзаменов, но и просто для себя, ведь мы учимся не ради оценок, а ради знаний, без которых после окончания института диплом не стоит ничего.

Листинг 1

// Определение контекста рисования окна

HDC dc = GetDC(hWnd);

// Задаем формат пикселя

static PIXELFORMATDESCRIPTOR pfd = {

sizeof(PIXELFORMATDESCRIPTOR),

1,

PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,

PFD_TYPE_RGBA,

24,

0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

32,

0, 0,

PFD_MAIN_PLANE,

0, 0, 0, 0

};

// Выбираем созданный формат

int pixelFormat = ::ChoosePixelFormat(dc, &pfd);

if (pixelFormat == 0){

MessageBox(0, "ChoosePixelFormat error", "Error", MB_OK);

return FALSE;

}

// Установить формат пикселя

if (::SetPixelFormat(dc, pixelFormat, &pfd) == FALSE) {

MessageBox(0, "SetPixelFormat error", "Error", MB_OK);

return FALSE;

}

// Создаем контекст рисования OpenGL

hrc = wglCreateContext(dc);

wglMakeCurrent(dc,hrc);

// Позиция источника освещения и позиция просмотра

GLfloat position[] = { 0.0, 1, -1.5, 0.0 };

GLfloat local_view[] = { 0.0 };

// Включаем тест глубины

glEnable(GL_DEPTH_TEST);

glDepthFunc(GL_LESS);

// Включаем освещение

glLightfv(GL_LIGHT0, GL_POSITION, position);

glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);

glEnable(GL_LIGHTING);

glEnable(GL_LIGHT0);

// Включаем нормализацию

glEnable(GL_AUTO_NORMAL);

glEnable(GL_NORMALIZE);

// Раскомментировать следующую строку, чтобы отображать в каркасном виде

//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

// Задать размеры окна

MoveWindow(hWnd, 10, 10, 640, 480, TRUE);

Листинг 2

case WM_SIZE:

// Определяем размеры окна

RECT r;

GetWindowRect(hWnd, &r);

width = r.right-r.left;

height = r.bottom-r.top;

// Использовать матрицу Projection

glMatrixMode(GL_PROJECTION);

// Загрузить единичную матрицу преобразований

glLoadIdentity();

// В зависимости от размеров окна создаем параллельную проекцию

if (width <= height)

glOrtho (-2, 2, -2*height/width, 2*height/width, 2.0, -2.0);

else

glOrtho (-2*width/height, 2*width/height, -2, 2, 2.0, -2.0);

// Задаем область просмотра

glViewport(0, 0, width, height);

break;

Листинг 3

void DrawScene(HWND hWnd)

{

PAINTSTRUCT ps;

HDC hdc;

hdc = BeginPaint(hWnd, &ps);

// Сохранить матрицу преобразований

glPushMatrix();

// Настроить тест глубины

glEnable(GL_DEPTH_TEST);

glDepthFunc(GL_LESS);

glDepthFunc(GL_LEQUAL);

// Очистка буферов

glClearColor(1,0.5,0.5,1);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Вывод полигона

glBegin(GL_TRIANGLES);

// Здесь необходимо вывести координаты треугольников,

// из которых будет строиться сфера

glEnd();

// Восстановить матрицу преобразований

glPopMatrix();

// Завершить формирование буфера и переключить задний буфер

glFlush();

SwapBuffers(hdc);

EndPaint(hWnd, &ps);

}

Содержание