Е.Ю. Андиева КОМПЬЮТЕРНАЯ ГРАФИКА: ПРОГРАММИРОВАНИЕ C ИСПОЛЬЗОВАНИЕМ OPENGL Омск • 2012 3 Министерство образования и науки РФ Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «Сибирская государственная автомобильно-дорожная академия (СибАДИ)» Е. Ю. Андиева КОМПЬЮТЕРНАЯ ГРАФИКА: ПРОГРАММИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ OPENGL Учебное пособие Омск СибАДИ 2012 4 УДК 004. 92 ББК 73 А65 Рецензенты: д-р техн. наук, проф. кафедры КИАС С.Н. Чуканов (Сибирская автомобильно-дорожная академия); канд. техн. наук, доцент кафедры ИБ И.И. Семенова (Сибирская автомобильно-дорожная академия); Работа одобрена редакционно-издательским советом академии в качестве учебного пособия. Андиева Е.Ю. А65 Компьютерная графика: программирование с использованием OpenGL: учебное пособие / Е.Ю. Андиева Омск: СибАДИ, 2012. 208 с. Учебное пособие содержит теоретический материал с примерами и подробными пояснениями, а так же темы курсового проекта и краткие методические указания к его выполнению. Учебное пособие рекомендовано для изучения дисциплины «Компьютерная графика» в учебном процессе для специальностей и направлений: 230100, 230277, 080801, 090100, 090900, 090300. Табл. 1 . Ил. 36 . Библиогр.: 14 назв. ФГБОУ 2012 5 ВПО «СибАДИ», Введение История OpenGL ведется с 1992 года. Компания Silicon Graphics создала его как открытый стандарт. Отсюда и первая часть названия, а GL означает Graphic Library (графическая библиотека). На данный момент OpenGL находится под контролем комитета Architectural Review Board (ARB), куда входят представители наиболее влиятельных в 3D-секторе корпораций – nVidia, ATI, SGI, Apple, Intel, id Software. OpenGL разработан как аппаратно-независимый интерфейс для работы на различных аппаратных платформах. Эффективные реализации OpenGL существуют для Windows, Unix-платформ, PlayStation 3 и Mac OS. Эти реализации обычно предоставляются изготовителями видеоадаптеров и активно используют возможности последних. Спецификация OpenGL пересматривается Консорциумом ARB (Architecture Review Board), который был сформирован в 1992 году. Консорциум состоит из компаний, заинтересованных в создании широко распространённого и доступного API. Согласно официальному сайту OpenGL, членами ARB с решающим голосом на ноябрь 2004 года являются производители профессиональных графических аппаратных средств SGI, 3Dlabs, Matrox и Evans & Sutherland (военные приложения), производители потребительских графических аппаратных средств ATI и NVIDIA, производитель процессоров Intel, и изготовители компьютеров и компьютерного оборудования IBM, Apple, Dell, Hewlett-Packard и Sun Microsystems, а также один из лидеров компьютерной игровой индустрии id Software. Microsoft, один из основоположников Консорциума, покинула его в марте 2003 года. Помимо постоянных членов, каждый год приглашается большое количество других компаний, становящихся частью OpenGL ARB в течение одного года. Такое большое число компаний, вовлеченных в разнообразный круг интересов, позволило OpenGL стать прикладным интерфейсом широкого назначения с большим количеством возможностей. Авторами оригинальной спецификации являются: OpenGL.Курт Экли (Kurt Akeley) и Марк Сигал (Mark Segal). 11 марта 2010 года Khronos Group представила финальный вариант спецификации OpenGL 4.0 и языка описания шейдеров 6 GLSL4.0. OpenGL 4.0 полностью обратно совместим со старыми расширениями OpenGL, используя режим совместимости введенный в OpenGL 3.2. 8 августа 2011 года Khronos Group опубликовала актуальную спецификацию OpenGL 4.2 и языка шейдеров GLSL 4.2 OpenGL, создавался для профессионального сектора и прочно в нем закрепился. На платформе Windows конкурирует с Direct3D. Иначе, можно сказать, что OpenGL является программным интерфейсом к графическому оборудованию. Этот интерфейс содержит порядка 250 отдельных команд (около 200 в ядре OpenGL и еще 50 в библиотеке OpenGL Utility Library), которые используются для задания объектов и операций, необходимых для создания интерактивных приложений трехмерной графики. Следует помнить, что в OpenGL не включены команды для управления окнами и для организации пользовательского ввода. Вся такая работа ведется через операционную систему. Аналогично, OpenGL не имеет высокоуровневых команд для описания трехмерных сложных моделей. Задача OpenGL – дать возможность создать модель из небольшого набора графических примитивов: точек, линий и многоугольников, то есть основным принципом работы OpenGL является получение наборов векторных графических примитивов в виде точек, линий и многоугольников с последующей математической обработкой полученных данных и построением растровой картинки на экране и/или в памяти. Векторные трансформации и растеризация выполняются графическим конвейером (graphics pipeline), который по сути представляет собой дискретный автомат. Абсолютное большинство команд OpenGL попадают в одну из двух групп: либо они добавляют графические примитивы на вход в конвейер, либо конфигурируют конвейер на различное исполнение трансформаций. Высокоуровневые средства предоставляются библиотеками, являющимися надстройками над OpenGL. Для моделирования кривых и поверхностей предназначена библиотека GLU (OpenGL Utility Library), имеющая множество инструментов, таких как вычислители и NURBS. Библиотека GLU является стандартной составляющей любой реализации OpenGL. 7 I. РАБОТА С OPENGL 1.1. Основные понятия OpenGL Программы OpenGL могут быть достаточно сложными и запутанными, тем не менее, общая структура программ проста: инициализация состояний, управляющих рисованием, и указание объектов для рисования. Сначала следует определить следующие основные термины: ­ рендеринг (rendering) – отображение, иначе воспроизведение изображения на экран на основе моделей из графических примитивов – точек, линий и многоугольников, определяемых вершинами (vertices); ­ пиксель (pixel) – минимальный элемент изображения, и информация о пикселях хранится в битовых плоскостях; ­ битовая плоскость (bitplane) – область памяти, в которой каждому пикселю соответствует один бит информации. Бит, например, может определять составляющую цвета. В свою очередь, битовые плоскости образуют видеобуфер (буфер кадров), который хранит всю информацию, необходимую графическому дисплею для управления цветом и яркостью всех пикселей на экране. Окончательное прорисованное изображение состоит из пикселей на экране. Синтаксис для команд отличается тем, что для определения OpenGL используется префикс g1, а первые буквы каждого слова имени команды являются заглавными (например, glClearColor()). Константы в OpenGL начинаются с GL_ ,и записываются прописными буквами, а отдельные слова в них выделяются подчеркиванием, например: GL_COLOR_BUFFER_BIT). К некоторым именам команд добавляются суффиксы (например, 3f, как в glColor3f () и glVertex3f ()). Как уже отмечалось, все команды OpenGL начинаются с префикса gl, все константы также начинаются с префикса GL_ . Например, в команде glColor3f() (Color в имени определяет команду, которая устанавливает текущий цвет) цифра 3 говорит о том, что координат 3, а префикс 'f' говорит о том, что аргумент имеет тип floating-point . В OpenGL имеется 8 сновных типов Data Type: ­ 8-bit integer; ­ 16-bit integer; ­ 32-bit integer; 8 ­ 32-bit floating-point; ­ 64-bit floating-point; ­ 8-bit unsigned integer; ­ 16-bit unsigned integer; ­ 32-bit unsigned integer. Так, две команды glVertex2i(1, 3) и glVertex2f(1.0, 3.0) фактически эквивалентны, но имеют различные типы. Функции glGetBooleanv(), glGetDoublev(), glGetFloatv(), glGetIntegerv(), glGetPointerv(), glIsEnabled() – устанавливают тип данных. Некоторые команды имееют последним символом 'v' , что указывает на вектор, определяет массив и дает векторный указатель на него. Доступ к этому массиву определяется с помощью часто употребляемой команды GLvoid(), например: glColor3f(1.0, 0.0, 0.0); GLfloat color_array[] = {1.0, 0.0, 0.0}; glColor3fv(color_array); Как только установлен цвет с помощью glColor3f(), все последующие обьекты будут выводиться именно установленным цветом. Доступ к переменным можно осуществить с помощью glEnable() или glDisable(). Функции glPushAttrib() и glPushClientAttrib() пишут в стек, glPopAttrib() и glPopClientAttrib() восстанавливают. OpenGL очень четко организован в смысле очередности выполнения операций. Существуют три типа основных графических операций: очистка экрана, рисование геометрических обьектов, рисование растеризованных обьектов (текстур). Например: очистка RGBA mode window в черный цвет: glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); В OpenGL определены такие буферы, как: Color buffer – буфер цвета; Depth buffer (Z-buffer) – буфер глубины – двумерный массив данных, дополняющий двумерное изображение, где для каждого пикселя (фрагмента) изображения сопоставляется «глубина» (расстояние от наблюдателя до поверхности изображаемого объекта); Stencil buffer – буфер шаблона или буфер трафарета – это дополнительный буфер, соответствующий размеру выводимого кадра, то есть каждому пикселю изображения на экране соответствует свое значение в стенсил буфере. При выводе пикселей в буфер кадра иногда 9 возникает необходимость выводить не все пиксели, а только некоторое их подмножество, т.е. как бы наложить трафарет на изображение. Кроме наложения трафарета, этот буфер предоставляет еще несколько интересных возможностей; Accumulation buffer – буфер накопления – это дополнительный внутренний буфер OpenGL, в котором можно сохранять визуализированное изображение, применяя при этом попиксельно специальные операции; Frame buffer – буфер кадра - участок видеопамяти, в котором производится работа по формированию изображения (Обычно используются два (реже три) буфера кадра: один (передний, или frontbuffer) отображается на экране, а во второй (задний, или back-buffer) выполняется рендеринг. Как только очередной кадр изображения будет готов, они поменяются ролями: второй буфер будет показан на экране, а первый перерисован заново); Index Buffer – буфер индексов – буфер, определяющий порядок следования вершин при построении геометрии. Индексный буфер представляет собой массив индексов — «номеров» вершин в вершинном буфере. 1.2. Графические примитивы OpenGL Все графические примитивы OpenGL в конечном счете задаются с помощью вершин (vertrces) – ординат, определяющих точки, концы сегментов линий и углы многоугольников. Точка (point) представляется набором вещественных чисел, называемых вершиной. Все внутренние вычисления выполняются для трехмерных вершин. Определяемые пользователем двухмерные вершины (когда заданы только координаты х и у) также являются трехмерными, но с координатой г, равной 0. В OpenGL термин линия (line) связан с отрезком, а не с математическим термином, определяющим бесконечную линию – это линейный сегмент. Существует простой способ определения серии соединенных отрезков или замкнутой последовательности отрезков. Во всех случаях линии состоят из последовательности соединенных отрезков, определяемых вершинами их концов. Полигон (polygon), иначе многоугольник – площадь, ограниченная одиночным замкнутым контуром, состоящим из отрезков, определяемых вершинами их концов. Обычно полигоны рисуются с закрашенными внутри пикселями, но можно также их рисовать 10 контуром или набором точек, то есть ребра в полигоне не должны пересекаться, полигон также должен быть выпуклым. На число сегментов ограничений нет. OpenGL работает в так называемых гомогенных (приведенных) координатах, поэтому у точки имеется четвернтый дополнительный параметр – (x, y, z, w). Если w не равно нулю, фактически имеем дело с координатой (x/w, y/w, z/w). Таким образом, под вершиной понимается точка в трехмерном пространстве, координаты которой задаются следующим образом: void glVertex[2 3 4][s i f d](type coords); void glVertex[2 3 4][s i f d]v(type *coords); и координаты точки задаются максимум четырьмя значениями: x, y, z, w, при этом можно указывать два (x,y) или три (x,y,z) значения, а для остальных переменных в этих случаях используются значения по умолчанию: z=0, w=1. Как уже было сказано выше, число в названии команды соответствует числу явно задаваемых значений, а последующий символ – их типу. Координатные оси расположены так, что точка (0,0) находится в левом нижнем углу экрана, ось x направлена влево, ось y- вверх, а ось zиз экрана. Это расположение осей мировой системы координат, в которой задаются координаты вершин объекта. Однако чтобы задать какую-нибудь фигуру одних координат вершин недостаточно, и эти вершины надо объединить в одно целое, определив необходимые свойства. Для этого в OpenGL используется понятие примитивов, к которым относятся точки, линии, связанные или замкнутые линии, треугольники и так далее. Задание примитива происходит внутри командных скобок: void glBegin(GLenum mode); void glEnd(void); Параметр mode определяет тип примитива, который задается внутри и может принимать следующие значения: ­ GL_POINTS каждая вершина задает координаты некоторой точки; ­ GL_LINES каждая отдельная пара вершин определяет отрезок; если задано нечетное число вершин, то последняя вершина игнорируется; ­ GL_LINE_STRIP каждая следующая вершина задает отрезок вместе с предыдущей; ­ GL_LINE_LOOP отличие от предыдущего примитива только в том, что последний отрезок определяется последней и первой вершиной, образуя замкнутую ломаную; 11 ­ GL_TRIANGLES каждая отдельная тройка вершин определяет треугольник; если задано не кратное трем число вершин, то последние вершины игнорируются; ­ GL_TRIANGLE_STRIP каждая следующая вершина задает треугольник вместе с двумя предыдущими; ­ GL_TRIANGLE_FAN треугольники задаются первой и каждой следующей парой вершин (пары не пересекаются); ­ GL_QUADS каждая отдельная четверка вершин определяет четырехугольник; если задано не кратное четырем число вершин, то последние вершины игнорируются; ­ GL_QUAD_STRIP четырехугольник с номером n определяется вершинами с номерами 2n-1, 2n, 2n+2, 2n+1; ­ GL_POLYGON последовательно задаются вершины выпуклого многоугольника. Таким образом, все геометрические обьекты – примитивы определяются в OpenGL с помощью набора вершин. Вершина задается командой glVertex*(), например: glVertex2s(2, 3); glVertex3d(0.0, 0.0, 3.1415926535898); glVertex4f(2.3, 1.0, -2.2, 2.0); GLdouble dvect[3] = {5.0, 9.0, 1992.0}; glVertex3dv(dvect); Все эти команды представляют 3-мерные вершины в различных форматах. Аналогично прорисовывается, например, полигон из пяти вершин: glBegin(GL_POLYGON); glVertex2f(0.0, 0.0); glVertex2f(0.0, 3.0); glVertex2f(4.0, 3.0); glVertex2f(6.0, 1.5); glVertex2f(4.0, 0.0); glEnd(); Корректно работают в цикле glBegin()... glEnd() следующие команды: glVertex*() glColor*() glIndex*() glNormal*() glTexCoord*() glEdgeFlag*() glMaterial*() glArrayElement() 12 glEvalCoord*() glEvalPoint*() glCallList() glCallLists() Далее приведен пример прорисовки круга. #define PI 3.1415926535898 GLint circle_points = 100; glBegin(GL_LINE_LOOP); for (i = 0; i < circle_points; i++) { angle = 2*PI*i/circle_points; glVertex2f(cos(angle), sin(angle)); } glEnd(); Необходимо предварительно вычислить массив и потом делать на него ссылку в цикле glBegin()... glEnd(). Каждый полигон имеет две стороны – front и back. Это позволяет правильно рисовать обьекты, имеющие внутренние невидимые полости. Полигоны, у которых вершины появляются в порядке против часовой стрелки, называются front-facing. Функция glFrontFace() контролирует этот процесс. Функция glPolygonMode() – контролирует прорисовку полигонов: glPolygonMode(GL_FRONT, GL_FILL); glPolygonMode(GL_BACK, GL_LINE); Команда glCullFace() задает порядок прорисовки – front или bac, выполняется совместно с командой glEnable() . OpenGL позволяет определить нормаль для каждого полигона или вершины. Нормаль определяет ориентацию полигона, например, относительно источника света. Нормаль определяет, сколько света падает на вершину. Если рассматривать плоскость, то нормальный вектор к ней буден един для всех точек плоскости. Нормаль устанавливается командой glNormal*(). Для каждой вершины она, как правило, различна. glBegin (GL_POLYGON); glNormal3fv(n0); glVertex3fv(v0); glNormal3fv(n1); glVertex3fv(v1); glNormal3fv(n2); glVertex3fv(v2); glNormal3fv(n3); 13 glVertex3fv(v3); glEnd(); При вычислении ротации или трансформации вектор нужно нормализовать, для этого команда glEnable() выполняется с параметром GL_NORMALIZE . Рассмотрим простой пример – прорисовка белого прямоугольника на черном фоне: main() { glClearColor (0.0, 0.0, 0.0, 0.0); glClear (GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0); glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); glBegin(GL_POLYGON); glVertex3f (0.25, 0.25, 0.0); glVertex3f (0.75, 0.25, 0.0); glVertex3f (0.75, 0.75, 0.0); glVertex3f (0.25, 0.75, 0.0); glEnd(); glFlush(); } Строки кода имеют следующий смысл: ­ glClearColor() устанавливает черный цвет фона, glClear() очищает фон, и в дальнейшем, всякий раз, когда glClear () будет вызываться, она будет очищать окно в черный цвет; ­ glColor3f() устанавливает цвет прорисовки – белый цвет; ­ glOrtho() определяет координатную систему; ­ glBegin() и glEnd() определяют обьект, который будет прорисован; ­ glVertex3f() определяет вершины полигона, например – три координаты x, y, z; ­ glFlush() гарантирует, что прорисовка полигона будет выполнена немедленно. 1.3. Цвет В OpenGL описание очертания рисуемого объекта независимо от описания его цвета. Всегда часть геометрического объекта рисуется с использованием текущей цветовой модели (схемы). До тех пор пока цвет или цветовая модель не будут изменены, все объекты отображаются этим цветом или в этой цветовой модели. 14 Данный метод позволяет OpenGL добиваться более высокопроизводительного рисования, чем при явном задании цвета каждый раз. Для установки цвета используется команда glColor3f(). У нее три аргумента, все вещественного типа с плавающей запятой, принимающие значения от 0,0 до 1,0. Эти параметры по порядку задают интенсивность красного, зеленого и синего компонентов цвета. Их можно считать спецификацией смешивания цветов: 0,0 означает полное отсутствие данного компонента, а 1,0 – максимальную интенсивность данного цвета. Таким образом, код glColor3f(1.0, 0.0, 0.0) задает наиболее яркий красный цвет, который может отобразить система, без примесей зеленого или синего. Все нули определяют черный цвет, а все единицы – белый. Установка всех трех составляющих в 0,5 даст серый тон. В табл. 1 приведены основные цвета цветовой модели. Табл. 1 Код основных цветов glColor3f(0.0, 0.0, 0.0) glColor3f(1.0, 0.0, 0.0) glColor3f(0.0, 1.0, 0.0) glColor3f(1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 1.0) glColor3f(1.0, 0.0, 1.0) glColor3f(0.0, 1.0, 1.0) glColor3f(1.0, 1.0, 1.0) черный красный зеленый желтый синий маджента циан белый Информация о пикселях может быть записана в двух режимах – RGBA mode или color-index mode, в котором цвет представлен единственным числом. Таблица со значениями индексов называется color map. Изменять значения в этой таблице во время работы приложения нельзя, во всяком случае, с использованием GLUT. Каждый пиксель занимает определенное количество памяти для хранения информации о его цвете. Память для хранения пикселов называется color buffer . 8-битовый буффер отводит на каждый пиксел по 8 бит – это 256 цветов. Команды для определения цвета простые и приведены ниже: glColor3f (1.0, 0.0, 0.0); glBegin (GL_POINTS); glVertex3fv (point_array); glEnd (); Дисплей может быть установлен в один из режимов RGBA mode или color-index, который в дальнейшем не может быть изменен. 15 Цвет устанавливается командой glColor(), которая указывает численные значения для цветовых составляющих или указывает на цветовой массив: void glColor3 (TYPEr, TYPEg, TYPEb); void glColor4 (TYPEr, TYPEg, TYPEb, TYPEa); void glColor3 (const TYPE*v); void glColor4 (const TYPE*v); Для того чтобы каждый обьект был нарисован своим цветом, используют наиболее простую схему: set_color(RED); draw_item(A); draw_item(B); set_color(GREEN); set_color(BLUE); draw_item(C); Полигон может быть нарисован единственным цветом (flat shading) или с оттенками (smooth shading или Gouraud shading). Для задания текущего цвета вершины используются следующие команды: void glColor[3 4][b s i f](GLtype components) void glColor[3 4][b s i f]v(GLtype components) Первые три параметра задают R, G, B компоненты цвета, а последний параметр определяет alpha-компоненту, которая задает уровень прозрачности объекта. Если в названии команды указан тип ‘f’ (float), то значения всех параметров должны принадлежать отрезку [0,1], при этом по умолчанию значение alpha-компоненты устанавливается равным 1.0, что соответствует полной непрозрачности. Если указан тип ‘ub’ (unsigned byte), то значения должны лежать в отрезке [0,255]. Разным вершинам можно назначать различные цвета, и тогда будет проводиться линейная интерполяция цветов по поверхности примитива. Для управления режимом интерполяции цветов используется команда void glShadeModel(GLenummode) вызов которой с параметром GL_SMOOTH включает интерполяцию (установка по умолчанию), а с GL_FLAT отключает. Интерполяция цветов устанавливается командой glShadeModel() с параметром. По умолчанию стоит параметр GL_SMOOTH. При таком закрашивании цвет каждой вершины индивидуален, при прорисовке линии цвет интерполируется между вершинами. Далее приведен пример прорисовки треугольника со смешанным закрашиванием – smooth-shaded треугольник (GLUT): #include < GL/gl.h > 16 #include < GL/glut.h > void init(void) { glClearColor (0.0, 0.0, 0.0, 0.0); glShadeModel (GL_SMOOTH); } void triangle(void) { glBegin (GL_TRIANGLES); glColor3f (1.0, 0.0, 0.0); glVertex2f (5.0, 5.0); glColor3f (0.0, 1.0, 0.0); glVertex2f (25.0, 5.0); glColor3f (0.0, 0.0, 1.0); glVertex2f (5.0, 25.0); glEnd(); } void display(void) { glClear (GL_COLOR_BUFFER_BIT); triangle (); glFlush (); } void reshape (int w, int h) { glViewport (0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode (GL_PROJECTION); glLoadIdentity (); if (w <= h) gluOrtho2D (0.0, 30.0, 0.0, 30.0*(GLfloat) h/(GLfloat) w); else gluOrtho2D (0.0, 30.0*(GLfloat) w/(GLfloat) h, 0.0, 30.0); glMatrixMode(GL_MODELVIEW); } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB); glutInitWindowSize (500, 500); glutInitWindowPosition (100, 100); glutCreateWindow (argv[0]); init (); 17 glutDisplayFunc(display); glutReshapeFunc(reshape); glutMainLoop(); return 0; } Для задания цвета фона используется команда void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha). Значения должны находиться в отрезке [0,1] и по умолчанию равны нулю. После этого вызов команды void glClear(GLbitfield mask) с параметром GL_COLOR_BUFFER_BIT устанавливает цвет фона во все буфера, доступные для записи цвета (иногда удобно использовать несколько буферов цвета). Кроме цвета аналогичным образом можно определить нормаль в вершине, используя команды: void glNormal3[b s i f d](type coords); void glNormal3[b s i f d]v(type coords); Задаваемый вектор может не иметь единичной длины, но он будет нормироваться автоматически в режиме нормализации, который включается вызовом команды glEnable(GL_NORMALIZE). Команды производят включение и отключение того или иного режима работы конвейера OpenGL: void glEnable(GLenum mode); void glDisable(GLenum mode); Эти команды применяются достаточно часто, и их использование следует рассматривать в конкретных случаях. Вообще, внутри командных скобок glBegin() и glEnd() можно производить вызов лишь нескольких команд, в которые входят glVertex..(), glColor..()glNormal..(), glRect..(), glMaterial..() и glTexCoord..(). С помощью команды void glRect[s i f d]( GLtype x1, GLtype y1, GLtype x2, GLtype y2 ), void glRect[s i f d]v( GLtype *v1, GLtype *v2 ) можно нарисовать прямоугольник в плоскости z=0 с координатами противоположных углов (x1,y1) и (x2,y2), либо набор прямоугольников с координатами углов в массивах v1 и v2. Кроме задания самих примитивов можно определить метод их отображения на экране, где под примитивами в данном случае понимаются многоугольники. Однако сначала надо определить понятие лицевых и обратных граней. Под гранью понимается одна из сторон многоугольника, и по умолчанию лицевой считается та сторона, вершины которой обходятся против часовой стрелки. Направление обхода вершин лицевых сторон 18 можно изменить вызовом команды void glFrontFace(GLenum mode) со значением параметра mode равным GL_CW, а отменить- с GL_CCW. Чтобы изменить метод отображения многоугольника используется команда void glPolygonMode(GLenum face, Glenum mode) Параметр mode определяет, как будут отображаться многоугольники, а параметр face устанавливает тип многоугольников, к которым будет применяться эта команда и может принимать следующие значения: ­ GL_FRONT для лицевых граней; ­ GL_BACK для обратных граней; ­ GL_FRONT_AND_BACK для всех граней. Параметр mode может быть равен: ­ GL_POINT при таком режиме будут отображаться только вершины многоугольников; ­ GL_LINE при таком режиме многоугольник будет представляться набором отрезков; ­ GL_FILL при таком режиме многоугольники будут закрашиваться текущим цветом с учетом освещения и этот режим установлен по умолчанию. Кроме того, можно указывать, какой тип граней отображать на экране. Для этого сначала надо установить соответствующий режим вызовом команды glEnable(GL_CULL_FACE), а затем выбрать тип отображаемых граней с помощью команды void glСullFace(GLenum mode). Вызов с параметром GL_FRONT приводит к удалению из изображения всех лицевых граней, а с параметром GL_BACK- обратных (установка по умолчанию). Кроме рассмотренных стандартных примитивов в библиотеках GLU и GLUT описаны более сложные фигуры, такие как сфера, цилиндр, диск (в GLU) и сфера, куб, конус, тор, тетраэдр, додекаэдр, икосаэдр, октаэдр или чайник (в GLUT). Автоматическое наложение текстуры предусмотрено только для фигур из библиотеки GLU. Например, чтобы нарисовать сферу или цилиндр, надо сначала создать объект специального типа GLUquadricObj с помощью команды GLUquadricObj* gluNewQuadric(void), затем вызвать соответствующую команду: void gluSphere(GLUquadricObj * qobj, GLdouble radius, GLint slices, GLint stacks) void gluCylinder(GLUquadricObj * qobj, GLdouble baseRadius, GLdouble topRadius, GLdouble height, GLint slices, GLint stacks) где параметр slices задает число разбиений вокруг оси z, а stacks – вдоль оси z. 19 Для корректного построения перечисленных примитивов необходимо удалять невидимые линии и поверхности, для чего надо включить соответствующий режим вызовом команды glEnable(GL_DEPTH_TEST). 1.4. Преобразования координат и проекции В OpenGL используются три основные системы координат: левосторонняя, правосторонняя и оконная. Первые две системы являются трехмерными и отличаются друг от друга направлением оси z: в правосторонней она направлена на наблюдателя, а в левосторонней – в глубь экрана. Левосторонняя система используется для задания значений параметрам команды gluPerspective(), glOrtho(), а правосторонняя или мировая система координат во всех остальных случаях. Отображение трехмерной информации происходит в двумерную оконную систему координат. Для задания различных преобразований объектов сцены в OpenGL используются операции над матрицами, при этом различают три типа матриц: видовая, проекций и текстуры. Все они имеют размер 4x4. Видовая матрица определяет преобразования объекта в мировых координатах, такие как параллельный перенос, изменение масштаба и поворот. Матрица проекций задает, как будут проецироваться трехмерные объекты на плоскость экрана (в оконные координаты), а матрица текстуры определяет наложение текстуры на объект. Для выбора матрицы, которую надо изменить, используется команда, вызов которой со значением параметра mode равным GL_MODELVIEW, GL_PROJECTION, GL_TEXTURE включает режим работы с видовой, проекций и матрицей текстуры соответственно: void glMatrixMode(GLenum mode). Для вызова команд, задающих матрицы того или иного типа необходимо сначала установить соответствующий режим. Для определения элементов матрицы текущего типа вызывается команда, где m указывает на массив из 16 элементов типа float или double в соответствии с названием команды, при этом сначала в нем должен быть записан первый столбец матрицы, затем второй, третий и четвертый: void glLoadMatrix[f d](GLtype *m). Команда void glLoadIdentity(void) заменяет текущую матрицу на единичную. Часто нужно сохранить содержимое текущей матрицы для дальнейшего использования, для чего используют команды: void glPushMatrix(void) ; 20 void glPopMatrix(void); Они записывают и восстанавливают текущую матрицу из стека, причем для каждого типа матриц стек свой. Для видовых матриц его глубина равна как минимум 32, а для двух оставшихся типов как минимум 2. Для умножения текущей матрицы слева на другую матрицу используется команда void glMultMatrix[f d](GLtype *m). Где m должен задавать матрицу размером 4x4 в виде массива с описанным расположением данных. Однако обычно для изменения матрицы того или иного типа удобно использовать специальные команды, которые по значениям своих параметров создают нужную матрицу и перемножают ее с текущей. Чтобы сделать текущей созданную матрицу, надо перед вызовом этой команды вызвать glLoadIdentity(). В целом, для отображения трехмерных объектов сцены в окно приложения используется следующая последовательность действий: Координаты объекта => Видовые координаты => Усеченные координаты => Нормализованные координаты =>Оконные координаты. Рассмотрим каждое из этих преобразований отдельно. К видовым преобразованиям относятся: перенос, поворот и изменение масштаба вдоль координатных осей. Для проведения этих операций достаточно умножить на соответствующую матрицу каждую вершину объекта и получить измененные координаты этой вершины: (x’, y’, z’, 1)T =M * (x, y, z, 1)T, где M матрица видового преобразования. Перспективное преобразование и проектирование производится аналогично. Сама матрица может быть создана с помощью следующих команд: void glTranslate[f d](GLtype x, GLtype y, GLtype z) void glRotate[f d](GLtype angle, GLtype x, GLtype y, GLtype z) void glScale[f d](GLtype x, GLtype y, GLtype z) glTranlsate..() производит перенос объекта, прибавляя к координатам его вершин значения своих параметров. glRotate..() производит поворот объекта против часовой стрелки на угол angle (измеряется в градусах) вокруг вектора (x,y,z ). glScale..() производит масштабирование объекта (сжатие или растяжение), домножая соответствующие координаты его вершин на значения своих параметров. Все эти преобразования будут применяться к примитивам, описания которых будут находиться ниже в программе. В случае если, например, надо повернуть один объект сцены, а другой оставить неподвижным, удобно сначала сохранить текущую видовую матрицу в стеке командой glPushMatrix(), затем вызвать glRotate..() с нужными 21 параметрами, описать примитивы, из которых состоит этот объект, а затем восстановить текущую матрицу командой glPopMatrix(). Кроме изменения положения самого объекта иногда бывает нужно изменить положение точки наблюдения, что также приводит к изменению видовой матрицы. Это можно сделать с помощью команды, где точка (eyex,eyey,eyez) определяет точку наблюдения, (centerx, centery, centerz) задает центр сцены, который будет проектироваться в центр области вывода, а вектор (upx,upy,upz) задает положительное направление оси у, определяя поворот камеры: void gluLookAt (GLdouble eyex, GLdouble eyey, GLdouble eyez, GLdouble centerx, GLdouble centery, GLdouble centerz, GLdouble upx, GLdouble upy, GLdouble upz) Если, например, камеру не надо поворачивать, то задается значение (0, 1, 0), а со значением (0, -1, 0) – сцена будет перевернута. Фактически, эта команда совершает перенос и поворот объектов сцены, но в таком виде задавать параметры бывает удобнее. В OpenGL существуют ортографическая (параллельная) и перспективная проекция. Первый тип проекции может быть задан командами: void glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far); void gluOrtho2D(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top). Первая команда создает матрицу проекции в усеченный объем видимости (параллелограмм видимости) в левосторонней системе координат. Параметры команды задают точки (left, bottom, -near) и (right, top, -near), которые отвечают левому нижнему и правому верхнему углам окна вывода. Параметры near и far задают расстояние до ближней и дальней плоскостей отсечения по дальности от точки (0,0,0) и могут быть отрицательными. Во второй команде, в отличие от первой, значения near и far устанавливаются равными -1 и 1 соответственно. Перспективная проекция определяется командой: void gluPerspective(GLdouble angley, GLdouble aspect, GLdouble znear, GLdouble zfar). Она задает усеченный конус видимости в левосторонней системе координат. Параметр angley определяет угол видимости в градусах по оси у и должен находиться в диапазоне от 0 до 180. Угол видимости вдоль оси x задается параметром aspect, который обычно задается как отношение сторон области вывода. Параметры zfar и znear задают расстояние от наблюдателя до плоскостей отсечения по глубине и 22 должны быть положительными. Чем больше отношение zfar/znear, тем хуже в буфере глубины будут различаться расположенные рядом поверхности, так как по умолчанию в него будет записываться «сжатая» глубина в диапазоне от 0 до 1. После применения матрицы проекций на вход следующего преобразования подаются так называемые усеченные (clip) координаты, для которых значения всех компонент (xc, yc, zc, wc)T находятся в отрезке [-1,1]. Область вывода представляет собой прямоугольник в оконной системе координат, размеры которого задаются командой: void glViewPort(GLint x, GLint y, GLint width, GLint height). Значения всех параметров задаются в пикселях и определяют ширину и высоту области вывода с координатами левого нижнего угла (x,y) в оконной системе координат. Размеры оконной системы координат определяются текущими размерами окна приложения, точка (0,0)находится в левом нижнем углу окна. Используя параметры команды glViewPort(), вычисляются оконные координаты центра области вывода (ox, oy). При этом целые положительные величины n и f задают минимальную и максимальную глубину точки в окне и по умолчанию равны 0 и 1 соответственно. Глубина каждой точки записывается в специальный буфер глубины (z-буфер), который используется для удаления невидимых линий и поверхностей. Установить значения n и f можно вызовом функции: void glDepthRange(GLclampd n, GLclampdf) Команда glViewPort() обычно используется в функции, зарегистрированной с помощью команды glutReshapeFunc(), которая вызывается, если пользователь изменяет размеры окна приложения, изменяя соответсвующим образом область вывода. 1.5. Текстуры Наложение текстуры на поверхность объектов сцены повышает ее реалистичность, однако при этом надо учитывать, что этот процесс требует значительных вычислительных затрат. Текстура – некоторое изображение, которое надо определенным образом нанести на объект. Для этого следует выполнить следующие этапы: ­ выбрать изображение и преобразовать его к нужному формату; ­ загрузить изображение в память; 23 ­ определить, как текстура будет наноситься на объект и как она будет с ним взаимодействовать. Далее рассмотрим каждый из этих этапов. Подготовка текстуры. Принятый в OpenGL формат хранения изображений отличается от стандартного формата Windows DIB только тем, что компоненты (R,G,B) для каждой точки хранятся в прямом порядке, а не в обратном и выравнивание задается программистом. Считывание графических данных из файла и их преобразование можно проводить и вручную, однако удобней воспользоваться функцией, входящей в состав библиотеки GLAUX (для ее использования надо дополнительно подключить glaux.lib), которая сама проводит необходимые операции, где file – название файла с расширением *.bmp или *.dib. В качестве результата функция возвращает указатель на область памяти, где хранятся преобразованные данные: AUX_RGBImageRec* auxDIBImageLoad(string file). При создании образа текстуры в памяти следует учитывать следующие требования. Во-первых, размеры текстуры как по горизонтали, так и по вертикали должны представлять собой степени двойки. Это требование накладывается для компактного размещения текстуры в памяти и способствует ее эффективному использованию. Использовать только текстуры с такими размерами конечно неудобно, поэтому перед загрузкой их надо преобразовать. Изменение размеров текстуры проводится с помощью команды: void gluScaleImage(GLenum format, GLint widthin, GL heightin, GLenum typein, const void *datain, GLint widthout, GLint heightout, GLenum typeout, void *dataout) В качестве значения параметра format обычно используется значение GL_RGB или GL_RGBA, определяющее формат хранения информации. Параметры widthin, heightin, widhtout, heightout определяют размеры входного и выходного изображений, а с помощью typein и typeout задается тип элементов массивов, расположенных по адресам datain и dataout. Как и обычно, то может быть тип: GL_UNSIGNED_BYTE, GL_SHORT, GL_INT Результат своей работы функция заносит в область памяти, на которую указывает параметр dataout. Во-вторых, надо предусмотреть случай, когда объект по размерам значительно меньше наносимой на него текстуры. Чем меньше объект, тем меньше должна быть наносимая на него текстура и поэтому вводится понятие уровней детализации текстуры. Каждый уровень детализации задает некоторое изображение, которое является как правило уменьшенной в два раза копией оригинала. Такой подход позволяет улучшить качество нанесения текстуры на объект. Например, 24 для изображения размером 2mx2n можно построить max(m,n)+1 уменьшенных изображений, соответствующих различным уровням детализации. Эти два этапа создания образа текстуры в памяти можно провести с помощью команды: void gluBuild2DMipmaps(GLenum target, GLint components, GLint width, GLint height, GLenum format, GLenum type, const void *data) Где параметр target должен быть равен GL_TEXTURE_2D, components определяет количество цветовых компонент текстуры, которые будут использоваться при ее наложении и может принимать значения от 1 до 4 (1-только красный,2-красный и alpha, 3-красный, синий, зеленый, 4-все компоненты). Параметры width, height, data определяют размеры и расположение текстуры соответственно, а format и type имеют аналогичный смысл, что и в команде gluScaleImage(). В OpenGL допускается использование одномерных текстур, то есть размера 1xN, однако это всегда надо указывать, используя в качестве значения target константу GL_TEXTURE_1D. Существует одномерный аналог рассматриваемой команды- gluBuild1DMipmaps(), который отличается от двумерного отсутствием параметра height. При использовании в сцене нескольких текстур, в OpenGL применяется подход, напоминающий создание списков изображений. Вначале, с помощью команды надо создать n идентификаторов для используемых текстур, которые будут записаны в массив textures: void glGenTextures(GLsizei n, GLuint*textures) Перед началом определения свойств очередной текстуры следует вызвать команду, где target может принимать значения GL_TEXTURE_1D или GL_TEXTURE_2D, а параметр texture должен быть равен идентификатору той текстуры, к которой будут относиться последующие команды: void glBindTexture(GLenum target, GLuint texture) Для того, чтобы в процессе рисования сделать текущей текстуру с некоторым идентификатором, достаточно опять вызвать команду glBindTexture() c соответствующим значением target и texture. Таким образом, команда glBindTexture() включает режим создания текстуры с идентификатором texture, если такая текстура еще не создана, либо режим ее использования, то есть делает эту текстуру текущей. Рассмотрим методы наложения текстуры. При наложении текстуры, как уже упоминалось, надо учитывать случай, когда размеры текстуры отличаются от размеров объекта, на который она 25 накладывается. При этом возможно как растяжение, так и сжатие изображения, и то, что влияет на качество построенного изображения. Для определения положения точки на текстуре используется параметрическая система координат (s,t), причем значения s и t находятся в отрезке [0,1]. Для изменения различных параметров текстуры применяются команды: void glTexParameter[i f](GLenum target, GLenum pname, GLenum param); void glTexParameter[i f]v(GLenum target, GLenum pname, GLenum *params). При этом target имеет аналогичный смысл, что и раньше, pname определяет, какое свойство будем менять, с помощью param или params устанавливается новое значение. Далее приведены возможные значения pname. GL_TEXTURE_MIN_FILTER параметр param определяет функцию, которая будет использоваться для сжатия текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры: ­ значение по умолчанию: GL_LINEAR. GL_TEXTURE_MAG_FILTER параметр param определяет функцию, которая будет использоваться для увеличения (растяжения) текстуры. При значении GL_NEAREST будет использоваться один (ближайший), а при значении GL_LINEAR четыре ближайших элемента текстуры; ­ значение по умолчанию: GL_LINEAR. GL_TEXTURE_WRAP_S параметр param устанавливает значение координаты s, если оно не входит в отрезок [0,1]. При значении GL_REPEAT целая часть s отбрасывается, и в результате изображение размножается по поверхности. При значении GL_CLAMP используются краевые значения: 0 или 1, что удобно использовать, если на объект накладывается один образ; ­ значение по умолчанию: GL_REPEAT. GL_TEXTURE_WRAP_T аналогично предыдущему значению, только для координаты t. Использование режима GL_NEAREST значительно повышает скорость наложения текстуры, однако при этом снижается качество, так как в отличие от GL_LINEAR интерполяция не производится. Для того, чтобы определить, как текстура будет взаимодействовать с материалом, из которого сделан объект, используются команды: void glTexEnv[i f](GLenum target, GLenum pname, GLtype param); 26 void glTexEnv[i f]v(GLenum target, GLenum pname, GLtype *params). Параметр target должен быть равен GL_TEXTURE_ENV, а в качестве pname рассмотрим только одно значение GL_TEXTURE_ENV_MODE, которое наиболее часто применяется. Параметр param может быть равен: ­ GL_MODULATE конечный цвет находится как произведение цвета точки на поверхности и цвета соответствующей ей точки на текстуре; ­ GL_REPLACE в качестве конечного цвета используется цвет точки на текстуре; ­ GL_BLEND конечный цвет находится как сумма цвета точки на поверхности и цвета соответствующей ей точки на текстуре с учетом их яркости. Координаты текстуры. Перед нанесением текстуры на объект осталось установить соответствие между точками на поверхности объекта и на самой текстуре. Задавать это соответствие можно двумя методами: отдельно для каждой вершины или сразу для всех вершин, задав параметры специальной функции отображения. Первый метод реализуется с помощью команд: void glTexCoord[1 2 3 4][s i f d](type coord); void glTexCoord[1 2 3 4][s i f d]v(type *coord). Чаще всего используется команды вида glTexCoord2..(type s, type t), задающие текущие координаты текстуры. Понятие текущих координат текстуры аналогично понятиям текущего цвета и текущей нормали, и является атрибутом вершины. Однако даже для куба нахождение соответствующих координат текстуры является довольно трудоемким занятием, поэтому в библиотеке GLU помимо команд, проводящих построение таких примитивов, как сфера, цилиндр и диск, предусмотрено также наложение на них текстур. Для этого достаточно вызвать команду: void gluQuadricTexture(GLUquadricObj*quadObject, GLboolean textureCoords). Параметр textureCoords должен быть равен GL_TRUE, и тогда текущая текстура будет автоматически накладываться на примитив. Второй метод реализуется с помощью команд: void glTexGen[i f d](GLenum coord, GLenum pname, GLtype param); void glTexGen[i f d]v(GLenum coord, GLenum pname, const GLtype *params). Параметр coord определяет для какой координаты задается формула и может принимать значение GL_S, GL_T; pname определяет 27 тип формулы и может быть равен GL_TEXTURE_GEN_MODE, GL_OBJECT_PLANE, GL_EYE_PLANE. С помощью params задаются необходимые параметры, а param может быть равен GL_OBJECT_LINEAR, GL_EYE_LINEAR, GL_SPHERE_MAP. Рассмотрение всех возможных комбинаций значений аргументов этой команды заняло бы слишком много места, поэтому в качестве примера рассмотрим, как можно задать зеркальную текстуру. При таком наложении текстуры изображение будет как бы отражаться от поверхности объекта, вызывая интересный оптический эффект. Для этого сначала надо создать два целочисленных массива коэффициентов s_coeffs и t_coeffs со значениями (1,0,0,1) и (0,1,0,1) соответственно, а затем вызвать команды: glEnable(GL_TEXTURE_GEN_S); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR); glTexGendv(GL_S, GL_EYE_PLANE, s_coeffs). И такие же команды для координаты t с соответствующими изменениями. 1.6. Использование эффекта тумана Для использования «тумана» следует включить его, передавая аргумент GL_FOG, команде glEnable(). Также выбрать цвет тумана и уравнение, управляющее его плотностью, с помощью команды glFog*(). Кроме того, можно установить качество расчета тумана с помощью аргумента GL_FOG_HINT команды glHint(). Клавиша ‘f’ выбирает уравнение плотности тумана. Задает параметры и функцию для вычисления тумана. Если pname имеет значение GL_FOG_MODE, то param может принимать значения GL_EXP (значение по умолчанию), GL_EXP2 или GL_LINEAR и задает метод вычисления фактора тумана. Если pname равен GL_FOG_DENSITY, GL_FOG_START или GL_FOG_END, то param содержит (или для векторной версии команды указывает на) величины density, start или end для использования в уравнениях (значения по умолчанию – 1, 0 и 1 соответственно). В RGBA режиме pname может также содержать значение GL_FOG_COLOR. В таком случае params указывает на четверку величин, задающую RGBA цвет тумана. Соответствующее значение pname для индексного режима – GL_FOG_INDEX, в этом случае param должен содержать единственную величину, задающую цветовой индекс тумана. 28 1.7. Список отображения Список отображения (дисплейные списки) – это группа команд OpenGL, сохраненная для дальнейшего исполнения. Когда исполнтся список отображения, команды, включенные в него, исполняются в том порядке, в котором они были заданы. Большинство команд OpenGL могут быть, как сохранены в списке отображения, так и выполняться в непосредственном режиме, в котором они выполняются немедленно. Если нужно несколько раз обращаться к одной и той же группе команд, эти команды можно объединить в так называемый список изображений (display list) и вызывать его при необходимости. Для того, чтобы создать новый список изображений надо поместить все команды, которые должны в него войти между командными скобками: void glNewList(GLuint list, GLenum mode); void glEndList(). Для различения списков используются целые положительные числа, задаваемые при создании списка значением параметра list, а параметр mode определяет режим обработки команд, входящих в список: ­ GL_COMPILE команды записываются в список без выполнения; ­ GL_COMPILE_AND_EXECUTE команды сначала выполняются, а затем записываются в список После того, как список создан, его можно вызвать командой: void glCallList(GLuint list). Здесь же, в параметре list, указывается идентификатор нужного списка. Чтобы вызвать сразу несколько списков, можно воспользоваться командой вызывающей n списков с идентификаторами из массива lists, тип элементов которого указывается в параметре type: void glCallLists(GLsizei n, GLenum type, const GLvoid *lists). Это могут быть типы GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_INT, GL_UNSIGNED_INT >и некоторые другие. Для удаления списков используется команда: void glDeleteLists(GLint list, GLsizei range). Команда удаляет списки с идентификаторами ID из диапазона list <=ID<= list+range-1. 29 1.8. Шрифты Для рисования символов наиболее часто используются битовые карты. Битовая карта – это прямоугольный массив из нулей и единиц, который служит в качестве маски для прямоугольной части окна. Например, если прорисовывается символ красного цвета, везде, где в битовой карте встречается 1, соответствующий пиксель в буфере кадра замещается красным. Там, где в битовой карте стоит 0, фрагменты не генерируются и пиксели не изменяются. OpenGL предоставляет только низкоуровневую поддержку для рисования строк символов и манипуляций со шрифтами. Команды glRasterPos*() и glBitmap() позиционируют и рисуют на экране одну битовую карту. Кроме того, с помощью механизма списков отображения можно использовать последовательности кодов символов для индексации соответствующих серий битовых карт, представляющих эти символы. Шрифт обычно состоит из набора символов, где каждый символ имеет идентификационный номер (как правило, свой ASCII код) и метод начертания. В стандартном наборе символов ASCII заглавная буква A (в латинице) имеет номер 65, B – 66 и так далее. Строка «DAB» может быть представлена тремя индексами 68, 65 и 66. В простейшем случае список отображения номер 65 рисует A, номер 66 – B и так далее. То есть для того, чтобы вывести строку из символов 68, 65, 66 просто вызовите к исполнению соответствующие списки отображения. Первый аргумент, n, индицирует количество выводимых символов, type обычно равен GL_BYTE, а lists– это массив кодов символов. Можно использовать команду glCallLists() следующим образом: void glCallLists(GLsizei n, GLenum type, const GLvoid *lists). Поскольку многим приложениям требуется отображать символы разных размеров и из разных шрифтов, простейший случай не устраивает. Оптимальное решение заключается в том, чтобы добавлять некоторое смещение к каждому вхождению в строку до того, как производится выбор списка отображения. В этом случае символы A, B и C будут представлены номерами 1065, 1066 и 1067 в шрифте 1, а в шрифте 2 они могут быть представлены числами 2065, 2066 и 2067. Тогда, чтобы нарисовать символы шрифтом 1, нужно установить смещение равным 1000 и вызвать к исполнению списки отображения 65, 66 и 67. Чтобы нарисовать ту же строку шрифтом 2, нужно 30 установить смещение равным 2000 и вызвать к исполнению те же списки. Для того, чтобы установить смещение, используется командуа glListBase(). Для предыдущих примеров она должна быть вызвана с 1000 и 2000 в качестве единственного аргумента. Теперь все, что нужно – это непрерывный диапазон неиспользуемых индексов списков отображения, который может быть получен с помощью команды: glGenLists():GLuint glGenLists (GLsizei range). II. ПРИМЕРЫ ПРОГРАММИРОВАНИЯ КОМПЬЮТЕРНОЙ ГРАФИКИ С ИСПОЛЬЗОВАНИЕМ OPENGL 2.1. Инициализация OpenGL окна в Windows Первым этапом чвляется создание пустого OpenGL окна для дальнейшей работы с ним. Сначала следует установить MSVisualC++2010, далее реализовать библиотеки OpenGL, которые входят в поставку Windows – это библиотеки opengl32.dll & glu32.dll. Для этого надо скопировать opengl.dll и glu.dll в windows\system и положить opengl.lib, glu.lib в подкаталог Lib. В меню Project/setting, необходимо выбрать вкладку LINK. В строке «Object/Library Modules» выбрать «OpenGL32.lib GLu32.lib GLaux.lib». После создания нового Win32 приложения в среде Visual C++, необходимо добавить для сборки проекта библиотеки OpenGL. В OpenGL не включены команды для управления окнами и для организации пользовательского ввода. Вся такая работа ведется через операционную систему. Первые четыре строки – заголовочные файлы, включаемые для последующего использования дополнительных библиотек: #include <windows.h> // Заголовочные файлы для Windows #include <gl\gl.h> // Заголовочные файлы для библиотеки OpenGL32 #include <gl\glu.h> // Заголовочные файлы для библиотеки GLu32 #include <gl\glaux.h> // Заголовочные файлы для библиотеки GLaux 31 Далее необходимо инициализировать все переменные, которые планируется использовать в программе. Эта программа будет создавать пустое OpenGL окно, нет необходимости инициализировать большое количество переменных. Первые строки устанавливают Контекст Рендеринга (Rendering Context). Каждая OpenGL программа связывается с Контекстом Рендеринга, который в свою очередь вызывает Контекст Устройства (Device Context). Контекст Рендеринга OpenGL определен как hRC. Для того чтобы рисовать в окне, необходимо создать Контекст Устройства Windows, который определен как hDC. DC соединяет окно с GDI (Graphics Device Interface). RC соединяет OpenGL с DC. В третьей строке инициализирована переменная hWnd, которая хранит дескриптор (уникальную ссылку-идентификатор) окна. В четвёртой строке объявляется дескриптор приложения (экземпляр) программы: HGLRC hRC=NULL; // Постоянный контекст рендеринга HDC hDC=NULL; // Приватный контекст устройства GDI HWND hWnd=NULL; // Здесь будет хранится дескриптор окна HINSTANCE hInstance // Здесь будет хранится дескриптор приложения В первой строке, приведённой ниже, инициализируем массив, который используется для отслеживания нажатия клавиш на клавиатуре. Следующая переменная необходима для того, чтобы приложение знало, будет ли окно минимизировано в панель задач или нет. Если окно минимизировано, то возможна заморозка выполнения программы и появляется возможность выйти из нее. Таким образом, программа не будет выполняться, если она минимизирована. Смысл переменной fullscreen заключается в следующем: если программа запущена в полноэкранном режиме, fullscreen имеет значение true, если же она запущенна в оконном режиме, то fullscreen будет false. Эти переменные должны быть глобальными, чтобы было определено, что приложение запущенно в полноэкранном режиме либо в оконном: bool keys[256]; // Массив, используемый для операций с клавиатурой bool active=true; // Флаг активности окна, установленный в true по умолчанию bool fullscreen=true; // Флаг режима окна, установленный в полноэкранный по умолчанию 32 Далее требуется описание прототипа функции WndProc. Функция CreateGLWindow() вызывает функцию WndProc(), но WndProc() описывается после CreateGLWindow: LRESULT CALL BACK WndProc(HWND, UINT, WPARAM, LPARAM); // Прототип функции WndProc Назначение следующей части кода заключается в изменении размеров OpenGL сцены всякий раз, когда будут изменены размеры окна (при условии, использовании оконного, а не полноэкранного режима). Размеры сцены OpenGL будут изменены в соответствии с шириной и высотой окна, в которое выводится изображение: GLvoid ReSizeGLScene(GLsizei width, GLsizei height ) // Изменить размер и инициализировать окно GL { if(height == 0) // Предотвращение деления на ноль { height = 1; } glViewport (0, 0, width, height); // Сброс текущей области вывода Следующие строки настраивают экран для перспективного вида. Предметы с увеличением расстояния становятся меньше. Это придаёт реалистичность сцене. Охват перспективы составляет 45 градусов, угол поворота оси рассчитывается на основе ширины и высоты окна. Значения 0.1f, 100.0f – отправная и конечная точки для того, чтобы определить какая будет глубина у экрана. Функция glMatrixMode(GL_PROJECTION) сообщает о том, что следующие две команды будут воздействовать на матрицу проекции. Матрица проекции отвечает за добавление в сцену перспективного вида. glLoadIdentity() – это функция работает подобно сбросу. Она восстанавливает выбранную матрицу в первоначальное состояние. Раз матрица проекции сброшена, необходимо вычислить перспективу для сцены. После вызова glLoadIdentity() происходит инициализация перспективного вида сцены. glMatrixMode(GL_MODELVIEW) сообщает, что любые новые трансформации будут воздействовать на матрицу вида модели. Позже необходимо сбросить матрицу вида модели: glMatrixMode(GL_PROJECTION); // Выбор матрицы проекций glLoadIdentity(); // Сброс матрицы проекции // Вычисление соотношения геометрических размеров для окна gluPerspective(45.0f,GLfloat)width/(GLfloat)height, 0.1f, 100.0f ); 33 glMatrixMode(GL_MODELVIEW); // Выбор матрицы вида модели glLoadIdentity(); // Сброс матрицы вида модели } Далее будут произведены все настройки для OpenGL: цвет для очистки экрана, глубина буфера, плавное сглаживание цветов, и другое. Эта функция не должна быть вызвана до тех пор, пока OpenGL окно не будет создано: int InitGL( GLvoid ) // Все установки касаемо OpenGL происходят здесь { В следующей строке разрешается сглаживание с плавным цветовым переходам: glShadeModel( GL_SMOOTH ); // Разрешить плавное цветовое сглаживание Плавное цветовое сглаживание смешивает цвета вдоль всего полигона и учитывает освещение: Следующая строка устанавливает цвет, которым будет очищен экран. Все значения могут быть в диапазоне от 0.0f до 1.0f, при этом 0.0 самый темный, а 1.0 самый светлый. Первый аргумент в glClearColor – это интенсивность красного (Red), второй – зеленного (Green), третий – синего (Blue). Наибольшее значение – 1.0f, является самым ярким значением данного цвета. Последнее число - для Альфа (Прозрачность) значения. Возможно получение различных цветов, смешивая три компоненты цвета. Поэтому, необходимо вызвать glClearColor(0.0f,0.0f,1.0f,0.0f). Для того чтобы сделать белый фон, Надо установить все цвета в (1.0f). Черный – все компоненты цвета равны 0.0f: glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Очистка экрана в черный цвет Следующие три строки создают буфер глубины. Буфер глубины указывает, как далеко объекты находятся от экрана и позволяет сортировать объекты для обрисовки, поэтому квадрат, расположенный под кругом не будет изображен поверх него: glClearDepth( 1.0f ) ;// Разрешить очистку буфера глубины glEnable( GL_DEPTH_TEST ); // Разрешить тест глубины glDepthFunc( GL_LEQUAL ); // Тип теста глубины glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST); // Улучшение в вычислении перспективы 34 Если необходимо узнать, прошла ли инициализация как положено, необходимо проверить возвращаемое значение на true и false: return true; // Инициализация прошла успешно } В следующей секции содержится весь код для рисования. Возвращение значения true говорит о том, что в этой части не возникло никаких проблем. Если, по каким-либо причинам, необходимо остановить выполнение программы надо добавить строчку return false перед return true, таким образом, происходит сообщение программе, что в части кода, выполняющего рисование, произошла ошибка – произойдёт выход из программы: int DrawGLScene( GLvoid ) // Здесь будет происходить вся прорисовка { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // Очистить экран и буфер глубины glLoadIdentity(); // Сбросить текущую матрицу return true; // Прорисовка прошла успешно } Следующая часть кода вызывается только перед выходом из программы. Задача KillGLWindow() – освободить Контекст Рендеринга (hRC), Контекст Устройства (hDC) и, наконец, дескриптор окна (hWnd). Если программа неспособна удалить какую-нибудь часть из контекстов окна, появится окно (Message Box) с соответствующим сообщением об ошибке. Чем их больше будет создано (Message Box-ов), тем проще будет найти ошибку: GLvoid KillGLWindow(GLvoid) // Корректное разрушение окна { Первое, что надо сделать в этой функции – проверить, запущена ли программа в полноэкранном режиме. Если это так, то необходимо переключиться назад на рабочий стол. Следует уничтожить окно, предварительно выйдя из полноэкранного режима, на некоторых видеокартах, если уничтожить полноэкранный режим, рабочий стол искажается. Именно поэтому разрушение полноэкранного режима будет производится в первую очередь: if( fullscreen ) //Программа в полноэкранном режиме? { Далее используется ChangeDisplaySettings(NULL,0) для возврата рабочего стола в первоначальное состояние. Передача NULL в первом параметре и 0 во втором принуждает окно использовать параметры, хранящиеся в регистре окна (устанавливая разрешение, битовую глубину, частоту обновления экрана и другое по умолчанию), 35 действительно восстанавливая первоначальное состояние рабочего стола: ChangeDisplaySettings (NULL, 0); // Если да, то переключение обратно в оконный режим ShowCursor( true ); // Показать курсор мышки } Код, приведённый ниже проверяет, существует ли Контекст Рендеринга (hRC): if( hRC ) // Существует ли Контекст Рендеринга? { Если его нет, то программа переходит на часть кода, расположенную ниже и проверяющие существует ли Контекст Устройства (hDC). Если код существует, код ниже проверит, возможно ли освободить его (отсоединить RC от DC): if( !wglMakeCurrent(NULL, NULL)) // Возможно ли освободить RC и DC? { Если невозможно уничтожить контексты RC и DC, выскочит сообщение об ошибке, это позволит понять, что контексты не уничтожены. NULL в функции MessageBox() означает, что у сообщения не будет родительского окна. Текст справа от NULL – текст, который будет содержать сообщение: MessageBox(NULL, "Release Of DC And RC Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION); } "SHUTDOWN ERROR" – это текст, который будет содержаться в заголовке окна-сообщения. Потом следует удалить Контекст Рендеринга. Если это не возможно выскочит соответствующее сообщение: if(!wglDeleteContext(hRC)) // Возможно ли удалить RC? { Если невозможно удалить Контекст Рендеринга код, приведённый ниже, выведет окно-сообщение позволяющее понять, что его удаление невозможно. hRC будет установлено в NULL: MessageBox( NULL, "Release Rendering Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION ); } hRC = NULL; // Установить RC в NULL } Теперь следует проверить, имеет ли программа Контекст Устройства, и если это так, то производим его уничтожение: 36 if(hDC && !ReleaseDC(hWnd, hDC)) // Возможно ли уничтожить DC? { MessageBox(NULL, "Release Device Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION); hDC=NULL; // Установить DC в NULL } Если это невозможно окно-сообщение выведет соответствующее сообщение и hDC будет установлен в NULL. Теперь необходимо проверить, есть ли дескриптор окна, а если есть, пробуем уничтожить окно, используя DestroyWindow(hWnd). Если это невозможно окно-сообщение выведет соответствующее сообщение и hWnd будет установлен в NULL: if(hWnd && !DestroyWindow(hWnd)) // Возможно ли уничтожить окно? { MessageBox(NULL, "Could Not Release hWnd.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION); hWnd = NULL; // Установить hWnd в NULL } Последнее, что необходимо сделать так это разрегистрировать (операция, обратная регистрации) класс окна: if(!UnregisterClass("OpenGL", hInstance) // Возможно ли разрегистрировать класс { MessageBox(NULL, "Could Not Unregister Class.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION); hInstance=NULL; // Установить hInstance в NULL } } Это позволяет корректным образом уничтожить окно и открыть другое без получения сообщения об ошибке "Windows Class already registered" (Класс окна уже зарегистрирован). Таким образом, функция возвращает BOOL (true или false), всего она получает пять аргументов: Заголовок Окна, Ширину Окна, Высоту Окна, Число Битов (16/24/32), и флаг режима (true для полноэкранного или false для оконного). Возвращаем логическую переменную, которая говорит о том, возможно ли создать окно. Следующая секция кода создаёт OpenGL окно: BOOL CreateGLWindow(LPCWSTR title, int width, int height, int bits, bool fullscreenflag) { 37 GLuint PixelFormat; // Хранит результат после поиска Переменная wc будет использоваться для хранения структуры класса окна. Структура класса содержит информацию о данном окне. Изменяя различные поля класса, есть возможность изменить вид окна и его поведение. Каждому окну соответствует определённый класс: WNDCLASS wc; // Структура класса окна dwExStyle и dwStyle будут хранить расширенную и обычную информацию о стиле окна. DWORD dwExStyle; // Расширенный стиль окна DWORD dwStyle; // Обычный стиль окна Следующие пять строк кода устанавливают значения координат левого верхнего и правого нижнего угла прямоугольника (окна): RECT WindowRect; // Grabs Rectangle Upper Left / Lower Right Values WindowRect.left=(long)0; // Установить левую составляющую в 0 WindowRect.right=(long)width; // Установить правую составляющую в Width WindowRect.top=(long)0; // Установить верхнюю составляющую в0 WindowRect.bottom=(long)height; // Установить нижнюю составляющую в Height Следует использовать эти значения для выравнивания окна так, чтобы область, в которой происходит рисование, имела такое разрешение, какое желаемо. Обычно, если создаётся окно 1024х768, границы окна занимают некоторую часть разрешения. В следующей строке кода присваивается глобальной переменной fullscreen значение переменной fullscreenflag: fullscreen=fullscreenflag; // Устанавка значения глобальной переменной fullscreen Стили CS_HREDRAW и CS_VREDRAW принуждают перерисовать окно всякий раз, когда оно перемещается. CS_OWNDC создает скрытый DC для окна: hInstance = GetModuleHandle(NULL); // Считывание дескриптора нашего приложения wc.style = CS_HREDRAW|CS_VREDRAW| CS_OWNDC; // Перерисовка при перемещении и создании скрытого DC wc.lpfnWndProc = (WNDPROC) WndProc; // Процедура обработки сообщений wc.cbClsExtra = 0; // Нет дополнительной информации для окна wc.cbWndExtra = 0; // Нет дополнительной информации для окна 38 wc.hInstance = hInstance; // Устанавление дескриптора wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Загрузка иконки по умолчанию wc.hCursor= LoadCursor(NULL, IDC_ARROW); // Загрузка указателя мышки wc.hbrBackground = NULL; // Фон не требуется для GL wc.lpszMenuName = NULL; // Меню в окне не будет wc.lpszClassName = "OpenGL"; // Устанавка имя классу Это означает, что DC не используется совместно несколькими приложениями. WndProc – процедура, которая перехватывает сообщения для программы. Дополнительной информации для нашего окна нет, поэтому заполняем два этих (cbClsExtra и cbWndExtra) поля нулями. Далее надо зарегистрировать класс. Если возникнет какая-либо проблема или ошибка, выскочит соответствующее окно-сообщение: if(!RegisterClass(&wc)) // Пытаемся зарегистрировать класс окна { MessageBox( NULL, "Failed To Register The Window Class.", "ERROR", MB_OK | MB_ICONEXCLAMATION ); return false; // Выход и возвращение функцией значения false } Далее необходимо проверить формат программы: if(fullscreen) // Полноэкранный режим? { Далее необходимо убедиться, что ширина и высота, которые используются в полноэкранном режиме те же самые, что и ширина и высота, которую планировали создать в окне, и, что тоже очень важно, установка полноэкранного режима должна происходить перед созданием окна. В коде очищается память для хранения видео настроек и устанавливается ширина, высота и глубина цвета, которые необходимы: DEVMODE dmScreenSettings; // Режим устройства memset(&dmScreenSettings,0,sizeof (dmScreenSettings)); // Очистка для хранения установок dmScreenSettings.dmSize=sizeof (dmScreenSettings); // Размер структуры Devmode dmScreenSettings.dmPelsWidth = width; // Ширина экрана 39 dmScreenSettings.dmPelsHeight = height; // Высота экрана dmScreenSettings.dmBitsPerPel = bits; // Глубина цвета dmScreenSettings.dmFields=DM_BITSPERPEL|M_PELSWIDTH | DM_PELSHEIGHT; //Режим Пикселя Далее в коде подается запрос на установление полноэкранного режима: // Попытка установить выбранный режим и получить результат. if(ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN) ! = DISP_CHANGE_SUCCESSFUL) { Вся информация о ширине, высоте и глубине цвета хранится в dmScreenSettings. В строке ниже функция ChangeDisplaySettings пробует переключить экран в режим, настройки которого хранятся в dmScreenSettings. Строка CDS_FULLSCREEN убирает панель управления. Если режим не установлен будет задействован следующий код, если соответствующий полноэкранный режим не существует, выскочит окно-сообщение предлагающее два варианта: запустить приложение в оконном режиме или выйти: // Если переключение в полноэкранный режим невозможно, будет предложено два варианта: оконный режим или выход. if( MessageBox( NULL, "The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?", "Проект GL", MB_YESNO | MB_ICONEXCLAMATION) == IDYES ) { Если пользователь решил, использовать оконный режим – переменной fullscreen присвоится значение false и выполнение программы продолжится: fullscreen = false; // Выбор оконного режима(fullscreen = false) } else { Если пользователь решил выйти из приложения выскочит окно с сообщением о закрытие программы: // Выскакивающее окно, сообщающее пользователю о закрытие окна. MessageBox(NULL, "Program Will Now Close.", "ERROR", MB_OK | MB_ICONSTOP); return false; // Выход и возвращение функцией false } } 40 } Функция вернёт false, что сообщит программе, что создание окна не завершилось успехом. Так как данный код мог вызвать ошибку инициализации полноэкранного режима, и пользователь мог решить запускать программу всё же в оконном режиме, следует проверить ещё раз значение переменной fullscreen (true или false) перед тем, как устанавливается режим экрана/окна: if(fullscreen) // Полноэкранный режим? { Теперь необходимо запретить использование указателя мышки, так как в случае, если программа не является интерактивной, правилом хорошего тона является сокрытия указателя мышки в полноэкранном режиме: dwExStyle = WS_EX_APPWINDOW; // Расширенный стиль окна dwStyle = WS_POPUP; // Обычный стиль окна ShowCursor( false ); // Скрыть указатель мышки } else { Если используется оконный режим, вместо полноэкранного режима, следует добавить WS_EX_WINDOWEDGE в расширенный стиль. Это придаст окну более объёмный вид. Для обычного стиля задается параметр WS_OVERLAPPEDWINDOW вместо WS_POPUP. WS_OVERLAPPEDWINDOW создаёт окно с заголовком, границы для регулировки размера, оконное меню и кнопки для сворачивания/разворачивания: dwExStyle = WS_EX_APPWINDOW|EX_WINDOWEDGE; // Расширенный стиль окна dwStyle = WS_OVERLAPPEDWINDOW;// Обычный стиль окна } Строка ниже, регулирует наше окно в зависимости от того, какой стиль окна мы создаем. Настройка сделает окно таким, какие параметры ему были заданны. Обычно границы перекрывают часть окна. Используя функцию AdjustWindowRectEx указывается, чтобы ни одна часть окна OpenGL не была перекрыта границами, вместо этого будут созданы дополнительные пиксели специально для рамки границ: AdjustWindowRectEx(&WindowRect, dwStyle, false, dwExStyle): // Подбирает окну подходящие размеры На полноэкранный режим эта операция никак не повлияет. В следующей секции кода происходит создание окна и проверка создано ли оно должным образом: 41 if(!(hWnd = CreateWindowEx(dwExStyle, // Расширенный стиль для окна _T("OpenGL"), // Имя класса title, // Заголовок окна WS_CLIPSIBLINGS | // Требуемый стиль для окна WS_CLIPCHILDREN |. // Требуемый стиль для окна dwStyle, // Выбираемые стили для окна 0, 0, // Позиция окна WindowRect.right-WindowRect.left, // Вычисление подходящей ширины WindowRect.bottom-WindowRect.top, // Вычисление подходящей высоты NULL, // Нет родительского NULL, // Нет меню hInstance, // Дескриптор приложения NULL))) // Не передаём ничего до WM_CREATE (???) Происходит передача CreateWindowEx() всех параметров, в которых она нуждается: определённый ранее расширенный стиль; имя класса (которое должно быть тем самым, что Вы использовали, когда регистрировали класс окна); заголовок окна; обычный стиль окна; X левого угла окна; Y левого угла окна; ширина окна; высота окна; родительское окно; дескриптор меню; дескриптор приложения; дополнительные данные. Далее создается проверка, создано ли окно правильно. Если окно было создано, hWnd будет хранить дескриптор окна. Если же окно не было создано – код ниже вызовет всплывающее сообщение об ошибке и программа завершится. { KillGLWindow(); // Восстановить экран MessageBox( NULL, "Window Creation Error.", "ERROR", MB_OK | MB_ICONEXCLAMATION ); return false; // Вернуть false 42 } Следующая часть кода описывает формат пикселей (Pixel Format): static PIXELFORMATDESCRIPTOR pfd= // pfd сообщает Windows каким будет вывод на экран каждого пикселя { sizeof(PIXELFORMATDESCRIPTOR), // Размер дескриптора данного формата пикселей 1, // Номер версии PFD_DRAW_TO_WINDOW | // Формат для Окна PFD_SUPPORT_OPENGL | // Формат для OpenGL PFD_DOUBLEBUFFER, // Формат для двойного буфера PFD_TYPE_RGBA, // Требуется RGBA формат bits, // Выбирается бит глубины цвета 0, 0, 0, 0, 0, 0, // Игнорирование цветовых битов 0, // Нет буфера прозрачности 0, // Сдвиговый бит игнорируется 0, // Нет буфера накопления 0, 0, 0, 0, // Биты накопления игнорируются 32, // 32 битный Z-буфер 0, // Нет буфера трафарета 0 // Нет вспомогательных буферов PFD_MAIN_PLANE, // Главный слой рисования 0, // Зарезервировано 0, 0, 0, // Маски слоя игнорируются } Выбирается формат, который поддерживает OpenGL и двойной буфер, в соответствии с RGBA (Red-красный, Green-зелёный, Blueсиний, Alpha Channel-альфа канал (канал прозрачности)). Затем устанавливается 32 битный Z-буфер (буфер глубины). Остальные параметры или не используются, или не важны (кроме буфера трафарета и (медленного) буфера накопления). Если во время создания окна не возникло ни одной ошибки, происходит попытка получения Контекста Устройства (DC) OpenGL. Если невозможно получить DC, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false): if(!(hDC = GetDC(hWnd))) // Возможно ли получить Контекст Устройства? { KillGLWindow(); // Восстановить экран MessageBox( NULL, "Can't Create A GL Device Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION ); 43 return false; // Вернуть false } Если получен Контекст Устройства для нашего OpenGL окна происходит попытка найти формат пикселя, который описан выше. Если Windows не может найти подходящий формат, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false): if(!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Найден ли подходящий формат пикселя? { KillGLWindow(); // Восстановить экран MessageBox(NULL, "Can't Find A Suitable PixelFormat.", "ERROR", MB_OK | MB_ICONEXCLAMATION); return false; // Вернуть false } Выбирается формат, который поддерживает OpenGL и двойной буфер, в соответствии с RGBA (Red-красный, Green-зелёный, Blueсиний, Alpha Channel-альфа канал (канал прозрачности)). Затем устанавливается 32 битный Z-буфер (буфер глубины). Остальные параметры или не используются, или не важны (кроме буфера трафарета и (медленного) буфера накопления). Если Windows нашёл соответствующий формат, происходит его установка. Если же он не может быть установлен, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false): if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // Возможно ли установить Формат Пикселя? { KillGLWindow(); // Восстановить экран MessageBox(NULL, "Can't Set The PixelFormat.", "ERROR", MB_OK | MB_ICONEXCLAMATION); return false; // Вернуть false } Если формат был корректно установлен, получается Контекст Рендеринга. Если это не возможно, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false): if( !( hRC = wglCreateContext( hDC ) ) ) // Возможно ли установить Контекст Рендеринга? { KillGLWindow(); // Восстановить экран MessageBox(NULL, "Can't Create A GL Rendering Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION); 44 return false; // Вернуть false } Если до сих пор не возникало ошибок, т. е. созданы и Контекст Устройства, и Контекст Рендеринга, далее надо сделать Контекст Рендеринга активным. Если этого не получится, на экран выскочит сообщение об ошибке и программа завершит работу (функция возвратит false): if( !wglMakeCurrent( hDC, hRC ) ) // Попробовать активировать Контекст Рендеринга { KillGLWindow(); //Восстановить экран MessageBox(NULL, "Can't Activate The GL Rendering Context.", "ERROR", MB_OK | MB_ICONEXCLAMATION); return false; // Вернуть false } Если всё прошло правильно, и OpenGL окно было создано, показывается окно, устанавливается на передний план (присвоив более высокий приоритет) и затем устанавливается фокус для этого окна, потом вызывается ReSizeGLScene, передавая ширину и высоту экрана для настройки перспективы для нашего OpenGL экрана: ShowWindow(hWnd, SW_SHOW); // Показать окно SetForegroundWindow(hWnd); // Слегка повысим приоритет SetFocus(hWnd); // Установить фокус клавиатуры на наше окно ReSizeGLScene(width, height); // Настройка перспективы для OpenGL экрана. Далее выполненяем функции InitGL() для настройки освещения и текстуры, также возможно сделать дополнительные проверки на ошибки в InitGL(), и передать true (всё OK) или false: if( !InitGL() ) // Инициализация только что созданного окна { KillGLWindow(); // Восстановить экран MessageBox(NULL, _T("Initialization Failed."), _T("ERROR"), MB_OK | MB_ICONEXCLAMATION); return false; // Вернуть false } Если программа дошла до этого момента, логично предположить, что создание окна закончилось успехом, далее происходит возврат true в WinMain(), что сообщает о том, что не возникло никаких ошибок: return true; 45 } То есть программа не завершается, а благополучно продолжает работу. Далее будет происходить обработка сообщений для окна. LRESULT CALLBACK WndProc(HWND hWnd, // Дескриптор нужного окна UINT uMsg, // Сообщение для этого окна WPARAM wParam, // Дополнительная информация LPARAM lParam) // Дополнительная информация { Когда регистрируется класс окна, указывается функция обработки сообщений. Код ниже устанавливает uMsg как параметр, с которым будут сравниваться все блоки. uMsg будет хранить название сообщения, с которым будет работать. switch (uMsg) // Проверка сообщения для окна { Если окно минимизировано, переменная active будет равна false, если окно активно, переменная active будет равна true: case WM_ACTIVATE : // Проверка сообщения активности окна { if( !HIWORD( wParam ) ) // Проверить состояние минимизации { active = true; // Программа активна } else { active = false; // Программа теперь не активна } return 0; // Возвращаемся в цикл обработки сообщений } Если возникло сообщение WM_SYSCOMMAND (системная команда), то сравнивается состояние по параметру wParam. Если wParam – SC_SCREENSAVE или SC_MONITORPOWER, то или запускается скринсейвер (программа сохранения экрана) или монитор пытается перейти в режим сбережения энергии. Возвращая ноль – предотвращается наступлению обоих этих событий: case WM_SYSCOMMAND: // Перехват системной команды { switch(wParam) // Останавка системный вызов { 46 case SC_SCREENSAVE: // Пытается ли запустится скринс.? case SC_MONITORPOWER: // Пытается ли монитор перейти в режим сбережения энергии? return 0; // Предотвращаем это } break; // Выход } Если uMsg – WM_CLOSE окно будет закрыто. Переменная done будет установлена в true, главный цикл в WinMain() будет остановлен и программа будет закрыта: case WM_CLOSE: // Получено сообщение о закрытии? { PostQuitMessage(0); // Отправка сообщения о выходе return 0; // Вернуться назад } Если произошло нажатие кнопки (на клавиатуре) происходит распознавание, какая клавиша это была, считав wParam: case WM_KEYDOWN: // Была ли нажата кнопка? { keys[wParam] = true; // Если так, этой ячейке присваивается true return 0; // Возврат } Таким образом, можно считать этот массив позже и найти какая клавиша была нажата. Это позволяет отследить нажатия сразу несколько клавиш одновременно. Всякий раз, когда изменяются размеры окна uMsg в конечном счёте будет иметь значение WM_SIZE. Происходит считывание LOWORD и HIWORD (младшее и старшее слова) переменной lParam для того, чтобы узнать новые высоту и ширину окна. Происходит передача аргументов функции ReSizeGLScene(). OpenGL сцена перерисуется с новой шириной и высотой: case WM_SIZE: // Изменены размеры OpenGL окна { ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // Младшее слово=Width, старшее слово=Height return 0; // Возврат } } Любое сообщение, которое не проверено, будет передано в качестве фактического параметра функции DefWindowProc для того, чтобы Windows могла его обработать: 47 //пересылаем все необработанные сообщения DefWindowProc return DefWindowProc(hWnd,uMsg,wParam,lParam); } Можно сказать, что это входная точка в Windows приложение, где вызывается операция создания окна: int WINAPI WinMain(HINSTANCE hInstance, // Дескриптор приложения HINSTANCE hPrevInstance, // Дескриптор родительского приложения LPSTR lpCmdLine, // Параметры командной строки int nCmdShow) // Состояние отображения окна { Далее инициализируются две переменные: MSG msg; // Структура для хранения сообщения Windows BOOL done = false; // Логическая переменная для выхода из цикла Переменная msg будет использоваться для того, чтобы проверить существует ли какое-нибудь ожидающее обработки сообщение. Переменная done при старте будет равна false. Это означает, что программа не закончила своего выполнения. Пока done равно false программа будет продолжать выполнение. Как только done изменит значение с false на true – программа закончит выполнение. Эта секция часть кода является полностью дополнительной. Она вызовет всплывающее окно, которое спросит, надо ли запустить приложение в полноэкранном режиме. Если пользователь кликнет на кнопке "NO", переменная изменится с true (по умолчанию) на false и программа запустится в оконном режиме: // Спрашивает пользователя, какой режим экрана он предпочитает if(MessageBox( NULL, "Хотите ли Вы запустить приложение в полноэкранном режиме?", "Запустить в полноэкранном режиме?", MB_YESNO | MB_ICONQUESTION) == IDNO) { fullscreen = false;// Оконный режим } Далее задается, как будет создано окно: // Создани OpenGL окна if(!CreateGLWindow("Проект OpenGL окно", 1024, 768, 32, fullscreen)) { 48 return 0; // Выйти, если окно не может быть создано } Передается заголовок, ширина, высота, глубина цвета и true (полноэкранный режим) или false (оконный режим) функции CreateGLWindow. Если окно не будет создано по какой бы то ни было причине, CreateGLWindow вернёт false и наша программа немедленно завершиться (return 0). Далее стартует новый цикл – пока done равно false цикл будет повторяться: while(!done) // Цикл продолжается, пока done не равно true { Сначала проверяется стоит ли в очереди какое-нибудь сообщение, и используя PeekMessage(), это делается без остановки выполнения нашей программы: if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ли в очереди какое-нибудь сообщение? { В следующей части кода проверяется, есть ли в очереди сообщение о выходе: if( msg.message == WM_QUIT ) // Получено сообщение о выходе? { done = true; //Если так, done=true } else // Если нет, обработка сообщения { Если текущее сообщение WM_QUIT, это повод для вызова PostQuitMessage( 0 ), установки переменной done в true и завершения программы. Если сообщение в очереди не сообщение о выходе, преобразовывается сообщение, затем отсылаемт его так, чтобы WndProc() или Windows могли работать с ним: TranslateMessage( &msg ); // Перевод сообщения DispatchMessage( &msg ); // Отсылка сообщения } } else // Если нет сообщений { Если не было сообщений, происходит рисование сцены, то первая строчка кода ниже проверяет активно ли окно, если нажата кнопка ESC, переменная done устанавливается в true, приводя к выходу из программы: 49 // Прорисовка сцены. if( active ) // Активна ли программа? { if(keys[VK_ESCAPE]) // Была ли нажата клавиша ESC? { done = true; // ESC говорит об останове выполнения программы } else // Ообновим экран. { Если программа активна и не нажата ESC визуализируется сцена и меняется буфер: DrawGLScene(); // Рисование сцены SwapBuffers( hDC ); // Двойная буферизация } } Используя двойную буферизацию происходит рисование всего того, что происходит на скрытом экране (второй буфер) так, чтобы пользователь не мог видеть этого. Когда буфер изменяется, экран (первый буфер), который видит пользователь, становится скрытым, а скрытый – становится видимым. Таким образом, не видно саму прорисовку сцены, а только результат визуализации. Следующий код позволяет изменять полноэкранный режим на оконный и обратно, нажимая клавишу F1: if(keys[VK_F1]) // Была ли нажата F1? { keys[VK_F1] = false; // Если так, замена значения ячейки массива на false KillGLWindow(); // Разрушение текущего окна fullscreen = !fullscreen; // Переключение режима // Пересоздание OpenGL окна if( !CreateGLWindow( _T("Проект OpenGL структура"), 1024, 768, 32, fullscreen ) ) { return 0; // Выход, если это невозможно } } } } Если переменная done больше не false, программа завершается. 50 Далее корректно разрушается OpenGL окно, чтобы всё было освобождено и производится выход из программы: // Shutdown KillGLWindow(); // Разрушение окна return ( msg.wParam ); // Выход из программы } В результате получаем OpenGL окно (рис.1), которое следует использовать для выполнения всех последующих программ. Рис. 1. OpenGL окно Контрольные вопросы 1. Что такое OpenGL? 2. В чем заключается основной принцип работы OpenGL? 3. Для каких целей предназначена библиотека GLU? 4. Поясните такие основные понятия компьютерной графики, как: пиксель, рендеринг и битовая плоскость. 5. Как в OpenGL осуществляется организация и управление окном? 6. Поясните следующие понятия: Контекст Рендеринга и Контекст Устройства. 7. Поясните смысл переменной fullscreen. 8. Как реализовать возможность изменения размеров OpenGL сцены всякий раз, когда будут изменены размеры окна? 9. Как и зачем настраивают экран для перспективного вида? 10. Как установить цвет для очистки экрана, включить глубину буфера, плавное сглаживание цветов? 11. Для чего предназначен буфер глубины? 12. Какую задачу выполняет KillGLWindow? 51 13. Что такое Message Box? 14. Как и для чего необходимо предусмотреть возможность корректным образом уничтожить окно и открыть другое без получения сообщения об ошибке? 15. Как задать значения координат левого верхнего и правого нижнего угла прямоугольника (окна)? 16. Как дать возможность пользователю осуществлять запуска программы в полноэкранном или в оконном режимах? 17. Как задается массив, используемый для операций с клавиатурой? 18. Что означает NULL в функции MessageBox() ? 19. Перечислите параметры, в которых нуждается CreateWindowEx() при создании окна. 20. Объясните различия между функциями вызова сообщений PeekMessage() и GetMessage(). 21. В какой секции кода происходит создание окна и проверка создано ли оно должным образом? 22. Какая секции кода описывает Формат Пикселей (Pixel Format). 2.2. Прорисовка примитивов Целью программы, приведенной в качестве примера, является создания фигур – треугольник и квадрат при помощи функций 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. Координаты, расположенные слева, снизу и вглубь от него, имеют отрицательное значение, расположенные справа, сверху и по направлению к наблюдателю – положительное. 52 Функция glTranslate(x, y, z) перемещает оси координат на указанные значения. Следующая строчка кода перемещает ось X на 1.5 единиц и ось Z на 6 единиц. Следует заметить, что перевод осей координат осуществляется не относительно центра экрана, а от их текущего расположения: glTranslatef(-1.5f,0.0f,-6.0f); //Сдвиг влево на 1.5 единицы и в экран на 6.0 Функция glBegin(GL_TRIANGLES) означает начало рисования треугольника. Далее следует перечисление его вершин. После указания всех вершин, производится вызов функции glEnd(). Первая строка после glBegin описывает первую вершину полигона. Функция glVertex3f() получает в качестве параметров ее X, Y и Z координаты. Первая вершина треугольника смещена только от оси Y на 1, таким образом, она расположится в центре и она будет самой верхней. Следующая вершина будет располагаться на оси Х слева от центра и на оси Y вниз от центра. Эта вершина располагается внизу слева. Третья вершина будет справа и снизу от центра. Функция glEnd() указывает, что вершин больше не будет. Результат данного кода – залитый цветом по умолчанию треугольник: 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 единицы Далее изображается квадрат: 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(); } Так как он является четырехсторонним полигоном, используется GL_QUADS. Создание квадрата напоминает создание треугольника, но указывать нужно четыре вершины. В следующем порядке – левая вверху, правая вверху, правая снизу и левая снизу. В результате должны 53 быть получены на экране геометрическиефигуры треугольник и квадрат (рис. 2). Рис. 2. Ввывод геометрических фигур на экране Контрольные вопросы 1. Какая функция устанавливает начало системы координат в центр экрана? 2. Каково назначение функции glTranslate(x, y, z)? 3. Какие параметры имеет функция glVertex3f() и за что отвечает каждый параметр? 4. В какой последовательности указываются вершины при рисовании квадрата? 2.3. Отображение цвета Целью примера является реализация возможностей применения цветов в OpenGL. В программе будут отображаться фигуры, используемые в предыдущем примере, но закрашенные в разные цвета, например, квадрат требуется залить одним цветом, а треугольник тремя разными цветами (по одному на каждую вершину) с гладкими переходами. Возможно использование предыдущего кода, изменив лишь процедуру DrawGLScene(), а именно, заменить, или добавить те строки, которые там отсутствуют: GLvoid DrawGLScene(GLvoid) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); 54 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); Далее проделаем те же операции с третьей и последней вершиной (правый нижний угол): glColor3f(0.0f,0.0f,1.0f); // Синий цвет glVertex3f(1.0f,-1.0f, 0.0f); glEnd(); glTranslatef(3.0f,0.0f,0.0f); Перед тем как отобразить ее, следует установить синий цвет. После выполнения команды glEnd() треугольник будет залит указанными цветами. Так как для каждой вершины указан свой цвет, каждая часть фигуры будет залита по-разному. При переходе в другую вершину заливка будет плавно изменять свой цвет на цвет вершины. В середине треугольника все три цвета будут слиты в один. Теперь следует отобразить квадрат, но залить его одним цветом. Очень важно помнить, что если установлен какой-либо цвет, все примитивы в дальнейшем будет отображаться именно им. Каждый последующий проект, который будет создаваться, так или иначе, будет использовать цвета. Если, например, создана сцена, где все фигуры текстурированы, цвет будет использоваться для тона текстур, и т.д. Так как квадрат будет реализован в одном цвете (для примера – в красном), для начала необходимо установить этот цвет, а затем отобразить саму фигуру. Синий цвет будет использоваться OpenGL для каждой вершины, так как он неизменен: glColor3f(0.5f,0.5f,1.0f); // Установление синего цвета только один раз 55 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(); } В этом примере детально описаны процедуры установки цветов для вершин, показаны различия между однотонной заливкой и разноцветной сглаженной. Результат работы программы представлен на рис. 3. Измените координально цвет квадрата и треугольника. Измените программу так, чтобы квадрат был залит смешанным цветом, а треугольник, наоборот – сплошным цветом, как представлено на рис. 4. Рис. 3. Реализация смешанной заливки треугольника и окрашивания в яркий красные цвет квадрата 56 Рис. 4. Реализация градиентной заливки квадрата и окрашивание в яркий красные цвет треугольника Контрольные вопросы 1. Какую цветовую модель реализует OpenGL? 2. Какие параметры использует команда glColor3f? 3. Какими значениями можно задавать насыщенность цветов функции glColor3f? 4. При установлении какого-либо цвета будут ли в дальнейшем примитивы отображаться именно этим цветом? 5. Какой цвет описывается данной командой glColor3f(0.0f,0.0f,1.0f)? 6. С какой целью используется команда void glClearColor? 7. Как получить линейную интерполяцию цветов по поверхности примитива? 8. Что означает команда void glShadeModel(GLenummode) и ее параметры GL_SMOOTH и GL_FLAT? 2.5. Реализации вращения фигур Целью работы программы является вращение фигур на примере созданных в предыдущем проекте, примитивов (треугольник и квадрат). Следует использовать код из примера реализации возможностей применения цветов в OpenGL путем добавления новых строк кода: #include <windows.h> //Заголовочный файл для Windows #include <gl\gl.h> //Заголовочный файл для OpenGL32 библиотеки 57 #include <gl\glu.h> //Заголовочный файл для GLu32 библиотеки #include <gl\glaux.h> //Заголовочный файл для GLaux библиотеки static HGLRC hRC; //Постоянный контекст рендеринга static HDC hDC; //Приватный контекст устройства GDI BOOL keys[256]; //Массив для процедуры обработки клавиатуры GLfloat rtri; // Угол для треугольник GLfloat rquad; // Угол для четырехугольника Вначале добавляются две переменные для хранения угла вращения каждого объекта. После строки с объявлением переменной BOOL keys[256] объявляются две переменные с плавающей запятой, которые используются для очень точного поворота объектов. Числа с плавающей запятой учитывают значения меньше единицы. Вместо использования 1, 2, 3 для угла, допустимо использование 1.1, 1.7, 2.3 или 1.015 для точности. Необходимо модифицировать код в 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. В следующей строке кода, если rtri равно 7, будет вращение на 7 градусов по оси 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); // Левая точка - зеленая 58 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 сверху вниз, Z от переднего плана и далее. Происходит движение на 1.5 вправо, вместо 3.0, как было в последней работе, чтобы оказаться в 0.0. После того сдвига в новое место на правой стороне экрана, осуществляется вращение квадрата по оси 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 в контейнер. Контейнер rquad уменьшиться на 0.15. Отрицательные значения вращения приводят к тому, что объект вращается в противоположную сторону. Как если бы значения были положительные. С увеличением значения объект будет вращаться быстрее. С уменьшением значения будет вращаться медленнее: rtri+=0.2f; // Увеличение переменной вращения для треугольнка rquad-=0.15f; // Уменьшение переменной вращения для квадрата } Результат работы программы представлен на рис. 5. 59 Измените программу так, чтобы квадрат был залит градиентом, а треугольник, наоборот, сплошным цветом (например, желтым). Измините угол и скорость ващения фигур. Измените оси вращения для фигур. Рис. 5 Вывод и вращение квадрата и треугольника Контрольные вопросы 1. Каково назначение переменной rtri? 2. В коде ниже вызов glLoadIdentity() делается для инициализации просмотра. 3. Что делает функция glRotatef(Angle,Xtrue,Ytrue,Ztrue)? 4. Что означает данное выражение: glRotatef(10.0f,0.0f,1.0f,1.0f)? 5. Что означает строка «rquad-=0.15f»? 2.6. Создание фигур в 3D В данном примере требуется построить 3D объект на основе предыдущего проекта, который реализует вывод и вращение квадрата и треугольника. Это осуществляется добавлением с левой, задней и правой сторон треугольника, и с левой, правой, верхней и нижней сторон квадрата. В результате требуется получить пирамиду и куб. Необходимо смешать цвета на пирамиде, создавая сглаженный закрашенный объект, а для квадрата назначить каждой грани свой цвет: 60 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); // Начало рисования пирамиды Чтобы объект вращался вокруг оси, он должен быть разработан для вращения вокруг оси. Следует, помнить, что центр любого объекта должен быть в 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. Поэтому эта грань имеет наклон от центральной точки сверху вниз с правой стороны. Также надо обратить внимание на то, что левая точка нарисована синим цветом в этот раз. Так как она нарисована синей, это будет тот же самый цвет, какой у точки правого нижнего угла лицевой грани. Градиент синего цвета идет от одного угла вдоль лицевой и правой граней пирамиды. 61 Важно отметить, что все четыре грани включены внутрь тех же самых glBegin(GL_TRIANGLES) и glEnd(), словно одна сторона. Поскольку создается целый объект из треугольников, известно, что каждые три точки рисуются как три точки одного треугольника. Треугольник рисуется из трех точек, если больше трех точек, то 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); // Право треугольника (Сзади) В завершении необходимо построить левую грань: 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(); // Пирамида прорисована Цвета переключаются в последний раз. Левая точка синего цвета, и смешивается с правой точкой на задней грани. Правая точка зеленного цвета, и смешивается с левой точкой на передней грани. На этом шаге рисование пирамиды окончено. Поскольку пирамида только крутиться вдоль оси Y, то дна пирамиды не видно, поэтому нет необходимости выводить низ пирамиды. В результате работы цвет каждого угла в четырехугольнике совпадает с цветом, который использован в каждом из четырех углов пирамиды. На данном этапе производится рисование куба. Чтобы сделать это надо шесть квадратов. Все квадраты рисуются против часовой стрелке. 62 Первая точка должна располагаться справа вверху, вторая точка слева вверху, третья точка слева внизу, и последняя справа внизу. В примере куб сдвинут немного вглубь экрана, поэтому размер куба будет казаться меньше размера пирамиды. Если переместить куб на 6 единиц к экрану, то куб будет казаться больше чем пирамида, и часть куба будет за пределами экрана (рис.6). Рис. 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); // Право низ 63 Нижняя часть квадрата рисуется таким же образом, как и верхняя, но поскольку это низ, требуется сдвинуться вниз на одну единицу от центра куба. Стоит заметить, что ось 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 равна нулю: 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); // Низ право Задняя грань квадрата такая же, как передняя грань, но сдвинута вглубь экрана: 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); // Низ право Отметим, что ось Z всегда минус один во всех точках. На данном этапе осталось нарисовать только два квадрата: 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 всегда равна единице. Рисовать следует против часовой стрелки: glColor3f(1.0f,0.0f,1.0f); // Фиолетовый glVertex3f(1.0f, 1.0f,-1.0f); // Верх право квадрата (Право) glVertex3f( 1.0f, 1.0f, 1.0f); // Верх лево 64 glVertex3f( 1.0f,-1.0f, 1.0f); // Низ лево glVertex3f( 1.0f,-1.0f,-1.0f); // Низ право glEnd(); // Конец рисование квадратов rtri+=0.2f; // Увеличение переменную вращения для треугольника rquad-=0.15f;// Уменьшение переменную вращения для квадрата } Ререзультат работы программы представлен на рис. 7,8. Рис. 7. Вращение пирамиды и куба Рис. 8. Вращение куба 65 Контрольные вопросы 1. Какова будет реакция OpenGL, если в рисовании треугольника создать более трех точек? 2. Почему при рисовании куба и пирамиды одинаковых размеров в данной лабораторной работе куб выглядит меньше при демонстрации результата? 3. Почему в данной лабораторной работе не было необходимости выводить дно нарисованной пирамиды? 4. Какой командой реализуется вращение пирамиды по оси Y? 2.7. Наложение текстур на объекты В качестве объекта для наложения текстуры выберем куб. Далее следует добавить новые строки в код самого первого проекта инициализации окна. Первые три строки задают четыре вещественных переменных 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 пикселов. 66 AUX_RGBImageRec *texture1 задает указатель на структуру для хранения первой картинки, которая загружается используется как текстура. // Загрузка картинки и конвертирование в текстуру GLvoid LoadGLTextures() { // Загрузка картинки AUX_RGBImageRec *texture1; texture1 = auxDIBImageLoad("Data/….bmp"); Структура содержит красную, зеленую и синию компоненты цвета, которые используются при создании изображения. Так размещается в памяти загруженная картинка. Структура AUX_RGBImageRec определена в библиотеке glAux, и делает возможной загрузку картинки в память. В следующей строке происходит непосредственная загрузка. Файл картинки будет загружен и сохранен в структуре texture1, которую задали выше с помощью AUX_RGBImageRec. Вызов glGenTextures(1, &texture[0]) передает OpenGL то, что построение текстуры будет в нулевом элементе массива texture[] (Первая действительная область для сохранения имеет номер 0). Во второй строке вызов glBindTexture(GL_TEXTURE_2D, texture[0]) говорит OpenGL, что texture[0] (первая текстура) будет 2D текстурой. 2D текстуры имеют и высоту (по оси Y) и ширину (по оси X). Основная задача glGenTexture указать OpenGL на доступную память. Память доступна в &texture[0]. Затем создается текстура, и она будет сохранена в этой памяти: // Создание текстуры glGenTextures(1, &texture[0]); glBindTexture(GL_TEXTURE_2D, texture[0]); В следующих двух строках указывается какой тип фильтрации надо использовать, когда изображение больше на экране, чем оригинальная текстура (GL_TEXTURE_MAG_FILTER), или когда оно меньше на экране, чем текстура (GL_TEXTURE_MIN_FILTER). для обоих случаев используется GL_LINEAR: glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR); При этом текстура выглядит сглаженной на расстоянии, и вблизи. Использование GL_LINEAR требует много работы для процессора/видеокарты. 67 В завершении создается фактическая текстура. Текстура будет двухмерной (GL_TEXTURE_2D). Ноль задает уровень детализации, это обычно ноль. Три – число компонент цветовых данных, так как изображение сделано из трех цветовых компонент (красный, зеленый, синий). texture1->sizeX – это ширина текстуры, автоматически. texture1>sizeY – высота текстуры. Ноль – это бордюр. GL_RGB означает, что данные изображения представлены в порядке следования красных, зеленых и голубых компонент цвета. GL_UNSIGNED_BYTE означает, что данные, из которых состоит изображение имеют размер байта и все числа без знака, и в конце texture1->data указывает на то, где брать сами данные. В этом случае указатель на данные в записи texture1: glTexImage2D(GL_TEXTURE_2D, 0, 3, texture1->sizeX, texture1>sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, texture1->data); } В первой строке происходит вызов процедуры 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); } Далее первые две строки glClear() и glLoadIdentity() взяты из оригинального кода первого проекта: GLvoid DrawGLScene(GLvoid) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glTranslatef(0.0f,0.0f,-5.0f); 68 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) очищает экран цветом, который выбран в InitGL(). В этом случае, экран будет очищен в синий цвет. Буфер глубины будет также очищен. Просмотр будет сброшен с помощью glLoadIdentity(). Следующие три строки кода вращают куб по оси X, затем по оси Y, и в конце по оси Z: 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 Насколько велико будет вращение (угол) по каждой оси будет зависеть от значения указанного в xrot, yrot и zrot. В следующей строке кода происходит выбор – какую текстуру использовать для наложения текстуры: glBindTexture(GL_TEXTURE_2D, texture[0]); Первый аргумент glTexCoord2f – координата X. 0.0f – левая сторона текстуры. 0.5f – середина текстуры, и 1.0f – правая сторона текстуры, второе значение glTexCoord2f – это Y координата. 0.0f – низ текстуры. 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); // Низ лево 69 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, yrot и zrot: xrot+=0.3f; // Ось вращения X yrot+=0.2f; // Ось вращения Y zrot+=0.4f; // Ось вращения Z } Результат работы программы представлен на рис.9. 70 Рис. 9. Наложение текстуры на куб Контрольные вопросы 1. Какой код загружает файл картинки, и конвертирует его в текстуру? 2. Для чего служит AUX_RGBImageRec *texture1? 3. задает указатель на структуру для хранения первой картинки, которая загружается используется как текстура. Почему изображение должно иметь высоту и ширину кратную двум? 4. Для чего служит процедура LoadGLTextures()? 5. Какая функция разрешает наложение текстуры. 6. Зачем необходим вызов glGenTextures(1, &texture[0])? 7. Где именно в коде происходит выбор текстуры для наложения? 8. Как изменить направление вращения куба? 9. Как уменьшить скорость вращения фигуры? 2.8. Перемещения объектов с помощью клавиатуры. Режимы фильтрации текстур. Освещение Далее рассмотрим различные режимы фильтрации текстур, применение простого освещения OpenGL сцены с основами перемещения объектов с помощью клавиатуры. Основой программы является код первого проекта. Для начала необходимо добавить несколько новых переменных к коду программы: 71 #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 light; // Свет ВКЛ / ВЫКЛ BOOL lp; // L нажата? BOOL fp; // F нажата? Здесь следует создать переменную называемую light, чтобы отслеживать, действительно ли освещение включено или выключено. Переменные lp и fpь используются, для отслеживания нажатия клавиш 'L' и '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; // Сдвиг вглубь экрана Далее задаются массивы, которые будут использоваться для освещения. Следует учитывать, что фоновый свет не имеет никакого определенного направления. Все объекты будут освещены фоновым светом. Второй тип света – диффузный свет. Диффузный свет создается при помощи вашего источника света и отражается от поверхности 72 объекта в сцене. Любая поверхность объекта, на которую падает прямо свет, будет очень яркой и области, куда свет падает под углом, будут темнее. Свет создается так же как цвет. Поэтому в строке ниже, задается значение белого фонового света половиной интенсивности (0.5f): GLfloat LightAmbient[]={0.5f, 0.5f, 0.5f, 1.0f}; // Значения фонового света Поскольку все числа – 0.5f, получится свет средней яркости между черным (выключен свет) и белым (полная яркость). Смешанные в равных значениях красный, синий и зеленый дадут оттенки от черного (0.0f) до белого (1.0f). Без фонового света пятна, где нет диффузного света, будут очень темными. Все значения – 1.0f. Это означает, что свет настолько яркий, насколько возможно получить его. Диффузный свет эти яркое пятно перед кубом. В следующей строке задается значение для яркого, полного интенсивного диффузного света: GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; // Значения диффузного света Наконец задается позиция света: GLfloat LightPosition[]={0.0f,0.0f,2.0f,1.0f}; // Позиция света Первые три числа совпадают с тремя первыми аргументами функции glTranslate. Первое число смещение влево или вправо по оси x, второе число – для перемещения вверх или вниз по оси y, и третье число для перемещения к или от экрана по оси z. Поскольку в данном примере необходимо, чтобы свет, падал прямо на переднею часть куба, не стоит сдвигаться влево или вправо, поэтому первое значение – 0.0f (нет движения пооси x), а также не стоит сдвигаться и вверх или вниз, поэтому второе значение – 0.0f. Третье значение задается так, чтобы свет был всегда перед кубом. Поэтому свет помещается вне экрана по отношению к наблюдателю. Последнее число необходимо оставить в 1.0f. Это говорит о том, что данные координаты – позиция источника света. Переменная filter должна отслеживать, каким образом будут отображаться текстуры. Первая текстура (texture[0]) использует gl_nearest (без сглаживания). Вторая текстура (texture[1]) использует фильтрацию gl_linear, которая немного сглаживает изображение. Третья текстура (texture[2]) использует текстуры с мип-наложением (mipmapped, или множественное наложение), что повышает качество отображения. Переменная filter будет равна 0, 1 или 2 в зависимости от текстуры. 73 Объявление GLuint texture[3] создает место для хранения трех разных текстур. Текстуры будут сохранены в texture[0], texture[1] и texture[2]: GLuint filter; // Какой фильтр использовать GLuint texture[3]; // Место для хранения 3 текстур LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM); // Декларация WndProc Для наложения текстуры необходимо выбрать и загрузить картинку (bitmap, или растровый (побитный) образ изображения), и создать три различные текстуры из нее. Сразу же после кода выше, и до ReSizeGLScene(), необходимо добавить следующую часть кода: 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]; // Создать место для текстуры 74 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 текстур: glGenTextures(3, &texture[0]); // Создание трех текстур Строка сообщает, что необходимо построить три текстуры, и требуется, чтобы текстура была сохранена в texture[0], texture[1] и texture[2]. В предыдущем проекте, была использована линейная фильтрация образов текстур. Это способ фильтрации требует много мощности процессора, но текстуры при этом выглядят реалистичными. Первый тип текстуры, которую необходимо создать в этом уроке, использует 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, 75 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); Следующая текстура, которую необходимо построить, имеет тот же самый тип текстуры, которую был использован прежде – линейный режим фильтрации. Изменилось только то, что сохранение этой текстуры происходит в texture[1] вместо texture[0], потому что здесь она вторая текстура: // Создание текстуры с линейной фильтрацией 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 будет выбирать наиболее лучшую для отображения текстуру из построенных текстур (текстура с таким размером, как размер полигона на экране, т.е. наиболее детальная) и нарисует ее на экран вместо масштабирования первоначального изображения (что и вызывает потерю качества). Поскольку это – текстура номер три, храниться эта текстура будет в 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); Поэтому, на данном этапе обработаны 3 текстуры: texture[0], которая не имеет никакой фильтрации, texture[1], которая использует линейную фильтрацию, и texture[2], которая использует текстуру с мипналожением. На этом построение текстур заканчивается. Следующая строка строит текстуру с мип-наложением: gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); } 76 Создется 2D-текстура, с использованием трех цветов. TextureImage[0]->sizeX – ширина картинки, TextureImage[0] -> sizeY – высота картинки, GL_RGB означает, используеются цвета в порядке Красный, Зеленый, Синий (Red, Green, Blue). GL_UNSIGNED_BYTE означает что данные, из которых состоит текстура из байтов, и TextureImage[0]->data – указатель на растр картинки, из которого строится текстура. Далее можно освободить память, которая использовалась для картинок. Здесь происходит проверка, была ли картинка сохранена в TextureImage[0]: If (TextureImage[0]) // Если текстура существует { If (TextureImage[0]->data); // Если изображение текстуры существует { free(TextureImage[0]->data); // Освобождение памяти изображения текстуры } free(TextureImage[0]); // Освобождение памяти под структуру } Если картинка была сохранена, то проверяется, были ли данные сохранены. Если данные были сохранены, они удаляются, тогда освобождается структура изображения. Наконец происходит возврат статуса. Если все происходит правильно, переменная Status будет TRUE. Если что-нибудь прошло не так, как надо, Status будет FALSE: return Status; // Возврат статуса } Теперь происходит загрузка текстуры, и инициализирование параметров настройки OpenGL: 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); // Черный фон 77 glClearDepth(1.0f); // Установка буфера глубины glEnable(GL_DEPTH_TEST); // Разрешить тест глубины glDepthFunc(GL_LEQUAL); // Тип теста глубины glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST) // Улучшенные вычисления перспективы В первой строке InitGL загружаются текстуры, используя код выше. После того, как текстуры созданы, разрешатся 2D наложение текстуры с помощью glEnable (GL_TEXTURE_2D). Режим закрашивания (shade) задается как сглаженное закрашивание. Цвет фона задан как черный, разрешается тест глубины, затем разрешаются хорошие перспективные вычисления. Далее задается освещение. Строка ниже задает интенсивность фонового света, которое light1 будет давать: glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); // Установка Фонового Света В начале этого проекта была задана интенсивность фонового света в LightAmbient. Значения, которые находятся в массиве, будут использованы (фоновый свет половиной интенсивности):Затем задается интенсивность диффузного света, который источник света номер один будет давать. Интенсивность диффузного света уже задана в LightDiffuse. Значения, которые находятся в этом массиве, будут использованы (белый свет полной интенсивности): glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); // Установка Диффузного Света Теперь задается позиция источника света: glLightfv(GL_LIGHT1, GL_POSITION, LightPosition); // Позиция света Данная позиция помещена в LightPosition. Значения, которые находятся в этом массиве, будут использованы (справа в центре передней грани, 0.0f по x, 0.0f по y, и 2 единицы вперед к наблюдателю {выходит за экран} по оси z): Наконец, задается источник света номер один: glEnable(GL_LIGHT1); // Разрешение источника света номер один return TRUE; // Инициализация прошла успешно } В следующей части кода, рисуется текстура, наложенная на куб: int DrawGLScene(GLvoid) // Здесь происходит все рисование { glClear(GL_COLOR_BUFFER_BIT GL_DEPTH_BUFFER_BIT); // Очистка Экрана и Буфера Глубины glLoadIdentity(); // Сброс Просмотра 78 В данном проекте запрещен GL_LIGHTING, поэтому никакого освещения пользователь не увидит. Свет установлен, и позиционирован, и даже разрешен, но пока не разрешен GL_LIGHTING, освещение не будет работать: В следующих строках кода куб с наложенной текстурой позиционируется и вращается: 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 Вызов 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: Следующая строка подобна строке, которая была использована в уроке шесть, но вместо связывания к texture[0], происходит привязывание texture[filter]: glBindTexture(GL_TEXTURE_2D, texture[filter]); // Выбор текстуры основываясь на filter glBegin(GL_QUADS); // Начало рисования четырехугольников glNormal3f новая функция в данном примере. Если нажата клавиша 'F', значение в filter увеличится. Если значение больше чем два, переменная filter сбрасывается в ноль. Когда программа запускается, filter будет установлен в ноль – glBindTexture(GL_TEXTURE_2D, texture[0]). Если клавиша "F" нажата еще раз, переменная filter будет равна единице – glBindTexture(GL_TEXTURE_2D, texture[1]). Используя, переменную filter появляется возможность выбирать любую из трех текстур, созданных ранее. Нормаль – линия, которая берет начало из середины полигона под 90 углом градусов. Когда в программе используется освещение, обязательно должны быть заданы нормали. Нормаль сообщает OpenGL, в каком направлении у полигона лицевая часть, какое направление верхнее. Если нормали не заданы, то возможно, например следующая ситуация: грань, которая не освещена, будет освещена, неверная сторона полигона будет освещена, и т.д. Нормаль должна указываться вне полигона. Посмотрев на переднюю грань, можно заметить, что нормаль имеет положительное направление по оси Z. Это означает, что нормаль указывает на наблюдателя. На обратной грани, нормаль указывает от 79 наблюдателя вглубь экрана. Если куб повернут на 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); // Нормаль указывает вниз 80 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: xrot+=xspeed; // Добавить в xspeed значение xrot yrot+=yspeed; // Добавить в yspeed значение yrot return TRUE; // Выйти } Если значение в xspeed или yspeed большое, xrot и yrot увеличивается быстро. Чем быстрее увеличение xrot, или yrot, тем быстрее куб вращается по соответствующей оси. Теперь следует обратиться к WinMain (). Необходимо добавить код для включения или выключения освещения, вращения куба, смены фильтра и перемещения его ближе или дальше от экрана: 81 SwapBuffers(hDC); // Переключение буферов (Двойная буферизация) if (keys['L'] && !lp) // Клавиша 'L' нажата и не удерживается? { Ближе к концу WinMain () необходима реализация вызова SwapBuffers (hDC). Сразу после этой строки, необходимо добавить следующий код. Этот код отслеживает нажатие клавиши 'L'. В первой строке проверяется, нажата ли 'L'. Если 'L' нажата, но lp – не ложь, что значит клавиша 'L' уже была нажата, или она удерживается нажатой, то тогда ничего не происходит. Если lp – ложь то, это означает, что клавиша 'L' не нажата, иначе, если она уже отпущена, lp – истина. Это гарантирует, что клавишу 'L' отпустят прежде, чем этот код выполнится снова. Если не будет проверки удержания клавиши, освещение будет мерцать, постоянно включаясь и выключаясь много раз. Переменная lp будучи равной истине, сообщает, что 'L' отпущена, происходит включение или выключение освещения. Переменная light может только быть истина или ложь: lp=TRUE; // lp присвоили TRUE light=!light; // Переключение света TRUE/FALSE Теперь проверяется, какое значение light получилось в конце: if(!light) // Если не свет { glDisable(GL_LIGHTING); // Запрет освещения } Else // В противном случае { glEnable(GL_LIGHTING); // Разрешить освещение } } Первая строка означает: если light равняется ложь. Поэтому, если Вы совместите все строки вместе, то они делают следующее: если light равняется ложь, то надо запретить освещение. Это выключает все освещение. Команда 'else' означает: если light – не ложь. Поэтому, если light не была ложь то, она истинна, поэтому освещение включается. Следующая строка отслеживает нажатие клавиши 'L': if(!keys['L']) // Клавиша 'L' Отжата? { lp=FALSE; // Если так, то lp равно FALSE } Если переменной lp присвоено значение ложь, то это, означает, что клавиша 'L' не нажата. 82 Делее проделаем аналогичные операции с клавишей 'F': 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 } Если клавиша нажата, и она не удерживается, или она не была нажата до этого, тогда присваивается значение переменной fp равное истине, что значит клавиша 'F' нажата и удерживается. При этом увеличится значение переменной filter. Если filter больше чем 2 (т.е. texture[3], которой не существует), значение переменной texture сбрасывается назад в ноль. Если так, то происходит уменьшение значения переменной z. Если эта переменная уменьшается, куб будет двигаться вглубь экрана, поскольку используется glTranslatef (0.0f, 0.0f, z) в процедуре DrawGLScene. В следующих четырех строках проверяется, нажата ли клавиша 'Page up': if (keys[VK_PRIOR]) // Клавиша 'Page Up' нажата? { z-=0.02f; // Если так, то сдвинем вглубь экрана } Далее проверяется, нажата ли клавиша 'Page down': if (keys[VK_NEXT]) // Клавиша 'Page Down' нажата? { z+=0.02f; } Если так, то увеличивается значение переменной z и куб смещается к наблюдателю, поскольку используется glTranslatef (0.0f, 0.0f, z) в процедуре DrawGLScene. Далее проверяются клавиши курсора: if (keys[VK_UP]) // Клавиша стрелка вверх нажата? { xspeed-=0.01f; // Если так, то уменьшение xspeed } 83 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); // Выход из программы } При нажатии клавиш влево или вправо – xspeed увеличивается или уменьшается, при вверх или вниз – yspeed увеличивается или уменьшается. Чем дольше удерживается одна из клавиш курсора, тем, быстрее куб вращается в соответствующем направлении. Здесь появляется новая функция KillGLWindow, которая корректно уничтожает окно и все что с ним связано. Код KillGLWindow, который необходимо разместить сразу за функцией InitGL: GLvoid KillGLWindow(GLvoid) // Правильное уничтожение окна { if (fullscreen) // Полноэкранный режим? 84 { ChangeDisplaySettings(NULL,0); // Переход в режим разрешения рабочего стола ShowCursor(TRUE); // Показать указатель мыши } if (hRC) // Существует контекст рендеринга? { if (!wglMakeCurrent(NULL,NULL)) // Можно ли освободить DC и RC контексты? { MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } if (!wglDeleteContext(hRC)) // Можно ли уничтожить RC? { MessageBox(NULL,"Release Rendering Context Failed.", "SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } hRC=NULL; // Установление RC в NULL } if (hDC && !ReleaseDC(hWnd,hDC)) // Можно ли уничтожить DC? { MessageBox(NULL,"Release Device Context ailed.","SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION); hDC=NULL; // Установление DC в NULL } if (hWnd && !DestroyWindow(hWnd)) // Можно ли уничтожить окно? { MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK |MB_ICONINFORMATION); hWnd=NULL; // Установление hWnd в NULL } if (!UnregisterClass("OpenGL",hInstance)) { MessageBox(NULL,"Could Not Unregister Class.", "SHUTDOWN ERROR",MB_OK |MB_ICONINFORMATION); hInstance=NULL; // Устанавление hInstance в NULL } } 85 В данном примере реализовано создание и оперирование объектами из четырехугольников с высококачественным, реалистичным наложением текстур, а также, преимущества каждого из трех фильтров (рис.10-11). Рис. 10. Наложение реалистичной текстуры Рис. 11. Вращение куба с текстурой Контрольные вопросы 1. Каково назначение переменной filter? 2. В чем сущность типа текстуры GL_NEAREST? 3. Что такое мип-наложение текстур в OpenGL? 86 4. Каким образом изображения текстуры? происходит освобождения памяти 2.9. Эффект смешивание цветов смежных пикселей В проекте рассмотрим эффект смешивания или, иначе комбинирование цвета данного пикселя, который должен быть выведен с пикселем, уже находящемся на экране. Используем код из предыдущего проекта создания текстурированного куба с использованием освещения: #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; // Флаг полноэкранного режима, по умолчанию полный экран boolblend; // Смешивание НЕТ/ДА? (НОВОЕ) boollight; // Освещение Вкл./Выкл. boollp; // L Нажата? boolfp; // F Нажата? boolbp; // B Нажата? ( Новое ) GLfloatxrot; // Вращение вдоль оси X GLfloatyrot; // Вращение вдоль оси Y GLfloat xspeed; // Скорость вращения вдоль оси X GLfloat yspeed; // Скорость вращения вдоль оси X GLfloatz=-5.0f; // Глубина в экран. // Задаем параметры освещения 87 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/create.bmp"), изменяется на строку ниже. Используется текстура окрашенного стекла вместо текстуры куба: // Загрузка текстуры стекла (МОДИФИЦИРОВАННО) texture1 = auxDIBImageLoad("Data/glass.bmp"); Далее дополняется InitGL(). Первая строка задает яркость для отрисовки объекта, равную полной яркости с альфой 50 % (непрозрачность). Это означает, когда включается смешивание, объект будет на 50% прозрачный. Вторая строка задает тип смешивания: 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 { 88 glDisable(GL_BLEND); // Выключаем смешивание glEnable(GL_DEPTH_TEST); // Включаем тест глубины } } if (!keys['B']) // ’B’ отжата? { bp=FALSE; // Тогда bp возвращает ложь } В результате работы программы получен эффект смешивания в цветов смежных пикселей (рис 12,13). Рис. 12. Наложение текстуры с эффектом смешивания 89 Рис. 13. Текстурированный куб, который имеет 100% прозрачность Контрольные вопросы 1. Какая функция задает значение параметра прозрачности альфа? 2. Каково назначение функции glBlendFunc(GL_SRC_ALPHA,GL_ONE)? 3. Какие параметры имеет функция glColor4f() и за что отвечает каждый параметр? 4. В чем отличие функций glColor4f() и glColor3f()? 2.10. Передвижение изображений в 3D Цель проекта – реализация перемещения изображений (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 устройства HGLRChRC=NULL; // Постоянный контекст для визуализации HWNDhWnd=NULL; // Содержит дескриптор для окна HINSTANCEhInstance; // Содержит данные для нашей программы boolkeys[256]; // Массив, использующийся для сохранения состояния клавиатуры boolactive=TRUE; // Флаг состояния активности приложения (по умолчанию: TRUE) 90 boolfullscreen=TRUE; // Флаг полноэкранного режима (по умолчанию: полноэкранное) Логические переменные twinkle и tp могут принимать значения TRUE (истина) или FALSE (ложь). Twinkle будет говорить о включении/выключении эффекта twinkle. Tp используется для определения состояния клавиши 'T' (была ли нажата или нет). Если нажата, то tp=TRUE, иначе tp=FALSE: BOOLtwinkle; // Twinkling Stars (Вращающиеся звезды) BOOLtp; // 'T' клавиша нажата? Переменная num хранит информацию о количестве звезд, которые рисуются на экране. Она определена как константа. Это значит, в коде программы её значение неизменно. Причина, по которой она определяется как константа, в том, что невозможно переопределить (увеличить/уменьшить) массив: constnum=50; // Количество рисуемых звезд На данном этапе создается структура (structure) – совокупность простых данных (переменных, и т.д.) сгруппированных по какому-либо признаку в одну группу. В программе необходимо хранить цепочку звезд: typedef struct // Создание структуры для звезд { int r, g, b; // Цвет звезды GLfloat dist; // Расстояние от центра GLfloat angle; // Текущий угол звезды } stars; // Имя структуры – Stars stars star[num]; // Создание массива 'star' длинной 'num', где элементом является структура 'stars' Известно, что каждая звезда имеет 3 значения для цвета, и все эти значения целые числа: 3-я строчка: int r,g,b задаёт эти значения. Одно для красного (red) (r), одно для зелёного (green) (g), и одно для голубого (blue) (b). Необходимо, чтобы каждая звезда имела разное расстояние от центра экрана, и была расположена на одном из 360 углов от центра. Для этого создаётся переменная типа число с плавающей точкой (floating point value), которая называется dist. Она означает расстояние. 5-ая строчка создаёт переменную того же типа с именем angle. Она будет отвечать за угол звезды. Далее задается переменная для хранения расстояния от наблюдателя до звезд (zoom), и какой будет начальный угол(tilt): GLfloatzoom=-15.0f; // Расстояние от наблюдателя до звезд GLfloat tilt=90.0f; // Начальный угол GLfloatspin; // Для вращения звезд 91 GLuintloop; // Используется для цикло GLuinttexture[1]; // Массив для одной текстуры LRESULTCALLBACK WndProc(HWND,UINT,WPARAM,LPARAM); // Объявления для WndProc Также создается переменная spin, которая будет вращать звезды по оси z, и это будет выглядеть как вращение вокруг их текущей позиции. Loop – это переменная, которая используется в программе для обрисовки всех 50-ти звезд, и texture[1] будет использоваться для хранения одной черно-белой текстуры, которая загружается. Если необходимо использовать больше текстур, можно увеличить длину массива, с одного до нужной длины. Сразу же за переменными необходимо добавить код для загрузки текстур. Можно использовать тот же код что был в предыдущих проектах. Изображение, которое будет использоваться в данном примере называется 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; } Эта часть кода загружает изображение (описанным выше кодом) и конвертирует в текстуру: int LoadGLTextures() // Функция загрузки изображения и конвертирования в текстуру 92 { 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; // Возврат статуса } Status хранит информацию об успехе операции: Далее необходимо настроить OpenGL для обрисовки того, что будет нужно. Z-буфер (тест глубины) использоваться не будет, поэтому необходимо удалить строчки из кода первой программы: glDepthFunc(GL_LEQUAL); и glEnable(GL_DEPTH_TEST); 93 В коде используется текстурирование, значит необходимо добавить все необходимые строки, которых не было, т.е. текстурирование и смешивание: 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); // Включение смешивания Следующий фрагмент кода так же новый. Он устанавливает начальные углы, расстояние и цвет для каждой звезды. Чтобы изменить угол 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. Если установить zoom подальше в экран, можно использовать большее число, чем 5.0f, но звезды должны быть немного меньше (из-за перспективы). 94 Следует отметить, что цвета для каждой звезды задаются случайным образом от 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 тот же самый, так что необходимо рассмотреть код обрисовки сцены. 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: // Поворот на угол звезды вокруг оси glRotatef(star[loop].angle,0.0f,1.0f,0.0f); // Движение вперед по оси x 95 glTranslatef(star[loop].dist,0.0f,0.0f); Звезда – плоская текстура. Необходимо, чтобы звезды были направлены на наблюдателя всё время, вне зависимости от их вращения и движения по экрану. Этого можно добиться путем отмены вращения, которое было сделано, перед тем как была нарисована звезда. Следует отменить вращение в обратном порядке. Выше был описан поворот экрана, когда было описано вращение на угол звезды. В обратном порядке, необходимо повернуть обратно звезду на текущий угол. Чтобы сделать это, следует использовать обратное значение угла, и повернуть звезду на этот угол. И так как звезда была повернута на 10 градусов, то, поворачивая обратно на -10 градусов получаем звезду, повернутую к наблюдателю по y. Первая строчка ниже отменяет вращение по оси y. Потом аналогично нужно отменить вращение по оси x. Чтобы сделать это следует вращать звезду на угол -tilt. После всех этих операций звезда полностью повернута к наблюдателю: glRotatef(-star[loop].angle,0.0f,1.0f,0.0f); // Отмена текущего поворота звезды glRotatef(-tilt,1.0f,0.0f,0.0f); // Отмена поворота экрана Для того чтобы получить различные цвета, необходимо взять максимальное количество звезд (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); 96 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+=0.01f; // Вращение звезды star[loop].angle+=float(loop)/num; // Измена угла звезды star[loop].dist-=0.01f; // Измена расстояния до центра Звезду вращать необходимо увеличением значения spin. А затем изменить угол каждой звезды. Угол каждой звезды увеличивается на loop/num, что ускоряет вращение звезды с отдалением от центра. Чем ближе к центру, тем медленнее вращается звезда. Наконец описывается уменьшение расстояния до центра для каждой звезды. Это создаёт эффект засасывания звезд в центр экрана. Нижеследующие строки проверяют видимость звезд: if (star[loop].dist<0.0f) // Звезда в центре экрана? { star[loop].dist+=5.0f; 97 // Перемещение на 5 единиц от центра // Новое значение красной компоненты цвета star[loop].r=rand()%256; // Новое значение зеленной компоненты цвета star[loop].g=rand()%256; // Новое значение синей компоненты цвета star[loop].b=rand()%256; } } return TRUE } Попала звезда в центр экрана или нет. Если попала, то звезде задается новый цвет и она двигается на пять единиц от центра, так она может снова начать своё движение к центру как новая звезда. Теперь необходимо добавить код для проверки нажатия клавиш. Обратимся в WinMain(). Код для новой клавиши добавляем прямо под строкой SwapBuffers(hDC). Строкой ниже идет проверка нажатия клавиши 'T': SwapBuffers(hDC); // Смена буфера (Double Buffering) if (keys['T'] && !tp) // Если 'T' нажата и tp равно FALSE { tp=TRUE; // То присвоение tp = TRUE twinkle=!twinkle; // Измена значения twinkle на обратное } Если нажата, и не была до этого нажата, то этот пункт пропускается. Если twinkle равно FALSE, то она станет TRUE. Если была TRUE, то станет FALSE. Одно нажатие 'T' установит tp равное TRUE. Это предотвращает постоянное выполнение этого кода, если клавиша 'T' удерживается. Код ниже проверяет выключение (повторное нажатие) клавиши 'T': if (!keys['T']) // Клавиша 'T' была отключена { tp=FALSE; // Присваивание tp значения, равного FALSE } Если да, то присваивается tp=FALSE. Нажатие 'T' не делает ничего кроме установки tp равной FALSE, так что эта часть кода очень важная. Следующий код проверяет, нажаты ли клавиши 'стрелка вверх', 'стрелка вниз', 'page up', 'page down': if (keys[VK_UP]) // Стрелка вверх была нажата? 98 { 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; KillGLWindow(); // Закрытие текущего окна fullscreen=!fullscreen; // Переключение режимов Fullscreen (полноэкранный) / Windowed (обычный) // Пересоздание OpenGL окно if (!CreateGLWindow("Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen)) { return 0; //Выход если не получилось } В данном примере показано, как загружать черно-белое изображение, удалять черный фон на изображении (используя смешивание), добавлять цвет в картинку, и двигать ее по экрану в трехмерном пространстве, а так же, как сделать красивые цвета и анимацию путем наложения второй копии изображения поверх оригинала (рис. 15). 99 Рис. 14. Вращающаяся спираль из звезд Попробуйте изменить код программы так, чтобы получить вместо звезд другие объекты, а также изменить вращение данной спирали (рис. 16). Рис. 15. Вращающаяся спираль из смешанной текстуры Контрольные вопросы 1. Какие значения могут принимать логические переменные twinkle и tp? 2. Каково назначение логической переменной Twinkle? 100 3. Каким образом было достигнуто эффект того, что, звезды направлены на наблюдателя всё время, вне зависимости от их вращения и движения по экрану. 4. Как получить сдвиг каждой звезды немного дальше, чем предыдущей? 5. Как храненится расстояние от наблюдателя до звезд (zoom), и какой будет начальный угол(tilt) вращения звезд? 6. Что произойдет при увеличении параметра spin? 7. Как изменить форму звезд? 8. Как и для какой цели звезда помещается в цент экрана? 2.11. Эффект развевающегося флага Данная программа реализует эффект развевающегося флага и основана на коде шестого примера. Для начала необходимо добавить приведенный ниже код #include сразу за другими из кода шестого примера: #include <math.h> // Для функции Sin() Данный #include позволяет использовать различные библиотечные математические функции, как, например, синус и косинус. Для хранения отдельных x, y и z – координат сетки необходимо использовать массив точек (points). Размер сетки 45x45, и она в свою очередь образует 44x44 квадрата. wiggle_count будет использоваться для определения того, насколько быстро «развивается» текстура. Каждые три кадра выглядят достаточно хорошо, и переменная hold будет содержать число с плавающей запятой для сглаживания волн. Эти строки можно добавить в начале программы под последней строчкой с #include и перед строкой GLuint texture[1]: float points[ 45 ][ 45 ][3]; // Массив точек сетки "волны" int wiggle_count = 0; // Счетчик для контроля быстроты развевания флага GLfloat hold; // Временно содержит число с плавающей запятой В функции LoadGLTextures() необходимо заменить текстуру LoadBMP("Data/….bmp") на LoadBMP("Data/Tim.bmp"). Таким образом, в программе будет использоваться текстура с именем Tim.bmp: if (TextureImage[0]=LoadBMP("Data/Tim.bmp")) // Загружаем изображение Далее необходимо добавить приведенный ниже код в конец функции InitGL() перед return TRUE: 101 glPolygonMode( GL_BACK, GL_FILL ); // Нижняя (задняя) сторона заполнена glPolygonMode( GL_FRONT, GL_LINE ); // Верхняя (передняя) сторона прорисована линиями Этот код отвечает за то, чтобы полигоны нижней (задней) стороны были зарисованы полностью и чтобы полигоны верхней (передней) стороны были лишь очерчены. // по оси 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)); } } Два цикла вверху инициализируют точки на сетке. В программе используются целочисленные циклы, чтобы предупредить неполадки, которые появляются при вычислениях с плавающей запятой. Здесь переменные x и y делятся на 5 (т.е. 45 / 9 = 5) и затем отнимается 4,5 от каждой из них, чтобы отцентрировать "волну". Того же эффекта можно достигнуть переносом (translate). Окончательное значение points[x][y][2] – это значение синуса. Функция sin() принимает параметр в радианах. Необходимо взять градусы, которые получились умножением float_x (x/5.0f) на 40.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); // Очистить экран и буфер глубины 102 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]); // Выбрать текстуру Сена отодвигается от камеры чуть дальше: glBegin(GL_QUADS); // Начало рисования квадратов for(x= 0; x< 44;иx++ ) // По оси X 0-44 (45 точек) { for(y= 0; y< 44; y++ ) // По оси Y 0-44 (45 точек) { Далее запускается цикл рисования полигонов: 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=loat(y+1)/44.0f; // Создать значение Y как float плюс 0.0227f Здесь используются целые числа, чтобы избежать использования функции int(), которая была использована ранее для получения индекса ссылки массива как целое значение. Для координат текстуры выше используются четыре переменных. Каждый полигон (квадрат в сетке) имеет секцию размером 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]); // Четвертая координата текстуры (нижняя правая) 103 glTexCoord2f( float_xb, float_y ); glVertex3f( points[x+1][y][0], points[x+1][y][1], points[x+1][y][2]); } } glEnd(); // Закончили работу с квадратами Приведенные строки вызывают передачу всех данных: четыре отдельных вызова каждой функции 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++; // Увеличение счетчика Далее необходимо сохранить первое значение каждой линии, затем двигать волну к левому краю, заставляя текстуру развеваться: xrot+=0.3f; // Увеличение значения переменной вращения по X yrot+=0.2f; // Увеличение значения переменной вращения по Y zrot+=0.4f; // Увеличение значения переменной вращения по Z return TRUE; // Возврат из функции } 104 Значение, которое было сохранено, потом вставляется в конец, чтобы создать бесконечную волну, идущую по поверхности текстуры. Затем обнуляется счетчик wiggle_count для продолжения анимации. Результы работы программы представлены на рис.16, 17. Рис. 16. Наложение текстуры на развевающийся флаг Рис. 17. Применение эффектов при наложении текстуры на развевающийся флаг Измените текст программы, загрузив текстуру СибАДИ на полученный флаг. А также попробуйте вращение по другим осям (рис.18). 105 Рис. 18. Развевающися флаг СибАДИ Контрольные вопросы 1. С какой целью в программе используются именно целочисленные циклы? 2. Для чего используется #include <math.h>? 3. Каким образом в данной лабораторной работе происходит «центрирование» волны? 4. С какой целью необходимо занулять переменную «wiggle_count»? 2.12. Использование списков изображений В основе кода следующего проекта лежит код из шестого примера, который посвящен реализации наложения текстуры на куб. #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; //Содержит экземпляр приложения 106 boolkeys[256]; //Массив применяемый для подпрограммы клавиатуры boolactive=TRUE; //Флаг "Активное" окна устанавливается истинным (TRUE) по умолчанию. boolfullscreen=TRUE; //Флаг "На полный экран" устанавливается в полноэкранный режим по умолчанию. Теперь указаны переменные. Резерв места для одной текстуры. Две новых переменных для двух списков отображения. Эти переменные – указатели на списки отображения, хранящиеся в ОЗУ. Затем еще две переменные xloop и yloop, которые используются для задания позиций кубов на экране и две переменные xrot и yrot для вращения по осям x и y. GLuintexture[1]; // Память для одной текстуры GLuintbox; //Память для списка отображения box (коробка) GLuinttop; // Память для второго списка отображения top (крышка) GLuintxloop; // Цикл для оси x GLuintyloop; // Цикл для оси y GLfloatxrot; // Вращает куб на оси x GLfloatyrot; // Вращает куб на оси 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.0,0.5 f,0.5f} 107 } LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //Объявление для WndProc Далее приведен список отображения: GLvoid BuildLists() // Создание списка отображения { Создание двух списков. glGenLists(2) создает место для двух списков, и возвращает указатель на первый из них. 'box' будет хранить расположение первого списка. Всякий раз при вызове box будет нарисован первый список: box=glGenLists(2); // Создание двух списка Создание первого списка. box указывает на место, где мы храним первый список. Необходимо задать начало списка и его тип. Для этого используется команда glNewList(). box в качестве первого параметра. Это определяет место хранения списка, на которое указывает переменная box. Второй параметр GL_COMPILE – предварительное создание списка в памяти: glNewList(box,GL_COMPILE); // Новый откомпилированный список отображения box В следующей части кода содержится рисование куба без верха, которая не будет отражена на экране и будет сохранена в списке отображения. Между glNewList() и glEndList() могут быть любые команды, кроме единственного типа кода – это код, который изменит список отображения. Создав список отображения, его нельзя изменить. 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); // Нижний правый угол текстуры и четырехугольник // Передняя поверхность 108 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); четырехугольник // Нижний левый угол текстуры и // Нижний правый угол текстуры // Верхний правый угол текстуры // Верхний левый угол текстуры и // Нижний правый угол текстуры // Верхний правый угол текстуры // Верхний левый угол текстуры и // Нижний левый угол текстуры и // Нижний правый угол текстуры // Верхний правый угол текстуры // Верхний левый угол текстуры и // Нижний левый угол текстуры и // Нижний левый угол текстуры и 109 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() создается список: glEndList(); // Закончили создание списка box Все, что находится между glNewList() и glEndList – часть списка отображения, все, что находится до glNewList() или после glEndList() не является частью списка отображения. Вывдем второй список отображения: top=box+1; // Значение top это значение box + 1 Чтобы найти, где список отображения хранится в памяти, берется значение старого списка отображения (box) и добавляется единица. Нижеследующий код присваивает переменной 'top' местоположение второго списка отображения. Второй список создается как первый, но в этот раз указывается список под названием 'top' в отличие от предыдущего 'box': glNewList(top,GL_COMPILE); // Новый откомпилированный список отображения 'top' Следующая часть кода рисует верх коробки – это четырехугольник нарисованный на плоскости z: lBegin(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); // Верхний правый угол текстуры и четырехугольник 110 glEnd(); // Конец рисования четырехугольника glEndList(); // Конец создания списка отображения 'top' } Код создания текстур тот же, который использовался в предыдущих примерах для загрузки и создания текстур. Текстура для загрузки называется 'cube.bmp'. Она хранится в каталоге под названием 'data'. Строку с LoadBMP изменить на: if(TextureImage[0]=LoadBMP("Data/Cube.bmp")) // Загрузить картинку. В коде инициализации есть изменения. Добавлена строка 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); // Тип выполняемой проверки глубины Следующие три строки кода включают быстрое и простое освещение: glEnable(GL_LIGHT0); // Быстрое простое освещение (устанавливает в качестве источника освещения Light0) glEnable(GL_LIGHTING); // Включает освещение glEnable(GL_COLOR_MATERIAL); // Включает раскрашивание материала После того как включено light0, включается освещение. 111 Строка GL_COLOR_MATERIAL позволяет добавить цвет к текстурам. glColor3f(r,g,b) не будет действовать на расцвечивание, поэтому важно включить следующие строки. Установка коррекции перспективы для улучшения внешнего вида и возвращение TRUE (инициализация прошла успешно): glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Изящная коррекция перспективы return TRUE; // Инициализация прошла успешно } Следующая часть программы предназначена для рисования. Привязка текстуры к кубу. Цикл по yloop используется для позиционирования кубов по оси Y (вверх и вниз): int DrawGLScene(GLvoid) // Выполнение рисования { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экран и буфер глубины glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбор текстуры Далее создается пять строк кубиков вверх и вниз, и так создается цикл от 1 до 6: for (yloop=1;yloop<6;yloop++) // Цикл по оси Y { Другой цикл по xloop используется для позиционирования кубиков по оси x (слева направо). Количество кубов зависит от того, в какой они рисуются строке. В верхней строчке xloop будет изменяться только от 0 до 1 (рисование одного кубика). В следующей строке xloop изменяется от 0 до 2 (рисование двух кубиков) и так далее: for (xloop=0;xloop< yloop;xloop++) // Цикл по оси X { Очистка окна с помощью glLoadIdentity(): glLoadIdentity(); // Очистка вида Следующая строка задает особую точку на экране: // Размещение кубиков на экране glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f), ((6.0ffloat(yloop))*2.4f)-7.0f,-20.0f); На оси X происходит следующее: смещение вправо на 1.4 единицы так, чтобы центр пирамиды оказался в центре экрана; умножение xloop на 2.8 и добавление к ней 1.4 (умножается на 2.8, так как кубы стоят друг на друге, 2.8 это приближенная ширина куба, когда он повернут на 45 градусов, по диагонали куба); ыычитание yloop*1.4 – перемещение кубика влево в зависимости от того, в какой строке он 112 находится. Если не сместить влево, пирамида начнется у левого края экрана и не будет выглядеть как пирамида. На оси Y вычитается yloop из 6, иначе пирамида будет нарисована сверху вниз. Результат умножаетсям на 2.4. Иначе кубы будут находиться друг на друге. (2.4 примерная высота куба). Затем мы отнимается 7, чтобы пирамида начиналась от низа экрана и строилась вверх. По оси Z осуществляется перемещение на двадцать единиц в глубь экрана. Таким способом пирамида вписывается в экран. Поворот вокруг оси 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, что дает разные цвета для каждого ряда кубиков: glColor3fv(boxcol[yloop-1]); // Выбор цвета куба Куб будет нарисован цветом, выбранным командой glColor3fv(), в месте, куда он передвинут: glCallList(box); // Рисование куба Цвет крышки (темный). glColor3fv(topcol[yloop-1]); // Выбор цвета верха Рисование списка отображения top. glCallList(top); // Рисование крышки } } return TRUE; // Возвращение обратно. } Все необходимые изменения будут выполнены в WinMain(). Код будет добавлен сразу после строки SwapBuffers(hDC): SwapBuffers(hDC); // Двойная буферизация) if (keys[VK_LEFT]) // Была нажата стрелка влево? 113 { yrot-=0.2f; // Если так, то поворот кубов влево } if (keys[VK_RIGHT]) // Была нажата стрелка вправо? { yrot+=0.2f; // Если так, то поворот кубов вправо } if (keys[VK_UP]) // Была нажата стрелка вверх? { xrot-=0.2f; // Если так, то наклон кубов вверх } if (keys[VK_DOWN]) // Была нажата стрелка вниз? { xrot+=0.2f; // Если так, то наклон кубов вниз } Как во всех предыдущих программах далее следует проверка того, что заголовок на верху окна правильный: if (keys[VK_F1]) // Была нажата кнопка F1? { keys[VK_F1]=FALSE; // Если так - установка значения FALSE KillGLWindow(); // Закроем текущее окно OpenGL // Переключение режима "Полный экран"/"Оконный" fullscreen=!fullscreen; // Создание окна OpenGL if (!CreateGLWindow("Display List Tutorial", 640, 480, 160, fullscreen)) { return 0;// Выйти, если окно не было создано } } } } Списки отображения упрощают программирование сложных проектов, дают небольшое увеличение необходимое для поддержания высокой частоты обновления экрана (рис. 19). Измените код программы так, чтобы убрать верхнюю часть пирамиды (куб в вершине пирамиды), чтобы она казалась обрезанной, как на рис. 20. 114 Рис. 19. Списки отображения Рис. 20. Изменение пирамиды Контрольные вопросы 1. Для чего служат списки отображения? 2. Зачем необходим цикл for (yloop=1;yloop<6;yloop++)? 3. Почему BuildList() находится после LoagGLTextures(),а не наоборот? 4. Какие параметры имеет функция glNewList()и за что отвечает каждый параметр? 115 5. 6. 7. Что сохраняется в списке отображения? Для чего и как именуются списки отображения? Какой командой добавляется цвет к текстуре? 2.13. Растровые шрифты Вначале необходимо внести следующее примечание о том, что этот код применим только в Windows. Он использует функции wgl Windows, для построения шрифтов. Очевидно, Apple имеет функции agl, которые должны делать то же самое, а X имеет glx и невозможно гарантировать, что этот код переносим. Текст данного примера так же основывается на коде первого проекта. С начала необходимо добавить: заголовочный файл 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; // Сохраняет экземпляр приложения Также следует добавить три новых переменных. В base будет сохранен номер первого списка отображения, который создается. Каждому символу требуется собственный список отображения. Символ 'A' – 65 список отображения, 'B' – 66, 'C' – 67, и т.д. Поэтому 'A' будет сохранен в списке отображения base плюс 65. Затем добавляется два счетчика (cnt1 и cnt2), которые будут изменяться с разной частотой, и используются для перемещения текста 116 по экрану, использу SIN и COS, что будет создавать эффект хаотичного движения строки текста по экрану. Также счетчики будут использоваться, чтобы изменять цвет символов: GLuintbase; // База списка отображения GLfloatcnt1; // Первый счетчик для передвижения и закрашивания текста GLfloat cnt2; // Второй счетчик для передвижения и закрашивания текста boolkeys[256]; // Массив для работы с клавиатурой boolactive=TRUE; // Флаг активации окна, по умолчанию = TRUE boolfullscreen=TRUE; // Флаг полноэкранного режима LRESULTCALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc В следующей части кода происходит построение шрифта: GLvoid BuildFont(GLvoid) // Построение растрового шрифта { HFONT font; // Идентификатор фонта base = glGenLists(96); // Выделим место для 96 символов (Новая часть кода) Это наиболее трудная часть кода. Объявление 'HFONT font' задает шрифт в Windows. Затем происходит определение base. Здесь создается группа из 96 списков отображения, используя glGenLists(96). После того, как списки отображения созданы, переменная base будет содержать номер первого списка. Необходимо создать новый шрифт. Начать следует с задавания размера шрифта. Размер задается отрицательным числом. Минус указывает Windows, что надо найти нам шрифт, основанный на высоте символов: font = CreateFont(24), // Высота фонта ( НОВОЕ ) Если используется положительное число, выбирается шрифт, основанный на высоте ячейки. Затем необходимо определить ширину ячейки. Здесь она установлена в 0. При помощи установки значения в 0, Windows будет использовать значение по умолчанию: 0, // Ширина фонта Угол отношения (Angle of Escapement) позволяет вращать шрифт. Исключая 0, 90, 180, и 270 градусов, так как у шрифта будет обрезаться то, что не попало внутрь невидимой квадратной границы. Угол наклона (Orientation Angle), цитируя справку MSDN, определяет угол, в десятых долях градуса, между базовой линией символа и осью X устройства: 0, // Угол отношения 117 0, // Угол наклона В качестве параметра ширины шрифта можно использовать числа от 0-1000, или использовать одно из предопределенных значений: FW_DONTCARE-0, FW_NORMAL-400,FW_BOLD-700, и FW_BLACK900. Есть множество других предопределенных значений, но и эти четыре дают хорошее разнообразие. Чем выше значение, тем более толстый, или иначе жирный шрифт: FW_BOLD, // Ширина шрифта Курсив, подчеркивание и перечеркивание может быть или TRUE или FALSE. Если подчеркивание TRUE, шрифт будет подчеркнут. Если FALSE то, нет. FALSE, // Курсив FALSE, // Подчеркивание FALSE, // Перечеркивание Идентификатор набора символов описывает тип набора символов, который необходимо использовать. ANSI – это набор типов, используемый в этом примере. Для использования шрифта типа Webdings или Wingdings, необходимо использовать SYMBOL_CHARSET вместо ANSI_CHARSET: ANSI_CHARSET, // Идентификатор набора символов Точность вывода очень важна, так как этот параметр сообщает Windows какой из наборов символов использовать, если их доступно больше чем один. OUT_TT_PRECIS сообщает Windows что, если доступен больше чем один тип шрифта, то выбрать с тем же самым названием Truetype версию шрифта. OUT_TT_PRECIS, // Точность вывода Точность отсечения – тип отсечения, который применяется, когда вывод символов идет вне области отсечения: CLIP_DEFAULT_PRECIS, // Точность отсечения Качество вывода – очень важный параметр. Здесь возможно выбрать тип отсечения: PROOF, DRAFT, NONANTIALIASED, DEFAULT Или: ANTIALIASED. Сглаживание (Antialiasing) шрифта – это тот же самый эффект, который получается, когда включается сглаживание шрифта в Windows. При этом буквы выглядят более четко: ANTIALIASED_QUALITY, // Качество вывода Затем идут настройка шага. Для настройки шага необходимо выбрать: DEFAULT_PITCH, FIXED_PITCH и VARIABLE_PITCH. Для настройки семейства, необходимо выбрать: 118 FF_DECORATIVE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS, FF_DONTCARE, т.е. FF_DONTCARE|DEFAULT_PITCH, // Семейство и шаг В данной программе будем использовать шрифт Courier New. Для использования любого другого шрифта, необходимо просто ввести его имя вместо указанного: "Courier New"); // Имя шрифта Теперь необходимо выбрать шрифт, привязав его к DC, и построить 96 списков отображения, начиная с символа 32 (который является пробелом). Также можно построить все 256 символов, если это необходимо: SelectObject(hDC, font); // Выбрать созданный шрифт wglUseFontBitmaps(hDC, 32, 96, base); // Построить 96 символов, начиная с пробела } Следующий код удаляет 96 списков отображения из памяти, начиная с первого списка, заданного 'base': GLvoid KillFont(GLvoid) // Удаление шрифта { glDeleteLists(base, 96); // Удаление всех 96 списков отображения } Далее задается функция вывода текста GL. Эта часть кода вызывается по команде glPrint: GLvoid glPrint(const char *fmt, ...) // Заказная функция «Печати» GL { Текст находится в строке символов *fmt. В первой строке кода ниже выделяется память для строки на 256 символов: char text[256]; // Место для строки va_list ap; // Указатель на список аргументов Text – это строка, которую необходимо напечатать на экране. Во второй строке ниже создается указатель, который указывает на список параметров, которые передаются наряду со строкой. Если переменные посылаются вместе с текстом, строка укажет на них. В следующих двух строках кода проверяется, если, что-нибудь для вывода. Если нет текста, fmt – NULL, и ничего не будет выведено на экран: if (fmt == NULL) // Если нет текста return; // Ничего не делать va_start(ap, fmt); // Разбор строки переменных 119 vsprintf(text, fmt, ap); // Конвертирование символов в реальные коды va_end(ap); // Результат помещается в строку Затем в стек заносится GL_LIST_BIT, это защищает другие списки отображения, которые используются в программе, от влияния glListBase: glPushAttrib(GL_LIST_BIT); // Заполнить биты списка отображения glListBase(base - 32); // Задать базу символа в 32 Теперь, когда известно, где находятся символы, можно сообщить, что пора выводить текст на экран. glCallLists – команда, которая может вывести больше, чем один список отображения на экран одновременно. В строке ниже делается следующее: сообщается, что будут показываться на экране списки отображения; при вызове функции 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); // Разрешение теста глубины 120 glDepthFunc(GL_LEQUAL); // Тип теста глубины // Действительно хорошие вычисления перспективы glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST); BuildFont(); // Построить шрифт return TRUE; // Инициализация окончена } Теперь рассмотрим код для обрисовки. Вначале очищается экран и буфер глубины. Вызывается glLoadIdentity() чтобы все сбросить. Затем необходимо текст переместиться на одну единицу вглубь экрана. Если не сделать перемещения, текст не будет отображен. Растровые шрифты лучше применять с ортографической проекцией, а не с перспективной. Следует обратить внимание, что размер шрифта не зависит от перемещения текста вглубь. Если переместиться на 1 единицу в экран, то можно расположить текст, где-нибудь от -0.5 до +0.5 по оси X. Это дает возможность лучше контролировать точное позиционирование текста, не используя десятичные разряды. При этом размер текста не изменится: int DrawGLScene(GLvoid) // Здесь производится все рисование { glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT); // Очистка экран и буфера глубины glLoadIdentity(); // Сброс просмотра glTranslatef(0.0f,0.0f,-1.0f); // Передвижение на одну единицу вглубь Далее необходимо воспользоваться нестандартными вычислениями, чтобы сделать цветовую пульсацию. На этот раз, используются два счетчика, которые были созданы для перемещения текста по экрану, и для манипулирования красным, зеленым и синим цветом: // Цветовая пульсация, основанная на положении текста glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f0.5f*float(cos(cnt1+cnt2))); Красный меняется от 1.0 до 1.0, используя COS и счетчик 1, зеленый – от 1.0 до 1.0 используя SIN и счетчик 2, синий – от 0.5 до 1.5 используя COS и счетчики 1 + 2. Тем самым синий никогда не будет равен 0, и текст не должен никогда полностью исчезнуть. Далее команда glRasterPos2f(x,y) будет позиционировать растровый шрифт на экране. Центр экрана как прежде в 0,0. Следует отметить, что нет координаты Z. Растровые шрифты используют только ось X (лево/право) и ось Y (вверх/вниз). Поскольку перемещение происходит 121 на одну единицу в экран, левый край равен -0.5, и правый край равен +0.5. Необходимо переместиться на 0.45 пикселей влево по оси X. Это устанавливает текст в центр экрана, иначе он был бы правее на экране, потому что текст будет выводиться от центра направо. Нестандартные вычисления делают в большой степени то же самое, как и при вычислении цвета. Происходит перемещение текста по оси X от -0.50 до -0.40. При этом текст на экране будет всегда. Текст будет ритмично раскачиваться влево и вправо, используя COS и счетчик 1. Текст будет перемещаться от -0.35 до +0.35 по оси Y, используя SIN и счетчик 2: // Позиционирование текста на экране glRasterPos2f(-0.45f+0.05f*float(cos(cnt1)), 0.35f*float(sin(cnt2))); Далее реализуется сам вывод текста на экран. Текст будет выведен на экран точно в том месте, где его установили– glPrint("{любой текст}". glPrint("Active OpenGL Text With … - %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(); // Уничтожить шрифт } Результаты работы программ представлены на рис.21. 122 Рис. 21. Вывод текста на экран с использованием эффекта пульсации растрового шрифта Измените содержание текста. Измените цвет текста. Измените время мерцания (счетчик). Контрольные вопросы 1. Почему размер шрифта задается отрицательным числом? 2. Что будет содержать переменная base после создания списка, с использованием glGenLists? 3. Скакой целью и как задается цветовая пульсация? 4. С какой целью вызывается функция glLoadIdentity()? 5. Что описывает идентификатор набора символов? 2.14. Построение векторных шрифтов Текст следующего примера так же основывается на коде первого проекта: #include <windows.h> // Заголовочный файл для Windows #include <math.h> // Заголовочный файл для математической бибилиотеки Windows(добавлено) #include <stdio.h> // Заголовочный файл для стандартного ввода/вывода(добавлено) #include <stdarg.h> // Заголовочный файл для манипуляций с переменными аргументами (добавлено) #include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32 123 #include <gl\glu.h> // Заголовочный файл для библиотеки GLu32 #include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux HDC hDC=NULL; // Частный контекст устройства GDI HGLRC hRC=NULL; // Контекст текущей визуализации HWND hWnd=NULL; // Декриптор окна HINSTANCE hInstance; // Копия приложения Далее введем две новые переменные: переменная base будет содержать номер первого списка отображения, который будет задан, переменная rot будет использоваться при вычислениях для вращения текста на экране через функции синуса и косинуса, а также она будет использована при пульсации цветов: GLuint base; // База отображаемого списка для набора символов (добавлено) GLfloat rot; // Используется для вращения текста (добавлено) bool keys[256]; // Массив для манипуляций с клавиатурой bool active=TRUE; // Флаг активности окна, по умолчанию=TRUE bool fullscreen=TRUE; // Флаг полноэкранного режима, по умолчанию=TRUE GLYPHMETRICSFLOAT gmf[256] будет содержать информацию о местоположении и ориентации каждого из 256 списков отображения нашего векторного шрифта. Чтобы получить доступ к нужной букве просто определим gmf[num], где num – это номер списка отображения, соответствующий требуемой букве: GLYPHMETRICSFLOAT gmf[256]; // Массив с информацией о шрифте LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление оконной процедуры Следующая часть программы создает шрифт так же, как и при построении растрового шрифта. Переменная HFONT font будет содержать идентификатор шрифта Windows. Далее определим переменную base, создав набор из 256-ти списков отображения, вызвав функцию glGenLists(256). После этого переменная base будет содержать номер первого списка отображения: GLvoid BuildFont(GLvoid) // Построение растрового шрифта { HFONT font; // Идентификатор шрифта Windows base=glGenLists(256); // Массив для 256 букв Далее необходимо создать сам векторный шрифта и определить его размер: 124 font = CreateFont( -12, // Высота шрифта Определим ширину знакоместа. Ее значение установлено в ноль, тем самым Windows берет значение по умолчанию: 0, // Ширина знакоместа Угол отношения (Angle of Escapement) позволяет вращать шрифт. Угол наклона (Orientation Angle) определяет угол, в десятых долях градуса, между базовой линией символа и осью Х экрана: 0, //Угол перехода 0, //Угол направления Ширина шрифта – важный параметр, который может быть в пределах от 0 до 1000: FW_BOLD, //Ширина шрифта Также можно использовать предопределенные значения: FW_DONTCARE = 0, FW_NORMAL = 400, FW_BOLD = 700 и FW_BLACK = 900. Параметры Italic, Underline и Strikeout (наклонный, подчеркнутый и зачеркнутый) могут иметь значения TRUE или FALSE: FALSE, // Курсив FALSE, // Подчеркивание FALSE, // Перечеркивание Идентификатор набора символов определяется как: ANSI_CHARSET, //Идентификатор кодировки Точность вывода – также важный параметр, который означает, какой тип символа использовать, если их более одного. Например, значение OUT_TT_PRECIS означает, что надо взять TRUETYPE – версию шрифта. Можно также использовать значение OUT_TT_ONLY_PRECIS, которое означает, что всегда следует брать, если возможно, шрифт TRUETYPE: OUT_TT_PRECIS, // Точность вывода Точность отсечения – этот параметр указывает вид отсечения шрифта при попадании букв вне определенной области: CLIP_DEFAULT_PRECIS, //Точность отсечения Качество вывода – параметр, который имеет следующие варианты: ROOF, DRAFT, NONANTIALIASED, DEFAULT или 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. Здесь оба параметра установлены по умолчанию: 125 FF_DONTCARE|DEFAULT_PITCH, / Семейство и Шаг Имя используемого шрифта: "Comic Sans MS"); // Имя шрифта Далее осуществим выбор шрифта и свяжем его с контекстом устройста (DC): SelectObject(hDC, font); //Выбрать созданный шрифт Далее построим векторный шрифт командой wglUseFontOutlines – выбор контекста устройства (DC), начальный символ, количество создаваемых символов, и базовое значение списка отображения: wglUseFontOutlines( hDC, // Выбрать текущий контекст устройства (DC) 0, // Стартовый символ 256, // Количество создаваемых списков отображения base, // Стартовое значение списка отображения Уже при значении 0.0f сглаживание будет видимо. После установки отклонения идет определение толщины шрифта (ширины по оси Z). Толщина, равная 0.0f, даст плоские буквы, то есть двумерные. При значении 1.0f уже будет виден некоторый объем. Параметр WGL_FONT_POLYGONS создает "твердый" шрифт при помощи полигонов. Последний параметр, gmf указывает на буфер адреса для данных списка отображения: 0.0f, //Отклонение от настоящего контура 0.2f, //Толщина шрифта по оси Z WGL_FONT_POLYGONS, //Использовать полигоны, а не линии gmf), //Буфер адреса для данных списка отображения } Следующий код удаляет 256 списков отображения из памяти, начиная с первого списка, номер которого записан в base: GLvoid KillFont(GLvoid) // Удаление шрифта { glDeleteLists(base, 256); // Удаление всех 256 списков отображения } Далее текст помещается в символьную строку fmt: GLvoid glPrint(const char *fmt, ...) // Функция вывода текста в OpenGL { В первой строке, приведенной ниже, объявляется и инициализируется переменная length: float length=0; // Переменная для нахождения // физической длины текста 126 char text[256]; // Здесь строка va_list ap; //Указатель на переменный список аргументов Она будет использована при вычислении того, какой текст выйдет из строки. Во второй строке создается пустой массив для текстовой строки длиной в 256 символов. text – строка, из которой будет происходить вывод текста на экран. В третьей строке описывается указатель на список аргументов, передаваемый со строкой в функцию. Далее осущуствляется проверка наличия текста: if(fmt == NULL) // Если нет текста, return; В следующих трех строках, программа конвертирует любые символы в переданной строке в представляющие их числа: va_start(ap, fmt); // Анализ строки на переменные vsprintf(text, fmt, ap); // Конвертация символов в реальные коды va_end(ap); // Результат сохраняется в text Создание цикла, который проходит весь текст происходит следующим образом. Длину текста вычисляем в выражении strlen(text). После обработки данных цикла (проверки условия завершения) идет его тело, где к значению переменной lenght добавляется физическая ширина каждого символа. После завершения цикла величина, находящаяся в lenght, будет равна ширине всей строки. Значение ширины каждого символа получается из выражения gmf[text[loop]].gmfCellIncX. Если loop будет равна 0, то text[loop] – это будет первый символ строки. Соответственно, при loop, равной 1, то text[loop] будет означать второй символ этой же строки. Ширину символа дает gmfCellIncX. gmfCellIncX – это расстояние, на которое смещается вправо позиция графического вывода после отображения очередного символа для того, чтобы символы не наложились друг на друга. Расстояние и ширина символа – одно и то же значение. Высоту символа можно узнать при помощи команды gmfCellIncY: for (unsigned int loop=0; loop<(strlen(text));loop++) //Цикл поиска размера строки { length+=gmf[(unsigned char)text[loop]].gmfCellIncX; // Увеличение размера на ширину символа } Полученная величина размера строки преобразуется в отрицательную (потому что будет перемещение влево от центра для центровки текста), затем длина делится на два на – перемещение не всего текста влево, а только его половины: glTranslatef(-length/2,0.0f,0.0f); //Центровка на экране строки 127 Сохранение в стеке значения GL_LIST_BIT для сохранения glListBase от воздействия любых других списков отображения, используемых в программе Команда glListBase(base) является указателем, где искать для каждого символа соответствующий список отображения: glPushAttrib(GL_LIST_BIT); // Сохраняет в стеке значения битов списка отображения glListBase(base); // Устанавливает базовый символ в 0 Написание текста на экране. glCallLists выводит всю строку текста на экран сразу, создавая многочисленные списки отображения. Строки, приведенная ниже, делают следующее. Сначала следует указать, где на экране будут находиться списки отображения. strlen(text) находит количество букв, которые посылаются на экран. Значение GL_UNSIGNED_BYTE – байт позволяет хранить целые числа от 0 до 255, восстанавление из стека GL_LIST_BIT – установки OpenGL обратно, как они были перед установкой базовых значений командой glCallLists(base): // Создает списки отображения текста glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); glPopAttrib(); // Восстанавливает значение Display List Bits } В конце функции InitGL добавляются строки: 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; // Инициализация прошла успешно } 128 Управлять векторными шрифтами можно при помощи команды glScalef(x,y,z). Чтобы сделать буквы в два раза выше – glScalef(1.0f,2.0f,1.0f). Значение 2.0f здесь относится к оси Y и означает, что список отображения нужно нарисовать в двойной высоте. Если это значение поставить на место первого аргумента (Х), то буквы будут в два раза шире: int DrawGLScene(GLvoid) // Вывод на экран { // Очистка экрана и буфера глубины glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); // Сброс вида glTranslatef(0.0f,0.0f,-10.0f); // Смещение на 10 единиц в экран Вращение текста осуществляется следующим образом: следующие три строки поворачивают изображение по трем осям: 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 Возгорание и затухание цветов получается при помощи функций SIN и COS. // Цветовая пульсация основанная на вращении glColor3f(1.0f*float(cos(rot/20.0f)),1.0f*float(sin(rot/25.0f)), 1.0f-0.5f*float(cos(rot/17.0f))); rot делится на разные числа, так что бы каждый цвет не возрастал с такой же скоростью. В коде, приведенном ниже, печатается "текст", пробел, тире, пробел и число из переменной rot и деленное на 50, чтобы немного его уменьшить. Если число больше 999.99, разряды слева игнорируются: glPrint("ASOIU" – %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); 129 hInstance=NULL; // Установить копию приложения в ноль } KillFont(); // Уничтожить шрифт Результат работы программы представлен на рис. 21. Рис. 21. Вывод текта на экран с использованием векторного шрифта Измените код программы следующим образом: выберите другой тип шрифта, который Вам нравится, далее измените содержание текста. Контрольные вопросы 1. Для чего используется переменная rot? 2. Каково предназначение команды glEnable(GL_Color_Material)? 3. как в данном примере используется буфер глубины? 4. Какой командой шрифт выводится на экран? 5. В какой части кода содержится информация о шрифте? 6. Для чего служит фукция gmfCellIncX? 7. Для чего нужен параметр Точность вывода и как его задают? 8. Для чего нужен параметр Точность отсечения и как его задают? 9. Для чего нужен параметр Качество вывода и как его задают? 2.15. Использование текстурированных шрифтов 130 Код данной программы сновывается на коде предыдущего проекта, в котором был создан векторный шрифт и демонстрирует программу для создания текстурированных шрифтов. Код, как отмечалось ранее, применим только в Windows. Для построения шрифта используются функции wgl Windows. #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> // Заголовочный файл для математической библиотеки HDC hDC=NULL; // Приватный контекст устройства GDI HGLRC hRC=NULL; // Постоянный контекст рендеринга HWND hWnd=NULL; // Сохраняет дескриптор окна HINSTANCE hInstance; // Сохраняет экземпляр приложения bool keys[256]; // Массив для работы с клавиатурой bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE bool fullscreen=TRUE;// Флаг полноэкранного режима Необходимо добавить одну новую переменную целого типа, которая называется texture[], которая будет использоваться для хранения текстуры. Последние три строки не изменяются: GLuint texture[1]; // Одна текстура GLuint base; // База списка отображения для фонта GLfloat rot; // Используется для вращения текста LRESULT CALLBACK WndProc(HWND, UINT, WPARAM,PARAM); // Объявление WndProc Wingdings – специальный шрифт и требует некоторых модификаций, чтобы заставить программу работать с ним. При этом надо не просто сообщить Windows, что будет использоваться wingdingsшрифт. Необходимо сообщить Windows, что шрифт является специальным шрифтом, и не стандартным символьным шрифтом: GLvoid BuildFont(GLvoid) // Построение шрифта { GLYPHMETRICSFLOAT gmf[256]; // Адрес буфера для хранения шрифта HFONT font; // ID шрифта в Windows base = glGenLists(256); // Храним 256 символов font = CreateFont( -12, // Высота фонта 131 0, // Ширина фонта 0, // Угол отношения 0, // Угол наклона FW_BOLD, // Ширина шрифта FALSE, // Курсив FALSE, // Подчеркивание FALSE, // Перечеркивание Вместо того чтобы использовать ANSI_CHARSET, используем SYMBOL_CHARSET, т.е. шрифт, который строится – не обычный шрифт, составленный из букв. Специальный шрифт обычно составлен из крошечных картинок (символов): SYMBOL_CHARSET, // Модифицировано – идентификатор набора символов Следующие строки не изменились: OUT_TT_PRECIS, // Точность вывода CLIP_DEFAULT_PRECIS, // Точность отсечения ANTIALIASED_QUALITY, / Качество вывода FF_DONTCARE|DEFAULT_PITCH, // Семейство и шаг Теперь, когда выбран идентификатор набора символов, можно выбирать wingdings шрифт: "Wingdings"); // Имя шрифта ( Модифицировано ) SelectObject(hDC, font); // Выбрать шрифт, созданный нами wglUseFontOutlines(hDC, //Выбрать текущий контекст устройства (DC) 0, // Стартовый символ 256, // Количество создаваемых списков отображения base, // Стартовое значение списка отображения Следует установить большой уровень отклонения: 0.1f, // Отклонение от истинного контура При этом не будет точно отслеживаться контур шрифта. Если задано отклонение равное 0.0f, то могут возникнуть проблемы с текстурированием на очень изогнутых поверхностях. Если допустить некоторое отклонение, большинство проблем исчезнет. Следующие три строки кода: 0.2f, // Толщина шрифта по оси Z WGL_FONT_POLYGONS, // Использовать полигоны, а не линии gmf); // Буфер адреса для данных списка отображения } Перед ReSizeGLScene() следует добавить следующий раздел кода для загрузки текстуры. Здесь создается память для хранения растрового 132 изображения, загружается растровое изображение. OpenGL генерирует 1 текстуру, и сохраняет эту текстуру в texture[0]. Далее, в данном примере, будем использовать текстуру мипналожения, имя текстуры – lights.bmp: 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 } int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру { int Status=FALSE; // Индикатор состояния AUX_RGBImageRec *TextureImage[1]; // Создать место для текстуры memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL // Загрузка картинки, проверка на ошибки, если картинка не найдена - выход if (TextureImage[0]=LoadBMP("Data/Lights.bmp")) { Status=TRUE; // Установка Status в TRUE glGenTextures(1, &texture[0]); // Создание трех текстур // Создание текстуры с мип-наложением glBindTexture(GL_TEXTURE_2D, texture[0]); gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); 133 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } В следующих строках кода автоматически генерируются текстурные координаты для любого объекта, которые выводятся на экран Команда glTexGen включает достаточно сложную математику. GL_S и GL_T – координаты текстуры. По умолчанию они заданы так, чтобы брать текущее x положение на экране и текущее y положение на экране и из них вычислить вершину текстуру: // Текстуризация контура закрепленного за объектом glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); // Текстуризация контура закрепленного за объектом glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glEnable(GL_TEXTURE_GEN_S); // Автоматическая генерация glEnable(GL_TEXTURE_GEN_T); // Автоматическая генерация } if (TextureImage[0]) // Если текстура существует { if (TextureImage[0]->data) // Если изображение текстуры существует { free(TextureImage[0]->data); // Освобождение памяти изображения текстуры } free(TextureImage[0]); // Освобождение памяти под структуру } return Status; // Возвращаем статус } Стоит отметить, что объекты не текстурированны по z плоскости. Только появляются полосы. Передние и задние грани текстурированны, однако, именно это и необходимо. X(GL_S) охватывает наложение текстуры слева направо, а Y(GL_T) охватывает наложение текстуры сверху и вниз. 134 GL_TEXTURE_GEN_MODE позволяет выбирать режим наложения текстуры, которую необходимо использовать по координатам текстуры S и T. Есть три возможности: GL_EYE_LINEAR – текстура зафиксирована на экране. Она никогда не перемещается. Объект накладывается на любую часть текстуры, которую он захватывает. GL_OBJECT_LINEAR – в данной программе используется этот режим. Текстура привязана к объекту, перемещающемуся по экрану. GL_SPHERE_MAP – создает металлический отражающий тип объекта. Также необходимо задать GL_OBJECT_PLANE, но значение по умолчанию то, которое желаемо. Есть несколько новых строк кода в конце InitGL(). Вызов BuildFont() был помещен ниже кода, загружающего текстуру. Строка с glEnable(GL_COLOR_MATERIAL) была удалена. Если необходимо задать текстуре цвет, можно использовать glColor3f(r, г, b) и добавить строку glEnable(GL_COLOR_MATERIAL) в конце этой части кода: int InitGL(GLvoid) // Все начальные настройки OpenGL здесь { if (!LoadGLTextures())// Переход на процедуру загрузки текстуры { return FALSE; // Если текстура не загружена возвращаем FALSE } BuildFont(); // Построить шрифт glShadeModel(GL_SMOOTH); // Разрешить плавное затенение glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон glClearDepth(1.0f); // Установка буфера глубины glEnable(GL_DEPTH_TEST); // Разрешение теста глубины glDepthFunc(GL_LEQUAL) // Тип теста глубины glEnable(GL_LIGHT0); // Быстрое простое освещение (Устанавливает в качестве источника освещения Light0) glEnable(GL_LIGHTING); // Включает освещение // Действительно хорошие вычисления перспективы glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); Далее дадим разрешение наложения 2D текстуры, и выбор текстуры номер один. При этом будет отображена текстура номер один на любой 3D-объект, который выводится на экран. Здесь возможно разрешать и запрещать наложение текстуры самостоятельно: 135 glEnable(GL_TEXTURE_2D); // Разрешение наложения текстуры glBindTexture(GL_TEXTURE_2D, texture[0]);// Выбор текстуры return TRUE; // Инициализация окончена успешно } Код изменения размера не изменился, но код DrawGLScene изменился: int DrawGLScene(GLvoid) // Здесь происходит рисование { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // Очистка экран и буфера глубины glLoadIdentity(); // Сброс просмотра В этой части кода происходит первое изменение. Вместо того, чтобы поместить объект в середину экрана, будем вращать его, используя COS и SIN. По оси X будет происходить раскачивание от -1.1 слева до +1.1 вправо. Здесь будет использоваться переменная rot для управления раскачиванием слева направо. Раскачивание будет происходить от +0.8 верх до -0.8 вниз. Для раскачивания будет использоваться переменная rot: // Позиция текста glTranslatef(1.1f*float(cos(rot/16.0f)),0.8f*float(sin(rot/20.0f)),-3.0f); Далее идет описание вращения. Символ будет вращаться по осям X, Y и Z: glRotatef(rot,1.0f,0.0f,0.0f); // Вращение по оси X glRotatef(rot*1.2f,0.0f,1.0f,0.0f); // Вращение по оси Y glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Вращение по оси Z Следует смещать символ по каждой оси немного налево, вниз, и вперед, чтобы центрировать его, иначе, когда он вращается, он будет вращаться не вокруг собственного центра. (-0.35f, -0.35f, 0.1f) – наиболее подходящие числа, но они могут изменяться в зависимости от шрифта: glTranslatef(-0.35f,-0.35f,0.1f); // Центр по осям X, Y, Z Наконец, далее осуществляется вывод эмблемы, затем увеличение переменной rot, поэтому, символ вращается и перемещается по экрану: glPrint("N"); // Нарисуем символ эмблемы rot+=0.1f; // Увеличим переменную вращения return TRUE; // Заканчиваем процедуру } Далее надо добавить KillFont() в конце KillGLWindow(), что позволит почистит память прежде, чем произойдет выход из программы: 136 if (!UnregisterClass("OpenGL",hInstance)) // Если класс не зарегистрирован { MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK|MB_ICONINFORMATION); hInstance=NULL; // Установить копию приложения в ноль } KillFont(); // Уничтожить шрифт } Результат работы программы представлен на рис. 23. Измените код программы, используя другие шрифты. Также попробуйте заменить используемые эмблемы и их цвета (рис 24). Рис. 23. Использование текстурированного векторного шрифта Рис. 24. Использование измененного текстурированного векторного шрифта 137 Контрольные вопросы 1. С какой целью в OpenGL используется шрифт «wingdings»? 2. Что произойдет, если на очень изогнутых поверхностях задать отклонение шрифта равное 0.0f? 3. Как по умолчанию в OpenGL заданы координаты текстуры (GL_S и GL_T)? 4. Что будет происходить при отсутствии центрирования символа при его вращении? 2.16 Создание эффекта тумана Код основан на программном коде проекта, в котором реализовано наложение текстуры на куб, т.е. следует использовать результат работы программы – куб, с наложенной текстурой. Сначала подготовим данные: Bool gp; // G Нажата? (Новая строка) GLuint filter; // Используемый фильтр для текстур GLuint fogMode[]= { GL_EXP, GL_EXP2, GL_LINEAR }; // Хранит три типа тумана GLuint fogfilter= 0; // Тип используемого тумана GLfloat fogColor[4]= {0.5f, 0.5f, 0.5f, 1.0f}; // Цвет тумана Подготовка всех необходимых переменных, содержащих параметры затуманивания. Массив fogMode будет хранить три значения: GL_EXP, GL_EXP2 и GL_LINEAR, – это три типа тумана. Объявляются переменные в начале кода, после строки GLuint texture[3]. В переменной fogfilter будет храниться тип тумана который будет использоваться. fogColor будет содержать цвет, который придается туману. Двоичная переменная gp в начало кода, чтобы можно было узнать нажата ли клавиша 'g' во время выполнения программы-примера. Далее внесем изменения в DrawGLScene, а именно: функция InitGL, строка glClearColor() изменена, чтобы заполнить экран цветом тумана для достижения лучшего эффекта: glClearColor(0.5f,0.5f,0.5f,1.0f); // Очистка экран, заполнение его цветом тумана. glEnable(GL_FOG); // Включаем туман (GL_FOG) glFogi(GL_FOG_MODE, fogMode[fogfilter]); // Выбираем тип тумана glFogfv(GL_FOG_COLOR, fogColor); // Устанавливаем цвет тумана 138 glFogf(GL_FOG_DENSITY, 0.35f); // Выбираем плотность тумана glHint(GL_FOG_HINT, GL_DONT_CARE); // Вспомогательная установка тумана glFogf(GL_FOG_START, 1.0f); // Глубина, с которой начинается туман glFogf(GL_FOG_END, 5.0f); // Глубина, где туман заканчивается. Возьмем сначала первые три строчки этого кода. Первая строка glEnable(GL_FOG) инициализирует туман: GL_DONT_CARE; // Позволяет OpenGL выбрать формулу для расчета тумана (по вершинам или по пикселям). GL_NICEST; //Создает туман по пикселям. GL_FASTEST; // Вычисляет туман по вершинам. Далее строка, glFogi(GL_FOG_MODE, fogMode[fogfilter]) устанавливает режим фильтрации тумана. Массив fogMode содержал переменные GL_EXP, GL_EXP2 и GL_LINEAR, которые имеют следующий смысл: GL_EXP – обычный туман, заполняющий весь экран; GL_EXP2 – затуманивает весь экран, зато придает больше глубины всей сцене; GL_LINEAR – наиболее приемлемый режим прорисовки тумана, в котором бъекты выходят из тумана и исчезают в нем гораздо лучше. Строка glFogfv(GL_FOG_COLOR, fogcolor) задает цвет тумана. Строка glFogf(GL_FOG_DENSITY, 0.35f) устанавливает, насколько густым будет туман. hintval может быть: GL_DONT_CARE, GL_NICEST или GL_FASTEST Следующая строка glFogf(GL_FOG_START, 1.0f) устанавливает, насколько близко к экрану начинается затуманивание. Следующая строка glFogf(GL_FOG_END, 5.0f) сообщает программе, насколько глубоко в экран должен уходить сам туман. Далее рассмотрим события при нажатии клавиш: Код, предназначенный для того чтобы переключаться между разными способами затуманивания, идет в конце программы, вместе с обработкой нажатия клавиш. if (keys['G'] && !gp) // Нажата ли клавиша "G"? { gp=TRUE; // gp устанавливает в TRUE fogfilter+=1; // Увеличиние fogfilter на 1 if (fogfilter>2) // fogfilter больше 2 ... ? { 139 fogfilter=0; // Если так, установить fogfilter в ноль } glFogi (GL_FOG_MODE, fogMode[fogfilter]);// Режим тумана } if (!keys['G']) // Клавиша "G" отпущена? { gp=FALSE; // Если да, gp установить в FALSE } На рис. 24. представлен результат работы программы в случае, если использовать обычный туман, заполняющий весь экран – GL_EXP. Рис. 24. Обычный туман, заполняющий весь экран – GL_EXP Попробуйте изменить тип тумана: GL_EXP2 – затуманивает весь экран и придает больше глубины всей сцене . GL_LINEAR – лучший режим прорисовки тумана. Объекты как выходят из тумана и исчезают в нем гораздо лучше (рис. 25). 140 Рис. 25. Туман GL_LINEAR Контрольные вопросы 1) Что означает строка GLuint fogMode[]={GL_EXP, GL_EXP2, GL_LINEAR}? 2) Каково назчение функции glFogi()? 3) Какие параметры имеет функция glFogfv() и за что отвечает каждый параметр? 4) Какая команда инициализирует туман? 5) Как изхменить тип тумана? 6) Чтотакое плотность тумана и как ее изменить? 2.17. Квадратирование для отображения сложных объектов Используем квадратирование для отображения сложных объектов. Для этого большого проекта используем код, который был реализован при создании различных режимов фильтрации текстур, применение простого освещения с возможностью перемещения объектов с помощью клавиатуры: #include <windows.h> // Заголовочный файл для Windows #include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода #include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32 #include <gl\glu.h> // Заголовочный файл для библиотеки GLu32 #include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux 141 HDC hDC=NULL; // Приватный контекст устройства GDI HGLRC hRC=NULL; // Постоянный контекст рендеринга HWND hWnd=NULL; // Сохраняет дескриптор окна HINSTANCE hInstance; // Сохраняет экземпляр приложения bool keys[256]; // Массив для работы с клавиатурой bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE bool fullscreen=TRUE;// Флаг полноэкранного вывода bool light; // Освещение Вкл/Выкл bool lp; // L нажата? bool fp; // F нажата? bool sp; // Пробел нажат? (Новая строка) int part1; // Начало диска (Новая строка) int part2; // Конец диска (Новая строка) int p1=0; // Приращение 1 (Новая строка) int p2=1; // Приращение 2 (Новая строка) GLfloat xrot; // X вращение GLfloat yrot; // Y вращение GLfloat xspeed; // X скорость вращения GLfloat yspeed; // Y скорость вращения GLfloat z=-5.0f; // Глубина экрана GLUquadricObj *quadratic; // Место для хранения объекта Quadratic (Новая строка) 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-х текстур GLuint object=0; // Объект рисования LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc Далее внесем изменения в InitGL(). Добавим три строки кода для инициализации квадратичного объекта. Они должны быть после инициализации освещения (light1), но до строки return true. Первая строка инициализирует квадратичный объект и создает указатель на то место в памяти, где он будет содержаться. Если он не может быть создан, то будет возвращен 0. Вторая строка кода создает плавные нормали на квадратичном объекте, поэтому освещение будет выглядеть хорошо. Другое возможное 142 значение – GLU_NONE и GLU_FLAT. Далее осуществим включение текстурирования на квадратичном объекте: quadratic=gluNewQuadric(); // Создает указатель на квадратичный объект gluQuadricNormals(quadratic, GLU_SMOOTH); // Создает плавные нормали gluQuadricTexture(quadratic, GL_TRUE); // Создает координаты текстуры Код прорисовки куба приведен ниже: GLvoid glDrawCube() // Прорисовка куба { glBegin(GL_QUADS); // Прорисовка четырехугольников // Передняя сторона glNormal3f( 0.0f, 0.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); // Верх Лево на текстуре и четырехугольнике // Задняя сторона glNormal3f( 0.0f, 0.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); // Низ Лево на текстуре и четырехугольнике // Верхняя грань glNormal3f( 0.0f, 1.0f, 0.0f); // Нормаль вверх 143 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);// Верх Право на текстуре и четырехугольнике // Нижняя грань glNormal3f( 0.0f,-1.0f, 0.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); // Низ Право на текстуре и четырехугольнике // Правая грань glNormal3f( 1.0f, 0.0f, 0.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); // Низ Лево на текстуре и четырехугольнике // Левая грань glNormal3f(-1.0f, 0.0f, 0.0f); // Нормаль направлена влево 144 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(); // Заканчиваем рисование четырехугольников } Следующая функция DrawGLScene. case – оператор для рисования разных объектов: int DrawGLScene(GLvoid) // Прорисовка { // Очистка видео буфера и буфера глубины glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); // Сбрасывает вид glTranslatef(0.0f,0.0f,z); // Перемещается вглубь экрана glRotatef(xrot,1.0f,0.0f,0.0f);// Вращение по оси X glRotatef(yrot,0.0f,1.0f,0.0f);// Вращение по оси Y glBindTexture(GL_TEXTURE_2D, texture[filter]); // Выбирает фильтрацию текстуре // Далее следует новая секция кода switch(object) // Проверяет, какой объект рисовать { case 0: // Рисует первый объект glDrawCube(); // Рисует куб break; // Конец Второй создаваемый объект – цилиндр: case 1: // Рисует второй объект glTranslatef(0.0f,0.0f,-1.5f); // Центр цилиндра gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32);// Рисует цилиндр break; // Конец Первый параметр (1.0f) – радиус основания цилиндра (низ). Второй параметр (1.0f) –это радиус цилиндра сверху. Третий параметр (3.0f) – это высота цилиндра (какой он длины). Четвертый параметр (32) – это сколько делений будет "вокруг" оси Z, и пятый (32) – количество 145 делений "вдоль" оси Z. Большее количество делений приведет к увеличению детализации объекта. Третий объект – поверхность в виде CD-диска: case 2: // Рисует третий объект gluDisk(quadratic,0.5f,1.5f,32,32); // Рисует диск (в виде CD) break; // Конец Первый параметр диска (0.5f) – внутренний радиус цилиндра. Его значение может быть нулевым, что будет означать, что внутри нет отверстия. Чем больше будет внутренний радиус – тем больше будет отверстие внутри диска. Второй параметр (1.5f) - внешний радиус. Третий параметр (32) – количество частей, из которых состоит диск. Четвертый параметр (32) – это число колец, которые составляют диск (можно задать – одно кольцо). Кольца похожи на треки на записи. Круги внутри кругов. Эти кольца делят диск со стороны внутреннего радиуса к внешнему радиусу, улучшая детализацию. Четвертый объект – это сфера: case 3: // Рисует четвертый объект gluSphere(quadratic,1.3f,32,32); // Рисует сферу break; // Конец Первый параметр – это радиус сферы. Дальше идет количество разбиений "вокруг" оси Z (32), и количество разбиений "вдоль" оси Z (32). Большее количество придаст сфере большую гладкость. Для того, чтобы сфера была достаточно гладкой, обычно необходимо большое количество разбиений. Чтобы создать пятый объект – конус, используется та же команда, что и для цилиндра. Для того, чтобы сделать конус, имеет смысл сделать один из радиусов равный нулю. Это создаст точку на конце: case 4: // Рисует пятый объект glTranslatef(0.0f,0.0f,-1.5f); // Центр конуса. Конус с нижним радиусом 5 и высотой 2 gluCylinder(quadratic,1.0f,0.0f,3.0f,32,32); break; // Конец Шестой объект создан с помощью gluParticalDisc: case 5: // Рисует шестой объект part1+=p1; // Увеличивает стартовый угол part2+=p2; // Увеличивает конечный угол if(part1>359) // 360 градусов { p1=0; // Закончить увеличивать начальный угол part1=0; // Устанавливает начальный угол в 0 p2=1; // Начинает увеличивать конечный угол part2=0; // Начиная с 0 146 } if(part2>359) // 360 градусов { p1=1; // Начинает увеличивать начальный угол p2=0; // Перестает увеличивать конечный угол } gluPartialDisk(quadratic,0.5f,1.5f,32,32,part1,part2-part1); break; // Конец }; xrot+=xspeed; // Увеличивает угол поворота вокруг оси X yrot+=yspeed; // Увеличивает угол поворота вокруг оси Y return TRUE; // Продолжает } Объект, который создается этой командой точно такой же диск, который был до этого, но у команды gluParticalDisc есть еще два новых параметра. Пятый параметр (part1) – это угол, с которого начинается рисование диска. Шестой параметр – это конечный угол (или угол развертки). Рассмотрим процесс привязки к клавиатуре. Следует добавить код ниже туда, где происходит проверка нажатия клавиш: if (keys[' '] && !sp) // Нажата клавиша "пробел"? { sp=TRUE; // Если так, то устанавливает sp в TRUE object++; // Цикл по объектам if(object>5) // Номер объекта больше 5? object=0; // Если да, то устанавливает 0 } if (!keys[' ']) // Клавиша "пробел" отпущена? { sp=FALSE // Если да, то устанавливает sp в FALSE } Результаты работы программы представлены на рис. 26-33. Измените текстуру объетов. Измените прозрачность текстуры, наложенной на объекты. 147 Рис. 26. Куб, созданный как сложный объект Рис. 27. Цилиндр, созданный как сложный объект 148 Рис. 28. CD-диск, созданный как сложный объект Рис. 29. Сфера, созданный как сложный объект 149 Рис. 30. Конус, созданный как сложный объект Рис. 31. Сложный объект, созданный с помощью gluParticalDisc В приложении 1 приведен листинг программы, моделирующей стеклянную 3D вазу, которая реализована на языке Microsoft Visual C++ в консольном режиме с помощью библиотеки OPENGL в рамках курсового проекта. Пояснения к программе указаны в листинге в виде комментариев. Результаты работы программы представлены на рис. 32,33. При прорисовке вазы с гранями были изменены: радиусы цилиндра, высоту и количество полигонов и очищен экран в серый цвет: gluCylinder(quadratic,0.4f,0.8f,2.8f,8,8); gluDisk(quadratic,0.0f,0.4f,8,8); 150 //Изменяем радиус дна и количество полигонов glClearColor(0.8f, 0.8f, 0.8f, 0.0f); //Очищаем экран в серый цвет Рис. 32 Стеклянная 3D ваза, созданная как сложный объект цилиндр Рис. 33 Стеклянная 3D ваза, созданная с использованием полигонов Контрольные вопросы 1. В какой части программы происходит инициализация квадратичного объекта? 2. Каково предназначение команды gluPartialDisk()? 3. Какие параметры может иметь команда gluCylinder() ? 4. Почему внешний радиус должен быть больше внутреннего при рисовании поверхности диска? 151 2.18. Машина моделирования частиц Реализация программного кода рассматриваемого ниже примера имеет очень интересный и красивый результат. Рассмотрим пример создания машин моделирования частиц (Particle Engine). Код, как и в предыдущих работах, основан на первом примере, т.е. используется уже готовое окно, но для выполнения некоторых особенностей, необходимо добавить пять новых строк кода в начало программы: #include <windows.h> // Заголовочный файл для Windows #include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода (Новая строка) #include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32 #include <gl\glu.h> // Заголовочный файл для библиотеки GLu32 #include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux #define MAX_PARTICLES 1000 // Число частиц для создания (Новая строка) HDC hDC=NULL; // Контекст устройства GDI HGLRC hRC=NULL; // Постоянный контекст рендеринга HWND hWnd=NULL; // Сохраняет дескриптор окна HINSTANCE hInstance; // Сохраняет экземпляр приложения bool keys[256]; // Массив для работы с клавиатурой bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE bool fullscreen=TRUE;// Флаг полноэкранного режима bool rainbow=true; // Режим радуги? (Новая строка) bool sp; // Пробел нажат? (Новая строка) bool rp; // Ввод нажат? (Новая строка) Первая строка (stdio.h) позволяет читать данные из файлов. Во второй строке задается, сколько будет создаваться частиц, и отображаться на экране: пусть MAX_PARTICLES будет равно 1000. В третьей строке будет переключаться режим радуги (включен или выключен). sp и rp – переменные, которые будут использоваться для предотвращения автогенерации повторений нажатия клавиш 'пробел' или 'ввод' (enter), когда они нажаты. В следующих четырех строках – разнообразные переменные. Переменная slowdown (торможение) контролирует, как быстро 152 перемещаются частицы. Чем больше ее значение, тем медленнее они двигаются. Переменные xspeed и yspeed позволяют контролировать направление хвоста потока частиц. Переменная xspeed будет добавляться к текущей скорости частицы по оси X. Если у xspeed имеет положительное значение, то частица будет смещаться направо. Если у xspeed – отрицательное значение, то частица будет смещаться налево. Чем выше значение, тем больше это смещение в соответствующем направлении. yspeed работает также, но по оси Y. Последняя переменная zoom предназначена для панорамирования внутрь и вне сцены. В машине моделирования частиц, это позволяет увеличить размер просмотра, или резко его сократить: float slowdown=2.0f; // Торможение частиц float xspeed; // Основная скорость по X (с клавиатуры изменяется направление хвоста) float yspeed; // Основная скорость по Y (с клавиатуры изменяется направление хвоста) float zoom=-40.0f; // Масштаб пучка частиц Далее задается переменная loop, которая используется для задания частиц и вывода частиц на экран: GLuint loop; // Переменная цикла GLuint col; // Текущий выбранный цвет GLuint delay; // Задержка для эффекта радуги GLuint texture[1]; // Память для текстуры Переменная col используется для сохранения цвета, с которым созданы частицы. delay будет использоваться, чтобы циклически повторять цвета в режиме радуги. Булевая переменная active работает следующим образом: если эта переменная истинна, то частица ''жива'' и летит. Если она равно ложь – частица выключается Переменные life и fade управляют тем, как долго частица будет отображаться, и насколько яркой она будет, пока ''жива''. Переменная life постепенно уменьшается на значение fade. В этой программе некоторые частицы будут гореть дольше, чем другие: typedef struct // Структура частицы { bool active; // Активность (Да/нет) float life; // Жизнь float fade; // Скорость угасания Переменные r, g и b задают красную, зеленую и синюю яркости частицы. Чем ближе r к 1.0f, тем более красной будет частица. Если все три переменных равны 1.0f, то это создаст белую частицу: 153 float r; // Красное значение float g; // Зеленое значение float b; // Синее значение Переменные x, y и z задают, где частица будет отображена на экране. x задает положение нашей частицы по оси X. y задает положение нашей частицы по оси Y, и, наконец, z задает положение нашей частицы по оси Z: float x; // X позиция float y; // Y позиция float z; // Z позиция Следующие три переменные управляют тем, как быстро частица перемещается по заданной оси: float xi; // X направление float yi; // Y направление float zi; // Z направление Если xi имеет отрицательное значение, то частица будет двигаться влево. Если положительное, то вправо. Если yi имеет отрицательное значение, то частица будет двигаться вниз. Если положительное, то вверх. Если zi имеет отрицательное значение, то частица будет двигаться вглубь экрана, и, если положительное, то вперед к зрителю. Если xg имеет положительное значение, то частицу будет ''притягивать'' вправо, если отрицательное – влево. Значение yg ''притягивает'' вверх или вниз, и zg ''притягивает'' вперед или назад от зрителя: float xg; // X Гравитация float yg; // Y Гравитация float zg; // Z Гравитация Введем particles – название структуры: } particles; // Структура Частиц Затем создается массив называемый particle, который имеет размер MAX_PARTICLES. Это зарезервированная память будет хранить информацию о каждой индивидуальной частице: particles particle[MAX_PARTICLES]; // Массив частиц (Место для информации о частицах) Для создания эффекта радуги необходимо обеспечить запоминание двенадцати разных цветов, которые постепенно изменяются от красного до фиолетового в массиве цвета. Для каждого цвета от 0 до 11 запоминается красная, зеленая и синяя яркость. static GLfloat colors[12][3]= // Цветовая радуга { {1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f}, 154 {0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f}, {0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f} } LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc Далее, в следующей части кода загружается картинка и конвертирует его в текстуру: 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 } Status используется для того, чтобы отследить, действительно ли текстура была загружена и создана: int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру { int Status=FALSE; // Индикатор состояния AUX_RGBImageRec *TextureImage[1]; // Создать место для текстуры memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL Текстура загружается кодом, который будет загружать картинку частицы и конвертировать ее в текстуру с линейным фильтром: if (TextureImage[0]=LoadBMP("Data/Particle.bmp")) // Загрузка текстуры частицы { Status=TRUE; // Задать статус в TRUE glGenTextures(1, &texture[0]); // Создать одну текстуру glBindTexture(GL_TEXTURE_2D, texture[0]); 155 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_L INEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LI NEAR); 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; // Возвращает статус } Можно увеличить область просмотра, например: вместо 100.0f, можно рассматривать частицы на 200.0f единиц в глубине экрана: // Изменение размеров и инициализация окна GL 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,200.0f); // (Модифицировали)) glMatrixMode(GL_MODELVIEW); // Выбор матрицы просмотра модели glLoadIdentity(); // Сброс матрицы просмотра модели } Далее для загрузки текстуры и включения смешивания для частиц добавим код: 156 int InitGL(GLvoid) // Все начальные настройки OpenGL здесь { if (!LoadGLTextures())// Переход на процедуру загрузки текстуры { return FALSE; // Если текстура не загружена возвращает FALSE } Делее решим проблемы разрешения плавного затенения, очищения фона черным цветом, запрещения теста глубины, разрешения смешивания и наложения текстуры (После разрешения наложения текстуры выбирается текстура частицы): glShadeModel(GL_SMOOTH); // Разрешить плавное затенение glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Черный фон glClearDepth(1.0f); // Установка буфера глубины glDisable(GL_DEPTH_TEST); // Запрещение теста глубины glEnable(GL_BLEND); // Разрешает смешивание glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Тип смешивания // Улучшенные вычисления перспективы glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST); glHint(GL_POINT_SMOOTH_HINT,GL_NICEST); // Улучшенные точечное смешение glEnable(GL_TEXTURE_2D); // Разрешение наложения текстуры glBindTexture(GL_TEXTURE_2D,texture[0]); // Выбор текстуры В коде ниже инициализируется каждая из частиц: for (loop=0;loop<MAX_PARTICLES;loop++)// Инициализация всех частиц { particle[loop].active=true; // Сделать все частицы активными particle[loop].life=1.0f; // Сделать все частицы с полной жизнью //Случайная скорость угасания particle[loop].fade=float(rand()%100)/1000.0f+0.003f; Теперь, когда частица активна, задается ее цвет: particle[loop].r=colors[loop*(12/MAX_PARTICLES)][0]; // Выбор красного цвета радуги particle[loop].g=colors[loop*(12/MAX_PARTICLES)][1]; // Выбор зеленного цвета радуги particle[loop].b=colors[loop*(12/MAX_PARTICLES)][2]; // Выбор синего цвета радуги Далее задается направление, в котором каждая частица двигается, наряду со скоростью: particle[loop].xi=float((rand()%50)-26.0f)*10.0f; 157 // Случайная скорость по оси X particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z Далее установим величину гравитации, которая воздействует на каждую частицу: particle[loop].xg=0.0f; // Задает горизонтальное притяжение в ноль particle[loop].yg=-0.8f; // Задает вертикальное притяжение вниз particle[loop].zg=0.0f; // Задает притяжение по оси Z в ноль } return TRUE; // Инициализация завершена OK } Используя команду glVertex3f(), частицы позиционируются – вместо использования их перемещения, так как при таком способе вывода частиц не изменяется матрица просмотра модели при выводе частиц: int DrawGLScene(GLvoid) // Прорисовка { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины glLoadIdentity(); // Сброс матрицы просмотра модели Начинается вывод с цикла, который обновит каждую из частиц: for (loop=0;loop<MAX_PARTICLES;loop++) // Цикл по всем частицам { Далее происходит проверка активности частицы: if (particle[loop].active) // Если частицы активны { Следующие три переменные x, y и z – временные переменные, которые используются, чтобы запомнить позицию частицы по x, y и z: float x=particle[loop].x; // Захватим позицию X частицы float y=particle[loop].y; // Захватим позицию Н нашей частицы float z=particle[loop].z+zoom; // Позиция частицы по Z + Zoom Закрасим частицы: particle[loop].r – красная яркость частицы, particle[loop].g – зеленая яркость, и particle[loop].b – синяя яркость: // Вывод частицы, используя наши RGB значения, угасание частицы согласно её жизни glColor4f(particle[loop].r,particle[loop].g,particle[loop].b,particle[loop ].life); 158 Вместо использования текстурированного четырехугольника, используется текстурированная полоска из треугольников, чтобы ускорить работу программы: glBegin(GL_TRIANGLE_STRIP); // Построение четырехугольника из треугольной полоски Надо помнить, что число треугольников, которые видно на экране, будет равно числу вершин минус два: glTexCoord2d(1,1); glVertex3f(x+0.5f,y+0.5f,z);// Верхняя правая glTexCoord2d(0,1); glVertex3f(x-0.5f,y+0.5f,z); // Верхняя левая glTexCoord2d(1,0); glVertex3f(x+0.5f,y-0.5f,z); // Нижняя правая glTexCoord2d(0,0); glVertex3f(x-0.5f,y-0.5f,z); // Нижняя левая glEnd(); // Завершение построения полоски треугольников Перемещение частицы. // Передвижение по оси X на скорость по X particle[loop].x+=particle[loop].xi/(slowdown*1000); // Передвижение по оси Y на скорость по Y particle[loop].y+=particle[loop].yi/(slowdown*1000); // Передвижение по оси Z на скорость по Z particle[loop].z+=particle[loop].zi/(slowdown*1000); В первой строке ниже добавляется сопротивление (xg) к скорости перемещения (xi): particle[loop].xi+=particle[loop].xg; // Притяжение по X для этой записи particle[loop].yi+=particle[loop].yg; // Притяжение по Y для этой записи particle[loop].zi+=particle[loop].zg; // Притяжение по Z для этой записи Сопротивление применяется к скорости перемещения по y и z, так же, как и по x. В следующей строке забирается ''часть жизни'' от частицы, а именно: каждая частица имеет свое значение угасания, поэтому они будут гореть с различными скоростями: particle[loop].life-=particle[loop].fade; // Уменьшить жизнь частицы на ''угасание'' Далее проверим – ''жива '' ли частица, после изменения ее жизни: if (particle[loop].life<0.0f) // Если частица погасла { Если частица сгорела, то ей необходимо ''дать новую жизнь '' и сбросить позицию частицы в центр экрана. particle[loop].life=1.0f; // Дать новую жизнь // Случайное значение угасания particle[loop].fade=float(rand()%100)/1000.0f+0.003f; 159 particle[loop].x=0.0f; // На центр оси X particle[loop].y=0.0f; // На центр оси Y particle[loop].z=0.0f; // На центр оси Z После того, как частица была сброшена в центр экрана, ей задается новая скорость перемещения/направления: particle[loop].xi=xspeed+float((rand()%60)-32.0f); //Скорость и направление по оси X particle[loop].yi=yspeed+float((rand()%60)-30.0f); //Скорость и направление по оси Y particle[loop].zi=float((rand()%60)-30.0f); //Скорость и направление по оси Z Далее необходимо назначить новый цвет частице: particle[loop].r=colors[col][0]; // Выбор красного из таблицы цветов particle[loop].g=colors[col][1]; // Выбор зеленого из таблицы цветов particle[loop].b=colors[col][2]; // Выбор синего из таблицы цветов } Строка ниже контролирует, насколько гравитация будет притягивать вверх: // Если клавиша 8 на цифровой клавиатуре нажата и гравитация меньше чем 1.5, тогда увеличение притяжения вверх if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f; Далее строка создает точно противоположный эффект. При помощи нажатия '2' на цифровой клавиатуре уменьшается yg, создается более сильное притяжение вниз: // Если клавиша 2 на цифровой клавиатуре нажата и гравитация больше чем -1.5, тогда увеличение притяжения вниз if (keys[VK_NUMPAD2] && (particle[loop].yg>-1.5f)) particle[loop].yg-=0.01f; Притяжение вправо и влево осуществляется с клавиш: // Если клавиша 6 на цифровой клавиатуре нажата и гравитация меньше чем 1.5, тогда увеличение притяжения вправо if (keys[VK_NUMPAD6] && (particle[loop].xg<1.5f)) particle[loop].xg+=0.01f; // Если клавиша 4 на цифровой клавиатуре нажата и гравитация больше чем -1.5, тогда увеличение притяжения влево if (keys[VK_NUMPAD4] && (particle[loop].xg>-1.5f)) particle[loop].xg-=0.01f; Далее попробуем реализовать эффект взрыва: 160 if (keys[VK_TAB]) // Клавиша табуляции вызывает взрыв { particle[loop].x=0.0f; // Центр по оси X particle[loop].y=0.0f; // Центр по оси Y particle[loop].z=0.0f; // Центр по оси Z particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси X particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z } } } return TRUE; // Все правильно } При помощи нажатия клавиши табуляции все частицы будут отброшены назад к центру экрана, при этом скорость перемещения частиц будет в 10, раз больше, создавая большой взрыв частиц. После того, как частицы взрыва постепенно исчезнут, появиться предыдущий столб частиц: if (keys[VK_TAB]) // Клавиша табуляции вызывает взрыв { particle[loop].x=0.0f; // Центр по оси X particle[loop].y=0.0f; // Центр по оси Y particle[loop].z=0.0f; // Центр по оси Z particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z } } return TRUE; // Все правильно } И далее: код в KillGLWindow(), CreateGLWindow() и WndProc() не изменился, изменения следует внести в WinMain(): int WINAPI WinMain( HINSTANCE hInstance, // Экземпляр HINSTANCE hPrevInstance, // Предыдущий экземпляр LPSTR lpCmdLine, // Параметры командной строки 161 int nCmdShow) // Показать состояние окна { MSG msg; // Структура сообщения окна BOOL done=FALSE; // Булевская переменная выхода из цикла // Запросим у пользователя какой режим отображения он предпочитает if (MessageBox(NULL) // Полноэкранный режим "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Оконный режим } // Создадим окно OpenGL if (!CreateGLWindow (640,480,16,fullscreen)) { return 0; // Если окно не было создано } if (fullscreen) // Полноэкранный режим (Добавлена строка) { slowdown=1.0f; // Скорость частиц (для 3dfx) (Добавлена строка) } while (!done)// Цикл, который продолжается пока done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение? { if (msg.message==WM_QUIT) // Есть сообщение о выходе? { done=TRUE; // Если так done=TRUE } else // Если нет, продолжает работать с сообщениями окна { TranslateMessage(&msg); // Переводит сообщение DispatchMessage(&msg); // Отсылает сообщение } } else // Если сообщений нет { // Рисует сцену. Ожидает нажатия кнопки ESC и сообщения о выходе от DrawGLScene() // Если активно? Было получено сообщение о выходе? if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) 162 { done=TRUE; // ESC или DrawGLScene просигналили "выход" } else // Не время выходить, обновляет экран { SwapBuffers(hDC); // Переключает буферы В строке ниже проверяется, нажата ли клавиша '+' на цифровой клавиатуре, если она нажата, и slowdown больше чем 1.0f, то slowdown уменьшается на 0.01f. в таком случае частицы будут двигаться быстрее: if (keys[VK_ADD] && (slowdown>1.0f)) slowdown-=0.01f; //Скорость частицы увеличилась Далее проверяется, нажата ли клавиша '-' на цифровой клавиатуре, если она нажата, и slowdown меньше чем 4.0f, то увеличивается slowdown, это заставляет частицы двигаться медленнее: if (keys[VK_SUBTRACT] && (slowdown<4.0f)) slowdown+=0.01f; // Торможение частиц В строке ниже проверяется, нажата ли клавиша PAGE UP, если она нажата, то переменная zoom увеличивается. Это заставит частицы двигаться на крупном плане: if (keys[VK_PRIOR]) zoom+=0.1f; // Крупный план Можно задать возможность противоположного эффекта: нажимая клавишу Page down, уменьшиться zoom, и сцена сместиться глубже в экран: if (keys[VK_NEXT]) zoom-=0.1f; // Мелкий план В следующей части кода происходит проверка, была ли нажата клавиша Enter: if (keys[VK_RETURN] && !rp) // нажата клавиша Enter { rp=true; // Установка флага, что клавиша нажата rainbow=!rainbow; // Переключение режима радуги в Вкл/Выкл } if (!keys[VK_RETURN]) rp=false; // Если клавиша Enter не нажата – сбросить флаг Если она нажата в первый раз, и она не удерживается некоторое время, то устанавливается rp в true. Тем самым переключается режим радуги. Если радуга была true, она станет false. Если она была false, то станет true. В последней строке проверяется, была ли клавиша Enter отпущена. Если это так, то rp устанавливается в false, сообщая компьютеру, что клавиша больше не нажата. В первой строке ниже идет проверка, нажата ли клавиша 'пробел' и не удерживается ли она. Тут же проверяется, включен ли режим радуги, и если так, то проверяется значение переменной delay больше чем 25. 163 delay – счетчик, который используется для создания эффекта радуги. При помощи создания задержки, группа частиц останется с одним цветом, прежде чем цвет будет изменен на другой. Если клавиша 'пробел' была нажата, или радуга включена, и задержка больше чем 25сек., цвет будет изменен: if ((keys[' '] && !sp) || (rainbow && (delay>25))) // Пробел или режим радуги { Строка ниже была добавлена, для того чтобы режим радуги был выключен, если клавиша 'пробел' была нажата: if (keys[' ']) rainbow=false; // Если пробел нажат запрет режима радуги Таким образом, если клавиша 'пробел' была нажата, или режим радуги включен, и задержка больше чем 25 сек. – 'пробел' был нажат, делая sp равной true. Затем задается задержка равная 0, чтобы снова начать считать до 25 сек.: sp=true; // Установка флага нам скажет, что пробел нажат delay=0; // Сброс задержки циклической смены цветов радуги col++; // Изменить цвет частицы Если цвет превышает значение одиннадцать, сбрасывается обратно в ноль: if (col>11) col=0; // Если цвет выше, то сбросить его } if (!keys[' ']) sp=false; // Если клавиша пробел не нажата, то сбросим флаг В строке ниже проверяется, нажата ли 'стрелка вверх'. Если это так, то yspeed будет увеличено. Это заставит частицы двигаться вверх: //Если нажата клавиша вверх и скорость по Y меньше чем 200, то увеличить скорость if (keys[VK_UP] && (yspeed<200)) yspeed+=1.0f; Далее в строке проверяется, нажата ли клавиша 'стрелка вниз': // Если стрелка вниз и скорость по Y больше чем – 200, то увеличить скорость падения if (keys[VK_DOWN] && (yspeed>-200)) yspeed-=1.0f; Если это так, то yspeed будет уменьшено. Это заставит частицу двигаться вниз. И снова, задан максимум скорости вниз не больше чем 200. Теперь осуществим проверку нажата ли клавиша 'стрелка вправо': // Если стрелка вправо и X скорость меньше чем 200, то увеличить скорость вправо if (keys[VK_RIGHT] && (xspeed<200)) xspeed+=1.0f; 164 // Если стрелка влево и X скорость больше чем –200, то увеличить скорость влево if (keys[VK_LEFT] && (xspeed>-200)) xspeed-=1.0f; delay управляет скоростью смены цветов, когда используется режим радуги: delay++; // Увеличить счетчик задержки циклической смены цветов в режиме радуги Если это так, то xspeed будет увеличено. Это заставит частицы двигаться вправо. Задан максимум скорости не больше чем 200. Далее приведен код проверки заголовока сверху окна: if (keys[VK_F1]) // Была нажата кнопка F1? { keys[VK_F1]=FALSE; // Если так – установит значение FALSE KillGLWindow(); // Закроет текущее окно OpenGL fullscreen=!fullscreen; // Переключит режим "Полный экран"/"Оконный" // Заново создаст окно OpenGL if (!CreateGLWindow(640,480,16,fullscreen)) { return 0; // Выйти, если окно не было создано } } } } } // Сброс KillGLWindow(); // Закроет окно return (msg.wParam); // Выйдет из программы } Таким образом, в рассмотренном примере были детально объяснены все шаги, которые требуются для создания системы моделирования частиц. Эта система моделирования частиц может использоваться в играх для создания эффектов типа огня, воды, снега, взрывов, падающих звезд, и так далее. Код может быть легко модифицирован для обработки большего количество параметров, и создания новых эффектов (например, фейерверк). Примеры реализации программного кода с небольшими изменениями приведены на рис.3436. Контрольные вопросы 165 1. Какие параметры должна содержать текстура частицы? 2. Как создать эффект радуги? 3. Как изменить скорость движения частицы? 4. Зачем нужны переменные life и fade? 5. Как реализовать движение частиц вниз или вверх? 6. В какой части кода надо внести изменения и дополнения чтобы реализовать эффект взрыва? 7. Как разместить тот или иной эффект на крупном плане? 8. Поясните смысл команды: particle[loop].x+=particle[loop].xi/(slowdown*1000). 9. Поясните смысл команды: if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f. Рис. 34. Эффект огненного фонтана 166 Рис. 35 Эффект бенгальского огня Рис. 36 Эффект взрыва Заключение Сложность компьютерной графики определяется тем, что она базируется на знаниях многих и многих дисциплин, например, тригономтрии, начертательной геометрии, физики, оптики и др. Компьютерная графика «требовательна» к знаниям и опыту 167 программирования. Несмотря на это «Компьютерная графика» – дисциплина, которая позволяет программисту в полной мере проявить свои творческие способности и неординарные способности к программированию. Изучение данного пособия – первый шаг в программировании компьютерной графики. Пособие рассчитано на узучение дисциплины в объеме одного семестра и не может охватить обширный теоретический материал зарубежных авторов и, к сожалению, не многие разработки российских авторов. В сети Inernet любой заинтересованный программист может найти и очень похожие примеры, и множество более интересных и сложных. Наибольший интерес представляют выложенные коды для реализации сложнейших, в смысле эффектов компьютерной графики, анимационных 3D игр. 168 Библиографический список 1. Ву М., Дэвис Т., Нейдер Дж., Шрайндер Д. OpenGL. Руководство по программированию. Библиотека программиста. 4-е издание. – Питер, 2006. – 624 с. 2. Баяковский Ю.М., Игнатенко А.В. Начальный курс OpenGL. М: – Планета знаний, – 2007. – 221с. 3. Дональд Херн, М. Паулин Бейкер. Компьютерная графика и стандарт OpenGL = Computer Graphics with OpenGL. – 3-е изд. – М.: Вильямс, 2005. – 1168 с. 4. Поляков А.Ю. Методы и алгоритмы компьютерной графики примерах на Visual C++: Спб.: БХВ-Петербург, 2004 – 416 с. 5. Пореев В.Н. Компьютерная графика. – СПб.: БХВ-Петербург, 2004. – 432 с. 6. Ричард С. Райт мл, Липчак Б. OpenGL. Суперкнига – 3 изд. – М.: Вильямс, 2006. – 1040 с. 7. Энджел Э. Интерактивная компьютерная графика. Вводный курс на базе OpenGL – 2-е изд. – М.: Вильямс, 2001. – 592 с. 8. Каверин М. OpenGL. Официальное Руководство программиста. [Электронный ресурс] // URL: http://www.geo3dcgp.com/opengl/books/OpenGL_RedBook_rus.pdf (дата обращения 10.01.2012) 9. Тарасов И. OpenGL. [Электронный ресурс] // URL: http://www.helloworld.ru/texts/comp/games/opengl/opengl2/index.html (дата обращения 10.01.2012) 10. OpenGL. Программирование с использованием OpenGL. [Электронный ресурс] // URL: http://opengl.org.ru/books/open_gl/chapter2.8.html (дата обращения 12.01.2012) 11. Программирование в Интернет и под Windows. Программирование графики. [Электронный ресурс] // URL: http://www.ru-coding.com/index.php (дата обращения 22.12.2011) 12. Работа с OpenGL. Электронный ресурс. OpenGL – уроки от NeHe (англ.). [Электронный ресурс] // URL: http://nehe.gamedev.net/tutorial/volumetric_fog_ipicture_image_loading (дата обращения 08.11.2011) 13. Программирование с использованием OpenGL. Уроки OpenGL. [Электронный ресурс] // URL: http://opengl.org.ru/lesson/index.html (дата обращения: 05.11.2012) 14. Работа с OpenGL. [Электронный ресурс] // URL: http://pmg.org.ru/nehe/index.html (дата обращения: 05.12.2011). 169 Приложение 1 Листинг программы создания 3D объкета Стеклянная ваза #include <windows.h> Заголовочный файл для Windows #include <stdio.h> Заголовочный файл для стандартной библиотеки #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 light; Освещение Вкл/Выкл bool lp; L нажата? GLfloat xrot; X вращение GLfloat yrot; Y вращение GLfloat xspeed; X скорость вращения GLfloat yspeed; Y скорость вращения GLfloat z=­5.0f; Глубина экрана GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, Фоновое значение света GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, Значение рассеянного света GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, Позиция источника GLuint filter; Какой фильтр использовать GLuint texture[1]; Место для текстуры 170 ввода/вывода 1.0f }; 1.0f }; 1.0f }; LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Объявление WndProc 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 } int LoadGLTextures() Загрузка картинки и конвертирование в текстуру { int Status=FALSE; Индикатор состояния AUX_RGBImageRec *TextureImage[1]; Создать место для текстуры memset(TextureImage,0,sizeof(void *)*1); Установить указатель в NULL Загрузка картинки, проверка на ошибки, если картинка не найдена – выход if (TextureImage[0]=LoadBMP("Data/Wall.bmp")) { Status=TRUE; Установим Status в TRUE 171 glGenTextures(3, &texture[0]); Создание трех текстур glBindTexture(GL_TEXTURE_2D, texture[2]); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINE AR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINE AR_MIPMAP_NEAREST); gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]­>sizeX, TextureImage[0]­>sizeY, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]­>data); Создание текстуры с фильтром по соседним пикселям } if (TextureImage[0]) Если текстура существует { if (TextureImage[0]­>data) Если изображение текстуры существует { free(TextureImage[0]­>data); Освобождение памяти изображения текстуры } free(TextureImage[0]); Освобождение памяти под структуру } return Status; Возвращаем статус } GLvoid ReSizeGLScene(GLsizei width, GLsizei height) Изменить размер и инициализировать окно GL { if (height==0) Предотвращение деления на ноль { height=1; } glViewport(0,0,width,height); Сброс текущей области вывода glMatrixMode(GL_PROJECTION); Выбор матрицы проекций glLoadIdentity(); Сброс матрицы проекции Вычисление соотношения геометрических размеров для окна 172 gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,10 0.0f); glMatrixMode(GL_MODELVIEW); Выбор матрицы вида модели glLoadIdentity(); Сброс матрицы вида модели } 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); Улучшение в вычислении перспективы glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); Установка Фонового Света glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); Установка Диффузного Света glLightfv(GL_LIGHT1, GL_POSITION,LightPosition); Позициясвета glEnable(GL_LIGHT1); Разрешение источника света номер один 173 quadratic=gluNewQuadric(); Создаем указатель на квадратичный объект gluQuadricNormals(quadratic, GLU_SMOOTH); Создаем плавные нормали gluQuadricTexture(quadratic, GL_TRUE); Создаем координаты текстуры ( НОВОЕ ) return TRUE; Инициализация прошла успешно } int DrawGLScene(GLvoid) Здесь мы все рисуем { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); Здесь мы все рисуем glLoadIdentity(); Сбрасываем вид glTranslatef(0.0f,0.0f,z); Перемещаемся в глубь экрана glRotatef(xrot,1.0f,0.0f,0.0f); Вращение по оси X glRotatef(yrot,0.0f,1.0f,0.0f); Вращение по оси glBindTexture(GL_TEXTURE_2D,texture[filter]); Выбираем фильтрацию текстуре glTranslatef(0.0f,0.0f,­1.5f); Рисуем центр цилиндра gluCylinder(quadratic,0.5f,0.9f,3.0f,32,32); Рисуем цилиндр gluDisk(quadratic,0.0f,0.5f,32,32); Рисуем дно xrot+=xspeed; yrot+=yspeed; return TRUE; Продолжаем } GLvoid KillGLWindow(GLvoid) Корректное разрушение окна 174 { gluDeleteQuadric(quadratic); if (fullscreen) Мы в полноэкранном режиме? { ChangeDisplaySettings(NULL,0); Если да, то переключаемся обратно в оконный режим ShowCursor(TRUE); Показать курсор мышки } if (hRC) Существует ли Контекст Рендеринга? { if (!wglMakeCurrent(NULL,NULL)) Возможно ли освободить RC и DC? { MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } if (!wglDeleteContext(hRC)) Возможно ли удалить RC? { MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); } hRC=NULL; Установить RC в NULL } if (hDC&&!ReleaseDC(hWnd,hDC)) Возможно ли уничтожить DC? { MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hDC=NULL; Установить DC в NULL } if (hWnd&&!DestroyWindow(hWnd)) Возможно ли уничтожить окно? { MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hWnd=NULL; Установить hWnd в NULL } if (!UnregisterClass("OpenGL",hInstance)) 175 Возможно ли разрегистрировать класс { MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hInstance=NULL; Установить hInstance в NULL } } BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag) { GLuint PixelFormat; Хранит результат после поиска WNDCLASS wc; Структура класса окна DWORD dwExStyle; Расширенный стиль окна DWORD dwStyle; Обычный стиль окна RECT WindowRect; Grabs Rectangle Upper Left / Lower Right Values WindowRect.left=(long)0; Установить левую составляющую в 0 WindowRect.right=(long)width; Установить правую составляющую в Width WindowRect.top=(long)0; Установить верхнюю составляющую в 0 WindowRect.bottom=(long)height; Установить нижнюю составляющую в Height fullscreen=fullscreenflag; Устанавливаем значение глобальной переменной fullscreen hInstance = GetModuleHandle(NULL); Считаем дескриптор нашего приложения wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; Перерисуем при перемещении и создаём скрытый DC wc.lpfnWndProc = (WNDPROC) WndProc; Процедура обработки сообщений wc.cbClsExtra = 0; Нет дополнительной информации для окна 176 wc.cbWndExtra = 0; Нет дополнительной информации для окна wc.hInstance = hInstance; Устанавливаем дескриптор wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); Загружаем иконку по умолчанию wc.hCursor = LoadCursor(NULL, IDC_ARROW); Загружаем указатель мышки wc.hbrBackground = NULL; Фон не требуется для GL wc.lpszMenuName = NULL; Меню в окне не будет wc.lpszClassName = "OpenGL"; Устанавливаем имя классу if (!RegisterClass(&wc)) Пытаемся зарегистрировать класс окна { MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Выход и возвращение функцией значения false } if (fullscreen) Полноэкранный режим? { DEVMODE dmScreenSettings; Режим устройства memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); Очистка для хранения установок dmScreenSettings.dmSize=sizeof(dmScreenSettings); Размер структуры Devmode dmScreenSettings.dmPelsWidth = width; Ширина экрана dmScreenSettings.dmPelsHeight Высотаэкрана = height; dmScreenSettings.dmBitsPerPel Глубина цвета = bits; dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PEL SHEIGHT; Режим Пикселя 177 Пытаемся установить выбранный режим и получить результат. Примечание: CDS_FULLSCREEN убирает панель управления. if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=D ISP_CHANGE_SUCCESSFUL) { Если переключение в полноэкранный режим невозможно, будет предложено два варианта: оконный режим или выход. if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By\nYour Video Card. Use Windowed Mode Instead?","Приер GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES) { fullscreen=FALSE; Выбороконногорежима (fullscreen = false) } else { Выскакивающее окно, сообщающее пользователю о закрытие окна. MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP); return FALSE; Выход и возвращение функцией false } } } if (fullscreen) Мы остались в полноэкранном режиме? { dwExStyle=WS_EX_APPWINDOW; Расширенный стиль окна dwStyle=WS_POPUP; Обычный стиль окна ShowCursor(FALSE); Скрыть указатель мышки } else { dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; Расширенный стиль окна 178 dwStyle=WS_OVERLAPPEDWINDOW; Обычный стиль окна } AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); Подбирает окну подходящие размеры Создаём окно if (!(hWnd=CreateWindowEx( dwExStyle, // Расширенный стиль для окна "OpenGL" Имя класса title, Заголовок окна dwStyle | Выбираемые стили для окна WS_CLIPSIBLINGS | / Требуемый стиль для окна WS_CLIPCHILDREN, Требуемый стиль для окна 0, 0, Позиция окна WindowRect.right­WindowRect.left, Вычисление подходящей ширины WindowRect.bottom­WindowRect.top, Вычисление подходящей высоты NULL, Нет родительского NULL, Нет меню hInstance, Дескриптор приложения NULL))) Не передаём ничего до WM_CREATE (???) { KillGLWindow(); Восстановитьэкран MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; 179 Вернуть false } static PIXELFORMATDESCRIPTOR pfd= pfd сообщает Windows каким будет вывод на экран каждого пикселя { sizeof(PIXELFORMATDESCRIPTOR), Размер дескриптора данного формата пикселей 1, Номер версии PFD_DRAW_TO_WINDOW | ФорматдляОкна PFD_SUPPORT_OPENGL | Форматдля OpenGL PFD_DOUBLEBUFFER, Формат для двойного буфера PFD_TYPE_RGBA, Требуется RGBA формат bits, Выбирается бит глубины цвета 0, 0, 0, 0, 0, 0, Игнорирование цветовых битов 0, Нет буфера прозрачности 0, Сдвиговый бит игнорируется 0, Нет буфера накопления 0, 0, 0, 0, Биты накопления игнорируются 32, 32 битный Z­буфер (буфер глубины) 0, Нет буфера трафарета 0, Нет вспомогательных буферов PFD_MAIN_PLANE, Главный слой рисования 0, Зарезервировано 0, 0, 0 Маски слоя игнорируются }; if (!(hDC=GetDC(hWnd))) Можем ли мы получить Контекст Устройства? { KillGLWindow(); Восстановить экран 180 MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) Найден ли подходящий формат пикселя? { KillGLWindow(); Восстановить экран MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } if(!SetPixelFormat(hDC,PixelFormat,&pfd)) Возможно ли установить Формат Пикселя? { KillGLWindow(); Восстановить экран MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } if (!(hRC=wglCreateContext(hDC))) Возможно ли установить Контекст Рендеринга? { KillGLWindow(); Восстановить экран MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } if(!wglMakeCurrent(hDC,hRC)) Попробовать активировать Контекст Рендеринга { KillGLWindow(); Восстановить экран MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } ShowWindow(hWnd,SW_SHOW); Показать окно SetForegroundWindow(hWnd); 181 Слегка повысим приоритет SetFocus(hWnd); Установить фокус клавиатуры на наше окно ReSizeGLScene(width, height); Настроим перспективу для нашего OpenGL экрана. if (!InitGL()) Инициализация только что созданного окна { KillGLWindow(); Восстановить экран MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION); return FALSE; Вернуть false } return TRUE; Всё в порядке! } LRESULT CALLBACK WndProc( Дескриптор нужного окна HWND hWnd, UINT uMsg, Сообщение для этого окна WPARAM wParam, Дополнительная информация LPARAM lParam) Дополнительная информация { switch (uMsg) Проверка сообщения для окна { case WM_ACTIVATE: Проверка сообщения активности окна { if (!HIWORD(wParam)) Проверить состояние минимизации { active=TRUE; Программа активна } else { 182 active=FALSE; Программа теперь не активна } return 0; Возвращаемся в цикл обработки сообщений } case WM_SYSCOMMAND: Перехватываем системную команду { switch (wParam) Останавливаем системный вызов { case SC_SCREENSAVE: Пытается ли запустится скринсейвер? case SC_MONITORPOWER: Пытается ли монитор перейти в режим сбережения энергии? return 0; Предотвращаем это } break; Выход } case WM_CLOSE: Получили сообщение о закрытие? { PostQuitMessage(0); Отправить сообщение о выходе return 0; Вернуться назад } case WM_KEYDOWN: Была ли нажата кнопка? { keys[wParam] = TRUE; Если так, мы присваиваем этой ячейке true return 0; Возвращаемся } case WM_KEYUP: Была ли отпущена клавиша? { keys[wParam] = FALSE; Если так, мы присваиваем этой ячейке false 183 return 0; Возвращаемся } case WM_SIZE: Изменены размеры OpenGL окна { ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); Младшее слово=Width, старшее слово=Height return 0; Возвращаемся } } Пересылаем все необработанные сообщения DefWindowProc return DefWindowProc(hWnd,uMsg,wParam,lParam); } int WINAPI WinMain( Дескриптор приложения HINSTANCE hInstance, HINSTANCE hPrevInstance, Дескриптор родительского приложения LPSTR lpCmdLine, Параметрыкоманднойстроки int nCmdShow) Состояние отображения окна { MSG msg; Структура для хранения сообщения Windows BOOL done=FALSE; Логическая переменная для выхода из цикла Спрашивает пользователя, какой режим экрана он предпочитает if (MessageBox(NULL,"Запустить во весь экран?", "Старт!!!",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; Оконный режим } Создать наше OpenGL окно if (!CreateGLWindow("Ваза",640,480,32,fullscreen)) { return 0; Выйти, если окно не может быть создано } 184 while(!done) Цикл продолжается, пока done не равно true { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) Есть ли в очереди какое­нибудь сообщение? { if (msg.message==WM_QUIT) Мы поучили сообщение о выходе? { done=TRUE; Если так, done=true } else Если нет, обрабатывает сообщения { TranslateMessage(&msg); ереводим сообщение DispatchMessage(&msg); Отсылаем сообщение } } else Если нет сообщений { Прорисовываем сцену. if ((active && !DrawGLScene()) keys[VK_ESCAPE]) Активна ли программа? Было ли нажата клавиша ESC? { done=TRUE; ESC Говорит об останове выполнения программы } else Не время для выхода, обновим экран. { SwapBuffers(hDC); Меняем буфер (двойная буферизация) if (keys['L'] && !lp) { lp=TRUE; light=!light; if (!light) { glDisable(GL_LIGHTING); } else 185 { glEnable(GL_LIGHTING); } } if (!keys['L']) { lp=FALSE; } if (keys[VK_PRIOR]) { z­=0.01f; } if (keys[VK_NEXT]) { z+=0.01f; } if (keys[VK_UP]) { xspeed­=0.01f; } if (keys[VK_DOWN]) { xspeed+=0.01f; } if (keys[VK_RIGHT]) { yspeed+=0.01f; } if (keys[VK_LEFT]) { yspeed­=0.01f; } if (keys[VK_F1]) Была ли нажата F1? { keys[VK_F1]=FALSE; Если так, меняем значение ячейки массива на false KillGLWindow(); Разрушаем текущее окно fullscreen=!fullscreen; Переключаем режим Пересоздаём наше OpenGL окно if (!CreateGLWindow("ВАЗА",640,480,32,fullscreen)) { 186 return 0; Выходим, если это невозможно } } } } } KillGLWindow(); Разрушаем окно return (msg.wParam); Выходим из программы 187 Приложение 2 Методические указания к выполнению курсового проекта Целью выполнения курсового проекта является самостоятельное закрепления материала через практическое применение полученных теоретических знаний на лекциях и практических навыков – на лабораторных работах. Так как в дисциплине «Компьютерная графика» рассматривается широкий круг вопросов, курсовой проект содержит, помимо практической части, теоритическую. Структура и содержание основных разделов курсового проекта должны соответствовать структуре, предложенной в таблице П1. Темы курсового проекта представлены в табл. П1-П3, и могут изменяться и дополняться. Таблица П1 Структура курсового проекта Наименование раздела Раздел Объем курсовой работы Введение 1 с. Освещение теоретических 1 10-15с. вопросов в соответствии с темой (Табл. П2). Программная реализация графических объектов средствами 2 10-20с. OpenGL (в соответствии с темой (Табл. П3)). Заключение 1 с. Список использованных источников При необходимости полный Приложение листинг программного кода Программа и отчет предоставляется на электронном носителе 1. Таблица П2 Темы теоретической части курсового проекта Критерии оценки качества систем отображения информации. 188 2. Модели описания поверхностей: равномерная сетка, неравномерная сетка, изолинии. 3. Модели описания поверхностей: аналитическая модель, векторная полигональная модель, воксельная модель. 4. Модели отражения света: трассировка лучей, метод Гуро, метод Фонга. Базовые алгоритмы векторной графики: алгоритмы вывода прямой линии, алгоритм вывода окружности, кривая Безье. 5. Базовые растровые алгоритмы: алгоритмы вывода прямой линии, алгоритм вывода окружности, кривая Безье, алгоритмы закрашивания и т.д. 6. Методы построения теней (проективные тени, stencil shadows, shadow maps, perspective shadow map). 7. Требования к СКГ (Технические, эксплуатационные). 8. Продвинутые методики текстурирования (spherical environment mapping, cubic environment mapping, environment bump mapping, emboss bump mapping, per pixel lighting, polynomial texture maps). 9. Отображение двухмерной текстуры на трехмерную поверхность. 10. Текстурные координаты, билинейная, трилинейная и анизотропная фильтрации. Пертурбация нормалей. Трехмерные текстуры. 11. Мировые и экранные координаты. Преобразования координат. Проекции. 12. Эргономические требования к СКГ. 13. Тенденции построения современных графических систем: графическое ядро, приложения, инструментарий для написания приложений. 14. Форматы машинной графики, преобразование форматов, критерии оценки качества, примеры. 15. Стандарты цветного телевидения: NTSC, PAL, SECAM. 16. Телевидение. Уплотнение спектра (гребенчатый фильтр). Искажения, возникающие при передаче телевизионных изображений. 17. Алгоритмы сжатия изображений: JPEG, Wavelet. 18. Алгоритмы сжатия изображений: RLE, LZW. 19. Алгоритмы сжатия последовательностей изображений: MPEG (1,2,4). 20. Перспективы развития и применения компьютерной графики. 21. Алгоритмы удаления невидимых поверхностей. Алгоритм художника, Z буфер. Сортировка для алгоритма художника. 22. Графика в Internet. 23. Иерархические графические модели (модель руки робота). Обход древовидных структур. Граф сцены. Анимация. 189 24. Проективное наложение текстуры в системе OpenGL. Сглаживание погрешностей дискретизации. Смешивание изображений в OpenGL. 25. Буферы OpenGL. Удаление нелицевых граней. Растровое преобразование с использование Z-буфера. Сортировка по глубине. 26. Законы визуального восприятия: иерархия отношений зрения и слуха, принцип избыточности, принцип группировки, восприятие пространства, восприятие движения и событий. 27. Компьютерная графика в инженерных расчетах (примеры, организация работ, оценка эффективности). 28. Образный анализ данных (когнитивная графика). 29. Фреймы и абстрактные типы данных. Фреймы в OpenGL. Матрицы преобразований в OpenGL. Видовое преобразования в OpenGL. Проективные преобразования в OpenGL (перспективное и параллельное преобразование). 30. Стандартизация в СКГ. 31. Стандарты обмена графическими данными. 32. Новые направления и технологии в компьютерной графике: вейвлеты в компьютерной графике. 33. Поверхности произвольного вида с использованием кривых Безье и B-сплайнов. 34. Новые направления и технологии в компьютерной графике: фрактальная графика. 35. Алгоритмы сжатия видеоинформации. 36. Алгоритмы клиппирования, алгоритм Сазерленда-Коэна, алгоритм Лианга-Барского. 37. Аархитектура графических систем. 38. Высокоскоростные графические системы 39. Односторонние поверхности, отбраковка односторонних поверхностей. Заливка, алгоритм построчного сканирования. Понятия первичного и вторичных буферов. 40. Аппаратное обеспечение компьютерной графики: графический адаптер, видеопамять. Таблица П3 Темы практической части курсового проекта 1. Применение эффекта тумана средствами OPENGL для создания иллюзии глубины пространства на примере горного ланшафта. 2. Текстурирование куба средствами OPENGL с применением билинейной и трилинейной фильтрации. 3. Текстурирование куба средствами OPENGL одной из продвинутых техник текстурирования. 190 4. Моделирование средствами OPENGL броуновского движения. 5. Средствами OPENGL реализация трехмерного представления статистических данных. 6. Моделирование средствами OPENGL столкновения твердых тел на примере игры в бильярд. 7. Моделирование средствами OPENGL природного феномена: водной поверхности. Анимация параметров источника света и материалов. 8. Моделирование средствами OPENGL столкновения твердых тел на примере мозаики «Детский калейдоскоп». 9. Моделирование средствами OPENGL природного феномена: огня. Анимация параметров источника света и материалов. 10. Моделирование средствами OPENGL природного феномена: облаков. Анимация параметров источника света и материалов. 11. Моделирование средствами OPENGL 3D объекта «АСОИУ». Анимация матриц видового, проективного и модельного преобразования (горизонтальное и вертикальное движение, вращение по трем осям). 12. Моделирование средствами OPENGL 3D объекта «АСОИУ». Анимация матриц видового, проективного и модельного преобразования (изменение цвета букв в зависимости от выбранного источника света). 13. Моделирование средствами OPENGL 3D объекта «АСОИУ». Анимация матриц видового, проективного и модельного преобразования (изменение цвета букв в зависимости от выбранного источника света). 14. Моделирование средствами OPENGL 3D объекта «ОМСК-2012». Анимация матриц видового, проективного и модельного преобразования (изменение цвета букв в зависимости от выбранного источника света). 15. Моделирование средствами OPENGL 3D объектов «пирамида» и «шар» с осуществлением перемещения относительно друг друга. 16. Моделирование средствами OPENGL 3D объекта «АБИТУРИЕНТ 2012» внутри шара. Анимация параметров источника света и материалов. (изменение цвета букв в зависимости от «прозрачности» шара). 17. Моделирование средствами OPENGL 3D объекта «СИБАДИ 2012». Анимация параметров источника света и материалов (изменение текстуры и цвета текстуры букв, использование эффекта теней). 18. Моделирование средствами OPENGL 3D объекта «ГЛОБУС» с осуществлением зеркального и «бликующего» отражения. 19. Моделирование средствами OPENGL 3D объекта «Сосуд с 191 жидкостью» Анимация параметров источника света и материалов (Сосуд должен быть «наполнен» разными жидкостями по цвету и плотности). 20. Моделирование средствами OPENGL 3D объекта «Сосуд с жидкостью» Анимация параметров источника света и материалов (Сосуд должен быть «наполнен» жидкостью с движущимися «пузырьками»). 21. Моделирование средствами OPENGL 3D объекта «Развивающийся флаг СИБАДИ» на фоне здания академии 22. Реализация алгоритмов фрактальной графики средствами OPENG. Моделирование водной поверхности. 23. Реализация алгоритмов фрактальной графики (создание изображения). 24. Реализация алгоритмов фрактальной графики (сжатие изображения). 25. Моделирование средствами OPENGL 3D объекта «Стеклянная ваза с рисунком». 26. Реализация алгоритмов фрактальной графики средствами OPENG. Моделирование текстур (не менее пяти текстур). 27. Моделирование средствами OPENGL 3D объекта «Новогодние шары». Анимация параметров источника света и материалов. 28. Моделирование средствами OpenGL действия: окружность плавно перетекает в ромб, и наоборот, цвет периодически изменяется. 29. Моделирование средствами OpenGL действия: основание конуса плавно перетекает в вершину, и наоборот, цвет периодически изменяется. 30. Моделирование средствами OpenGL действия: «облако» (несколько пересекающихся эллипсоидов) объединяются в один, цвет периодически изменяется. 31. Имитация «воронки» (несколько дисков с разными радиусами, упорядоченными по возрастанию, глубина (расстояние между дисками) и цвет периодически изменяется). 32. Моделирование средствами OpenGL действия: окружность плавно перетекает в цилиндр, цвет периодически изменяется. 33. Моделирование средствами OpenGL действия: вращается куб, на каждой грани которого находятся различные текстуры. 34. Моделирование средствами OpenGL действия: экран заполнен текстурой, при нажатии на кнопку мыши количество текстур по горизонтали и по вертикали увеличивается вдвое. 35. Моделирование средствами OpenGL действия: текстура накладывается на цилиндр, конус, диск и частичный диск. 192 36. Моделирование средствами OpenGL действия: текстура накладывается на тор. 37. Моделирование средствами OpenGL действия: крутиться сфера «минус» куб. 38. Моделирование средствами OpenGL действия: крутиться пересечение сферы и куба. 39. Моделирование средствами OpenGL действия: крутиться пересечение сферы и тетраэдра. 40. Моделирование средствами OpenGL действия: крутиться тетраэдр «минус» сфера. 41. Моделирование средствами OpenGL действия: крутиться сфера «минус» тетраэдр. 42. Моделирование средствами OpenGL действия: крутиться конус «минус» цилиндр (центральные оси фигур совпадают, радиусы - нет). 43. Моделирование средствами OpenGL действия: крутиться тетраэдр «минус» конус. 44. Моделирование средствами OpenGL действия: крутиться конус «минус» тетраэдр. 45. Моделирование средствами OpenGL действия: крутиться пересечение тетраэдра и цилиндра. 46. Моделирование средствами OpenGL действия: крутиться пересечение конуса и тетраэдра 47. Имитация Вселенной (несколько звезд, время от времени одна из них вспыхивает, увеличиваясь в размерах, другая гаснет, уменьшаясь). 48. Изображение молекулы - несколько электронов вращаются вокруг ядра, время от времени меняя свои орбиты. 49. Моделирование средствами OpenGL действия: различные преобразования со сферой (перемещение, сжатие, растяжение, изменение цвета, освещение и т.д.), управление действиями задается с клавиатуры. 50. Моделирование средствами OpenGL действия: вывод в окно текстуры; при нажатии в каком-либо месте окна кнопки мыши, от этой позиции расходятся круги как по водной поверхности (т.е. текстура при этом должна колебаться). Курсовой проект должен быть оформлен в соответствии с требованиями ГОСТ 7.32-2001 «Система стандартов по информации, библиотечному и издательскому делу. Отчет о научно-исследовательской работе» и ГОСТ 2.105-95 «Единая система конструкторской документации. Общие требования к текстовым документам». 193 Программа должна быть предоставлена на электронном носителе. Защита курсовой работы должна сопровождаться презентацией проекта, выполненной в MS Office PowerPoint, и кратким докладом сопровождающим ее. 194 Содержание Введение.....................................................................................................................6 I. РАБОТА С OPENGL ..............................................................................................8 1.1. Основные понятия OpenGL................................................................................8 1.2. Графические примитивы OpenGL....................................................................10 1.3. Цвет ...................................................................................................................14 1.4. Преобразования координат и проекции...........................................................20 1.5. Текстуры............................................................................................................23 1.6. Использование эффекта тумана .......................................................................28 1.7. Список отображения.........................................................................................29 1.8. Шрифты.............................................................................................................30 II. ПРИМЕРЫ ПРОГРАММИРОВАНИЯ КОМПЬЮТЕРНОЙ ГРАФИКИ С ИСПОЛЬЗОВАНИЕМ OPENGL.............................................................................31 2.1. Инициализация OpenGL окна в Windows ........................................................31 2.2. Прорисовка примитивов...................................................................................52 2.3. Отображение цвета ...........................................................................................54 2.5. Реализации вращения фигур ............................................................................57 2.6. Создание фигур в 3D ........................................................................................60 2.7. Наложение текстур на объекты........................................................................66 2.8. Перемещения объектов с помощью клавиатуры. Режимы фильтрации текстур. Освещение................................................................................................................71 2.9. Эффект смешивание цветов смежных пикселей .............................................87 2.10. Передвижение изображений в 3D ..................................................................90 2.11. Эффект развевающегося флага .................................................................... 101 2.12. Использование списков изображений.......................................................... 106 2.13. Растровые шрифты........................................................................................ 116 2.14. Построение векторных шрифтов.................................................................. 123 2.15. Использование текстурированных шрифтов............................................... 130 2.16 Создание эффекта тумана.............................................................................. 138 2.17. Квадратирование для отображения сложных объектов .............................. 141 2.18. Машина моделирования частиц ................................................................... 152 Заключение ............................................................................................................ 167 Библиографический список .................................................................................. 169 Приложение 1 ........................................................................................................ 170 Приложение 2 ........................................................................................................ 188 195 Учебное издание Елена Юрьевна Андиева КОМПЬЮТЕРНАЯ ГРАФИКА: ПРОГРАММИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ OPENGL Учебное пособие *** Редактор ___________________ Штриховой код издания на основе ISBN инициалы, фамилия *** Подписано к печати __ .__ . 20__ Формат 6090 1/16. Бумага писчая Оперативный способ печати Гарнитура Times New Roman Усл. п. л. __ , уч.-изд. л. __ Тираж ____ экз. Заказ № ___ Цена договорная Издательство СибАДИ 644099, г. Омск, ул. П. Некрасова, 10 Отпечатано в подразделении ОП издательства СибАДИ 196